diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e4042dbd5..7c9b1f340 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -492,6 +492,124 @@ jobs: path: ${{ runner.temp }}/backend-conformance if-no-files-found: warn + paired-driver-benchmark: + name: factory/paired-driver-benchmark + runs-on: ubuntu-latest + # Operator acceptance (2026-11-13): the actual speed comparison between + # the two real drivers. Both arms run the SAME matched battery against + # actual ArangoDB and PostgreSQL services -- the same images, ports, + # and health options the backend-conformance lane adjudicates -- plus + # the actual ClickHouse service: authoring publishes through the + # ClickHouse-backed materialization chain (a ClickHouse-less + # deployment cannot author at all -- the 422 MATERIALIZATION_FAILED + # the local rehearsal produced is the proof), so this lane runs the + # full chain with --publication=run. Each arm materializes into its + # own loom_bench_* database; the timed path itself is the + # source-row preview route, identical on both arms. + # The gate fails closed if either arm is planned/not_run/excluded, if + # the chain did not run, or if either driver produced no real timing + # samples; a disclosure artifact can never turn this check green. + timeout-minutes: 10 + services: + arangodb: + image: arangodb:3.12 + env: + ARANGO_NO_AUTH: '1' + ports: + - 8529:8529 + options: >- + --health-cmd "wget --quiet --tries=1 --spider http://127.0.0.1:8529/_api/version" + --health-interval 3s + --health-timeout 5s + --health-retries 40 + postgres18: + image: postgres:18 + env: + POSTGRES_USER: loom_bench + POSTGRES_PASSWORD: loom_ci_test_only + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -h 127.0.0.1 -U loom_bench" + --health-interval 3s + --health-timeout 5s + --health-retries 40 + clickhouse: + image: clickhouse/clickhouse-server:25.3 + env: + CLICKHOUSE_USER: loom_ci + CLICKHOUSE_PASSWORD: loom_ci_password + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" + ports: + - 9000:9000 + - 8123:8123 + options: >- + --health-cmd "wget --no-verbose --tries=1 --spider http://127.0.0.1:8123/ping || exit 1" + --health-interval 5s + --health-timeout 3s + --health-retries 20 + env: + LOOM_TEST_ARANGO_URL: http://127.0.0.1:8529 + LOOM_TEST_POSTGRES_URL: postgresql://loom_bench:loom_ci_test_only@127.0.0.1:5432/loom_bench_paired?sslmode=disable + LOOM_BENCH_ADMIN_DSN: postgresql://loom_bench:loom_ci_test_only@127.0.0.1:5432/postgres?sslmode=disable + LOOM_BENCH_CH_USER: loom_ci + LOOM_BENCH_CH_PASSWORD: loom_ci_password + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.26.5 + cache: true + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 24.x + - name: Build the server binary and benchcli from this SHA + run: | + mkdir -p "$RUNNER_TEMP/paired-bin" + go build -o "$RUNNER_TEMP/paired-bin/loom-server" ./cmd/arango-fhir-server + go build -o "$RUNNER_TEMP/paired-bin/benchcli" ./internal/dataframe/benchmark/cmd/benchcli + - name: Run the paired benchmark against both services + # Five interleaved rounds x two measured iterations per driver: the + # post-restart process-cold sample plus one warm sample per round + # (claims cite the warm cell), one warmup, tiny matched fixture, + # full publication chain through the ClickHouse service (each arm + # materializes into its own loom_bench_* + # database). The script verifies the artifact through the Go + # verifier, classifies realness (both arms ran, the chain ran, + # both timed), and writes comparison.md; it exits nonzero on any + # shortfall, so the job goes red rather than reporting + # scaffolding as a comparison. + run: >- + node scripts/benchmark-backends.mjs --paired + --binary "$RUNNER_TEMP/paired-bin/loom-server" + --benchcli "$RUNNER_TEMP/paired-bin/benchcli" + --profile tiny + --iterations 10 + --warmup 1 + --rounds 5 + --source-commit "$GITHUB_SHA" + --clickhouse-url clickhouse://127.0.0.1:9000 + --window-note "granted by the paired-driver-benchmark CI job (isolated services, $GITHUB_RUN_ID)" + --out "$RUNNER_TEMP/paired-benchmark" + - name: Show the comparison table in the job log + if: always() + run: | + if [ -f "$RUNNER_TEMP/paired-benchmark/comparison.md" ]; then + cat "$RUNNER_TEMP/paired-benchmark/comparison.md" + else + echo "no comparison table: the paired check did not reach a real both-driver comparison" >&2 + fi + - name: Preserve paired benchmark evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: paired-driver-benchmark + path: ${{ runner.temp }}/paired-benchmark + if-no-files-found: error + node-unit: name: Node unit tests runs-on: ubuntu-latest diff --git a/docs/PACKAGE_AUDIT.csv b/docs/PACKAGE_AUDIT.csv index f1172b9cb..d48cba5dc 100644 --- a/docs/PACKAGE_AUDIT.csv +++ b/docs/PACKAGE_AUDIT.csv @@ -3,19 +3,19 @@ location,import_path,package_name,kind,responsibility,production_files,test_file cmd/arango-fhir-proto,github.com/calypr/loom/cmd/arango-fhir-proto,main,executable,"Provide operator commands for FHIR loading, catalog diagnostics, generation repair, and activation.",main.go,main_test.go; resource_inventory_test.go,—,—,842,470,0,—,internal/backends/arango; internal/catalog; internal/dataset; internal/fhir/schema; internal/ingest,internal/catalog,—,—,—,—,legacy=4; migration=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/arango-fhir-proto,inventory complete; semantic disposition pending cmd/arango-fhir-server,github.com/calypr/loom/cmd/arango-fhir-server,main,executable,Start the explicit Loom HTTP server binary.,main.go,—,—,—,7,0,0,—,internal/server,—,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/arango-fhir-server,inventory complete; semantic disposition pending cmd/dataframe-profile,github.com/calypr/loom/cmd/dataframe-profile,main,executable,Run and compare developer-facing dataframe profiling workloads.,main.go,—,—,—,331,0,0,—,conformance/compiler; generated/graphql/graph/model; internal/api/graphql/graph/query; internal/backends/arango; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/recipe; internal/dataframe/semantic,—,—,—,—,—,legacy=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/dataframe-profile,inventory complete; semantic disposition pending -cmd/dataframe-query,github.com/calypr/loom/cmd/dataframe-query,main,executable,Execute a checked-in GraphQL dataframe request and report response timing and diagnostics.,main.go,main_test.go,—,—,459,432,3,Error; String; Unwrap,—,—,—,—,—,—,compatibility=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/dataframe-query,inventory complete; semantic disposition pending +cmd/dataframe-query,github.com/calypr/loom/cmd/dataframe-query,main,executable,Execute a checked-in GraphQL dataframe request and report response timing and diagnostics.,main.go,backend_neutral_diagnostics_test.go; main_test.go,—,—,475,508,3,Error; String; Unwrap,—,—,—,—,—,—,legacy=2; compatibility=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/dataframe-query,inventory complete; semantic disposition pending cmd/explorer-config-v2-convert,github.com/calypr/loom/cmd/explorer-config-v2-convert,main,executable,Convert legacy Explorer configuration input into the V2 authoring representation offline.,main.go,main_test.go,—,—,377,62,0,—,internal/dataframe/recipe; internal/explorer; internal/explorer/authoringv2,internal/explorer/authoringv2,—,—,—,—,legacy=3,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/explorer-config-v2-convert,inventory complete; semantic disposition pending cmd/generate,github.com/calypr/loom/cmd/generate,main,executable,"Generate FHIR models, schema metadata, validation, extraction, and GraphQL artifacts from source schemas.",extraction.go; graphql.go; helpers.go; main.go; metadata.go; model.go; names.go; resources.go; schema.go; validation.go,extraction_test.go; graphql_test.go; metadata_test.go; model_test.go; resources_test.go; root_test.go; schema_fixture_test.go,fhir_helpers.go.tmpl,—,1750,522,10,BindingStrength; Definition; EdgeDocument; Link; Property; Result; Schema; TargetHints; TargetSchema; TemplatePointers,—,—,—,—,—,go:embed,compatibility=1,executable boundary; compare orchestration with sibling commands before combining; runtime/build signals present; static importer counts are insufficient for deletion,go test ./cmd/generate,inventory complete; semantic disposition pending cmd/gqlgenfix,github.com/calypr/loom/cmd/gqlgenfix,main,executable,Post-process gqlgen output to enforce Loom-specific generated-code invariants.,main.go,main_test.go,—,—,173,67,1,MarshalJSON,—,—,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/gqlgenfix,inventory complete; semantic disposition pending cmd/loom-acceptance,github.com/calypr/loom/cmd/loom-acceptance,main,executable,Run the locked real-data acceptance scenario and emit machine-readable verification artifacts.,main.go,main_test.go,—,—,225,46,0,—,internal/acceptance; internal/explorer,internal/explorer,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/loom-acceptance,inventory complete; semantic disposition pending -cmd/loom-data,github.com/calypr/loom/cmd/loom-data,main,executable,"Load one immutable generation into the backend chosen only by an explicit arango or postgres tag, routing to the Arango loader or the PostgreSQL lifecycle loader without inference or fallback.",load.go; main.go,load_test.go; main_test.go,—,—,964,1703,2,Error; Unwrap,internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/ingest,internal/backends/postgres/control; internal/catalog; internal/dataset; internal/ingest,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/loom-data,inventory complete; semantic disposition pending +cmd/loom-data,github.com/calypr/loom/cmd/loom-data,main,executable,"Load one immutable generation into the backend chosen only by an explicit arango or postgres tag, routing to the Arango loader or the PostgreSQL lifecycle loader without inference or fallback.",load.go; main.go,load_test.go; main_test.go,—,—,964,1735,2,Error; Unwrap,internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/ingest,internal/backends/postgres/control; internal/catalog; internal/dataset; internal/ingest,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/loom-data,inventory complete; semantic disposition pending cmd/loom-repository,github.com/calypr/loom/cmd/loom-repository,main,executable,Preflight a repository checkout and launch Loom against its META and CONFIG data with an owned Compose project.,main.go,main_test.go,—,—,426,231,0,—,internal/projectid; internal/repositoryseed,internal/projectid; internal/repositoryseed,—,—,—,—,legacy=2,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/loom-repository,inventory complete; semantic disposition pending cmd/preview-covering-index-artifact,github.com/calypr/loom/cmd/preview-covering-index-artifact,main,executable,Recreate a captured Builder proposal offline and emit the exact compiler artifact for index-coverage analysis.,main.go,main_test.go,—,—,1449,34,0,—,internal/authscope; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation,—,—,—,—,—,migration=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/preview-covering-index-artifact,inventory complete; semantic disposition pending conformance/compiler,github.com/calypr/loom/conformance/compiler,compilerfixture,conformance,Load and exercise the canonical dataframe compiler oracle corpus for conformance and performance checks.,types.go,benchmark_test.go; command_helpers_test.go; corpus_coverage_test.go; gdc_fixture_test.go; generated_semantics_coverage_test.go; physical_plan_cost_test.go; policy_ablation_test.go; run_test.go; schema_roots_test.go; types_test.go,—,—,119,1279,4,Expected; Fixture; LoadDir; Validate,internal/dataframe/recipe,internal/backends/arango; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema,cmd/dataframe-profile,—,—,—,—,non-runtime conformance boundary,go test ./conformance/compiler,inventory complete; semantic disposition pending -generated/fhir,github.com/calypr/loom/generated/fhir,fhir,generated,"Expose generated FHIR resource models and their extraction, validation, and GraphQL helpers.",extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go,—,—,extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go,72020,0,155,Address; Age; Annotation; Attachment; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; CodeableConcept; CodeableReference; Coding; ConcreteResource; Condition; ConditionParticipant; ConditionStage; ContactDetail; ContactPoint; Count; DataRequirement; DataRequirementCodeFilter; DataRequirementDateFilter; DataRequirementSort; DataRequirementValueFilter; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportSupportingInfo; Directory; Distance; DocumentReference; DocumentReferenceAttester; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceRelatesTo; Dosage; DosageDoseAndRate; Duration; EdgeDocument; Expression; ExtendedContactDetail; Extension; ExtractEdges; FHIRComments; FHIRPrimitiveExtension; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryParticipant; FamilyMemberHistoryProcedure; GetID; GetResourceType; Group; GroupCharacteristic; GroupMember; HumanName; Identifier; ImagingStudy; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesPerformer; IsFHIRResource; MarshalJSON; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationPerformer; MedicationBatch; MedicationIngredient; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestSubstitution; MedicationStatement; MedicationStatementAdherence; Meta; Money; Narrative; NewConcreteResource; Observation; ObservationComponent; ObservationReferenceRange; ObservationTriggeredBy; Organization; OrganizationQualification; ParameterDefinition; Patient; PatientCommunication; PatientContact; PatientLink; Period; Practitioner; PractitionerCommunication; PractitionerQualification; PractitionerRole; Procedure; ProcedureFocalDevice; ProcedurePerformer; Quantity; Range; Ratio; RatioRange; Reference; RelatedArtifact; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyComparisonGroup; ResearchStudyLabel; ResearchStudyObjective; ResearchStudyOutcomeMeasure; ResearchStudyProgressStatus; ResearchStudyRecruitment; ResearchSubject; ResearchSubjectProgress; Resource; Result; SampledData; Signature; Specimen; SpecimenCollection; SpecimenContainer; SpecimenFeature; SpecimenProcessing; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCode; SubstanceDefinitionMoiety; SubstanceDefinitionMolecularWeight; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionProperty; SubstanceDefinitionRelationship; SubstanceDefinitionSourceMaterial; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceIngredient; Task; TaskInput; TaskOutput; TaskPerformer; TaskRestriction; Timing; TimingRepeat; TriggerDefinition; UnmarshalJSON; UsageContext; Validate; ValidateAndExtract; ValidateFhirBinary; ValidateFhirDate; ValidateFhirDateTime; ValidateFhirTime; ValidateFhirURI; ValidateFhirUUID,internal/fhir/schema,—,generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/dataframe/recipe; internal/dataframe/spec; internal/ingest,—,github.com/bytedance/sonic; github.com/google/uuid,1 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/fhir,inventory complete; semantic disposition pending +generated/fhir,github.com/calypr/loom/generated/fhir,fhir,generated,"Expose generated FHIR resource models and their extraction, validation, and GraphQL helpers.",extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go,—,—,extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go,72020,0,155,Address; Age; Annotation; Attachment; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; CodeableConcept; CodeableReference; Coding; ConcreteResource; Condition; ConditionParticipant; ConditionStage; ContactDetail; ContactPoint; Count; DataRequirement; DataRequirementCodeFilter; DataRequirementDateFilter; DataRequirementSort; DataRequirementValueFilter; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportSupportingInfo; Directory; Distance; DocumentReference; DocumentReferenceAttester; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceRelatesTo; Dosage; DosageDoseAndRate; Duration; EdgeDocument; Expression; ExtendedContactDetail; Extension; ExtractEdges; FHIRComments; FHIRPrimitiveExtension; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryParticipant; FamilyMemberHistoryProcedure; GetID; GetResourceType; Group; GroupCharacteristic; GroupMember; HumanName; Identifier; ImagingStudy; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesPerformer; IsFHIRResource; MarshalJSON; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationPerformer; MedicationBatch; MedicationIngredient; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestSubstitution; MedicationStatement; MedicationStatementAdherence; Meta; Money; Narrative; NewConcreteResource; Observation; ObservationComponent; ObservationReferenceRange; ObservationTriggeredBy; Organization; OrganizationQualification; ParameterDefinition; Patient; PatientCommunication; PatientContact; PatientLink; Period; Practitioner; PractitionerCommunication; PractitionerQualification; PractitionerRole; Procedure; ProcedureFocalDevice; ProcedurePerformer; Quantity; Range; Ratio; RatioRange; Reference; RelatedArtifact; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyComparisonGroup; ResearchStudyLabel; ResearchStudyObjective; ResearchStudyOutcomeMeasure; ResearchStudyProgressStatus; ResearchStudyRecruitment; ResearchSubject; ResearchSubjectProgress; Resource; Result; SampledData; Signature; Specimen; SpecimenCollection; SpecimenContainer; SpecimenFeature; SpecimenProcessing; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCode; SubstanceDefinitionMoiety; SubstanceDefinitionMolecularWeight; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionProperty; SubstanceDefinitionRelationship; SubstanceDefinitionSourceMaterial; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceIngredient; Task; TaskInput; TaskOutput; TaskPerformer; TaskRestriction; Timing; TimingRepeat; TriggerDefinition; UnmarshalJSON; UsageContext; Validate; ValidateAndExtract; ValidateFhirBinary; ValidateFhirDate; ValidateFhirDateTime; ValidateFhirTime; ValidateFhirURI; ValidateFhirUUID,internal/fhir/schema,—,generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/dataframe/recipe; internal/dataframe/spec; internal/ingest,internal/dataframe/benchmark,github.com/bytedance/sonic; github.com/google/uuid,1 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/fhir,inventory complete; semantic disposition pending generated/fhirschema,github.com/calypr/loom/generated/fhirschema,fhirschema,generated,Expose generated raw FHIR schema metadata tables.,generated.go,—,—,generated.go,55277,0,3,Definition; Property; Traversal,—,—,internal/fhir/schema,internal/catalog,—,—,deprecated=1; compatibility=1,"generated boundary; change its source/generator, not emitted code",go test ./generated/fhirschema,inventory complete; semantic disposition pending -generated/graphql/graph/executor,github.com/calypr/loom/generated/graphql/graph/executor,executor,generated,Provide gqlgen-generated GraphQL executable schema and execution plumbing.,fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go,—,—,fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go,155289,0,328,Address; AddressResolver; Age; AgeResolver; Annotation; AnnotationResolver; Attachment; AttachmentResolver; Availability; AvailabilityAvailableTime; AvailabilityAvailableTimeResolver; AvailabilityNotAvailableTime; AvailabilityNotAvailableTimeResolver; AvailabilityResolver; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmarkResolver; BodyStructureIncludedStructureBodyLandmarkOrientationResolver; BodyStructureIncludedStructureResolver; BodyStructureResolver; CodeableConcept; CodeableConceptResolver; CodeableReference; CodeableReferenceResolver; Coding; CodingResolver; Complexity; ComplexityRoot; Condition; ConditionParticipant; ConditionParticipantResolver; ConditionResolver; ConditionStage; ConditionStageResolver; Config; ContactDetail; ContactDetailResolver; ContactPoint; ContactPointResolver; Count; CountResolver; DataRequirement; DataRequirementCodeFilter; DataRequirementCodeFilterResolver; DataRequirementDateFilter; DataRequirementDateFilterResolver; DataRequirementResolver; DataRequirementSort; DataRequirementSortResolver; DataRequirementValueFilter; DataRequirementValueFilterResolver; DataframeAggregateResult; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeMaterialization; DataframeOptimizationDecision; DataframeOptimizationPolicy; DataframePageInfo; DataframeQueryDiagnostics; DataframeRecipeArangoAssessment; DataframeRecipeColumn; DataframeRecipeExecution; DataframeRecipeExecutionOutput; DataframeRecipeExpansionExplanation; DataframeRecipeExplainCollectionScan; DataframeRecipeExplainIndexLocation; DataframeRecipeExplainIndexSummary; DataframeRecipeExplainPlanEstimate; DataframeRecipeExplainWarning; DataframeRecipeExplanation; DataframeRecipeExpressionExplanation; DataframeRecipeOptimizationDecision; DataframeRecipeOptimizationExplanation; DataframeRecipeOptimizationRuleState; DataframeRecipeOutputExplanation; DataframeRecipeOutputValidation; DataframeRecipePhysicalExplanation; DataframeRecipePhysicalOutputExplanation; DataframeRecipePreflight; DataframeRecipePreview; DataframeRecipePreviewOutput; DataframeRecipeResult; DataframeRecipeResultOutput; DataframeRecipeRevision; DataframeRecipeValidation; DataframeRichSourceReuse; DataframeRowConnection; DataframeSelector; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportMediaResolver; DiagnosticReportResolver; DiagnosticReportSupportingInfo; DiagnosticReportSupportingInfoResolver; DirectiveRoot; Directory; Distance; DistanceResolver; DocumentReference; DocumentReferenceAttester; DocumentReferenceAttesterResolver; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceContentProfileResolver; DocumentReferenceContentResolver; DocumentReferenceRelatesTo; DocumentReferenceRelatesToResolver; DocumentReferenceResolver; Dosage; DosageDoseAndRate; DosageDoseAndRateResolver; DosageResolver; Duration; DurationResolver; Exec; Expression; ExpressionResolver; ExtendedContactDetail; ExtendedContactDetailResolver; Extension; ExtensionResolver; FHIRPrimitiveExtension; FHIRPrimitiveExtensionResolver; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryConditionResolver; FamilyMemberHistoryParticipant; FamilyMemberHistoryParticipantResolver; FamilyMemberHistoryProcedure; FamilyMemberHistoryProcedureResolver; FamilyMemberHistoryResolver; FhirDataframeResult; FhirGraphNode; FhirGraphPageInfo; FhirGraphPath; FhirGraphQueryExplanation; FhirGraphQueryResult; FhirGraphRelationship; Group; GroupCharacteristic; GroupCharacteristicResolver; GroupMember; GroupMemberResolver; GroupResolver; HumanName; HumanNameResolver; Identifier; IdentifierResolver; ImagingStudy; ImagingStudyResolver; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesInstanceResolver; ImagingStudySeriesPerformer; ImagingStudySeriesPerformerResolver; ImagingStudySeriesResolver; MarshalJSON; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationDosageResolver; MedicationAdministrationPerformer; MedicationAdministrationPerformerResolver; MedicationAdministrationResolver; MedicationBatch; MedicationBatchResolver; MedicationIngredient; MedicationIngredientResolver; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestDispenseRequestInitialFillResolver; MedicationRequestDispenseRequestResolver; MedicationRequestResolver; MedicationRequestSubstitution; MedicationRequestSubstitutionResolver; MedicationResolver; MedicationStatement; MedicationStatementAdherence; MedicationStatementAdherenceResolver; MedicationStatementResolver; Meta; MetaResolver; Money; MoneyResolver; Mutation; MutationResolver; Narrative; NarrativeResolver; NewExecutableSchema; Observation; ObservationComponent; ObservationComponentResolver; ObservationReferenceRange; ObservationReferenceRangeResolver; ObservationResolver; ObservationTriggeredBy; ObservationTriggeredByResolver; Organization; OrganizationQualification; OrganizationQualificationResolver; OrganizationResolver; ParameterDefinition; ParameterDefinitionResolver; Patient; PatientCommunication; PatientCommunicationResolver; PatientContact; PatientContactResolver; PatientLink; PatientLinkResolver; PatientResolver; Period; PeriodResolver; Practitioner; PractitionerCommunication; PractitionerCommunicationResolver; PractitionerQualification; PractitionerQualificationResolver; PractitionerResolver; PractitionerRole; PractitionerRoleResolver; Procedure; ProcedureFocalDevice; ProcedureFocalDeviceResolver; ProcedurePerformer; ProcedurePerformerResolver; ProcedureResolver; Quantity; QuantityResolver; QueryResolver; Range; RangeResolver; Ratio; RatioRange; RatioRangeResolver; RatioResolver; Reference; ReferenceResolver; RelatedArtifact; RelatedArtifactResolver; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyAssociatedPartyResolver; ResearchStudyComparisonGroup; ResearchStudyComparisonGroupResolver; ResearchStudyLabel; ResearchStudyLabelResolver; ResearchStudyObjective; ResearchStudyObjectiveResolver; ResearchStudyOutcomeMeasure; ResearchStudyOutcomeMeasureResolver; ResearchStudyProgressStatus; ResearchStudyProgressStatusResolver; ResearchStudyRecruitment; ResearchStudyRecruitmentResolver; ResearchStudyResolver; ResearchSubject; ResearchSubjectProgress; ResearchSubjectProgressResolver; ResearchSubjectResolver; ResolverRoot; Resource; ResourceResolver; SampledData; SampledDataResolver; Schema; Signature; SignatureResolver; Specimen; SpecimenCollection; SpecimenCollectionResolver; SpecimenContainer; SpecimenContainerResolver; SpecimenFeature; SpecimenFeatureResolver; SpecimenProcessing; SpecimenProcessingResolver; SpecimenResolver; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCharacterizationResolver; SubstanceDefinitionCode; SubstanceDefinitionCodeResolver; SubstanceDefinitionMoiety; SubstanceDefinitionMoietyResolver; SubstanceDefinitionMolecularWeight; SubstanceDefinitionMolecularWeightResolver; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionNameOfficialResolver; SubstanceDefinitionNameResolver; SubstanceDefinitionProperty; SubstanceDefinitionPropertyResolver; SubstanceDefinitionRelationship; SubstanceDefinitionRelationshipResolver; SubstanceDefinitionResolver; SubstanceDefinitionSourceMaterial; SubstanceDefinitionSourceMaterialResolver; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceDefinitionStructureRepresentationResolver; SubstanceDefinitionStructureResolver; SubstanceIngredient; SubstanceIngredientResolver; SubstanceResolver; Task; TaskInput; TaskInputResolver; TaskOutput; TaskOutputResolver; TaskPerformer; TaskPerformerResolver; TaskResolver; TaskRestriction; TaskRestrictionResolver; Timing; TimingRepeat; TimingRepeatResolver; TimingResolver; TriggerDefinition; TriggerDefinitionResolver; UsageContext; UsageContextResolver,generated/fhir; generated/graphql/graph/model,—,internal/api/graphql/graph; internal/api/graphql/graph/resolver,—,github.com/99designs/gqlgen/graphql; github.com/99designs/gqlgen/graphql/introspection; github.com/vektah/gqlparser/v2; github.com/vektah/gqlparser/v2/ast,138 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/graphql/graph/executor,inventory complete; semantic disposition pending -generated/graphql/graph/model,github.com/calypr/loom/generated/graphql/graph/model,model,generated,Provide gqlgen-generated GraphQL transport model types.,models.go,—,—,models.go,1333,0,95,DataframeAggregateInput; DataframeAggregateResult; DataframeAggregationSpecInput; DataframeAggregationsInput; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeDatasetInput; DataframeFilterInput; DataframeMaterialization; DataframeMaterializationState; DataframeOptimizationDecision; DataframeOptimizationPolicy; DataframePageInfo; DataframeQueryDiagnostics; DataframeRecipeArangoAssessment; DataframeRecipeBindingsInput; DataframeRecipeColumn; DataframeRecipeExecution; DataframeRecipeExecutionOutput; DataframeRecipeExecutionState; DataframeRecipeExpansionExplanation; DataframeRecipeExplainCollectionScan; DataframeRecipeExplainIndexLocation; DataframeRecipeExplainIndexSummary; DataframeRecipeExplainPlanEstimate; DataframeRecipeExplainWarning; DataframeRecipeExplanation; DataframeRecipeExpressionExplanation; DataframeRecipeOptimizationDecision; DataframeRecipeOptimizationExplanation; DataframeRecipeOptimizationRuleState; DataframeRecipeOutputExplanation; DataframeRecipeOutputValidation; DataframeRecipePhysicalExplanation; DataframeRecipePhysicalOutputExplanation; DataframeRecipePreflight; DataframeRecipePreview; DataframeRecipePreviewOutput; DataframeRecipeResult; DataframeRecipeResultOutput; DataframeRecipeRevision; DataframeRecipeValidation; DataframeRichSourceReuse; DataframeRowConnection; DataframeRowsInput; DataframeSelector; DataframeSelectorInput; DataframeSortInput; ExplainDataframeRecipeInput; FHIRResource; FHIRResourceType; FhirAggregateInput; FhirAggregateOperation; FhirCatalogProjectionInput; FhirCodeValueInput; FhirColumnNaming; FhirDataframeInput; FhirDataframeResult; FhirFieldPredicateInput; FhirFieldPredicateOperation; FhirFieldSelectInput; FhirFieldSelectorInput; FhirFilterInput; FhirFilterOperator; FhirFilterQuantifier; FhirFilterValueInput; FhirFilterValueKind; FhirGraphNode; FhirGraphPageInfo; FhirGraphPath; FhirGraphQueryExplanation; FhirGraphQueryInput; FhirGraphQueryResult; FhirGraphRelationship; FhirGraphTraversalInput; FhirPivotDiscoveryInput; FhirPivotInput; FhirRepresentativeSliceInput; FhirTraversalMatchMode; FhirTraversalStepInput; FhirValueMode; IsValid; MarshalGQL; MarshalJSON; Mutation; PreflightDataframeRecipeInput; PreviewDataframeRecipeInput; Query; RegisterDataframeRecipeRevisionInput; RunDataframeRecipeInput; String; UnmarshalGQL; UnmarshalJSON; ValidateDataframeRecipeInput,—,—,cmd/dataframe-profile; generated/graphql/graph/executor; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver,internal/server,—,1 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/graphql/graph/model,inventory complete; semantic disposition pending +generated/graphql/graph/executor,github.com/calypr/loom/generated/graphql/graph/executor,executor,generated,Provide gqlgen-generated GraphQL executable schema and execution plumbing.,fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go,—,—,fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go,155331,0,328,Address; AddressResolver; Age; AgeResolver; Annotation; AnnotationResolver; Attachment; AttachmentResolver; Availability; AvailabilityAvailableTime; AvailabilityAvailableTimeResolver; AvailabilityNotAvailableTime; AvailabilityNotAvailableTimeResolver; AvailabilityResolver; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmarkResolver; BodyStructureIncludedStructureBodyLandmarkOrientationResolver; BodyStructureIncludedStructureResolver; BodyStructureResolver; CodeableConcept; CodeableConceptResolver; CodeableReference; CodeableReferenceResolver; Coding; CodingResolver; Complexity; ComplexityRoot; Condition; ConditionParticipant; ConditionParticipantResolver; ConditionResolver; ConditionStage; ConditionStageResolver; Config; ContactDetail; ContactDetailResolver; ContactPoint; ContactPointResolver; Count; CountResolver; DataRequirement; DataRequirementCodeFilter; DataRequirementCodeFilterResolver; DataRequirementDateFilter; DataRequirementDateFilterResolver; DataRequirementResolver; DataRequirementSort; DataRequirementSortResolver; DataRequirementValueFilter; DataRequirementValueFilterResolver; DataframeAggregateResult; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeMaterialization; DataframeOptimizationDecision; DataframeOptimizationPolicy; DataframePageInfo; DataframeQueryDiagnostics; DataframeRecipeArangoAssessment; DataframeRecipeColumn; DataframeRecipeExecution; DataframeRecipeExecutionOutput; DataframeRecipeExpansionExplanation; DataframeRecipeExplainCollectionScan; DataframeRecipeExplainIndexLocation; DataframeRecipeExplainIndexSummary; DataframeRecipeExplainPlanEstimate; DataframeRecipeExplainWarning; DataframeRecipeExplanation; DataframeRecipeExpressionExplanation; DataframeRecipeOptimizationDecision; DataframeRecipeOptimizationExplanation; DataframeRecipeOptimizationRuleState; DataframeRecipeOutputExplanation; DataframeRecipeOutputValidation; DataframeRecipePhysicalExplanation; DataframeRecipePhysicalOutputExplanation; DataframeRecipePreflight; DataframeRecipePreview; DataframeRecipePreviewOutput; DataframeRecipeResult; DataframeRecipeResultOutput; DataframeRecipeRevision; DataframeRecipeValidation; DataframeRichSourceReuse; DataframeRowConnection; DataframeSelector; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportMediaResolver; DiagnosticReportResolver; DiagnosticReportSupportingInfo; DiagnosticReportSupportingInfoResolver; DirectiveRoot; Directory; Distance; DistanceResolver; DocumentReference; DocumentReferenceAttester; DocumentReferenceAttesterResolver; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceContentProfileResolver; DocumentReferenceContentResolver; DocumentReferenceRelatesTo; DocumentReferenceRelatesToResolver; DocumentReferenceResolver; Dosage; DosageDoseAndRate; DosageDoseAndRateResolver; DosageResolver; Duration; DurationResolver; Exec; Expression; ExpressionResolver; ExtendedContactDetail; ExtendedContactDetailResolver; Extension; ExtensionResolver; FHIRPrimitiveExtension; FHIRPrimitiveExtensionResolver; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryConditionResolver; FamilyMemberHistoryParticipant; FamilyMemberHistoryParticipantResolver; FamilyMemberHistoryProcedure; FamilyMemberHistoryProcedureResolver; FamilyMemberHistoryResolver; FhirDataframeResult; FhirGraphNode; FhirGraphPageInfo; FhirGraphPath; FhirGraphQueryExplanation; FhirGraphQueryResult; FhirGraphRelationship; Group; GroupCharacteristic; GroupCharacteristicResolver; GroupMember; GroupMemberResolver; GroupResolver; HumanName; HumanNameResolver; Identifier; IdentifierResolver; ImagingStudy; ImagingStudyResolver; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesInstanceResolver; ImagingStudySeriesPerformer; ImagingStudySeriesPerformerResolver; ImagingStudySeriesResolver; MarshalJSON; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationDosageResolver; MedicationAdministrationPerformer; MedicationAdministrationPerformerResolver; MedicationAdministrationResolver; MedicationBatch; MedicationBatchResolver; MedicationIngredient; MedicationIngredientResolver; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestDispenseRequestInitialFillResolver; MedicationRequestDispenseRequestResolver; MedicationRequestResolver; MedicationRequestSubstitution; MedicationRequestSubstitutionResolver; MedicationResolver; MedicationStatement; MedicationStatementAdherence; MedicationStatementAdherenceResolver; MedicationStatementResolver; Meta; MetaResolver; Money; MoneyResolver; Mutation; MutationResolver; Narrative; NarrativeResolver; NewExecutableSchema; Observation; ObservationComponent; ObservationComponentResolver; ObservationReferenceRange; ObservationReferenceRangeResolver; ObservationResolver; ObservationTriggeredBy; ObservationTriggeredByResolver; Organization; OrganizationQualification; OrganizationQualificationResolver; OrganizationResolver; ParameterDefinition; ParameterDefinitionResolver; Patient; PatientCommunication; PatientCommunicationResolver; PatientContact; PatientContactResolver; PatientLink; PatientLinkResolver; PatientResolver; Period; PeriodResolver; Practitioner; PractitionerCommunication; PractitionerCommunicationResolver; PractitionerQualification; PractitionerQualificationResolver; PractitionerResolver; PractitionerRole; PractitionerRoleResolver; Procedure; ProcedureFocalDevice; ProcedureFocalDeviceResolver; ProcedurePerformer; ProcedurePerformerResolver; ProcedureResolver; Quantity; QuantityResolver; QueryResolver; Range; RangeResolver; Ratio; RatioRange; RatioRangeResolver; RatioResolver; Reference; ReferenceResolver; RelatedArtifact; RelatedArtifactResolver; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyAssociatedPartyResolver; ResearchStudyComparisonGroup; ResearchStudyComparisonGroupResolver; ResearchStudyLabel; ResearchStudyLabelResolver; ResearchStudyObjective; ResearchStudyObjectiveResolver; ResearchStudyOutcomeMeasure; ResearchStudyOutcomeMeasureResolver; ResearchStudyProgressStatus; ResearchStudyProgressStatusResolver; ResearchStudyRecruitment; ResearchStudyRecruitmentResolver; ResearchStudyResolver; ResearchSubject; ResearchSubjectProgress; ResearchSubjectProgressResolver; ResearchSubjectResolver; ResolverRoot; Resource; ResourceResolver; SampledData; SampledDataResolver; Schema; Signature; SignatureResolver; Specimen; SpecimenCollection; SpecimenCollectionResolver; SpecimenContainer; SpecimenContainerResolver; SpecimenFeature; SpecimenFeatureResolver; SpecimenProcessing; SpecimenProcessingResolver; SpecimenResolver; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCharacterizationResolver; SubstanceDefinitionCode; SubstanceDefinitionCodeResolver; SubstanceDefinitionMoiety; SubstanceDefinitionMoietyResolver; SubstanceDefinitionMolecularWeight; SubstanceDefinitionMolecularWeightResolver; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionNameOfficialResolver; SubstanceDefinitionNameResolver; SubstanceDefinitionProperty; SubstanceDefinitionPropertyResolver; SubstanceDefinitionRelationship; SubstanceDefinitionRelationshipResolver; SubstanceDefinitionResolver; SubstanceDefinitionSourceMaterial; SubstanceDefinitionSourceMaterialResolver; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceDefinitionStructureRepresentationResolver; SubstanceDefinitionStructureResolver; SubstanceIngredient; SubstanceIngredientResolver; SubstanceResolver; Task; TaskInput; TaskInputResolver; TaskOutput; TaskOutputResolver; TaskPerformer; TaskPerformerResolver; TaskResolver; TaskRestriction; TaskRestrictionResolver; Timing; TimingRepeat; TimingRepeatResolver; TimingResolver; TriggerDefinition; TriggerDefinitionResolver; UsageContext; UsageContextResolver,generated/fhir; generated/graphql/graph/model,—,internal/api/graphql/graph; internal/api/graphql/graph/resolver,—,github.com/99designs/gqlgen/graphql; github.com/99designs/gqlgen/graphql/introspection; github.com/vektah/gqlparser/v2; github.com/vektah/gqlparser/v2/ast,138 exported interface(s),legacy=1,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/graphql/graph/executor,inventory complete; semantic disposition pending +generated/graphql/graph/model,github.com/calypr/loom/generated/graphql/graph/model,model,generated,Provide gqlgen-generated GraphQL transport model types.,models.go,—,—,models.go,1334,0,95,DataframeAggregateInput; DataframeAggregateResult; DataframeAggregationSpecInput; DataframeAggregationsInput; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeDatasetInput; DataframeFilterInput; DataframeMaterialization; DataframeMaterializationState; DataframeOptimizationDecision; DataframeOptimizationPolicy; DataframePageInfo; DataframeQueryDiagnostics; DataframeRecipeArangoAssessment; DataframeRecipeBindingsInput; DataframeRecipeColumn; DataframeRecipeExecution; DataframeRecipeExecutionOutput; DataframeRecipeExecutionState; DataframeRecipeExpansionExplanation; DataframeRecipeExplainCollectionScan; DataframeRecipeExplainIndexLocation; DataframeRecipeExplainIndexSummary; DataframeRecipeExplainPlanEstimate; DataframeRecipeExplainWarning; DataframeRecipeExplanation; DataframeRecipeExpressionExplanation; DataframeRecipeOptimizationDecision; DataframeRecipeOptimizationExplanation; DataframeRecipeOptimizationRuleState; DataframeRecipeOutputExplanation; DataframeRecipeOutputValidation; DataframeRecipePhysicalExplanation; DataframeRecipePhysicalOutputExplanation; DataframeRecipePreflight; DataframeRecipePreview; DataframeRecipePreviewOutput; DataframeRecipeResult; DataframeRecipeResultOutput; DataframeRecipeRevision; DataframeRecipeValidation; DataframeRichSourceReuse; DataframeRowConnection; DataframeRowsInput; DataframeSelector; DataframeSelectorInput; DataframeSortInput; ExplainDataframeRecipeInput; FHIRResource; FHIRResourceType; FhirAggregateInput; FhirAggregateOperation; FhirCatalogProjectionInput; FhirCodeValueInput; FhirColumnNaming; FhirDataframeInput; FhirDataframeResult; FhirFieldPredicateInput; FhirFieldPredicateOperation; FhirFieldSelectInput; FhirFieldSelectorInput; FhirFilterInput; FhirFilterOperator; FhirFilterQuantifier; FhirFilterValueInput; FhirFilterValueKind; FhirGraphNode; FhirGraphPageInfo; FhirGraphPath; FhirGraphQueryExplanation; FhirGraphQueryInput; FhirGraphQueryResult; FhirGraphRelationship; FhirGraphTraversalInput; FhirPivotDiscoveryInput; FhirPivotInput; FhirRepresentativeSliceInput; FhirTraversalMatchMode; FhirTraversalStepInput; FhirValueMode; IsValid; MarshalGQL; MarshalJSON; Mutation; PreflightDataframeRecipeInput; PreviewDataframeRecipeInput; Query; RegisterDataframeRecipeRevisionInput; RunDataframeRecipeInput; String; UnmarshalGQL; UnmarshalJSON; ValidateDataframeRecipeInput,—,—,cmd/dataframe-profile; generated/graphql/graph/executor; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver,internal/server,—,1 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/graphql/graph/model,inventory complete; semantic disposition pending generated/loomapi,github.com/calypr/loom/generated/loomapi,loomapi,generated,"Provide OpenAPI-generated HTTP request, response, and strict-server contracts.",api.gen.go; strict_json.go,construction_json_test.go; correlated_json_test.go; request_boundary_test.go; selection_json_test.go; strict_json_test.go,—,api.gen.go,23408,512,1620,APIVersion; AcceptedConstruction; AcceptedConstructionReceipt; Action; ActionColumn; ActivateDatasetGeneration; ActivateDatasetGeneration200JSONResponse; ActivateDatasetGeneration400JSONResponse; ActivateDatasetGeneration401JSONResponse; ActivateDatasetGeneration403JSONResponse; ActivateDatasetGeneration409JSONResponse; ActivateDatasetGeneration500JSONResponse; ActivateDatasetGeneration503JSONResponse; ActivateDatasetGenerationParams; ActivateDatasetGenerationRequestObject; ActivateDatasetGenerationResponseObject; AggregateOperationCapability; AggregateOperationCapabilityOperation; AggregateOperationCapabilityResultCardinality; AggregateOperationCapabilityRowContext; AggregateSource; AggregateSourceOperation; AggregateTransformationCapabilities; ApplyCommandsRequest; ApplyCommandsResponse; ApplyExplorerBuilderCommands; ApplyExplorerBuilderCommands200JSONResponse; ApplyExplorerBuilderCommands400JSONResponse; ApplyExplorerBuilderCommands401JSONResponse; ApplyExplorerBuilderCommands403JSONResponse; ApplyExplorerBuilderCommands404JSONResponse; ApplyExplorerBuilderCommands409JSONResponse; ApplyExplorerBuilderCommands422JSONResponse; ApplyExplorerBuilderCommands500JSONResponse; ApplyExplorerBuilderCommands503JSONResponse; ApplyExplorerBuilderCommandsJSONRequestBody; ApplyExplorerBuilderCommandsParams; ApplyExplorerBuilderCommandsRequestObject; ApplyExplorerBuilderCommandsResponseObject; Artifact; ArtifactFormat; ArtifactMediaType; ArtifactPrepareRequest; ArtifactState; AsConfiguredColumnReadyResolution; AsConfiguredColumnUnavailableResolution; AsConstructionGroupCodingIdentitySourceOption; AsConstructionGroupFieldScalarSourceOption; AsConstructionGroupFieldSourceChoice; AsConstructionGroupFieldSourceCodingIdentityBinding; AsConstructionGroupFieldSourceCurrentStageInput; AsConstructionGroupFieldSourceInputBinding; AsConstructionGroupFieldSourceRelatedOccurrenceInput; AsConstructionGroupFieldSourceScalarBinding; AsConstructionGroupKeyFieldSource; AsConstructionGroupKeyInputColumn; AsConstructionGroupKeySemanticCategory; AsConstructionGroupSemanticCategoryChoice; AsConstructionRowValue0; AsConstructionRowValue1; AsContributorCodeValue; AsContributorStringValue; AsExactCategoryRecodeTransformation; AsExactSemanticInventoryItemIdentity; AsFamilySemanticInventoryItemIdentity; AsFieldChoiceSource; AsFieldConstructionChoiceSearchSource; AsFieldPresentationGroupUnderSemantic; AsFieldPresentationKeepRaw; AsFieldPresentationReady; AsFieldPresentationUnavailable; AsLegacyErrorBody; AsLegacyErrorResponseError0; AsRemoveColumnTransformation; AsRowDefinitionComparisonAvailable; AsRowDefinitionComparisonUnavailable; AsRowDefinitionExpandedSelection; AsRowDefinitionExplicitGroupSelection; AsRowDefinitionFieldGroupSelection; AsRowDefinitionRecordsSelection; AsSemanticBindingChoiceSource; AsSemanticConstructionChoiceSearchSource; AsSetColumnTransformation; AsTableShapeComparisonAvailable; AsTableShapeComparisonUnavailable; AssessExplorerRowChange; AssessExplorerRowChange200JSONResponse; AssessExplorerRowChange400JSONResponse; AssessExplorerRowChange401JSONResponse; AssessExplorerRowChange403JSONResponse; AssessExplorerRowChange404JSONResponse; AssessExplorerRowChange409JSONResponse; AssessExplorerRowChange422JSONResponse; AssessExplorerRowChange500JSONResponse; AssessExplorerRowChange503JSONResponse; AssessExplorerRowChangeJSONRequestBody; AssessExplorerRowChangeRequestObject; AssessExplorerRowChangeResponseObject; AssessRowChangeRequest; AssessRowChangeRequestMode; AuthResourcePath; AuthoringBadRequest; AuthoringBadRequestJSONResponse; AuthoringCapability; AuthoringCapabilityKind; AuthoringCapabilityOperations; AuthoringClientClosedRequest; AuthoringClientClosedRequestJSONResponse; AuthoringCommand; AuthoringCommandForm; AuthoringCommandInitialPresentation; AuthoringCommandMatchMode; AuthoringCommandRowValuePolicy; AuthoringCommandType; AuthoringConflict; AuthoringConflictJSONResponse; AuthoringFeatures; AuthoringForbidden; AuthoringForbiddenJSONResponse; AuthoringGatewayTimeout; AuthoringGatewayTimeoutJSONResponse; AuthoringInternalError; AuthoringInternalErrorJSONResponse; AuthoringNotFound; AuthoringNotFoundJSONResponse; AuthoringPayloadTooLarge; AuthoringPayloadTooLargeJSONResponse; AuthoringTooManyRequests; AuthoringTooManyRequestsJSONResponse; AuthoringUnavailable; AuthoringUnavailableJSONResponse; AuthoringUnprocessable; AuthoringUnprocessableJSONResponse; BrowseExplorerFrameSourceOptions; BrowseExplorerFrameSourceOptions200JSONResponse; BrowseExplorerFrameSourceOptions400JSONResponse; BrowseExplorerFrameSourceOptions401JSONResponse; BrowseExplorerFrameSourceOptions403JSONResponse; BrowseExplorerFrameSourceOptions404JSONResponse; BrowseExplorerFrameSourceOptions409JSONResponse; BrowseExplorerFrameSourceOptions422JSONResponse; BrowseExplorerFrameSourceOptions500JSONResponse; BrowseExplorerFrameSourceOptions503JSONResponse; BrowseExplorerFrameSourceOptionsJSONRequestBody; BrowseExplorerFrameSourceOptionsRequestObject; BrowseExplorerFrameSourceOptionsResponseObject; BrowseExplorerSemanticInventory; BrowseExplorerSemanticInventory200JSONResponse; BrowseExplorerSemanticInventory400JSONResponse; BrowseExplorerSemanticInventory401JSONResponse; BrowseExplorerSemanticInventory403JSONResponse; BrowseExplorerSemanticInventory404JSONResponse; BrowseExplorerSemanticInventory409JSONResponse; BrowseExplorerSemanticInventory422JSONResponse; BrowseExplorerSemanticInventory500JSONResponse; BrowseExplorerSemanticInventory503JSONResponse; BrowseExplorerSemanticInventoryJSONRequestBody; BrowseExplorerSemanticInventoryRequestObject; BrowseExplorerSemanticInventoryResponseObject; BuilderState; BuilderStateKind; BuilderStateLifecycleState; CandidateSearchRequest; CandidateSearchResponse; CandidateSearchResponseKind; Catalog; CatalogAvailability; CatalogAvailabilityReadiness; CatalogAvailabilityReadinessStatus; CatalogAvailabilitySourceReadiness; CatalogAvailabilitySourceReadinessStatus; CatalogCandidate; CatalogEdge; CatalogEdgeStorageDirection; CatalogNode; CatalogObservedField; CatalogObservedRelationship; CatalogObservedRelationshipBuilderDirection; CatalogObservedRelationshipStorageDirection; CatalogObservedResource; CellTraceBinding; CellTraceContribution; CellTraceFeature; CellTraceRequest; CellTraceResponse; CellTraceTrace; CellTraceTraceStatus; ChartPresentation; CheckExplorerPopulationMapping; CheckExplorerPopulationMapping200JSONResponse; CheckExplorerPopulationMapping400JSONResponse; CheckExplorerPopulationMapping401JSONResponse; CheckExplorerPopulationMapping403JSONResponse; CheckExplorerPopulationMapping404JSONResponse; CheckExplorerPopulationMapping409JSONResponse; CheckExplorerPopulationMapping422JSONResponse; CheckExplorerPopulationMapping500JSONResponse; CheckExplorerPopulationMapping503JSONResponse; CheckExplorerPopulationMapping504JSONResponse; CheckExplorerPopulationMappingJSONRequestBody; CheckExplorerPopulationMappingRequestObject; CheckExplorerPopulationMappingResponseObject; CodedValueKey; Column; ColumnSource; ColumnSourceKind; ColumnSourceRequest; ColumnSourceResponse; ColumnSourceRouteStep; ColumnSourceRouteStepMatchMode; ColumnSourceRouteStepStorageDirection; ColumnTransformationChange; ColumnValueTransformation; ColumnValueTransformationCapabilities; CommandResult; CommandResultType; CompileResponse; CompileResponseKind; ConceptCandidate; ConfiguredColumnContext; ConfiguredColumnContextRequest; ConfiguredColumnContextResponse; ConfiguredColumnReadyResolution; ConfiguredColumnReadyResolutionState; ConfiguredColumnResolution; ConfiguredColumnUnavailableResolution; ConfiguredColumnUnavailableResolutionState; Construction; ConstructionActiveRelatedRecordDescriptor; ConstructionCapabilitiesRequest; ConstructionCapabilitiesResponse; ConstructionCategoryDiscoveryRequest; ConstructionCategoryDiscoveryResponse; ConstructionCategoryDiscoveryResponseOutcome; ConstructionChoice; ConstructionChoiceFact; ConstructionChoiceOption; ConstructionChoiceOptionContributorPredicateOperators; ConstructionChoiceOptionDecision; ConstructionChoiceOptionForm; ConstructionChoiceOptionPreservation; ConstructionChoiceOptionRowEffect; ConstructionChoiceOptionShape; ConstructionChoiceOptionSupport; ConstructionChoicePresentation; ConstructionChoiceProposalRequest; ConstructionChoiceProposalResponse; ConstructionChoiceProposalResponsePreviewStatus; ConstructionChoiceProposalSelection; ConstructionChoiceProposalSelectionForm; ConstructionChoiceProposalSelectionRowValuePolicy; ConstructionChoiceSearchRequest; ConstructionChoiceSearchRequest_Source; ConstructionChoiceSearchResponse; ConstructionChoiceSelection; ConstructionChoiceSelectionForm; ConstructionChoiceSelectionRowValuePolicy; ConstructionChoice_Source; ConstructionCodeValue; ConstructionCodedGroup; ConstructionCodedGroupChoice; ConstructionCodedGroupMissingKeyPolicy; ConstructionCodedGroupSourceCardinality; ConstructionCodedGroupSourceFhirType; ConstructionCodedGroupSourceShape; ConstructionCodedPivot; ConstructionCodedPivotCategory; ConstructionCodedPivotDuplicatePolicy; ConstructionCodedPivotMissingCellPolicy; ConstructionCodedPivotSource; ConstructionCombine; ConstructionCombineJoinType; ConstructionCombineKey; ConstructionCombineKind; ConstructionCombineMembershipMode; ConstructionCombineProjection; ConstructionCombineRightMatchPolicy; ConstructionDependencyImpact; ConstructionDependencyIssue; ConstructionDerive; ConstructionDeriveDivisionByZeroPolicy; ConstructionDeriveMissingInputPolicy; ConstructionDeriveOperation; ConstructionDeriveSourceChoice; ConstructionDeriveSourceInput; ConstructionDeriveSourceSelection; ConstructionDiscoveredCategory; ConstructionExpand; ConstructionExpandEmptyPolicy; ConstructionExpandSourceChoice; ConstructionExpandSourceInput; ConstructionExpandSourceSelection; ConstructionExpandedFieldSource; ConstructionExpandedFieldSourceCardinality; ConstructionFilter; ConstructionFilterOperator; ConstructionFilterSourceChoice; ConstructionFilterSourceInput; ConstructionFilterSourceSelection; ConstructionFilterValue; ConstructionFilterValueKind; ConstructionGroup; ConstructionGroupAggregate; ConstructionGroupAggregateOperation; ConstructionGroupCodingIdentitySourceOption; ConstructionGroupCodingIdentitySourceOptionKind; ConstructionGroupCodingIdentitySourceOptionSourceRole; ConstructionGroupCodingIdentitySourceOptionValueKind; ConstructionGroupFieldChoice; ConstructionGroupFieldChoiceRelatedOccurrence; ConstructionGroupFieldCurrentStage; ConstructionGroupFieldCurrentStageSourceRole; ConstructionGroupFieldOwner; ConstructionGroupFieldOwnerRole; ConstructionGroupFieldRelatedOccurrence; ConstructionGroupFieldRelatedOccurrenceCodingEmptyPolicy; ConstructionGroupFieldRelatedOccurrenceEmptyPolicy; ConstructionGroupFieldRelatedOccurrenceMultiplicity; ConstructionGroupFieldRelatedOccurrenceOrdinalSemantics; ConstructionGroupFieldRelatedRouteStep; ConstructionGroupFieldRelatedRouteStepMatchMode; ConstructionGroupFieldRelatedRouteStepStorageDirection; ConstructionGroupFieldScalarSourceOption; ConstructionGroupFieldScalarSourceOptionKind; ConstructionGroupFieldScalarSourceOptionSourceRole; ConstructionGroupFieldScalarSourceOptionValueKind; ConstructionGroupFieldSource; ConstructionGroupFieldSourceAncestor; ConstructionGroupFieldSourceChoice; ConstructionGroupFieldSourceChoiceKind; ConstructionGroupFieldSourceCodingIdentityBinding; ConstructionGroupFieldSourceCodingIdentityBindingKind; ConstructionGroupFieldSourceCodingIdentityBindingSourceRole; ConstructionGroupFieldSourceCodingIdentityBindingValueKind; ConstructionGroupFieldSourceContext; ConstructionGroupFieldSourceCurrentStageInput; ConstructionGroupFieldSourceCurrentStageInputKind; ConstructionGroupFieldSourceExpansion; ConstructionGroupFieldSourceInput; ConstructionGroupFieldSourceInputBinding; ConstructionGroupFieldSourceInputBindingKind; ConstructionGroupFieldSourceInputBindingValueKind; ConstructionGroupFieldSourceRelatedOccurrenceInput; ConstructionGroupFieldSourceRelatedOccurrenceInputKind; ConstructionGroupFieldSourceRouteStep; ConstructionGroupFieldSourceRouteStepDirection; ConstructionGroupFieldSourceScalarBinding; ConstructionGroupFieldSourceScalarBindingKind; ConstructionGroupFieldSourceScalarBindingSourceRole; ConstructionGroupFieldSourceScalarBindingValueKind; ConstructionGroupKey; ConstructionGroupKeyFieldSource; ConstructionGroupKeyInputColumn; ConstructionGroupKeySemanticCategory; ConstructionGroupMissingKeyPolicy; ConstructionGroupSemanticCategory; ConstructionGroupSemanticCategoryChoice; ConstructionGroupSemanticCategoryChoiceKind; ConstructionGroupSemanticCategoryChoiceSemanticCategoryKind; ConstructionGroupSemanticCategoryKind; ConstructionGroupSemanticCategorySourceKind; ConstructionGroupSource; ConstructionInputColumn; ConstructionInputRef; ConstructionInputRefKind; ConstructionInputRevision; ConstructionInputRevisionKind; ConstructionInputsRequest; ConstructionInputsResponse; ConstructionLiteral; ConstructionLiteralKind; ConstructionOperand; ConstructionOperandKind; ConstructionOperation; ConstructionOperationCapability; ConstructionOperationCapabilityKind; ConstructionOperationKind; ConstructionOutputPresentation; ConstructionPivot; ConstructionPivotCategory; ConstructionPivotDuplicatePolicy; ConstructionPivotMissingCellPolicy; ConstructionPivotSourceSelection; ConstructionPivotUnlistedCategoryPolicy; ConstructionProposalRequest; ConstructionProposalResponse; ConstructionProposalResponsePreviewStatus; ConstructionRelatedEligibility; ConstructionRelatedEligibilityMatch; ConstructionRelatedEligibilityMatchKind; ConstructionRelatedExpand; ConstructionRelatedExpandAnchor; ConstructionRelatedExpandAnchorKind; ConstructionRelatedExpandEmptyPolicy; ConstructionRelatedExpandStageDescriptor; ConstructionRelatedExpandStageDescriptorAnchorKind; ConstructionRelatedField; ConstructionRelatedSource; ConstructionRelatedSourceForm; ConstructionRelatedSourceRowValuePolicy; ConstructionRouteStep; ConstructionRouteStepMatchMode; ConstructionRouteStepStorageDirection; ConstructionRowValue; ConstructionRowValue0; ConstructionRowValue1; ConstructionRowValueFieldSource; ConstructionRowValuePolicy; ConstructionSemanticGroupChoice; ConstructionSourceInput; ConstructionSourceInputChoice; ConstructionSourceProjection; ConstructionSourceProjectionAvailability; ConstructionStageColumn; ConstructionStageColumnDescriptor; ConstructionStageColumnDescriptorCardinality; ConstructionStageDescriptor; ConstructionStep; ConstructionTableScalar; ConstructionTableScalarKind; ConstructionUnpivot; ConstructionUnpivotInput; ConstructionUnpivotNullRowPolicy; ConstructionUnpivotSourceChoice; ConstructionUnpivotSourceInput; ConstructionUnpivotSourceSelection; ConstructionWorkspaceInput; ConstructionWorkspaceInputColumn; ConstructionWorkspaceInputColumnCardinality; ContractColumn; ContractColumnStructuralSuitability; ContributorCodeValue; ContributorCodeValueKind; ContributorPredicate; ContributorPredicateOperator; ContributorPredicateQuantifier; ContributorStringValue; ContributorStringValueKind; ContributorValue; ContributorWindowSource; ContributorWindowSourcePrecision; CorrelatedBinding; CorrelatedBindingSelectionScope; CorrelatedBindingValueScope; CreateDatasetGeneration; CreateDatasetGeneration200JSONResponse; CreateDatasetGeneration400JSONResponse; CreateDatasetGeneration401JSONResponse; CreateDatasetGeneration403JSONResponse; CreateDatasetGeneration409JSONResponse; CreateDatasetGeneration415JSONResponse; CreateDatasetGeneration422JSONResponse; CreateDatasetGeneration500JSONResponse; CreateDatasetGeneration503JSONResponse; CreateDatasetGenerationMultipartBody; CreateDatasetGenerationMultipartRequestBody; CreateDatasetGenerationRequestObject; CreateDatasetGenerationResponseObject; CreateExplorer; CreateExplorer201JSONResponse; CreateExplorer400JSONResponse; CreateExplorer401JSONResponse; CreateExplorer403JSONResponse; CreateExplorer409JSONResponse; CreateExplorer422JSONResponse; CreateExplorer500JSONResponse; CreateExplorerExplicitGroupRevision; CreateExplorerExplicitGroupRevision201JSONResponse; CreateExplorerExplicitGroupRevision400JSONResponse; CreateExplorerExplicitGroupRevision401JSONResponse; CreateExplorerExplicitGroupRevision403JSONResponse; CreateExplorerExplicitGroupRevision404JSONResponse; CreateExplorerExplicitGroupRevision409JSONResponse; CreateExplorerExplicitGroupRevision422JSONResponse; CreateExplorerExplicitGroupRevision500JSONResponse; CreateExplorerExplicitGroupRevision503JSONResponse; CreateExplorerExplicitGroupRevisionJSONRequestBody; CreateExplorerExplicitGroupRevisionParams; CreateExplorerExplicitGroupRevisionRequestObject; CreateExplorerExplicitGroupRevisionResponseObject; CreateExplorerJSONRequestBody; CreateExplorerParams; CreateExplorerRequestObject; CreateExplorerResponseObject; CreateExplorerSelection; CreateExplorerSelection201JSONResponse; CreateExplorerSelection400JSONResponse; CreateExplorerSelection401JSONResponse; CreateExplorerSelection403JSONResponse; CreateExplorerSelection404JSONResponse; CreateExplorerSelection409JSONResponse; CreateExplorerSelection422JSONResponse; CreateExplorerSelection500JSONResponse; CreateExplorerSelection503JSONResponse; CreateExplorerSelectionJSONRequestBody; CreateExplorerSelectionParams; CreateExplorerSelectionRequestObject; CreateExplorerSelectionResponseObject; CreateInterpretationRevision; CreateInterpretationRevision201JSONResponse; CreateInterpretationRevision400JSONResponse; CreateInterpretationRevision401JSONResponse; CreateInterpretationRevision403JSONResponse; CreateInterpretationRevision404JSONResponse; CreateInterpretationRevision409JSONResponse; CreateInterpretationRevision422JSONResponse; CreateInterpretationRevision500JSONResponse; CreateInterpretationRevision503JSONResponse; CreateInterpretationRevisionFromColumn; CreateInterpretationRevisionFromColumn201JSONResponse; CreateInterpretationRevisionFromColumn400JSONResponse; CreateInterpretationRevisionFromColumn401JSONResponse; CreateInterpretationRevisionFromColumn403JSONResponse; CreateInterpretationRevisionFromColumn404JSONResponse; CreateInterpretationRevisionFromColumn409JSONResponse; CreateInterpretationRevisionFromColumn422JSONResponse; CreateInterpretationRevisionFromColumn500JSONResponse; CreateInterpretationRevisionFromColumn503JSONResponse; CreateInterpretationRevisionFromColumnJSONRequestBody; CreateInterpretationRevisionFromColumnParams; CreateInterpretationRevisionFromColumnRequestObject; CreateInterpretationRevisionFromColumnResponseObject; CreateInterpretationRevisionJSONRequestBody; CreateInterpretationRevisionParams; CreateInterpretationRevisionRequestObject; CreateInterpretationRevisionResponseObject; Diagnostic; DiscoverExplorerConstructionCategories; DiscoverExplorerConstructionCategories200JSONResponse; DiscoverExplorerConstructionCategories400JSONResponse; DiscoverExplorerConstructionCategories401JSONResponse; DiscoverExplorerConstructionCategories403JSONResponse; DiscoverExplorerConstructionCategories404JSONResponse; DiscoverExplorerConstructionCategories409JSONResponse; DiscoverExplorerConstructionCategories422JSONResponse; DiscoverExplorerConstructionCategories500JSONResponse; DiscoverExplorerConstructionCategories503JSONResponse; DiscoverExplorerConstructionCategoriesJSONRequestBody; DiscoverExplorerConstructionCategoriesRequestObject; DiscoverExplorerConstructionCategoriesResponseObject; DiscoverExplorerTableShapeCategories; DiscoverExplorerTableShapeCategories200JSONResponse; DiscoverExplorerTableShapeCategories400JSONResponse; DiscoverExplorerTableShapeCategories401JSONResponse; DiscoverExplorerTableShapeCategories403JSONResponse; DiscoverExplorerTableShapeCategories404JSONResponse; DiscoverExplorerTableShapeCategories409JSONResponse; DiscoverExplorerTableShapeCategories422JSONResponse; DiscoverExplorerTableShapeCategories500JSONResponse; DiscoverExplorerTableShapeCategories503JSONResponse; DiscoverExplorerTableShapeCategoriesJSONRequestBody; DiscoverExplorerTableShapeCategoriesRequestObject; DiscoverExplorerTableShapeCategoriesResponseObject; Discriminator; Document; DocumentKind; DownloadExplorerArtifact; DownloadExplorerArtifact200ApplicationzipResponse; DownloadExplorerArtifact200ResponseHeaders; DownloadExplorerArtifact401JSONResponse; DownloadExplorerArtifact403JSONResponse; DownloadExplorerArtifact404JSONResponse; DownloadExplorerArtifact409JSONResponse; DownloadExplorerArtifact410JSONResponse; DownloadExplorerArtifact500JSONResponse; DownloadExplorerArtifact503JSONResponse; DownloadExplorerArtifactRequestObject; DownloadExplorerArtifactResponseObject; ErrorBody; ErrorResponse; ExactCategoryMapping; ExactCategoryRecodeTransformation; ExactCategoryRecodeTransformationExactCategoryRecodeUnknownPolicy; ExactCategoryRecodeTransformationKind; ExactSemanticInventoryItemIdentity; ExactSemanticInventoryItemIdentityKind; ExecuteDataframeGraphQL; ExecuteDataframeGraphQL200JSONResponse; ExecuteDataframeGraphQL400JSONResponse; ExecuteDataframeGraphQL401JSONResponse; ExecuteDataframeGraphQL403JSONResponse; ExecuteDataframeGraphQL404JSONResponse; ExecuteDataframeGraphQL409JSONResponse; ExecuteDataframeGraphQL413JSONResponse; ExecuteDataframeGraphQL422JSONResponse; ExecuteDataframeGraphQL429JSONResponse; ExecuteDataframeGraphQL499JSONResponse; ExecuteDataframeGraphQL500JSONResponse; ExecuteDataframeGraphQL503JSONResponse; ExecuteDataframeGraphQL504JSONResponse; ExecuteDataframeGraphQLJSONRequestBody; ExecuteDataframeGraphQLRequestObject; ExecuteDataframeGraphQLResponseObject; ExecuteGraphQL; ExecuteGraphQL200JSONResponse; ExecuteGraphQL400JSONResponse; ExecuteGraphQL401JSONResponse; ExecuteGraphQL403JSONResponse; ExecuteGraphQL404JSONResponse; ExecuteGraphQL409JSONResponse; ExecuteGraphQL413JSONResponse; ExecuteGraphQL422JSONResponse; ExecuteGraphQL429JSONResponse; ExecuteGraphQL499JSONResponse; ExecuteGraphQL500JSONResponse; ExecuteGraphQL503JSONResponse; ExecuteGraphQL504JSONResponse; ExecuteGraphQLJSONRequestBody; ExecuteGraphQLRequestObject; ExecuteGraphQLResponseObject; ExplicitGroupCreateRequest; ExplicitGroupInput; ExplicitGroupRevisionChoice; ExplicitGroupRevisionChoiceUnassignedMemberPolicies; ExplicitGroupRevisionSummary; ExplicitGroupSummary; ExplorerId; ExplorerMetadata; ExtensionBinding; FamilySemanticInventoryItemIdentity; FamilySemanticInventoryItemIdentityKind; FiberServerOptions; FieldChoiceSource; FieldChoiceSourceKind; FieldConstructionChoiceSearchSource; FieldConstructionChoiceSearchSourceKind; FieldPresentation; FieldPresentationContext; FieldPresentationDecision; FieldPresentationGroupUnderSemantic; FieldPresentationGroupUnderSemanticAccess; FieldPresentationGroupUnderSemanticKind; FieldPresentationKeepRaw; FieldPresentationKeepRawKind; FieldPresentationReady; FieldPresentationReadyKind; FieldPresentationReason; FieldPresentationRequest; FieldPresentationUnavailable; FieldPresentationUnavailableKind; FieldSource; FieldSourceProjectionMode; FileActions; FilterPresentation; FixedFilter; FrameDefinition; FrameDefinitionForm; FrameDefinitionManyPolicy; FrameDefinitionZeroPolicy; FrameSourceForm; FrameSourceFormDecision; FrameSourceFormForm; FrameSourceFormManyPolicy; FrameSourceFormZeroPolicy; FrameSourceOption; FrameSourceOptionDefaultForm; FrameSourceOptionsRequest; FrameSourceOptionsResponse; FromConfiguredColumnReadyResolution; FromConfiguredColumnUnavailableResolution; FromConstructionGroupCodingIdentitySourceOption; FromConstructionGroupFieldScalarSourceOption; FromConstructionGroupFieldSourceChoice; FromConstructionGroupFieldSourceCodingIdentityBinding; FromConstructionGroupFieldSourceCurrentStageInput; FromConstructionGroupFieldSourceInputBinding; FromConstructionGroupFieldSourceRelatedOccurrenceInput; FromConstructionGroupFieldSourceScalarBinding; FromConstructionGroupKeyFieldSource; FromConstructionGroupKeyInputColumn; FromConstructionGroupKeySemanticCategory; FromConstructionGroupSemanticCategoryChoice; FromConstructionRowValue0; FromConstructionRowValue1; FromContributorCodeValue; FromContributorStringValue; FromExactCategoryRecodeTransformation; FromExactSemanticInventoryItemIdentity; FromFamilySemanticInventoryItemIdentity; FromFieldChoiceSource; FromFieldConstructionChoiceSearchSource; FromFieldPresentationGroupUnderSemantic; FromFieldPresentationKeepRaw; FromFieldPresentationReady; FromFieldPresentationUnavailable; FromLegacyErrorBody; FromLegacyErrorResponseError0; FromRemoveColumnTransformation; FromRowDefinitionComparisonAvailable; FromRowDefinitionComparisonUnavailable; FromRowDefinitionExpandedSelection; FromRowDefinitionExplicitGroupSelection; FromRowDefinitionFieldGroupSelection; FromRowDefinitionRecordsSelection; FromSemanticBindingChoiceSource; FromSemanticConstructionChoiceSearchSource; FromSetColumnTransformation; FromTableShapeComparisonAvailable; FromTableShapeComparisonUnavailable; Generation; GenerationActivationResponse; GenerationStatusResponse; Get; GetApolloSandbox; GetApolloSandbox200TexthtmlResponse; GetApolloSandbox401JSONResponse; GetApolloSandbox404JSONResponse; GetApolloSandboxRequestObject; GetApolloSandboxResponseObject; GetDatasetGenerationStatus; GetDatasetGenerationStatus200JSONResponse; GetDatasetGenerationStatus400JSONResponse; GetDatasetGenerationStatus401JSONResponse; GetDatasetGenerationStatus403JSONResponse; GetDatasetGenerationStatus404JSONResponse; GetDatasetGenerationStatus500JSONResponse; GetDatasetGenerationStatus503JSONResponse; GetDatasetGenerationStatusParams; GetDatasetGenerationStatusRequestObject; GetDatasetGenerationStatusResponseObject; GetExplorer; GetExplorer200JSONResponse; GetExplorer401JSONResponse; GetExplorer403JSONResponse; GetExplorer404JSONResponse; GetExplorer500JSONResponse; GetExplorerAuthoringCapability; GetExplorerAuthoringCapability200JSONResponse; GetExplorerAuthoringCapability401JSONResponse; GetExplorerAuthoringCapability403JSONResponse; GetExplorerAuthoringCapability500JSONResponse; GetExplorerAuthoringCapabilityRequestObject; GetExplorerAuthoringCapabilityResponseObject; GetExplorerBuilder; GetExplorerBuilder200JSONResponse; GetExplorerBuilder401JSONResponse; GetExplorerBuilder403JSONResponse; GetExplorerBuilder404JSONResponse; GetExplorerBuilder409JSONResponse; GetExplorerBuilder500JSONResponse; GetExplorerBuilder503JSONResponse; GetExplorerBuilderRequestObject; GetExplorerBuilderResponseObject; GetExplorerColumnSource; GetExplorerColumnSource200JSONResponse; GetExplorerColumnSource400JSONResponse; GetExplorerColumnSource401JSONResponse; GetExplorerColumnSource403JSONResponse; GetExplorerColumnSource404JSONResponse; GetExplorerColumnSource409JSONResponse; GetExplorerColumnSource500JSONResponse; GetExplorerColumnSource503JSONResponse; GetExplorerColumnSourceJSONRequestBody; GetExplorerColumnSourceRequestObject; GetExplorerColumnSourceResponseObject; GetExplorerConstructionCapabilities; GetExplorerConstructionCapabilities200JSONResponse; GetExplorerConstructionCapabilities400JSONResponse; GetExplorerConstructionCapabilities401JSONResponse; GetExplorerConstructionCapabilities403JSONResponse; GetExplorerConstructionCapabilities404JSONResponse; GetExplorerConstructionCapabilities409JSONResponse; GetExplorerConstructionCapabilities422JSONResponse; GetExplorerConstructionCapabilities500JSONResponse; GetExplorerConstructionCapabilities503JSONResponse; GetExplorerConstructionCapabilitiesJSONRequestBody; GetExplorerConstructionCapabilitiesRequestObject; GetExplorerConstructionCapabilitiesResponseObject; GetExplorerConstructionInputs; GetExplorerConstructionInputs200JSONResponse; GetExplorerConstructionInputs400JSONResponse; GetExplorerConstructionInputs401JSONResponse; GetExplorerConstructionInputs403JSONResponse; GetExplorerConstructionInputs404JSONResponse; GetExplorerConstructionInputs409JSONResponse; GetExplorerConstructionInputs422JSONResponse; GetExplorerConstructionInputs500JSONResponse; GetExplorerConstructionInputs503JSONResponse; GetExplorerConstructionInputsJSONRequestBody; GetExplorerConstructionInputsRequestObject; GetExplorerConstructionInputsResponseObject; GetExplorerRequestObject; GetExplorerResponseObject; GetExplorerSelection; GetExplorerSelection200JSONResponse; GetExplorerSelection400JSONResponse; GetExplorerSelection401JSONResponse; GetExplorerSelection403JSONResponse; GetExplorerSelection404JSONResponse; GetExplorerSelection409JSONResponse; GetExplorerSelection422JSONResponse; GetExplorerSelection500JSONResponse; GetExplorerSelection503JSONResponse; GetExplorerSelectionParams; GetExplorerSelectionRequestObject; GetExplorerSelectionResponseObject; GetExplorerTableShapeCapabilities; GetExplorerTableShapeCapabilities200JSONResponse; GetExplorerTableShapeCapabilities400JSONResponse; GetExplorerTableShapeCapabilities401JSONResponse; GetExplorerTableShapeCapabilities403JSONResponse; GetExplorerTableShapeCapabilities404JSONResponse; GetExplorerTableShapeCapabilities409JSONResponse; GetExplorerTableShapeCapabilities422JSONResponse; GetExplorerTableShapeCapabilities500JSONResponse; GetExplorerTableShapeCapabilities503JSONResponse; GetExplorerTableShapeCapabilitiesJSONRequestBody; GetExplorerTableShapeCapabilitiesRequestObject; GetExplorerTableShapeCapabilitiesResponseObject; GetGraphQLPlayground; GetGraphQLPlayground200TexthtmlResponse; GetGraphQLPlayground401JSONResponse; GetGraphQLPlayground404JSONResponse; GetGraphQLPlaygroundRequestObject; GetGraphQLPlaygroundResponseObject; GetHealth; GetHealth200JSONResponse; GetHealth503JSONResponse; GetHealthRequestObject; GetHealthResponseObject; GetInterpretationRevision; GetInterpretationRevision200JSONResponse; GetInterpretationRevision401JSONResponse; GetInterpretationRevision403JSONResponse; GetInterpretationRevision404JSONResponse; GetInterpretationRevision500JSONResponse; GetInterpretationRevision503JSONResponse; GetInterpretationRevisionRequestObject; GetInterpretationRevisionResponseObject; GetLiveness; GetLiveness200JSONResponse; GetLivenessRequestObject; GetLivenessResponseObject; GetReadiness; GetReadiness200JSONResponse; GetReadiness503JSONResponse; GetReadinessRequestObject; GetReadinessResponseObject; GetRecipeExecution; GetRecipeExecution200JSONResponse; GetRecipeExecution401JSONResponse; GetRecipeExecution403JSONResponse; GetRecipeExecution404JSONResponse; GetRecipeExecution503JSONResponse; GetRecipeExecutionRequestObject; GetRecipeExecutionResponseObject; GetSpec; GetSpecJSON; GetSwagger; HTMLTexthtmlResponse; HandlerMiddlewareFunc; IdentifierBinding; InterpretationApplicability; InterpretationCandidatePayload; InterpretationFeatureDefinition; InterpretationLibrary; InterpretationLibraryListResponse; InterpretationLibrarySummary; InterpretationLibraryView; InterpretationPreviewCounts; InterpretationPreviewRequest; InterpretationPreviewResponse; InterpretationPreviewResponseCompleteness; InterpretationPreviewSample; InterpretationPreviewSampleState; InterpretationRevision; InterpretationRevisionCreateRequest; InterpretationRevisionFromColumnRequest; InterpretationRevisionSummary; InterpretationRule; InterpretationStructuralMatch; LegacyBadRequest; LegacyBadRequestJSONResponse; LegacyConflict; LegacyConflictJSONResponse; LegacyErrorBody; LegacyErrorResponse; LegacyErrorResponseError0; LegacyErrorResponse_Error; LegacyForbidden; LegacyForbiddenJSONResponse; LegacyInternalError; LegacyInternalErrorJSONResponse; LegacyNotFound; LegacyNotFoundJSONResponse; LegacyUnavailable; LegacyUnavailableJSONResponse; LegacyUnprocessable; LegacyUnprocessableJSONResponse; ListExplorerRowDefinitionChoices; ListExplorerRowDefinitionChoices200JSONResponse; ListExplorerRowDefinitionChoices400JSONResponse; ListExplorerRowDefinitionChoices401JSONResponse; ListExplorerRowDefinitionChoices403JSONResponse; ListExplorerRowDefinitionChoices404JSONResponse; ListExplorerRowDefinitionChoices409JSONResponse; ListExplorerRowDefinitionChoices422JSONResponse; ListExplorerRowDefinitionChoices500JSONResponse; ListExplorerRowDefinitionChoices503JSONResponse; ListExplorerRowDefinitionChoicesParams; ListExplorerRowDefinitionChoicesRequestObject; ListExplorerRowDefinitionChoicesResponseObject; ListExplorers; ListExplorers200JSONResponse; ListExplorers401JSONResponse; ListExplorers403JSONResponse; ListExplorers500JSONResponse; ListExplorersRequestObject; ListExplorersResponseObject; ListInterpretationLibraries; ListInterpretationLibraries200JSONResponse; ListInterpretationLibraries401JSONResponse; ListInterpretationLibraries403JSONResponse; ListInterpretationLibraries500JSONResponse; ListInterpretationLibraries503JSONResponse; ListInterpretationLibrariesRequestObject; ListInterpretationLibrariesResponseObject; LookupSource; LookupSourceProjectionMode; MarshalJSON; MergeConfiguredColumnReadyResolution; MergeConfiguredColumnUnavailableResolution; MergeConstructionGroupCodingIdentitySourceOption; MergeConstructionGroupFieldScalarSourceOption; MergeConstructionGroupFieldSourceChoice; MergeConstructionGroupFieldSourceCodingIdentityBinding; MergeConstructionGroupFieldSourceCurrentStageInput; MergeConstructionGroupFieldSourceInputBinding; MergeConstructionGroupFieldSourceRelatedOccurrenceInput; MergeConstructionGroupFieldSourceScalarBinding; MergeConstructionGroupKeyFieldSource; MergeConstructionGroupKeyInputColumn; MergeConstructionGroupKeySemanticCategory; MergeConstructionGroupSemanticCategoryChoice; MergeConstructionRowValue0; MergeConstructionRowValue1; MergeContributorCodeValue; MergeContributorStringValue; MergeExactCategoryRecodeTransformation; MergeExactSemanticInventoryItemIdentity; MergeFamilySemanticInventoryItemIdentity; MergeFieldChoiceSource; MergeFieldConstructionChoiceSearchSource; MergeFieldPresentationGroupUnderSemantic; MergeFieldPresentationKeepRaw; MergeFieldPresentationReady; MergeFieldPresentationUnavailable; MergeLegacyErrorBody; MergeLegacyErrorResponseError0; MergeRemoveColumnTransformation; MergeRowDefinitionComparisonAvailable; MergeRowDefinitionComparisonUnavailable; MergeRowDefinitionExpandedSelection; MergeRowDefinitionExplicitGroupSelection; MergeRowDefinitionFieldGroupSelection; MergeRowDefinitionRecordsSelection; MergeSemanticBindingChoiceSource; MergeSemanticConstructionChoiceSearchSource; MergeSetColumnTransformation; MergeTableShapeComparisonAvailable; MergeTableShapeComparisonUnavailable; MiddlewareFunc; NewStrictHandler; Output; OwnerRecordsSource; PathToRawSpec; Population; PopulationMappingBinding; PopulationMappingCounts; PopulationMappingRequest; PopulationMappingResponse; PopulationMappingResponseStatus; PopulationMemberRemovalProposalRequest; PopulationMemberRemovalProposalResponse; PopulationMemberRemovalProposalResponsePreviewStatus; PopulationRouteChoice; PopulationRouteStep; PopulationRouteStepStorageDirection; PopulationRoutesRequest; PopulationRoutesResponse; PrepareExplorerArtifact; PrepareExplorerArtifact200JSONResponse; PrepareExplorerArtifact400JSONResponse; PrepareExplorerArtifact401JSONResponse; PrepareExplorerArtifact403JSONResponse; PrepareExplorerArtifact404JSONResponse; PrepareExplorerArtifact409JSONResponse; PrepareExplorerArtifact413JSONResponse; PrepareExplorerArtifact422JSONResponse; PrepareExplorerArtifact500JSONResponse; PrepareExplorerArtifact503JSONResponse; PrepareExplorerArtifact504JSONResponse; PrepareExplorerArtifactJSONRequestBody; PrepareExplorerArtifactRequestObject; PrepareExplorerArtifactResponseObject; PreviewExplorer; PreviewExplorer200JSONResponse; PreviewExplorer400JSONResponse; PreviewExplorer401JSONResponse; PreviewExplorer403JSONResponse; PreviewExplorer404JSONResponse; PreviewExplorer409JSONResponse; PreviewExplorer413JSONResponse; PreviewExplorer422JSONResponse; PreviewExplorer429JSONResponse; PreviewExplorer499JSONResponse; PreviewExplorer500JSONResponse; PreviewExplorer503JSONResponse; PreviewExplorer504JSONResponse; PreviewExplorerJSONRequestBody; PreviewExplorerRequestObject; PreviewExplorerResponseObject; PreviewInterpretationCandidate; PreviewInterpretationCandidate200JSONResponse; PreviewInterpretationCandidate400JSONResponse; PreviewInterpretationCandidate401JSONResponse; PreviewInterpretationCandidate403JSONResponse; PreviewInterpretationCandidate404JSONResponse; PreviewInterpretationCandidate409JSONResponse; PreviewInterpretationCandidate413JSONResponse; PreviewInterpretationCandidate422JSONResponse; PreviewInterpretationCandidate429JSONResponse; PreviewInterpretationCandidate499JSONResponse; PreviewInterpretationCandidate500JSONResponse; PreviewInterpretationCandidate503JSONResponse; PreviewInterpretationCandidate504JSONResponse; PreviewInterpretationCandidateJSONRequestBody; PreviewInterpretationCandidateRequestObject; PreviewInterpretationCandidateResponseObject; PreviewRequest; PreviewResponse; PreviewResponseKind; PreviewRowSource; PreviewRowSourceKind; Project; ProjectionMode; ProposeExplorerConstruction; ProposeExplorerConstruction200JSONResponse; ProposeExplorerConstruction400JSONResponse; ProposeExplorerConstruction401JSONResponse; ProposeExplorerConstruction403JSONResponse; ProposeExplorerConstruction404JSONResponse; ProposeExplorerConstruction409JSONResponse; ProposeExplorerConstruction422JSONResponse; ProposeExplorerConstruction500JSONResponse; ProposeExplorerConstruction503JSONResponse; ProposeExplorerConstruction504JSONResponse; ProposeExplorerConstructionChoice; ProposeExplorerConstructionChoice200JSONResponse; ProposeExplorerConstructionChoice400JSONResponse; ProposeExplorerConstructionChoice401JSONResponse; ProposeExplorerConstructionChoice403JSONResponse; ProposeExplorerConstructionChoice404JSONResponse; ProposeExplorerConstructionChoice409JSONResponse; ProposeExplorerConstructionChoice422JSONResponse; ProposeExplorerConstructionChoice500JSONResponse; ProposeExplorerConstructionChoice503JSONResponse; ProposeExplorerConstructionChoice504JSONResponse; ProposeExplorerConstructionChoiceJSONRequestBody; ProposeExplorerConstructionChoiceRequestObject; ProposeExplorerConstructionChoiceResponseObject; ProposeExplorerConstructionJSONRequestBody; ProposeExplorerConstructionRequestObject; ProposeExplorerConstructionResponseObject; ProposeExplorerPopulationMemberRemoval; ProposeExplorerPopulationMemberRemoval200JSONResponse; ProposeExplorerPopulationMemberRemoval400JSONResponse; ProposeExplorerPopulationMemberRemoval401JSONResponse; ProposeExplorerPopulationMemberRemoval403JSONResponse; ProposeExplorerPopulationMemberRemoval404JSONResponse; ProposeExplorerPopulationMemberRemoval409JSONResponse; ProposeExplorerPopulationMemberRemoval422JSONResponse; ProposeExplorerPopulationMemberRemoval500JSONResponse; ProposeExplorerPopulationMemberRemoval503JSONResponse; ProposeExplorerPopulationMemberRemoval504JSONResponse; ProposeExplorerPopulationMemberRemovalJSONRequestBody; ProposeExplorerPopulationMemberRemovalRequestObject; ProposeExplorerPopulationMemberRemovalResponseObject; ProposeExplorerRowDefinition; ProposeExplorerRowDefinition200JSONResponse; ProposeExplorerRowDefinition400JSONResponse; ProposeExplorerRowDefinition401JSONResponse; ProposeExplorerRowDefinition403JSONResponse; ProposeExplorerRowDefinition404JSONResponse; ProposeExplorerRowDefinition409JSONResponse; ProposeExplorerRowDefinition422JSONResponse; ProposeExplorerRowDefinition500JSONResponse; ProposeExplorerRowDefinition503JSONResponse; ProposeExplorerRowDefinitionJSONRequestBody; ProposeExplorerRowDefinitionRequestObject; ProposeExplorerRowDefinitionResponseObject; ProposeExplorerTableShape; ProposeExplorerTableShape200JSONResponse; ProposeExplorerTableShape400JSONResponse; ProposeExplorerTableShape401JSONResponse; ProposeExplorerTableShape403JSONResponse; ProposeExplorerTableShape404JSONResponse; ProposeExplorerTableShape409JSONResponse; ProposeExplorerTableShape422JSONResponse; ProposeExplorerTableShape500JSONResponse; ProposeExplorerTableShape503JSONResponse; ProposeExplorerTableShapeJSONRequestBody; ProposeExplorerTableShapeRequestObject; ProposeExplorerTableShapeResponseObject; PublicationOutput; PublishExplorer; PublishExplorer200JSONResponse; PublishExplorer400JSONResponse; PublishExplorer401JSONResponse; PublishExplorer403JSONResponse; PublishExplorer404JSONResponse; PublishExplorer409JSONResponse; PublishExplorer422JSONResponse; PublishExplorer500JSONResponse; PublishExplorer503JSONResponse; PublishExplorerJSONRequestBody; PublishExplorerParams; PublishExplorerRequestObject; PublishExplorerResponseObject; PublishRepositoryExplorerConfig; PublishRepositoryExplorerConfig200JSONResponse; PublishRepositoryExplorerConfig400JSONResponse; PublishRepositoryExplorerConfig401JSONResponse; PublishRepositoryExplorerConfig403JSONResponse; PublishRepositoryExplorerConfig409JSONResponse; PublishRepositoryExplorerConfig422JSONResponse; PublishRepositoryExplorerConfig500JSONResponse; PublishRepositoryExplorerConfig503JSONResponse; PublishRepositoryExplorerConfigJSONRequestBody; PublishRepositoryExplorerConfigParams; PublishRepositoryExplorerConfigRequestObject; PublishRepositoryExplorerConfigResponseObject; PublishRequest; PublishResponse; PublishResponseKind; RawJSON; ReceiptOutput; ReceiptOutputRowMultiplication; ReceiptOutputStructuralSuitability; RecipeExecutionColumn; RecipeExecutionOutput; RecipeExecutionResponse; ReconcileExplorerBuilder; ReconcileExplorerBuilder200JSONResponse; ReconcileExplorerBuilder400JSONResponse; ReconcileExplorerBuilder401JSONResponse; ReconcileExplorerBuilder403JSONResponse; ReconcileExplorerBuilder404JSONResponse; ReconcileExplorerBuilder409JSONResponse; ReconcileExplorerBuilder422JSONResponse; ReconcileExplorerBuilder500JSONResponse; ReconcileExplorerBuilder503JSONResponse; ReconcileExplorerBuilderJSONRequestBody; ReconcileExplorerBuilderParams; ReconcileExplorerBuilderRequestObject; ReconcileExplorerBuilderResponseObject; ReconcileRequest; RegisterHandlers; RegisterHandlersWithOptions; RelatedExpandChoiceSearchRequest; RelatedExpandChoiceSearchResponse; RelatedExpandContributorChoice; RelatedExpandContributorChoiceOperators; RelatedExpandContributorChoiceSearchRequest; RelatedExpandContributorChoiceSearchResponse; RelatedExpandContributorChoiceSuggestionsSource; RelatedExpandContributorSource; RelatedExpandContributorSourceKind; RelatedExpandRouteChoice; RelatedExpandRouteChoiceKind; RelatedFieldChoice; RelatedFieldChoiceSearchRequest; RelatedFieldChoiceSearchResponse; RelatedSelection; RelatedSelectionKind; RelatedSourceContributorRule; RelatedSourceContributorRulePolicy; RelatedSourceField; RelatedSourceFieldCardinality; RelatedSourceFieldKind; RemoveColumnTransformation; RemoveColumnTransformationKind; RepeatedBoundary; RepeatedCoordinate; ResolveConfiguredColumnContext; ResolveConfiguredColumnContext200JSONResponse; ResolveConfiguredColumnContext400JSONResponse; ResolveConfiguredColumnContext401JSONResponse; ResolveConfiguredColumnContext403JSONResponse; ResolveConfiguredColumnContext404JSONResponse; ResolveConfiguredColumnContext409JSONResponse; ResolveConfiguredColumnContext422JSONResponse; ResolveConfiguredColumnContext500JSONResponse; ResolveConfiguredColumnContext503JSONResponse; ResolveConfiguredColumnContextJSONRequestBody; ResolveConfiguredColumnContextRequestObject; ResolveConfiguredColumnContextResponseObject; ResolveExplorerTableShape; ResolveExplorerTableShape200JSONResponse; ResolveExplorerTableShape400JSONResponse; ResolveExplorerTableShape401JSONResponse; ResolveExplorerTableShape403JSONResponse; ResolveExplorerTableShape404JSONResponse; ResolveExplorerTableShape409JSONResponse; ResolveExplorerTableShape422JSONResponse; ResolveExplorerTableShape500JSONResponse; ResolveExplorerTableShape503JSONResponse; ResolveExplorerTableShapeJSONRequestBody; ResolveExplorerTableShapeRequestObject; ResolveExplorerTableShapeResponseObject; RouteNode; RouteNodeMatchMode; RoutePolicy; RouteRebaseChoice; RowChangeAssessmentResponse; RowChangeAssessmentResponseMode; RowChangeAssessmentResponseStatus; RowChangeProposal; RowChangeProposalMode; RowChangeUnresolvedReference; RowChangeUnresolvedReferenceKind; RowDefinition; RowDefinitionChoice; RowDefinitionChoiceKind; RowDefinitionChoicePolicy; RowDefinitionChoicePolicyName; RowDefinitionChoiceValueType; RowDefinitionChoicesResponse; RowDefinitionComparison; RowDefinitionComparisonAvailable; RowDefinitionComparisonAvailableStatus; RowDefinitionComparisonExample; RowDefinitionComparisonUnavailable; RowDefinitionComparisonUnavailableStatus; RowDefinitionExpandedSelection; RowDefinitionExpandedSelectionExpandedEmptyCollectionPolicy; RowDefinitionExpandedSelectionKind; RowDefinitionExplicitGroupSelection; RowDefinitionExplicitGroupSelectionExplicitGroupUnassignedMemberPolicy; RowDefinitionExplicitGroupSelectionKind; RowDefinitionFieldGroupSelection; RowDefinitionFieldGroupSelectionFieldGroupMissingKeyPolicy; RowDefinitionFieldGroupSelectionKind; RowDefinitionPreviewSummary; RowDefinitionProposal; RowDefinitionProposalMode; RowDefinitionProposalRequest; RowDefinitionProposalRequest_Selection; RowDefinitionRecordsSelection; RowDefinitionRecordsSelectionKind; RowLineageCapability; RowLineageCapabilityStatus; RowLineageContributor; RowLineageRequest; RowLineageResponse; RowLineageResponseStatus; SchemaFieldOption; SchemaFieldOptionCardinality; SchemaFieldSearchRequest; SchemaFieldSearchResponse; SchemaFieldSearchResponseKind; SearchExplorerCandidates; SearchExplorerCandidates200JSONResponse; SearchExplorerCandidates400JSONResponse; SearchExplorerCandidates401JSONResponse; SearchExplorerCandidates403JSONResponse; SearchExplorerCandidates409JSONResponse; SearchExplorerCandidates500JSONResponse; SearchExplorerCandidates503JSONResponse; SearchExplorerCandidatesJSONRequestBody; SearchExplorerCandidatesRequestObject; SearchExplorerCandidatesResponseObject; SearchExplorerConstructionChoices; SearchExplorerConstructionChoices200JSONResponse; SearchExplorerConstructionChoices400JSONResponse; SearchExplorerConstructionChoices401JSONResponse; SearchExplorerConstructionChoices403JSONResponse; SearchExplorerConstructionChoices404JSONResponse; SearchExplorerConstructionChoices409JSONResponse; SearchExplorerConstructionChoices422JSONResponse; SearchExplorerConstructionChoices500JSONResponse; SearchExplorerConstructionChoices503JSONResponse; SearchExplorerConstructionChoicesJSONRequestBody; SearchExplorerConstructionChoicesRequestObject; SearchExplorerConstructionChoicesResponseObject; SearchExplorerPopulationRoutes; SearchExplorerPopulationRoutes200JSONResponse; SearchExplorerPopulationRoutes400JSONResponse; SearchExplorerPopulationRoutes401JSONResponse; SearchExplorerPopulationRoutes403JSONResponse; SearchExplorerPopulationRoutes404JSONResponse; SearchExplorerPopulationRoutes409JSONResponse; SearchExplorerPopulationRoutes422JSONResponse; SearchExplorerPopulationRoutes500JSONResponse; SearchExplorerPopulationRoutes503JSONResponse; SearchExplorerPopulationRoutesJSONRequestBody; SearchExplorerPopulationRoutesRequestObject; SearchExplorerPopulationRoutesResponseObject; SearchExplorerRelatedExpandChoices; SearchExplorerRelatedExpandChoices200JSONResponse; SearchExplorerRelatedExpandChoices400JSONResponse; SearchExplorerRelatedExpandChoices401JSONResponse; SearchExplorerRelatedExpandChoices403JSONResponse; SearchExplorerRelatedExpandChoices404JSONResponse; SearchExplorerRelatedExpandChoices409JSONResponse; SearchExplorerRelatedExpandChoices422JSONResponse; SearchExplorerRelatedExpandChoices500JSONResponse; SearchExplorerRelatedExpandChoices503JSONResponse; SearchExplorerRelatedExpandChoicesJSONRequestBody; SearchExplorerRelatedExpandChoicesRequestObject; SearchExplorerRelatedExpandChoicesResponseObject; SearchExplorerRelatedExpandContributors; SearchExplorerRelatedExpandContributors200JSONResponse; SearchExplorerRelatedExpandContributors400JSONResponse; SearchExplorerRelatedExpandContributors401JSONResponse; SearchExplorerRelatedExpandContributors403JSONResponse; SearchExplorerRelatedExpandContributors404JSONResponse; SearchExplorerRelatedExpandContributors409JSONResponse; SearchExplorerRelatedExpandContributors422JSONResponse; SearchExplorerRelatedExpandContributors500JSONResponse; SearchExplorerRelatedExpandContributors503JSONResponse; SearchExplorerRelatedExpandContributorsJSONRequestBody; SearchExplorerRelatedExpandContributorsRequestObject; SearchExplorerRelatedExpandContributorsResponseObject; SearchExplorerRelatedFieldChoices; SearchExplorerRelatedFieldChoices200JSONResponse; SearchExplorerRelatedFieldChoices400JSONResponse; SearchExplorerRelatedFieldChoices401JSONResponse; SearchExplorerRelatedFieldChoices403JSONResponse; SearchExplorerRelatedFieldChoices404JSONResponse; SearchExplorerRelatedFieldChoices409JSONResponse; SearchExplorerRelatedFieldChoices422JSONResponse; SearchExplorerRelatedFieldChoices500JSONResponse; SearchExplorerRelatedFieldChoices503JSONResponse; SearchExplorerRelatedFieldChoicesJSONRequestBody; SearchExplorerRelatedFieldChoicesRequestObject; SearchExplorerRelatedFieldChoicesResponseObject; SearchExplorerSchemaFields; SearchExplorerSchemaFields200JSONResponse; SearchExplorerSchemaFields400JSONResponse; SearchExplorerSchemaFields401JSONResponse; SearchExplorerSchemaFields403JSONResponse; SearchExplorerSchemaFields409JSONResponse; SearchExplorerSchemaFields422JSONResponse; SearchExplorerSchemaFields500JSONResponse; SearchExplorerSchemaFields503JSONResponse; SearchExplorerSchemaFieldsJSONRequestBody; SearchExplorerSchemaFieldsRequestObject; SearchExplorerSchemaFieldsResponseObject; SelectionBaseRevision; SelectionCreateRequest; SelectionExplicitResources; SelectionFilter; SelectionMember; SelectionPage; SelectionPublishedOutput; SelectionRequestSource; SelectionRequestSourceKind; SelectionResourceRef; SelectionRevision; SelectionRule; SelectionRuleKind; SelectionSource; SelectionSourceKind; SemanticBindingChoiceSource; SemanticBindingChoiceSourceKind; SemanticBindingChoiceSourceSourceKind; SemanticConstructionChoiceSearchSource; SemanticConstructionChoiceSearchSourceKind; SemanticConstructionChoiceSearchSourceSelectionMode; SemanticFrameFamily; SemanticInventoryBrowseRequest; SemanticInventoryBrowseRequestSelectionMode; SemanticInventoryBrowseResponse; SemanticInventoryBrowseResponseSourceAvailability; SemanticInventoryBrowseResponseState; SemanticInventoryItem; SemanticInventoryItemCompleteness; SemanticInventoryItemIdentity; SemanticInventoryItemSelectionMode; SemanticSelectionIntent; SemanticSelectionIntentProjectionMode; SemanticSelectionReadiness; SemanticSelectionReadinessStatus; SemanticSelectionResult; SemanticSelectionResultStatus; ServerInterface; ServerInterfaceWrapper; ServiceBadRequest; ServiceBadRequestJSONResponse; ServiceConflict; ServiceConflictJSONResponse; ServiceErrorBody; ServiceErrorResponse; ServiceForbidden; ServiceForbiddenJSONResponse; ServiceInternalError; ServiceInternalErrorJSONResponse; ServiceNotFound; ServiceNotFoundJSONResponse; ServiceUnauthorized; ServiceUnauthorizedJSONResponse; ServiceUnavailable; ServiceUnavailableJSONResponse; ServiceUnprocessable; ServiceUnprocessableJSONResponse; ServiceUnsupportedMediaType; ServiceUnsupportedMediaTypeJSONResponse; Set; SetColumnTransformation; SetColumnTransformationKind; SharedFilterBinding; SharedFilterBindingRef; StrictHandlerFunc; StrictMiddlewareFunc; StrictServerInterface; Tab; TablePresentation; TablePresentationCellRenderer; TableShapeBinaryOperatorChoice; TableShapeBinaryOperatorChoiceChoiceKind; TableShapeBinaryOperatorReference; TableShapeBinaryOperatorReferenceKind; TableShapeCapabilitiesRequest; TableShapeCapabilitiesResponse; TableShapeCategoryDiscoveryRequest; TableShapeCategoryDiscoveryResponse; TableShapeCategoryDiscoveryResponseKind; TableShapeCategoryValue; TableShapeCellContributor; TableShapeCellTrace; TableShapeCellTraceState; TableShapeCellValue; TableShapeChangedCell; TableShapeChangedRow; TableShapeChoiceAvailability; TableShapeChoiceAvailabilityKind; TableShapeColumnReference; TableShapeColumnReferenceKind; TableShapeComparison; TableShapeComparisonAvailable; TableShapeComparisonAvailableStatus; TableShapeComparisonUnavailable; TableShapeComparisonUnavailableStatus; TableShapeContributor; TableShapeDeclaredInformationLoss; TableShapeDeclaredInformationLossStatus; TableShapeDerivedColumnProposal; TableShapeDerivedOperandIntent; TableShapeDerivedOperandIntentKind; TableShapeDerivedOperandIntentRepresentation; TableShapeDerivedOperandReference; TableShapeDerivedOperandReferenceKind; TableShapeDerivedSelection; TableShapeDivisionByZeroPolicyReference; TableShapeDivisionByZeroPolicyReferenceKind; TableShapeDuplicatePolicyReference; TableShapeDuplicatePolicyReferenceKind; TableShapeEditorChoice; TableShapeEditorChoiceChoiceKind; TableShapeEvidenceLimitation; TableShapeExcludedRecord; TableShapeExclusionEvidence; TableShapeExclusionEvidenceStatus; TableShapeInformationLoss; TableShapeLogicalType; TableShapeMissingCellPolicyReference; TableShapeMissingCellPolicyReferenceKind; TableShapeMissingInputPolicyReference; TableShapeMissingInputPolicyReferenceKind; TableShapeOperandSelection; TableShapeOperandSelectionKind; TableShapeOutputDescriptorSupport; TableShapeOutputDescriptorSupportKind; TableShapeOutputName; TableShapeOutputSuggestion; TableShapeOutputSuggestionChoiceKind; TableShapePivotCategoryChoice; TableShapePivotCategoryChoiceChoiceKind; TableShapePivotCategoryDiscovery; TableShapePivotCategoryDiscoveryKind; TableShapePivotCategoryPair; TableShapePivotCategoryReference; TableShapePivotCategoryReferenceKind; TableShapePivotCategorySelection; TableShapePivotProposal; TableShapePivotSelection; TableShapePreviewSummary; TableShapeProposal; TableShapeProposalIntent; TableShapeProposalIntentKind; TableShapeProposalMode; TableShapeProposalRequest; TableShapeProposalRequestMode; TableShapeReshapeModeChoice; TableShapeReshapeModeChoiceChoiceKind; TableShapeReshapeModeChoiceMode; TableShapeReshapeModeReference; TableShapeReshapeModeReferenceKind; TableShapeResolutionRequest; TableShapeResolutionRequestKind; TableShapeResolutionResponse; TableShapeResolutionResponseKind; TableShapeResolvedCategory; TableShapeResolvedOutputDescriptor; TableShapeResolvedOutputDescriptorKind; TableShapeSavedPivotCategorySelection; TableShapeScalar; TableShapeScalarKind; TableShapeSourceIdentity; TableShapeTypeFact; TableShapeUnlistedCategoryPolicyReference; TableShapeUnlistedCategoryPolicyReferenceKind; TableShapeUnpivotNullRowPolicyReference; TableShapeUnpivotNullRowPolicyReferenceKind; TableShapeUnpivotProposal; TableShapeUnpivotSelection; TemporalFieldChoice; TemporalOrderingSource; TemporalOrderingSourceDirection; TemporalOrderingSourceTiePolicy; TemporalReductionCapabilities; TerminologyKey; TraceExplorerCell; TraceExplorerCell200JSONResponse; TraceExplorerCell400JSONResponse; TraceExplorerCell401JSONResponse; TraceExplorerCell403JSONResponse; TraceExplorerCell404JSONResponse; TraceExplorerCell409JSONResponse; TraceExplorerCell422JSONResponse; TraceExplorerCell500JSONResponse; TraceExplorerCell503JSONResponse; TraceExplorerCellJSONRequestBody; TraceExplorerCellRequestObject; TraceExplorerCellResponseObject; TraceExplorerRowLineage; TraceExplorerRowLineage200JSONResponse; TraceExplorerRowLineage400JSONResponse; TraceExplorerRowLineage401JSONResponse; TraceExplorerRowLineage403JSONResponse; TraceExplorerRowLineage404JSONResponse; TraceExplorerRowLineage409JSONResponse; TraceExplorerRowLineage422JSONResponse; TraceExplorerRowLineage500JSONResponse; TraceExplorerRowLineage503JSONResponse; TraceExplorerRowLineage504JSONResponse; TraceExplorerRowLineageJSONRequestBody; TraceExplorerRowLineageRequestObject; TraceExplorerRowLineageResponseObject; UnitIdentity; UnitNormalizationCapabilities; UnitNormalizationContract; UnitNormalizationPolicy; UnitNormalizationPresetCapability; UnitRuleReference; UnmarshalJSON; Valid; ValueByDiscriminator; ValueTransformationCapability; VisitActivateDatasetGenerationResponse; VisitApplyExplorerBuilderCommandsResponse; VisitAssessExplorerRowChangeResponse; VisitBrowseExplorerFrameSourceOptionsResponse; VisitBrowseExplorerSemanticInventoryResponse; VisitCheckExplorerPopulationMappingResponse; VisitCreateDatasetGenerationResponse; VisitCreateExplorerExplicitGroupRevisionResponse; VisitCreateExplorerResponse; VisitCreateExplorerSelectionResponse; VisitCreateInterpretationRevisionFromColumnResponse; VisitCreateInterpretationRevisionResponse; VisitDiscoverExplorerConstructionCategoriesResponse; VisitDiscoverExplorerTableShapeCategoriesResponse; VisitDownloadExplorerArtifactResponse; VisitExecuteDataframeGraphQLResponse; VisitExecuteGraphQLResponse; VisitGetApolloSandboxResponse; VisitGetDatasetGenerationStatusResponse; VisitGetExplorerAuthoringCapabilityResponse; VisitGetExplorerBuilderResponse; VisitGetExplorerColumnSourceResponse; VisitGetExplorerConstructionCapabilitiesResponse; VisitGetExplorerConstructionInputsResponse; VisitGetExplorerResponse; VisitGetExplorerSelectionResponse; VisitGetExplorerTableShapeCapabilitiesResponse; VisitGetGraphQLPlaygroundResponse; VisitGetHealthResponse; VisitGetInterpretationRevisionResponse; VisitGetLivenessResponse; VisitGetReadinessResponse; VisitGetRecipeExecutionResponse; VisitListExplorerRowDefinitionChoicesResponse; VisitListExplorersResponse; VisitListInterpretationLibrariesResponse; VisitPrepareExplorerArtifactResponse; VisitPreviewExplorerResponse; VisitPreviewInterpretationCandidateResponse; VisitProposeExplorerConstructionChoiceResponse; VisitProposeExplorerConstructionResponse; VisitProposeExplorerPopulationMemberRemovalResponse; VisitProposeExplorerRowDefinitionResponse; VisitProposeExplorerTableShapeResponse; VisitPublishExplorerResponse; VisitPublishRepositoryExplorerConfigResponse; VisitReconcileExplorerBuilderResponse; VisitResolveConfiguredColumnContextResponse; VisitResolveExplorerTableShapeResponse; VisitSearchExplorerCandidatesResponse; VisitSearchExplorerConstructionChoicesResponse; VisitSearchExplorerPopulationRoutesResponse; VisitSearchExplorerRelatedExpandChoicesResponse; VisitSearchExplorerRelatedExpandContributorsResponse; VisitSearchExplorerRelatedFieldChoicesResponse; VisitSearchExplorerSchemaFieldsResponse; VisitTraceExplorerCellResponse; VisitTraceExplorerRowLineageResponse; Workspace; WorkspaceOutputConsumer,internal/explorer/authoringv2,—,internal/server,internal/acceptance; internal/explorer/capability,github.com/getkin/kin-openapi/openapi3; github.com/gofiber/fiber/v3; github.com/oapi-codegen/runtime; github.com/oapi-codegen/runtime/types,60 exported interface(s),legacy=17; deprecated=1; compatibility=1,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/loomapi,inventory complete; semantic disposition pending internal/acceptance,github.com/calypr/loom/internal/acceptance,acceptance,domain/library,"Fetch and cache the locked FHIR fixture, drive the real-data acceptance scenario, and verify its outputs and performance evidence.",fixture.go; performance.go; runner.go; scenario.go,fixture_test.go; performance_test.go; runner_locking_test.go; runner_status_test.go; runner_zero_value_test.go; scenario_test.go,—,—,2283,1570,41,Acquire; Close; ComparePerformance; ComparePerformanceReportFiles; ComparePerformanceRuns; ComputedDigest; Config; Connections; DefaultLimits; Fetcher; FixtureDigest; FixtureLock; Lease; Limits; LoadLock; Namespace; New; Oracle; PerformanceComparisonReport; PerformanceFailure; PerformanceObservation; PerformanceStageSeconds; PerformanceSuspicion; RefreshLock; Report; ResourceLock; Run; RunID; RunScenario; RunSpec; Runner; ScenarioConfig; ScenarioResult; SelectionRule; StageReport; StaticTarget; Target; Validate; ValidatedFixture; WriteLock; WritePerformanceComparison,internal/backends/arango; internal/dataframe/publication; internal/explorer/authoringv2; internal/store/clickhouse,generated/loomapi; internal/dataframe/publication; internal/explorer/authoringv2,cmd/loom-acceptance,—,—,2 exported interface(s),legacy=1; migration=18; compatibility=1,one production importer (cmd/loom-acceptance); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/acceptance,inventory complete; semantic disposition pending internal/api/authpolicy,github.com/calypr/loom/internal/api/authpolicy,authpolicy,transport adapter,Centralize authorization-sentinel classification for transport operations while preserving operation-specific concealment.,policy.go,policy_test.go,—,—,56,44,2,Classify; Operation,internal/authscope; internal/dataframe/errors,internal/authscope; internal/dataframe/errors,internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/server,—,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/api/authpolicy,inventory complete; semantic disposition pending @@ -23,22 +23,22 @@ internal/api/bulk/load,github.com/calypr/loom/internal/api/bulk/load,load,transp internal/api/columncapabilities,github.com/calypr/loom/internal/api/columncapabilities,columncapabilities,transport adapter,"Derive transport-neutral filter, sort, and aggregate capabilities from published ClickHouse column types.",capabilities.go,capabilities_test.go,—,—,56,23,2,FromClickHouse; Tuple,—,—,internal/api/graphql/graph/dataframe; internal/server,—,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/api/columncapabilities,inventory complete; semantic disposition pending internal/api/graphql/graph,github.com/calypr/loom/internal/api/graphql/graph,graphqlapi,transport adapter,"Own GraphQL HTTP handler construction, error presentation, and generated executor binding.",doc.go; errors.go; handler.go,aggregate_http_integration_test.go; errors_test.go; handler_test.go; http_integration_test.go,—,—,511,944,8,Flush; Header; NewApolloSandboxHandler; NewHandler; NewPlaygroundHandler; RouteConfig; Write; WriteHeader,generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/errors,internal/api/graphql/graph; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataset,internal/server,internal/api/graphql/graph,github.com/99designs/gqlgen/graphql; github.com/99designs/gqlgen/graphql/handler; github.com/99designs/gqlgen/graphql/playground; github.com/gofiber/fiber/v3/middleware/adaptor; github.com/vektah/gqlparser/v2/gqlerror,—,—,one production importer (internal/server); inspect ownership/inline potential,go test ./internal/api/graphql/graph,completed/keep internal/api/graphql/graph/dataframe,github.com/calypr/loom/internal/api/graphql/graph/dataframe,dataframe,transport adapter,"Adapt published ClickHouse dataframe discovery, row, aggregate, and export operations to GraphQL.",aggregates.go; export.go; model.go; rows.go; service.go,aggregates_test.go; export_test.go; fb22_principal_auth_path_control_test.go; filter_test.go; model_test.go; service_test.go,—,—,1072,506,17,AggregateInput; AggregateRowsResult; AggregationsInput; AggregationsJSON; ColumnFromPhysical; ColumnModel; Config; Dataset; ExportDataframe; Model; NewService; PersistedFailure; ProjectDatasets; Rows; Service; WithOperationContext; Write,generated/graphql/graph/model; internal/api/authpolicy; internal/api/columncapabilities; internal/api/http; internal/authscope; internal/dataframe/errors; internal/dataframe/publication; internal/dataframe/published; internal/projectid,generated/graphql/graph/model; internal/authscope; internal/dataframe/errors; internal/dataframe/published,internal/api/graphql/graph/resolver; internal/server,—,—,—,legacy=9,no automatic consolidation signal; perform semantic review,go test ./internal/api/graphql/graph/dataframe,inventory complete; semantic disposition pending -internal/api/graphql/graph/query,github.com/calypr/loom/internal/api/graphql/graph/query,queryapi,transport adapter,Resolve and authorize Arango-backed FHIR dataframe and graph query requests for GraphQL.,active_generation.go; doc.go; errors.go; fhir_service.go; fieldrefs.go; graph_service.go; helpers.go; input_resolution.go; introspection.go; recipe_mapping.go; recipe_resolve.go; service.go; types.go,active_generation_test.go; auth_scope_test.go; errors_test.go; fhir_service_test.go; fieldrefs_test.go; graph_service_test.go; introspection_test.go; postgres_query_test.go; project_identity_test.go; read_scope_ingress_control_chars_e2e_test.go; recipe_mapping_test.go; service_test.go,—,—,1888,2361,28,Config; ExplainFHIRGraph; FHIRGraphExplanation; FHIRGraphNode; FHIRGraphPageInfo; FHIRGraphPath; FHIRGraphQuery; FHIRGraphRelationship; FHIRGraphResult; FHIRGraphTraversal; FHIRListRequest; FHIRListResult; FHIRReadContext; FieldHint; FieldPredicate; FieldSelector; Introspect; IntrospectionRequest; IntrospectionResponse; ListFHIR; ListFHIRInContext; NewService; RecipeBundleFromInput; RelatedResourceHints; ResourceHints; Run; RunFHIRGraph; Service,generated/graphql/graph/model; internal/api/authpolicy; internal/api/recipe; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataset; internal/fhir/schema; internal/projectid,generated/graphql/graph/model; internal/authscope; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataset; internal/ingest,cmd/dataframe-profile; internal/api/graphql/graph/resolver; internal/server,internal/api/graphql/graph,—,—,—,broad surface (13 production files); inspect cohesion,go test ./internal/api/graphql/graph/query,inventory complete; semantic disposition pending -internal/api/graphql/graph/resolver,github.com/calypr/loom/internal/api/graphql/graph/resolver,resolver,transport adapter,Implement gqlgen field resolvers and map GraphQL values to query and lifecycle services.,dataframe_diagnostics.go; fhir_reference_loader.go; fhir_resolver_helpers.go; fhir_schema.resolvers.go; graph_resolvers.go; graphql_output_helpers.go; operation_context.go; recipe_control_graphql.go; recipe_execution_reader.go; recipe_revision_model.go; resolver.go; schema.resolvers.go,fhir_reference_loader_test.go; output_mapping_test.go; recipe_control_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_oracle_probe_test.go; recipe_execution_reader_authz_e2e_test.go; recipe_execution_twin_wire_parity_test.go; recipe_revision_register_test.go,—,fhir_schema.resolvers.go; schema.resolvers.go,5459,2334,362,AbatementDateTime; AbatementString; Active; Actual; ActualComparisonGroup; ActualNumber; Address; Age; AgeString; Alias; AllDay; AllowedBoolean; AmountString; Annotation; AsNeeded; AssignedComparisonGroup; Attachment; AuthorString; AuthoredOn; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; AvailableEndTime; AvailableStartTime; BirthDate; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; BornDate; BornString; Citation; City; Code; CodeMap; CodeableConcept; CodeableReference; Coding; CollectedDateTime; Combined; Comment; Comparator; Conclusion; Condition; ConditionParticipant; ConditionStage; ContactDetail; ContactPoint; ContentType; ContributedToDeath; Count; CountMax; Country; Creation; Currency; Data; DataRequirement; DataRequirementCodeFilter; DataRequirementDateFilter; DataRequirementSort; DataRequirementValueFilter; DataframeAggregate; DataframeAggregations; DataframeDataset; DataframeRecipeExecution; DataframeRecipeRevision; DataframeRecipeRevisions; DataframeRows; Date; DateAsserted; DayOfWeek; DaysOfWeek; DeceasedBoolean; DeceasedDate; DeceasedDateTime; DeceasedString; Description; DescriptionSummary; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportSupportingInfo; Dimensions; Direction; Display; Distance; District; Div; DoNotPerform; DocStatus; DocumentReference; DocumentReferenceAttester; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceRelatesTo; Documentation; Dosage; DosageDoseAndRate; Duration; DurationMax; DurationUnit; EffectiveDateTime; EffectiveInstant; End; EndDate; EstimatedAge; Event; Exclude; ExpirationDate; Expiry; ExplainDataframeRecipe; ExplainFhirGraph; Expression; ExtendedContactDetail; Extension; FHIRPrimitiveExtension; Factor; Family; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryParticipant; FamilyMemberHistoryProcedure; FhirComments; FhirGraph; Frames; Frequency; FrequencyMax; Gender; Given; Group; GroupCharacteristic; GroupMember; Hash; Height; HumanName; Identifier; ImagingStudy; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesPerformer; ImplicitRules; Inactive; Instance; InstantiatesCanonical; InstantiatesURI; Intent; Interval; IntervalUnit; IsActive; IsDefining; IsSubPotent; Issued; Label; Language; LastModified; LastUpdated; Limit; Line; LinkID; LotNumber; LowerLimit; Max; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationPerformer; MedicationBatch; MedicationIngredient; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestSubstitution; MedicationStatement; MedicationStatementAdherence; Membership; Meta; Min; MolecularFormula; MolecularFormulaByMoiety; Money; MultipleBirthBoolean; MultipleBirthInteger; MustSupport; Mutation; Name; Narrative; NewAuthorizedRecipeExecutionReader; NewResolver; Number; NumberOfInstances; NumberOfRepeatsAllowed; NumberOfSeries; Observation; ObservationComponent; ObservationReferenceRange; ObservationTriggeredBy; OccurenceDateTime; OccurrenceDateTime; OccurrenceString; Offset; Offsets; OnsetDateTime; OnsetString; Organization; OrganizationQualification; Pages; ParameterDefinition; Path; Patient; PatientCommunication; PatientContact; PatientInstruction; PatientLink; PerformedDateTime; PerformedString; Period; PeriodMax; PeriodUnit; PostalCode; Practitioner; PractitionerCommunication; PractitionerQualification; PractitionerRole; Preferred; Prefix; PreflightDataframeRecipe; PreviewDataframeRecipe; Priority; Procedure; ProcedureFocalDevice; ProcedurePerformer; Profile; ProjectDataframeDatasets; PublicationDate; PublicationStatus; Quantity; Query; Range; Rank; Ratio; RatioRange; Reason; ReceivedTime; RecipeAuthorizer; RecipeControl; RecipeExecution; RecipeExecutionOutput; RecipeExecutionReader; RecipeExecutionStore; Recorded; RecordedDate; Reference; RegisterDataframeRecipeRevision; RelatedArtifact; RenderedDosageInstruction; Repetitions; Reported; ReportedBoolean; Representation; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyComparisonGroup; ResearchStudyLabel; ResearchStudyObjective; ResearchStudyOutcomeMeasure; ResearchStudyProgressStatus; ResearchStudyRecruitment; ResearchSubject; ResearchSubjectProgress; Resolver; ResolverConfig; Resource; RunDataframeRecipe; RunFhirDataframe; SampledData; SearchParam; Sequence; SigFormat; Signature; Size; Source; Specimen; SpecimenCollection; SpecimenContainer; SpecimenFeature; SpecimenProcessing; Start; StartDate; Started; State; Status; StatusChanged; StatusDate; SubscriptionTopic; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCode; SubstanceDefinitionMoiety; SubstanceDefinitionMolecularWeight; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionProperty; SubstanceDefinitionRelationship; SubstanceDefinitionSourceMaterial; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceIngredient; Suffix; System; TargetFormat; TargetNumber; Task; TaskInput; TaskOutput; TaskPerformer; TaskRestriction; Text; Time; TimeDateTime; TimeOfDay; Timing; TimingDate; TimingDateTime; TimingRepeat; Title; TriggerDefinition; Type; UID; URL; Unit; UpperLimit; UsageContext; Use; UserSelected; ValidateDataframeRecipe; Value; ValueBase64Binary; ValueBoolean; ValueCanonical; ValueCode; ValueDate; ValueDateTime; ValueDecimal; ValueID; ValueInstant; ValueInteger; ValueInteger64; ValueMarkdown; ValueOid; ValuePositiveInt; ValueSet; ValueString; ValueTime; ValueURI; ValueURL; ValueUUID; ValueUnsignedInt; Version; VersionID; When; Width; WithOperationContext,generated/fhir; generated/graphql/graph/executor; generated/graphql/graph/model; internal/api/authpolicy; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataset; internal/fhir/schema,generated/fhir; generated/graphql/graph/model; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec,internal/api/graphql/graph; internal/server,—,github.com/99designs/gqlgen/graphql; github.com/vektah/gqlparser/v2/ast,3 exported interface(s),—,broad surface (12 production files); inspect cohesion; mixed handwritten/generated package (2 generated file(s)); preserve regeneration boundary; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/api/graphql/graph/resolver,inventory complete; semantic disposition pending +internal/api/graphql/graph/query,github.com/calypr/loom/internal/api/graphql/graph/query,queryapi,transport adapter,Resolve and authorize Arango-backed FHIR dataframe and graph query requests for GraphQL.,active_generation.go; doc.go; errors.go; fhir_service.go; fieldrefs.go; graph_service.go; helpers.go; input_resolution.go; introspection.go; recipe_mapping.go; recipe_resolve.go; service.go; types.go,active_generation_test.go; auth_scope_test.go; errors_test.go; fhir_service_test.go; fieldrefs_test.go; graph_service_test.go; introspection_test.go; postgres_query_test.go; project_identity_test.go; read_scope_ingress_control_chars_e2e_test.go; recipe_mapping_test.go; service_test.go,—,—,1888,2364,28,Config; ExplainFHIRGraph; FHIRGraphExplanation; FHIRGraphNode; FHIRGraphPageInfo; FHIRGraphPath; FHIRGraphQuery; FHIRGraphRelationship; FHIRGraphResult; FHIRGraphTraversal; FHIRListRequest; FHIRListResult; FHIRReadContext; FieldHint; FieldPredicate; FieldSelector; Introspect; IntrospectionRequest; IntrospectionResponse; ListFHIR; ListFHIRInContext; NewService; RecipeBundleFromInput; RelatedResourceHints; ResourceHints; Run; RunFHIRGraph; Service,generated/graphql/graph/model; internal/api/authpolicy; internal/api/recipe; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataset; internal/fhir/schema; internal/projectid,generated/graphql/graph/model; internal/authscope; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataset; internal/ingest,cmd/dataframe-profile; internal/api/graphql/graph/resolver; internal/server,internal/api/graphql/graph,—,—,—,broad surface (13 production files); inspect cohesion,go test ./internal/api/graphql/graph/query,inventory complete; semantic disposition pending +internal/api/graphql/graph/resolver,github.com/calypr/loom/internal/api/graphql/graph/resolver,resolver,transport adapter,Implement gqlgen field resolvers and map GraphQL values to query and lifecycle services.,dataframe_diagnostics.go; fhir_reference_loader.go; fhir_resolver_helpers.go; fhir_schema.resolvers.go; graph_resolvers.go; graphql_output_helpers.go; operation_context.go; recipe_control_graphql.go; recipe_execution_reader.go; recipe_revision_model.go; resolver.go; schema.resolvers.go,dataframe_diagnostics_test.go; fhir_reference_loader_test.go; output_mapping_test.go; recipe_control_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_oracle_probe_test.go; recipe_execution_reader_authz_e2e_test.go; recipe_execution_twin_wire_parity_test.go; recipe_revision_register_test.go,—,fhir_schema.resolvers.go; schema.resolvers.go,5469,2376,362,AbatementDateTime; AbatementString; Active; Actual; ActualComparisonGroup; ActualNumber; Address; Age; AgeString; Alias; AllDay; AllowedBoolean; AmountString; Annotation; AsNeeded; AssignedComparisonGroup; Attachment; AuthorString; AuthoredOn; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; AvailableEndTime; AvailableStartTime; BirthDate; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; BornDate; BornString; Citation; City; Code; CodeMap; CodeableConcept; CodeableReference; Coding; CollectedDateTime; Combined; Comment; Comparator; Conclusion; Condition; ConditionParticipant; ConditionStage; ContactDetail; ContactPoint; ContentType; ContributedToDeath; Count; CountMax; Country; Creation; Currency; Data; DataRequirement; DataRequirementCodeFilter; DataRequirementDateFilter; DataRequirementSort; DataRequirementValueFilter; DataframeAggregate; DataframeAggregations; DataframeDataset; DataframeRecipeExecution; DataframeRecipeRevision; DataframeRecipeRevisions; DataframeRows; Date; DateAsserted; DayOfWeek; DaysOfWeek; DeceasedBoolean; DeceasedDate; DeceasedDateTime; DeceasedString; Description; DescriptionSummary; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportSupportingInfo; Dimensions; Direction; Display; Distance; District; Div; DoNotPerform; DocStatus; DocumentReference; DocumentReferenceAttester; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceRelatesTo; Documentation; Dosage; DosageDoseAndRate; Duration; DurationMax; DurationUnit; EffectiveDateTime; EffectiveInstant; End; EndDate; EstimatedAge; Event; Exclude; ExpirationDate; Expiry; ExplainDataframeRecipe; ExplainFhirGraph; Expression; ExtendedContactDetail; Extension; FHIRPrimitiveExtension; Factor; Family; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryParticipant; FamilyMemberHistoryProcedure; FhirComments; FhirGraph; Frames; Frequency; FrequencyMax; Gender; Given; Group; GroupCharacteristic; GroupMember; Hash; Height; HumanName; Identifier; ImagingStudy; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesPerformer; ImplicitRules; Inactive; Instance; InstantiatesCanonical; InstantiatesURI; Intent; Interval; IntervalUnit; IsActive; IsDefining; IsSubPotent; Issued; Label; Language; LastModified; LastUpdated; Limit; Line; LinkID; LotNumber; LowerLimit; Max; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationPerformer; MedicationBatch; MedicationIngredient; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestSubstitution; MedicationStatement; MedicationStatementAdherence; Membership; Meta; Min; MolecularFormula; MolecularFormulaByMoiety; Money; MultipleBirthBoolean; MultipleBirthInteger; MustSupport; Mutation; Name; Narrative; NewAuthorizedRecipeExecutionReader; NewResolver; Number; NumberOfInstances; NumberOfRepeatsAllowed; NumberOfSeries; Observation; ObservationComponent; ObservationReferenceRange; ObservationTriggeredBy; OccurenceDateTime; OccurrenceDateTime; OccurrenceString; Offset; Offsets; OnsetDateTime; OnsetString; Organization; OrganizationQualification; Pages; ParameterDefinition; Path; Patient; PatientCommunication; PatientContact; PatientInstruction; PatientLink; PerformedDateTime; PerformedString; Period; PeriodMax; PeriodUnit; PostalCode; Practitioner; PractitionerCommunication; PractitionerQualification; PractitionerRole; Preferred; Prefix; PreflightDataframeRecipe; PreviewDataframeRecipe; Priority; Procedure; ProcedureFocalDevice; ProcedurePerformer; Profile; ProjectDataframeDatasets; PublicationDate; PublicationStatus; Quantity; Query; Range; Rank; Ratio; RatioRange; Reason; ReceivedTime; RecipeAuthorizer; RecipeControl; RecipeExecution; RecipeExecutionOutput; RecipeExecutionReader; RecipeExecutionStore; Recorded; RecordedDate; Reference; RegisterDataframeRecipeRevision; RelatedArtifact; RenderedDosageInstruction; Repetitions; Reported; ReportedBoolean; Representation; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyComparisonGroup; ResearchStudyLabel; ResearchStudyObjective; ResearchStudyOutcomeMeasure; ResearchStudyProgressStatus; ResearchStudyRecruitment; ResearchSubject; ResearchSubjectProgress; Resolver; ResolverConfig; Resource; RunDataframeRecipe; RunFhirDataframe; SampledData; SearchParam; Sequence; SigFormat; Signature; Size; Source; Specimen; SpecimenCollection; SpecimenContainer; SpecimenFeature; SpecimenProcessing; Start; StartDate; Started; State; Status; StatusChanged; StatusDate; SubscriptionTopic; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCode; SubstanceDefinitionMoiety; SubstanceDefinitionMolecularWeight; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionProperty; SubstanceDefinitionRelationship; SubstanceDefinitionSourceMaterial; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceIngredient; Suffix; System; TargetFormat; TargetNumber; Task; TaskInput; TaskOutput; TaskPerformer; TaskRestriction; Text; Time; TimeDateTime; TimeOfDay; Timing; TimingDate; TimingDateTime; TimingRepeat; Title; TriggerDefinition; Type; UID; URL; Unit; UpperLimit; UsageContext; Use; UserSelected; ValidateDataframeRecipe; Value; ValueBase64Binary; ValueBoolean; ValueCanonical; ValueCode; ValueDate; ValueDateTime; ValueDecimal; ValueID; ValueInstant; ValueInteger; ValueInteger64; ValueMarkdown; ValueOid; ValuePositiveInt; ValueSet; ValueString; ValueTime; ValueURI; ValueURL; ValueUUID; ValueUnsignedInt; Version; VersionID; When; Width; WithOperationContext,generated/fhir; generated/graphql/graph/executor; generated/graphql/graph/model; internal/api/authpolicy; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataset; internal/fhir/schema,generated/fhir; generated/graphql/graph/model; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec,internal/api/graphql/graph; internal/server,—,github.com/99designs/gqlgen/graphql; github.com/vektah/gqlparser/v2/ast,3 exported interface(s),compatibility=1,broad surface (12 production files); inspect cohesion; mixed handwritten/generated package (2 generated file(s)); preserve regeneration boundary; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/api/graphql/graph/resolver,inventory complete; semantic disposition pending internal/api/http,github.com/calypr/loom/internal/api/http,httpapi,transport adapter,"Own shared Fiber server setup, middleware, request IDs, errors, health, and readiness behavior.",doc.go; errors.go; middleware.go; request_id.go; routes.go; server.go,backend_error_boundary_contract_test.go; errors_test.go; health_readiness_test.go; http_test.go; request_id_test.go,—,—,642,1145,15,App; ContextWithRequestID; ErrorResponse; HTTPConfig; HTTPErrorBody; HTTPServer; Health; Liveness; Logger; MapDataframeError; MappedError; NewHTTPServer; Readiness; RequestIDFromContext; StatusForErrorCode,internal/authscope; internal/dataframe/errors,internal/authscope; internal/backends/arango; internal/backends/postgres/control; internal/dataframe/errors,internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/server,internal/api/bulk/load; internal/backends/postgres/internal/store,github.com/gofiber/fiber/v3; github.com/google/uuid,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/api/http,inventory complete; semantic disposition pending internal/api/recipe,github.com/calypr/loom/internal/api/recipe,recipeapi,transport adapter,Adapt scoped catalog field discovery to recipe schema resolution for one-shot and stored recipes.,discovery.go,discovery_auth_mode_test.go; discovery_test.go,—,—,110,126,3,FieldDiscovery; Fields; NewFieldDiscovery,internal/authscope; internal/catalog; internal/dataframe/errors; internal/dataframe/recipe/schema,internal/authscope; internal/catalog; internal/dataframe/errors; internal/dataframe/recipe/schema,internal/api/graphql/graph/query; internal/server,—,—,—,legacy=1; compatibility=1,no automatic consolidation signal; perform semantic review,go test ./internal/api/recipe,inventory complete; semantic disposition pending -internal/authscope,github.com/calypr/loom/internal/authscope,authscope,domain/library,"Represent request principals and resolve authorized project, generation, and resource scopes.",authentication.go; doc.go; fence_resource_access.go; principal.go; scope_resolver.go,all_generations_cache_key_test.go; auth_path_control_ingress_test.go; auth_resource_path_options_conflict_test.go; authentication_test.go; covers_execution_paths_control_test.go; persisted_scope_fails_closed_test.go; probe_double_normalize_test.go; scope_cache_key_namespace_test.go; scope_test.go,—,—,1175,2104,36,AllowAllAuthorizer; Authenticate; Authenticator; AuthorizeProject; AuthorizeReadProject; AuthorizeWrite; Authorizer; BasicAuthenticator; BearerTokenAuthenticator; CalyprAuthenticator; Clone; ContextWithPrincipal; CoversExecutionPaths; FenceUserAccessClient; GetAllowedResources; InvalidateGeneration; InvalidateProject; NewFenceUserAccessClientWithTrustedIssuer; NewScopeResolver; NormalizeAuthResourcePath; NormalizeAuthResourcePaths; Permission; Principal; PrincipalFromContext; ReadScope; ReadScopeMode; ResolveReadScope; ResolveReadScopeForGeneration; ResolveWriteScopeForGeneration; ResourceAccessClient; ScopeAuthorizer; ScopeResolver; ScopeResolverConfig; StaticAuthenticator; Unrestricted; ValidateTrustedFenceIssuer,internal/catalog; internal/projectid,internal/catalog,cmd/preview-covering-index-artifact; internal/api/authpolicy; internal/api/bulk/load; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/backends/arango/internal/explorer; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer/lifecycle; internal/server,internal/api/graphql/graph; internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/store; internal/dataframe/compiler; internal/dataframe/compiler/render/aql,—,3 exported interface(s),legacy=5,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/authscope,inventory complete; semantic disposition pending +internal/authscope,github.com/calypr/loom/internal/authscope,authscope,domain/library,"Represent request principals and resolve authorized project, generation, and resource scopes.",authentication.go; doc.go; fence_resource_access.go; principal.go; scope_resolver.go,all_generations_cache_key_test.go; auth_path_control_ingress_test.go; auth_resource_path_options_conflict_test.go; authentication_test.go; covers_execution_paths_control_test.go; persisted_scope_fails_closed_test.go; probe_double_normalize_test.go; scope_cache_key_namespace_test.go; scope_test.go,—,—,1175,2104,36,AllowAllAuthorizer; Authenticate; Authenticator; AuthorizeProject; AuthorizeReadProject; AuthorizeWrite; Authorizer; BasicAuthenticator; BearerTokenAuthenticator; CalyprAuthenticator; Clone; ContextWithPrincipal; CoversExecutionPaths; FenceUserAccessClient; GetAllowedResources; InvalidateGeneration; InvalidateProject; NewFenceUserAccessClientWithTrustedIssuer; NewScopeResolver; NormalizeAuthResourcePath; NormalizeAuthResourcePaths; Permission; Principal; PrincipalFromContext; ReadScope; ReadScopeMode; ResolveReadScope; ResolveReadScopeForGeneration; ResolveWriteScopeForGeneration; ResourceAccessClient; ScopeAuthorizer; ScopeResolver; ScopeResolverConfig; StaticAuthenticator; Unrestricted; ValidateTrustedFenceIssuer,internal/catalog; internal/projectid,internal/catalog,cmd/preview-covering-index-artifact; internal/api/authpolicy; internal/api/bulk/load; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/backends/arango/internal/explorer; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer/lifecycle; internal/server,internal/api/graphql/graph; internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/store; internal/dataframe/benchmark; internal/dataframe/compiler; internal/dataframe/compiler/render/aql,—,3 exported interface(s),legacy=5,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/authscope,inventory complete; semantic disposition pending internal/backends/arango,github.com/calypr/loom/internal/backends/arango,arango,storage adapter,"Expose the only public entry to the ArangoDB 3.12 backend: a single Open factory validates explicit config, owns exactly one lifecycle client with the required collection bootstrap policy, and returns the typed runtime carrying the catalog, dataset lifecycle, Explorer, recipe registry/revisions, and published-dataframe ports assembled from the private implementation tree, with alias re-exports for witness-side reassembly.",aliases.go; doc.go; open.go,ownership_contract_test.go,—,—,336,800,4,Close; Config; Open; Runtime,internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/arango/internal/dataset; internal/backends/arango/internal/explorer; internal/backends/arango/internal/publication; internal/backends/arango/internal/recipeexec; internal/backends/arango/internal/store,internal/backends/arango,cmd/arango-fhir-proto; cmd/dataframe-profile; cmd/loom-data; internal/acceptance; internal/ingest; internal/server,conformance/compiler; internal/api/http; internal/backends/arango; internal/dataframe/compiler; internal/dataframe/execution; internal/dataframe/publication/clickhouse; internal/dataset; internal/explorer/compilation,—,—,—,storage adapter; preserve interface and persisted-data contracts,go test ./internal/backends/arango,inventory complete; semantic disposition pending -internal/backends/arango/internal/catalog,github.com/calypr/loom/internal/backends/arango/internal/catalog,arango,domain/library,Persist and retrieve catalog facts in ArangoDB.,codec.go; evidence.go; fields.go; relationships.go; resource_inventory.go; resources.go; semantic_coverage.go; semantic_coverage_envelope.go; semantic_inventory.go; store.go,all_generations_auth_path_conflict_test.go; all_generations_project_binding_integration_test.go; resource_inventory_integration_test.go; resource_inventory_unit_test.go; semantic_coded_category_integration_test.go; semantic_coverage_envelope_test.go; semantic_coverage_resource_integration_test.go; semantic_coverage_test.go; semantic_inventory_category_test.go; semantic_inventory_integration_test.go; semantic_inventory_selection_test.go; semantic_inventory_source_availability_test.go; store_test.go,—,—,5267,5466,54,AccumulateRelationshipCatalog; AddSource; AdvanceSemanticInventoryBackfill; AdvanceSemanticInventoryBuild; AuditRelationshipEdges; BeginSemanticCoverageBuild; BeginSemanticCoverageResource; BeginSemanticInventoryBuild; ClaimSemanticInventoryBackfill; CompleteRetainedSemanticCoverageResource; CompleteSemanticCoverageResource; CompleteSemanticInventoryBackfill; CompleteSemanticInventoryBuild; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverReferences; DiscoverRelationshipObservations; DiscoverResourceInventory; EncodeSemanticCoverageSourceEnvelope; EnsureSemanticInventoryEntries; FailSemanticCoverageBuild; FailSemanticCoverageResource; FailSemanticInventoryBackfill; FailSemanticInventoryBuild; FinalizeSemanticCoverageBuild; LookupSemanticCoverage; MarkSemanticCoverageResourceMissing; New; NewSemanticCoverageSourceSetAccumulator; PageSemanticInventory; PrepareSemanticInventoryBackfill; ReadFieldEnrichment; ReadRelationshipObservations; ReadResourceInventory; ReadRetainedSemanticInventoryPage; RebuildRelationshipCatalog; RebuildResourceInventory; RefreshSemanticInventorySourceAvailability; ResolveSemanticInventorySelections; SemanticCoverageBuildIdentity; SemanticCoverageCollectionSpec; SemanticCoveragePage; SemanticCoverageResourceExpectation; SemanticCoverageSourcePartitionExpectation; SemanticCoverageSourceSetAccumulator; SemanticCoverageWriteStats; Snapshot; Store; ValidateAuthResourcePathOptions; WriteFieldCatalog; WriteRelationshipCatalog; WriteSemanticCoverageContributions; WriteSemanticInventoryContributions,internal/backends/arango/internal/store; internal/catalog; internal/fhir/schema,internal/authscope; internal/backends/arango/internal/store; internal/catalog,internal/backends/arango,—,github.com/bytedance/sonic,—,—,one production importer (internal/backends/arango); inspect ownership/inline potential,go test ./internal/backends/arango/internal/catalog,inventory complete; semantic disposition pending +internal/backends/arango/internal/catalog,github.com/calypr/loom/internal/backends/arango/internal/catalog,arango,domain/library,Persist and retrieve catalog facts in ArangoDB.,codec.go; evidence.go; fields.go; relationships.go; resource_inventory.go; resources.go; semantic_coverage.go; semantic_coverage_envelope.go; semantic_inventory.go; store.go,all_generations_auth_path_conflict_test.go; all_generations_project_binding_integration_test.go; missing_field_catalog_integration_test.go; resource_inventory_integration_test.go; resource_inventory_unit_test.go; semantic_coded_category_integration_test.go; semantic_coverage_envelope_test.go; semantic_coverage_resource_integration_test.go; semantic_coverage_test.go; semantic_inventory_category_test.go; semantic_inventory_integration_test.go; semantic_inventory_selection_test.go; semantic_inventory_source_availability_test.go; store_test.go,—,—,5283,5594,54,AccumulateRelationshipCatalog; AddSource; AdvanceSemanticInventoryBackfill; AdvanceSemanticInventoryBuild; AuditRelationshipEdges; BeginSemanticCoverageBuild; BeginSemanticCoverageResource; BeginSemanticInventoryBuild; ClaimSemanticInventoryBackfill; CompleteRetainedSemanticCoverageResource; CompleteSemanticCoverageResource; CompleteSemanticInventoryBackfill; CompleteSemanticInventoryBuild; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverReferences; DiscoverRelationshipObservations; DiscoverResourceInventory; EncodeSemanticCoverageSourceEnvelope; EnsureSemanticInventoryEntries; FailSemanticCoverageBuild; FailSemanticCoverageResource; FailSemanticInventoryBackfill; FailSemanticInventoryBuild; FinalizeSemanticCoverageBuild; LookupSemanticCoverage; MarkSemanticCoverageResourceMissing; New; NewSemanticCoverageSourceSetAccumulator; PageSemanticInventory; PrepareSemanticInventoryBackfill; ReadFieldEnrichment; ReadRelationshipObservations; ReadResourceInventory; ReadRetainedSemanticInventoryPage; RebuildRelationshipCatalog; RebuildResourceInventory; RefreshSemanticInventorySourceAvailability; ResolveSemanticInventorySelections; SemanticCoverageBuildIdentity; SemanticCoverageCollectionSpec; SemanticCoveragePage; SemanticCoverageResourceExpectation; SemanticCoverageSourcePartitionExpectation; SemanticCoverageSourceSetAccumulator; SemanticCoverageWriteStats; Snapshot; Store; ValidateAuthResourcePathOptions; WriteFieldCatalog; WriteRelationshipCatalog; WriteSemanticCoverageContributions; WriteSemanticInventoryContributions,internal/backends/arango/internal/store; internal/catalog; internal/fhir/schema,internal/authscope; internal/backends/arango/internal/store; internal/catalog,internal/backends/arango,—,github.com/bytedance/sonic,—,—,one production importer (internal/backends/arango); inspect ownership/inline potential,go test ./internal/backends/arango/internal/catalog,inventory complete; semantic disposition pending internal/backends/arango/internal/chartifact,github.com/calypr/loom/internal/backends/arango/internal/chartifact,arango,domain/library,Persist private ClickHouse artifact manifests and leases in a dedicated Arango collection.,catalog.go,catalog_arango_test.go; catalog_test.go,—,—,379,723,10,BootstrapSpec; Catalog; ClaimCleanup; Create; Delete; ListExpired; NewCatalog; ReleaseCleanup; Renew; Update,internal/backends/arango/internal/store; internal/dataframe/execution/chartifact,internal/authscope; internal/backends/arango/internal/store; internal/dataframe/execution/chartifact; internal/store/clickhouse,internal/backends/arango,—,—,—,—,one production importer (internal/backends/arango); inspect ownership/inline potential,go test ./internal/backends/arango/internal/chartifact,inventory complete; semantic disposition pending -internal/backends/arango/internal/dataset,github.com/calypr/loom/internal/backends/arango/internal/dataset,arango,domain/library,Persist immutable dataset generation lifecycle state in ArangoDB.,collections.go; doc.go; document_codec.go; manifest_lifecycle.go; release_store.go,generation_activation_cas_e2e_test.go; release_policy_change_e2e_test.go; revision_inflation_e2e_test.go; store_test.go,—,—,764,1652,15,Activate; ActiveDocumentKey; BootstrapSpec; CollectionSpecs; CompareAndSwapActivateRelease; CompareAndSwapPublishRelease; CreateManifest; ManifestDocumentKey; New; ReadActiveRelease; ReadManifest; ResolveActiveManifest; SaveRelease; Store; TransitionManifest,internal/backends/arango/internal/store; internal/dataset,internal/backends/arango/internal/store; internal/dataset,internal/backends/arango; internal/backends/arango/internal/explorer,internal/backends/arango/internal/publication,github.com/arangodb/go-driver/v2/arangodb/shared,—,compatibility=2,no automatic consolidation signal; perform semantic review,go test ./internal/backends/arango/internal/dataset,inventory complete; semantic disposition pending +internal/backends/arango/internal/dataset,github.com/calypr/loom/internal/backends/arango/internal/dataset,arango,domain/library,Persist immutable dataset generation lifecycle state in ArangoDB.,collections.go; doc.go; document_codec.go; manifest_lifecycle.go; release_store.go,generation_activation_cas_e2e_test.go; release_policy_change_e2e_test.go; release_store_cas_retry_test.go; revision_inflation_e2e_test.go; store_test.go,—,—,791,1783,15,Activate; ActiveDocumentKey; BootstrapSpec; CollectionSpecs; CompareAndSwapActivateRelease; CompareAndSwapPublishRelease; CreateManifest; ManifestDocumentKey; New; ReadActiveRelease; ReadManifest; ResolveActiveManifest; SaveRelease; Store; TransitionManifest,internal/backends/arango/internal/store; internal/dataset,internal/backends/arango/internal/store; internal/dataset,internal/backends/arango; internal/backends/arango/internal/explorer,internal/backends/arango/internal/publication,github.com/arangodb/go-driver/v2/arangodb/shared,—,compatibility=2,no automatic consolidation signal; perform semantic review,go test ./internal/backends/arango/internal/dataset,inventory complete; semantic disposition pending internal/backends/arango/internal/explorer,github.com/calypr/loom/internal/backends/arango/internal/explorer,arango,domain/library,"Persist Explorer drafts, revisions, receipts, publications, capabilities, and repository state in ArangoDB.",activation.go; capability_snapshot_store.go; codec.go; collections.go; drafts.go; explicit_groups.go; explorers.go; interpretations.go; publication.go; receipts.go; repository.go; revisions.go; selection_references.go; selections.go; store.go; table_shape_capabilities.go,capability_snapshot_store_test.go; collections_test.go; cross_generation_publish_e2e_test.go; draft_conflict_test.go; draft_transaction_formal_replay_test.go; drafts_integration_test.go; drafts_test.go; explicit_group_storage_integration_test.go; explicit_groups_test.go; interpretations_test.go; receipt_store_test.go; repository_test.go; selection_references_test.go; selection_scale_integration_test.go; selection_storage_integration_test.go; selections_cleanup_e2e_test.go; store_test.go; table_shape_capabilities_test.go,—,—,3366,4552,56,AbortExplicitGroupRevision; AbortSelection; ActivateRepositoryGeneration; AppendExplicitGroupMemberships; AppendSelectionMembers; BeginExplicitGroupRevision; BeginSelection; BootstrapSpec; CapabilitySnapshotCollectionSpec; CapabilitySnapshotStore; CleanupExplicitGroupStaging; CleanupSelectionStaging; CollectionSpecs; CompleteExplicitGroupRevision; CompleteSelection; Create; CreateInterpretationRevision; DigestExplicitGroupRevision; DigestSelectionMembers; FailRevision; Get; GetByIdentity; GetByToken; GetCatalog; GetCatalogForLookup; GetCategoryScan; GetCompilationReceiptForExplorer; GetDraftRevision; GetExplicitGroupRevision; GetInterpretationRevision; GetResolution; GetRevision; GetSelection; InsertCompilationReceipt; InsertRevision; List; ListExplicitGroupDefinitions; ListExplicitGroupRevisions; ListInterpretationLibraries; MigrateLegacyRepositoryConfigs; New; NewCapabilitySnapshotStore; NewTableShapeCapabilityRepository; PublishAuthoring; Put; PutCatalog; PutCategoryScan; PutExplicitGroupDefinitions; PutResolution; SaveDraft; Store; TableShapeCapabilitiesCollectionSpec; TableShapeCapabilityRepository; ValidateSelectionReferences; VisitExplicitGroupMemberships; VisitSelectionMembers,internal/authscope; internal/backends/arango/internal/dataset; internal/backends/arango/internal/publication; internal/backends/arango/internal/store; internal/dataframe/publication; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/tableshapecap; internal/projectid,internal/authscope; internal/backends/arango/internal/dataset; internal/backends/arango/internal/publication; internal/backends/arango/internal/store; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/lifecycle; internal/explorer/tableshapecap; internal/projectid,internal/backends/arango,—,github.com/arangodb/go-driver/v2/arangodb/shared,—,legacy=12; migration=4; compatibility=1,one production importer (internal/backends/arango); inspect ownership/inline potential; broad surface (16 production files); inspect cohesion,go test ./internal/backends/arango/internal/explorer,inventory complete; semantic disposition pending internal/backends/arango/internal/publication,github.com/calypr/loom/internal/backends/arango/internal/publication,arango,domain/library,Persist dataframe publication registry and bundle metadata in ArangoDB.,bundle_registry.go; registry.go,bundle_registry_test.go; dangling_binding_e2e_test.go; execution_read_pin_integration_test.go; name_latest_binding_e2e_test.go; publish_atomicity_e2e_test.go; release_dangle_reconcile_e2e_test.go,—,—,533,2990,22,AcquireBundleLease; AcquireExecutionReadPin; BootstrapSpec; ClaimExecutionCleanup; Client; CompareAndSwapPointer; FindExecutionByKey; FindExecutionBySelector; GetExecution; GetPointer; ListExecutions; New; PublishExecution; Registry; ReleaseBundleLease; ReleaseExecutionCleanup; ReleaseExecutionReadPin; RenewBundleLease; RenewExecutionCleanup; RenewExecutionReadPin; SaveExecution; VisitExecutionPages,internal/backends/arango/internal/store; internal/dataframe/publication,internal/backends/arango/internal/dataset; internal/backends/arango/internal/publication; internal/backends/arango/internal/store; internal/dataframe/errors; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataset; internal/store/clickhouse,internal/backends/arango; internal/backends/arango/internal/explorer,internal/backends/arango/internal/publication,github.com/arangodb/go-driver/v2/arangodb/shared,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/backends/arango/internal/publication,inventory complete; semantic disposition pending internal/backends/arango/internal/recipeexec,github.com/calypr/loom/internal/backends/arango/internal/recipeexec,arango,domain/library,Persist recipe versions and execution-use records in ArangoDB.,registry.go; revisions.go,registry_test.go; revisions_test.go; revisions_user_faults_test.go,—,—,294,555,15,BootstrapSpec; Client; Get; List; LoadRecipe; LoadRecipeVersion; New; NewRevisionRegistry; RecipeVersionUsed; Register; Registry; ReplaceRecipeVersion; RevisionBootstrapSpec; RevisionRegistry; SaveRecipe,internal/backends/arango/internal/store; internal/dataframe/errors; internal/dataframe/recipe; internal/dataframe/recipe/exec,internal/backends/arango/internal/store; internal/dataframe/errors; internal/dataframe/recipe; internal/dataframe/recipe/exec,internal/backends/arango,—,—,—,legacy=1,one production importer (internal/backends/arango); inspect ownership/inline potential,go test ./internal/backends/arango/internal/recipeexec,inventory complete; semantic disposition pending internal/backends/arango/internal/store,github.com/calypr/loom/internal/backends/arango/internal/store,arango,domain/library,"Provide the low-level typed ArangoDB client, query streaming, profiling, and Explain assessment boundary.",client.go; errors.go; explain.go; explain_assessment.go; explain_client.go; options.go; preview_index.go; profile.go; profile_client.go; types.go,client_query_memory_limit_test.go; client_queryrows_test.go; client_revision_test.go; errors_test.go; explain_assessment_test.go; explain_client_test.go; explain_test.go; population_profile_integration_test.go; preview_index_test.go; profile_client_test.go; profile_integration_test.go; profile_test.go,—,—,1950,2210,70,AQLExecutor; AssessExplainResult; BatchInserter; Bootstrap; BootstrapSpec; Client; Close; CollectionCount; CollectionExists; CollectionRevision; CollectionSpec; ConnectionOptions; DatabaseExists; DiagnosticCause; EnsurePreviewCoveringIndex; EnsurePreviewCoveringIndexReplacing; EnsurePreviewCoveringIndexWithStoredValues; EnsurePreviewCoveringIndexWithStoredValuesReplacing; ExecuteAQL; Explain; ExplainAssessment; ExplainCollection; ExplainCollectionScan; ExplainIndex; ExplainIndexLocation; ExplainIndexSummary; ExplainIndexUse; ExplainIndexes; ExplainNode; ExplainOptions; ExplainPlan; ExplainPlanEstimate; ExplainRequest; ExplainResult; ExplainStats; ExplainWarning; InsertBatchRaw; IsArangoErrorResponse; IsArangoPermissionResponse; ListPreviewCoveringIndexes; Open; OptimizerOptions; ParseExplainResult; ParseProfileResult; PreviewCoveringIndex; Profile; ProfileExtra; ProfileNode; ProfileNodeGroup; ProfileNodeSummary; ProfileOptions; ProfilePhases; ProfileRequest; ProfileResult; ProfileStats; ProfileSummary; QueryMemoryLimitFromContext; QueryRows; QueryRowsWithMaxRuntime; RowBatchClient; RowQueryer; RowVisitor; SummarizeProfile; TransactionCollections; TransactionFunc; TransactionalRowClient; TranslateQueryError; UnmarshalJSON; WithQueryMemoryLimit; WithTransaction,internal/dataframe/errors,internal/dataframe/errors,internal/backends/arango; internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/arango/internal/dataset; internal/backends/arango/internal/explorer; internal/backends/arango/internal/publication; internal/backends/arango/internal/recipeexec,—,github.com/arangodb/go-driver/v2/arangodb; github.com/arangodb/go-driver/v2/arangodb/shared; github.com/arangodb/go-driver/v2/connection; github.com/arangodb/go-driver/v2/utils,5 exported interface(s),legacy=1,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/arango/internal/store,inventory complete; semantic disposition pending internal/backends/postgres,github.com/calypr/loom/internal/backends/postgres,postgres,storage adapter,"Own the PostgreSQL 18 factory: a single Open validates explicit config, opens exactly one readiness-gated pool with own-prefix connection tagging, composes the control runtime, and returns the typed runtime carrying the SourceRows and PlanJSON data-plane ports over tagged compiled commands through the typed Querier bridge; the sole legal importer of the private rows adapter, and no re-export surface - control-plane consumers import internal/backends/postgres/control directly (arch-backend-09 deleted aliases.go).",doc.go; open.go; plan_executor.go,engine_postgres_command_boundary_pg18_test.go; engine_postgres_pg18_test.go; explain_postgres_test.go; open_live_test.go; ownership_contract_test.go; plan_executor_test.go; sentinel_identity_test.go,—,—,178,4408,5,NewPlanJSONExecutor; NewSourceRows; Open; Querier; Runtime,internal/backends/postgres/control; internal/backends/postgres/internal/rows; internal/backends/postgres/internal/store; internal/dataframe/compiler/command; internal/dataframe/execution,internal/backends/postgres; internal/backends/postgres/control; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/rows; internal/backends/postgres/internal/store; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/server; internal/source,cmd/loom-data; internal/ingest; internal/server,internal/api/graphql/graph/query; internal/backends/postgres; internal/dataframe/execution,—,—,migration=1,storage adapter; preserve interface and persisted-data contracts,go test ./internal/backends/postgres,inventory complete; semantic disposition pending -internal/backends/postgres/control,github.com/calypr/loom/internal/backends/postgres/control,control,domain/library,"Own the cycle-free public control plane of the PostgreSQL backend as its single definition: configuration and series-assignment validation, the bare-pool client and store-owned batch vocabulary, the dataset manifest lifecycle and catalog evidence port assembly, and the structural query surface; production consumers and backend-adjacent witnesses import it directly for control-plane symbols, and it must never reach the factory root, the private rows adapter, or dataframe/execution (machine-enforced by scripts/packagepolicy).",catalog_read.go; config.go; errors.go; runtime.go; vocabulary.go; vocabulary_control.go,catalog_read_test.go,—,—,1386,284,65,AllSeries; Begin; CatalogStore; Close; Commit; Config; ConnSource; DiscoverExistingAuthResourcePaths; DiscoverFields; Error; Exec; FieldProfileScope; GenerationAdvisoryLockKey; Health; MigrateDatasetReleaseSchema; MigrateResult; MigrateSchema; NewBundleCatalog; NewCapabilitySnapshotStore; NewCatalogFieldProfileStore; NewCatalogRelationshipEvidenceRepository; NewCatalogRelationshipEvidenceStore; NewCatalogStore; NewChartifactCatalog; NewDatasetReleaseStore; NewEdgeRepository; NewEdgeWriter; NewExplicitGroupStore; NewExplorerStore; NewFieldProfileRepository; NewInterpretationStore; NewInventoryRepository; NewPublicationReconciler; NewPublishStore; NewRecipeExecRegistry; NewRecipeRevisionRegistry; NewResourceRepository; NewResourceWriter; NewSemanticContributionRepository; NewSemanticReadRepository; NewTableShapeCapabilityStore; Open; OpenPool; PoolClient; Query; QueryRow; Readiness; Rollback; Row; Rows; Runtime; Scan; SemanticContributionRepository; Series; SeriesSchemas; SetupPolicy; Unwrap; Validate; VerifyDatasetReleaseSchema; VerifyExplorerControlSchema; VerifyPublicationControlSchema; VerifyRecipeChainsSchema; VerifyRecipeExecRegistriesSchema; VerifySchema; WithAdvisoryLock,internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/redact; internal/backends/postgres/internal/store; internal/catalog; internal/dataset,internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/catalog,cmd/loom-data; internal/backends/postgres; internal/ingest; internal/server,internal/api/graphql/graph/query; internal/api/http; internal/dataframe/compiler; internal/dataframe/compiler/render/postgres; internal/dataframe/execution,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn,2 exported interface(s),migration=21,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/control,inventory complete; semantic disposition pending -internal/backends/postgres/internal/catalog,github.com/calypr/loom/internal/backends/postgres/internal/catalog,postgres,domain/library,"Persist and retrieve catalog facts in PostgreSQL 18: publish-last generation-scoped resource inventory builds, bounded field-profile records with completeness and truncation metadata, leased compare-and-swap-guarded semantic inventory build state, deterministic semantic-inventory contribution rows with content-addressed replay and conflict classification, byte-order-stable keyset pages over retained resources, and authorization-scoped semantic-inventory pages and selection resolution served directly from contribution aggregates.",field_discovery.go; field_profile.go; relationships.go; resource_inventory.go; semantic_build.go; semantic_contribution.go; semantic_page.go; store.go,field_discovery_test.go; field_profile_test.go; relationships_pg_test.go; relationships_test.go; resource_inventory_test.go; semantic_build_test.go; semantic_contribution_test.go; semantic_page_test.go,—,—,4340,11088,54,AdvanceSemanticInventoryBackfill; ClaimSemanticInventoryBackfill; CompleteSemanticInventoryBackfill; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverRelationshipObservations; DiscoverResourceInventory; EnsureSemanticInventoryEntries; Error; FailSemanticInventoryBackfill; FieldProfileStore; FieldProfileUnavailableError; FieldProfileWriteError; InventoryStore; New; NewFieldProfileStore; NewRelationshipEvidenceRepository; NewRelationshipEvidenceStore; NewSemanticBuildRepository; NewSemanticContributionRepository; NewSemanticInventoryBackfillStore; NewSemanticReadRepository; PageSemanticInventory; PrepareSemanticInventoryBackfill; ReadFieldProfile; ReadRelationshipObservations; ReadResourceInventory; ReadRetainedSemanticInventoryPage; ReadSemanticInventoryBuild; RebuildResourceInventory; RelationshipEvidenceRepository; RelationshipOccurrenceGroup; RelationshipScope; RelationshipStore; ResolveSemanticInventorySelections; ResourceInventoryUnavailableError; ResourceInventoryWriteError; RetainedSemanticSourceMissingError; ScanRelationshipOccurrences; SemanticBuildRepository; SemanticContributionRepository; SemanticInventoryBackfillStore; SemanticInventoryClaimLostError; SemanticInventoryClaimedError; SemanticInventoryContributionConflictError; SemanticInventoryEntriesUnsupportedError; SemanticInventoryStorageMissingError; SemanticInventoryUnsupportedError; SemanticReadRepository; Store; Unwrap; WriteFieldProfile; WriteSemanticInventoryContributions,internal/backends/postgres/internal/store; internal/catalog; internal/fhir/schema,internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/catalog; internal/dataset; internal/ingest,internal/backends/postgres/control,internal/backends/postgres/internal/catalog,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn; github.com/jackc/pgx/v5/pgxpool,3 exported interface(s),legacy=9; migration=16,one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/catalog,inventory complete; semantic disposition pending +internal/backends/postgres/control,github.com/calypr/loom/internal/backends/postgres/control,control,domain/library,"Own the cycle-free public control plane of the PostgreSQL backend as its single definition: configuration and series-assignment validation, the bare-pool client and store-owned batch vocabulary, the dataset manifest lifecycle and catalog evidence port assembly, and the structural query surface; production consumers and backend-adjacent witnesses import it directly for control-plane symbols, and it must never reach the factory root, the private rows adapter, or dataframe/execution (machine-enforced by scripts/packagepolicy).",catalog_read.go; config.go; errors.go; runtime.go; vocabulary.go; vocabulary_control.go,capability_compose_test.go; capability_evidence_assembly_test.go; catalog_read_test.go,—,—,1393,796,65,AllSeries; Begin; CatalogStore; Close; Commit; Config; ConnSource; DiscoverExistingAuthResourcePaths; DiscoverFields; Error; Exec; FieldProfileScope; GenerationAdvisoryLockKey; Health; MigrateDatasetReleaseSchema; MigrateResult; MigrateSchema; NewBundleCatalog; NewCapabilitySnapshotStore; NewCatalogFieldProfileStore; NewCatalogRelationshipEvidenceRepository; NewCatalogRelationshipEvidenceStore; NewCatalogStore; NewChartifactCatalog; NewDatasetReleaseStore; NewEdgeRepository; NewEdgeWriter; NewExplicitGroupStore; NewExplorerStore; NewFieldProfileRepository; NewInterpretationStore; NewInventoryRepository; NewPublicationReconciler; NewPublishStore; NewRecipeExecRegistry; NewRecipeRevisionRegistry; NewResourceRepository; NewResourceWriter; NewSemanticContributionRepository; NewSemanticReadRepository; NewTableShapeCapabilityStore; Open; OpenPool; PoolClient; Query; QueryRow; Readiness; Rollback; Row; Rows; Runtime; Scan; SemanticContributionRepository; Series; SeriesSchemas; SetupPolicy; Unwrap; Validate; VerifyDatasetReleaseSchema; VerifyExplorerControlSchema; VerifyPublicationControlSchema; VerifyRecipeChainsSchema; VerifyRecipeExecRegistriesSchema; VerifySchema; WithAdvisoryLock,internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/redact; internal/backends/postgres/internal/store; internal/catalog; internal/dataset,internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/catalog,cmd/loom-data; internal/backends/postgres; internal/dataframe/benchmark/cmd/benchcli; internal/ingest; internal/server,internal/api/graphql/graph/query; internal/api/http; internal/dataframe/benchmark; internal/dataframe/compiler; internal/dataframe/compiler/render/postgres; internal/dataframe/execution,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn,2 exported interface(s),migration=21,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/control,inventory complete; semantic disposition pending +internal/backends/postgres/internal/catalog,github.com/calypr/loom/internal/backends/postgres/internal/catalog,postgres,domain/library,"Persist and retrieve catalog facts in PostgreSQL 18: publish-last generation-scoped resource inventory builds, bounded field-profile records with completeness and truncation metadata, leased compare-and-swap-guarded semantic inventory build state, deterministic semantic-inventory contribution rows with content-addressed replay and conflict classification, byte-order-stable keyset pages over retained resources, and authorization-scoped semantic-inventory pages and selection resolution served directly from contribution aggregates.",field_discovery.go; field_profile.go; relationships.go; resource_inventory.go; semantic_build.go; semantic_contribution.go; semantic_page.go; store.go,capability_compose_pg_test.go; field_discovery_test.go; field_profile_test.go; relationships_pg_test.go; relationships_test.go; resource_inventory_test.go; semantic_build_test.go; semantic_contribution_test.go; semantic_page_test.go,—,—,4430,11834,55,AdvanceSemanticInventoryBackfill; ClaimSemanticInventoryBackfill; CompleteSemanticInventoryBackfill; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverRelationshipObservations; DiscoverResourceInventory; EnsureSemanticInventoryEntries; Error; FailSemanticInventoryBackfill; FieldProfileStore; FieldProfileUnavailableError; FieldProfileWriteError; InventoryStore; New; NewFieldProfileStore; NewFullStore; NewRelationshipEvidenceRepository; NewRelationshipEvidenceStore; NewSemanticBuildRepository; NewSemanticContributionRepository; NewSemanticInventoryBackfillStore; NewSemanticReadRepository; PageSemanticInventory; PrepareSemanticInventoryBackfill; ReadFieldProfile; ReadRelationshipObservations; ReadResourceInventory; ReadRetainedSemanticInventoryPage; ReadSemanticInventoryBuild; RebuildResourceInventory; RelationshipEvidenceRepository; RelationshipOccurrenceGroup; RelationshipScope; RelationshipStore; ResolveSemanticInventorySelections; ResourceInventoryUnavailableError; ResourceInventoryWriteError; RetainedSemanticSourceMissingError; ScanRelationshipOccurrences; SemanticBuildRepository; SemanticContributionRepository; SemanticInventoryBackfillStore; SemanticInventoryClaimLostError; SemanticInventoryClaimedError; SemanticInventoryContributionConflictError; SemanticInventoryEntriesUnsupportedError; SemanticInventoryStorageMissingError; SemanticInventoryUnsupportedError; SemanticReadRepository; Store; Unwrap; WriteFieldProfile; WriteSemanticInventoryContributions,internal/backends/postgres/internal/store; internal/catalog; internal/fhir/schema,internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/catalog; internal/dataset; internal/fhir/schema; internal/ingest,internal/backends/postgres/control,internal/backends/postgres/internal/catalog,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn; github.com/jackc/pgx/v5/pgxpool,3 exported interface(s),legacy=10; migration=16,one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/catalog,inventory complete; semantic disposition pending internal/backends/postgres/internal/chartifact,github.com/calypr/loom/internal/backends/postgres/internal/chartifact,postgres,domain/library,"Persist private ClickHouse artifact manifests and cleanup leases in the PostgreSQL 18 control plane with CAS progress writes, bounded expired scans, and one-winner cleanup claims while physical tables stay in ClickHouse.",catalog.go,catalog_manager_test.go; catalog_test.go,—,—,535,1605,10,Catalog; ClaimCleanup; Create; DB; Delete; ListExpired; NewCatalog; ReleaseCleanup; Renew; Update,internal/dataframe/execution/chartifact,internal/authscope; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/store; internal/dataframe/execution/chartifact; internal/store/clickhouse,internal/backends/postgres/control,internal/backends/postgres/internal/store,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn,1 exported interface(s),migration=6,one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/chartifact,inventory complete; semantic disposition pending internal/backends/postgres/internal/dataset,github.com/calypr/loom/internal/backends/postgres/internal/dataset,postgres,domain/library,"Persist dataset generation manifests, the active-generation compare-and-swap pointer, and immutable project release candidates with the active-release compare-and-swap pointer in PostgreSQL 18.",commands.go; manifest_lifecycle.go; migrate.go; release_store.go; store.go,commands_test.go; manifest_lifecycle_pg_test.go; release_store_pg_test.go,—,—,1725,2488,28,ActivateGeneration; Backend; Command; CommandBackend; CompareAndSwapActivateRelease; ConnSource; CreateManifest; EnsureGenerationPrerequisites; GenerationOwnerAdapter; MigrateSchema; New; NewPostgreSQLCommand; NewReleaseStore; OnReleaseActivated; OwnerMigrations; Parameter; ParameterType; PostgreSQL; ReadActiveRelease; ReadManifest; ReleaseActivation; ReleaseStore; ResolveActiveManifest; SaveRelease; Scan; Store; TransitionManifest; VerifySchema,internal/backends/postgres/internal/store; internal/dataset,internal/backends/postgres/internal/store; internal/dataset,internal/backends/postgres/control; internal/backends/postgres/internal/explorer,internal/backends/postgres; internal/backends/postgres/internal/store,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn; github.com/jackc/pgx/v5/pgxpool,go:embed; 2 exported interface(s),legacy=1; migration=28; compatibility=4,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/dataset,inventory complete; semantic disposition pending internal/backends/postgres/internal/explorer,github.com/calypr/loom/internal/backends/postgres/internal/explorer,postgres,domain/library,"Persist Explorer owner records, draft saves with atomic version/digest CAS and exact preimage history, insert-once compilation receipts, immutable Explorer revisions, create-once immutable capability snapshots and table-shape capability receipts, project-scoped interpretation libraries advancing heads over immutable content-addressed revisions by transactional parent CAS, the staged selection lifecycle with project-scoped idempotency, an expiring writer owner, and insert-once membership staged in bounded idempotent batches with the digest/count finalized under the current writer fence and bounded cancellation-aware keyset membership paging, atomic publish authoring that commits a verified compilation receipt, the immutable Explorer revision, the active Explorer owner pointer, and the project release activation CAS in one PostgreSQL transaction, and staging explicit-group revision headers with ordered definition rows and insert-once group membership (one row per revision/group/reference so an overlap is several rows, bounded keyset paging, and a fenced digest/count-verified STAGING to COMPLETE transition) under guarded writer-lease, abort, and expired-cleanup lifecycle in PostgreSQL 18 with unique project/Explorer identity, typed columns, lossless JSONB payloads, DB-enforced row freezing and content-digest collision checks, guard-triggered immutability, and CAS-guarded revision lifecycle on the shared pg-store pool and migration runner.",capabilities.go; drafts.go; explicit_group_memberships.go; explicit_groups.go; explorers.go; interpretations.go; publication.go; receipts.go; revisions.go; selection_members.go; selections.go,capabilities_test.go; drafts_test.go; explicit_groups_readpath_test.go; explicit_groups_test.go; explorers_test.go; interpretations_test.go; pgstate02_fixture_test.go; pubauth_fixture_test.go; publication_golden_test.go; publication_guarddrop_test.go; publication_race_test.go; publication_retry_test.go; publication_test.go; publication_zz_test.go; receipts_test.go; revisions_test.go; selection_members_test.go; selections_test.go,—,—,7425,21659,60,AbortExplicitGroupRevision; AbortSelection; AppendExplicitGroupMemberships; AppendSelectionMembers; BeginExplicitGroupRevision; BeginSelection; CapabilitySnapshotStore; CleanupExplicitGroupStaging; CleanupSelectionStaging; CompleteExplicitGroupRevision; CompleteSelection; Create; CreateInterpretationRevision; DigestExplicitGroupRevision; DigestSelectionMembers; ExplicitGroupStore; FailRevision; FailRevisionFrom; Get; GetByIdentity; GetByToken; GetCatalog; GetCatalogForLookup; GetCategoryScan; GetCompilationReceiptForExplorer; GetDraftRevision; GetExplicitGroupRevision; GetInterpretationRevision; GetResolution; GetRevision; GetRevisionForExplorer; GetSelection; InsertCompilationReceipt; InsertRevision; InterpretationStore; List; ListExplicitGroupDefinitions; ListExplicitGroupRevisions; ListInterpretationLibraries; MigrateSchema; New; NewCapabilitySnapshotStore; NewExplicitGroupStore; NewInterpretationStore; NewPublishStore; NewTableShapeCapabilityStore; OwnerMigrations; PublishAuthoring; PublishStore; Put; PutCatalog; PutCategoryScan; PutExplicitGroupDefinitions; PutResolution; SaveDraft; Store; TableShapeCapabilityStore; VerifySchema; VisitExplicitGroupMemberships; VisitSelectionMembers,internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/store; internal/dataframe/publication; internal/dataframe/recipe; internal/dataset; internal/explorer; internal/explorer/capability; internal/explorer/tableshapecap,internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/store; internal/dataframe/publication; internal/dataframe/recipe; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/tableshapecap,internal/backends/postgres/control,internal/backends/postgres/internal/store,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn; github.com/jackc/pgx/v5/pgxpool,go:embed,legacy=1; migration=44,one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/explorer,inventory complete; semantic disposition pending @@ -47,13 +47,15 @@ internal/backends/postgres/internal/recipeexec,github.com/calypr/loom/internal/b internal/backends/postgres/internal/recipes,github.com/calypr/loom/internal/backends/postgres/internal/recipes,postgres,domain/library,Persist project recipe revision chains with CAS head tracking in the PostgreSQL 18 control plane.,revisions.go,revisions_test.go,—,—,409,1072,9,ChainMigrations; DB; Get; List; MigrateSchema; New; Register; RevisionRegistry; VerifySchema,internal/backends/postgres/internal/store; internal/dataframe/errors; internal/dataframe/recipe,internal/backends/postgres/internal/store; internal/dataframe/errors; internal/dataframe/recipe,internal/backends/postgres/control,internal/backends/postgres/internal/store,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn,go:embed; 1 exported interface(s),migration=23,one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/recipes,inventory complete; semantic disposition pending internal/backends/postgres/internal/redact,github.com/calypr/loom/internal/backends/postgres/internal/redact,redact,domain/library,Scrub DSN passwords and secret parameter values from pool-open diagnostics so credential material never reaches rendered error text while the cause chain stays intact.,redact.go,redact_internal_test.go,—,—,79,84,3,ExtractDSNPassword; RedactDSN; ScrubSecretText,—,—,internal/backends/postgres/control,—,—,—,—,one production importer (internal/backends/postgres/control); inspect ownership/inline potential,go test ./internal/backends/postgres/internal/redact,inventory complete; semantic disposition pending internal/backends/postgres/internal/rows,github.com/calypr/loom/internal/backends/postgres/internal/rows,postgres,domain/library,"Stream typed rows from tagged compiled PostgreSQL commands over pgx connections, preserving bound-value integrity and three-state typed decoding, enforcing output column contracts, and classifying cancellation, visitor stops, contract violations, and driver failures with identifiable wrapped causes.",rows.go,rows_boundary_pg18_ab03_test.go; rows_postgres_test.go; rows_postquery_postgres_test.go; rows_test.go,—,—,483,3537,8,Error; Executor; NewExecutor; Querier; QueryError; RowVisitor; SourceRows; Unwrap,internal/backends/postgres/internal/store; internal/dataframe/compiler/command; internal/dataframe/execution,internal/backends/postgres/internal/store; internal/dataframe/compiler/command; internal/dataframe/errors; internal/dataframe/execution,internal/backends/postgres,—,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn; github.com/jackc/pgx/v5/pgtype,1 exported interface(s),migration=1,one production importer (internal/backends/postgres); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/rows,inventory complete; semantic disposition pending -internal/backends/postgres/internal/store,github.com/calypr/loom/internal/backends/postgres/internal/store,postgres,domain/library,"Provide the bounded pgx/v5-backed PostgreSQL 18 source store: readiness-pinged pool, advisory-lock serialized migration runner, scoped FHIR resource repository that accepts the logical internal/source contract on writes and answers reads with its store-owned physical row under the server-assigned keyset sequence, scoped typed edge-occurrence repository, publish-last inventory repository, scoped bounded field-profile record persistence, the ledgered idempotent COPY batch writer for resource and edge rows, and resource-only and edge-only batch facades over that writer.",advisory.go; batch_writer.go; client.go; commit_outcome.go; edge.go; edge_writer.go; errors.go; field_profile.go; inventory.go; migrate.go; migration_ownership.go; resource.go; resource_writer.go,batch_writer_test.go; client_test.go; edge_test.go; edge_writer_test.go; errors_test.go; field_profile_ddl_test.go; field_profile_filter_golden_test.go; inventory_test.go; migrate_test.go; ownership_contract_test.go; resource_test.go; resource_writer_test.go; verify_classify_test.go; verify_readonly_test.go,—,—,5898,13789,112,Acquire; AsUniqueConflict; Assignment; AssignmentConflict; Batch; BatchAccountingError; BatchEdgeConflictError; BatchIdentityConflictError; BatchResourceConflictError; BatchResult; BatchWriteError; BatchWriter; Cause; CheckServerVersion; Client; Close; CommitOutcomeUnknown; Config; ConnSource; ConnectError; DriverCondition; EdgeBatch; EdgeBatchResult; EdgeConflictError; EdgeOccurrenceRow; EdgeRepository; EdgeScope; EdgeSourceMissingError; EdgeWriter; EmbeddedMigrations; Error; FieldProfileCoverage; FieldProfileFilter; FieldProfileRepository; FieldProfileScope; FutureSchemaVersionError; GenerationAdvisoryLockKey; GetResource; HistoryEntry; HistoryManifest; InsertCounts; InsertEdge; InsertResource; InventoryCountConflictError; InventoryCountScope; InventoryPublishGuardError; InventoryRepository; InventorySourceGroup; IsAbortedTransaction; IsRefusedConnection; IsUniqueViolation; LedgerRecord; LoadMigrations; LookupManifest; Manifest; ManifestJSON; Migrate; MigrateResult; MigrateSchema; Migration; MigrationApplyError; MigrationChecksumMismatchError; MigrationRunner; MissingEmbeddedMigrationError; NewBatchWriter; NewEdgeRepository; NewEdgeWriter; NewFieldProfileRepository; NewInventoryRepository; NewMigrationRunner; NewResourceRepository; NewResourceWriter; Open; OutOfOrderMigrationError; Ping; PublishBuild; Query; QueryRow; ReadBuild; ReadCountSums; ReadEdgesFromSource; ReadEdgesToTarget; ReadFieldProfileRows; ReadFieldProfileRowsFiltered; ReadProfileCoverage; ReadResources; ResourceBatch; ResourceBatchResult; ResourceConflictError; ResourceNotFoundError; ResourceRepository; ResourceRow; ResourceWriter; RetryableTransactionConflict; Scan; ScanSourceCounts; SeriesID; TranslateCommitError; TranslateError; TranslatedError; UniqueConflictError; UnsupportedVersionError; Unwrap; UpsertFieldProfileBatch; Validate; ValidateAssignment; Verify; VerifySchema; WithAdvisoryLock; WriteBatch; WriteEdgeBatch; WriteResourceBatch,internal/catalog; internal/dataframe/errors; internal/source,internal/api/http; internal/authscope; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/store; internal/catalog; internal/dataframe/errors; internal/dataframe/execution/chartifact; internal/dataframe/publication; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataset; internal/explorer; internal/source,internal/backends/postgres; internal/backends/postgres/control; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/rows,internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/store,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn; github.com/jackc/pgx/v5/pgxpool,go:embed; 1 exported interface(s),legacy=16; migration=143,broad surface (13 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/store,inventory complete; semantic disposition pending -internal/catalog,github.com/calypr/loom/internal/catalog,catalog,domain/library,"Own persistence-neutral observed FHIR field, pivot, relationship, and capability facts.",auth_scope.go; backend_contract.go; cache.go; capability_evidence.go; generation.go; helpers.go; limits.go; rebuild.go; relationship_evidence.go; relationships.go; resource_inventory.go; retained_semantic_page.go; semantic.go; semantic_coverage.go; semantic_coverage_contract.go; semantic_inventory.go; semantic_schema.go; types.go; write_profiler.go,auth_scope_test.go; cache_test.go; capability_evidence_test.go; generation_test.go; relationship_evidence_census_test.go; relationship_evidence_test.go; relationships_test.go; semantic_coding_display_projection_test.go; semantic_coverage_diagnostics_test.go; semantic_coverage_test.go; semantic_inventory_buildid_cache_test.go; semantic_inventory_category_test.go; semantic_inventory_emitter_anchor_test.go; semantic_inventory_test.go; semantic_schema_identifier_type_test.go; semantic_schema_test.go; semantic_test.go; write_profiler_coverage_test.go; write_profiler_test.go,—,—,6471,3946,146,AuthResourcePathOptions; Cache; CacheOptions; CacheStats; CapabilityEvidence; CapabilityEvidenceOptions; CapabilityEvidenceReader; CensusSemanticCoverage; ConceptCandidate; DecodeSemanticInventoryCursor; DefaultProfileLimits; DiscoverFields; DiscoverReferences; DiscoverSemanticCodeableConceptTemplates; Documents; EffectiveAuthResourcePathsUnrestricted; EncodeSemanticInventoryCursor; EvidenceDiagnostic; EvidenceStatus; EvidenceStatusFor; ExplicitAuthResourcePathsUnrestricted; ExtensionValueObservation; FieldCatalogDocument; FieldEnrichmentDigest; FieldEnrichmentObservation; FieldEnrichmentOptions; FieldEnrichmentResult; FieldProfileScope; FieldProfileWriter; HasDatasetGeneration; InvalidRelationship; InvalidateAll; InvalidateProject; InventoryRebuilder; LegacySemanticBindingIDV1; Merge; MergeRelationshipCounts; NewCache; NewCacheWithOptions; NewProfilerForGenerationWithLimits; NewResourceInventoryBuild; NewSemanticInventoryBuild; NewSemanticInventoryEmitter; NewShapePlanCacheWithLimit; NewShapePlanCacheWithLimits; NormalizeDatasetGeneration; NormalizeProfileLimits; ObservePayload; ObservePayloadWithCoverage; ObservePayloadWithInventory; PopulatedField; PopulatedFieldOptions; PopulatedReference; PopulatedReferenceOptions; ProfileLimits; Profiler; ProjectRelationshipObservation; ReadCapabilityEvidence; ReconcileSemanticCoverage; RelationshipAuditOptions; RelationshipAuditSummary; RelationshipCatalogDocument; RelationshipCatalogDocuments; RelationshipCountsFromRawEdges; RelationshipDiscoverer; RelationshipEvidenceKey; RelationshipEvidenceKeyFor; RelationshipEvidenceLess; RelationshipKey; RelationshipObservation; RelationshipObservationDigest; RelationshipObservationFromKey; RelationshipObservationOptions; RelationshipObservationResult; RelationshipRebuildOptions; RelationshipRebuildSummary; RepeatedShapeDigest; ResourceInventoryBuild; ResourceInventoryBuildKey; ResourceInventoryCount; ResourceInventoryCoverageDigest; ResourceInventoryDigest; ResourceInventoryObservation; ResourceInventoryOptions; ResourceInventoryRebuildOptions; ResourceInventoryRebuildSummary; ResourceInventoryResult; RetainedSemanticInventoryPage; RetainedSemanticInventoryRow; RetentionBytes; SemanticCodeableConceptTemplate; SemanticContributor; SemanticCoverageBuildState; SemanticCoverageCandidatePath; SemanticCoverageCandidateResult; SemanticCoverageCandidateStatus; SemanticCoverageCensus; SemanticCoverageCensusGap; SemanticCoverageCensusStatus; SemanticCoverageCounts; SemanticCoverageDiagnostic; SemanticCoverageDispositionKind; SemanticCoverageEmissionEvidence; SemanticCoverageEmissionSink; SemanticCoverageFacetAggregate; SemanticCoverageLookupOptions; SemanticCoverageLookupResult; SemanticCoverageOccurrenceKind; SemanticCoverageOwnerLocator; SemanticCoveragePathAggregate; SemanticCoveragePresence; SemanticCoverageReason; SemanticCoverageReference; SemanticCoverageReferenceKind; SemanticCoverageRole; SemanticCoverageSourceContribution; SemanticCoverageSourceIdentity; SemanticCoverageSourceSink; SemanticInventoryBuild; SemanticInventoryBuildID; SemanticInventoryBuildKey; SemanticInventoryCheckpoint; SemanticInventoryContribution; SemanticInventoryEmitter; SemanticInventoryEntry; SemanticInventoryFamily; SemanticInventoryFamilyID; SemanticInventoryFamilyReference; SemanticInventoryObservationSink; SemanticInventoryPage; SemanticInventoryPageDigest; SemanticInventoryPageOptions; SemanticInventoryReference; SemanticInventoryResolveOptions; SemanticInventoryResolveResult; SemanticInventorySelectionMode; SemanticInventoryState; SemanticObservation; SemanticObservationCompleteness; SemanticObservationKey; SemanticObservationSource; SemanticObservationValue; ShapePlanCache; Stats; Truncated; ValidateSemanticInventoryBuild,internal/fhir/schema; internal/fhir/semantic,generated/fhirschema; internal/fhir/schema; internal/fhir/semantic,cmd/arango-fhir-proto; cmd/loom-data; internal/api/graphql/graph/query; internal/api/recipe; internal/authscope; internal/backends/arango/internal/catalog; internal/backends/postgres/control; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/explorer/authoringv2; internal/explorer/lifecycle; internal/ingest; internal/server,internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/explorer/capability; internal/explorer/compilation,github.com/bytedance/sonic,5 exported interface(s),legacy=13; migration=1; compatibility=1,broad surface (19 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/catalog,inventory complete; semantic disposition pending +internal/backends/postgres/internal/store,github.com/calypr/loom/internal/backends/postgres/internal/store,postgres,domain/library,"Provide the bounded pgx/v5-backed PostgreSQL 18 source store: readiness-pinged pool, advisory-lock serialized migration runner, scoped FHIR resource repository that accepts the logical internal/source contract on writes and answers reads with its store-owned physical row under the server-assigned keyset sequence, scoped typed edge-occurrence repository, publish-last inventory repository, scoped bounded field-profile record persistence, the ledgered idempotent COPY batch writer for resource and edge rows, and resource-only and edge-only batch facades over that writer.",advisory.go; batch_writer.go; client.go; commit_outcome.go; edge.go; edge_writer.go; errors.go; field_profile.go; inventory.go; migrate.go; migration_ownership.go; resource.go; resource_writer.go,batch_writer_test.go; client_test.go; edge_test.go; edge_writer_test.go; errors_test.go; field_profile_auth_paths_test.go; field_profile_ddl_test.go; field_profile_filter_golden_test.go; inventory_test.go; migrate_test.go; ownership_contract_test.go; resource_test.go; resource_writer_test.go; verify_classify_test.go; verify_readonly_test.go,—,—,5971,13963,113,Acquire; AsUniqueConflict; Assignment; AssignmentConflict; Batch; BatchAccountingError; BatchEdgeConflictError; BatchIdentityConflictError; BatchResourceConflictError; BatchResult; BatchWriteError; BatchWriter; Cause; CheckServerVersion; Client; Close; CommitOutcomeUnknown; Config; ConnSource; ConnectError; DriverCondition; EdgeBatch; EdgeBatchResult; EdgeConflictError; EdgeOccurrenceRow; EdgeRepository; EdgeScope; EdgeSourceMissingError; EdgeWriter; EmbeddedMigrations; Error; FieldProfileCoverage; FieldProfileFilter; FieldProfileRepository; FieldProfileScope; FutureSchemaVersionError; GenerationAdvisoryLockKey; GetResource; HistoryEntry; HistoryManifest; InsertCounts; InsertEdge; InsertResource; InventoryCountConflictError; InventoryCountScope; InventoryPublishGuardError; InventoryRepository; InventorySourceGroup; IsAbortedTransaction; IsRefusedConnection; IsUniqueViolation; LedgerRecord; ListDistinctAuthResourcePaths; LoadMigrations; LookupManifest; Manifest; ManifestJSON; Migrate; MigrateResult; MigrateSchema; Migration; MigrationApplyError; MigrationChecksumMismatchError; MigrationRunner; MissingEmbeddedMigrationError; NewBatchWriter; NewEdgeRepository; NewEdgeWriter; NewFieldProfileRepository; NewInventoryRepository; NewMigrationRunner; NewResourceRepository; NewResourceWriter; Open; OutOfOrderMigrationError; Ping; PublishBuild; Query; QueryRow; ReadBuild; ReadCountSums; ReadEdgesFromSource; ReadEdgesToTarget; ReadFieldProfileRows; ReadFieldProfileRowsFiltered; ReadProfileCoverage; ReadResources; ResourceBatch; ResourceBatchResult; ResourceConflictError; ResourceNotFoundError; ResourceRepository; ResourceRow; ResourceWriter; RetryableTransactionConflict; Scan; ScanSourceCounts; SeriesID; TranslateCommitError; TranslateError; TranslatedError; UniqueConflictError; UnsupportedVersionError; Unwrap; UpsertFieldProfileBatch; Validate; ValidateAssignment; Verify; VerifySchema; WithAdvisoryLock; WriteBatch; WriteEdgeBatch; WriteResourceBatch,internal/catalog; internal/dataframe/errors; internal/source,internal/api/http; internal/authscope; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/store; internal/catalog; internal/dataframe/errors; internal/dataframe/execution/chartifact; internal/dataframe/publication; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataset; internal/explorer; internal/source,internal/backends/postgres; internal/backends/postgres/control; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/rows,internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/store,github.com/jackc/pgx/v5; github.com/jackc/pgx/v5/pgconn; github.com/jackc/pgx/v5/pgxpool,go:embed; 1 exported interface(s),legacy=17; migration=143,broad surface (13 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/backends/postgres/internal/store,inventory complete; semantic disposition pending +internal/catalog,github.com/calypr/loom/internal/catalog,catalog,domain/library,"Own persistence-neutral observed FHIR field, pivot, relationship, and capability facts.",auth_scope.go; backend_contract.go; cache.go; capability_evidence.go; generation.go; helpers.go; limits.go; rebuild.go; relationship_evidence.go; relationships.go; resource_inventory.go; retained_semantic_page.go; semantic.go; semantic_coverage.go; semantic_coverage_contract.go; semantic_inventory.go; semantic_schema.go; types.go; write_profiler.go,auth_scope_test.go; cache_test.go; capability_evidence_test.go; generation_test.go; relationship_evidence_census_test.go; relationship_evidence_test.go; relationships_test.go; semantic_coding_display_projection_test.go; semantic_coverage_diagnostics_test.go; semantic_coverage_test.go; semantic_inventory_buildid_cache_test.go; semantic_inventory_category_test.go; semantic_inventory_emitter_anchor_test.go; semantic_inventory_test.go; semantic_schema_identifier_type_test.go; semantic_schema_test.go; semantic_test.go; write_profiler_coverage_test.go; write_profiler_test.go,—,—,6471,3946,146,AuthResourcePathOptions; Cache; CacheOptions; CacheStats; CapabilityEvidence; CapabilityEvidenceOptions; CapabilityEvidenceReader; CensusSemanticCoverage; ConceptCandidate; DecodeSemanticInventoryCursor; DefaultProfileLimits; DiscoverFields; DiscoverReferences; DiscoverSemanticCodeableConceptTemplates; Documents; EffectiveAuthResourcePathsUnrestricted; EncodeSemanticInventoryCursor; EvidenceDiagnostic; EvidenceStatus; EvidenceStatusFor; ExplicitAuthResourcePathsUnrestricted; ExtensionValueObservation; FieldCatalogDocument; FieldEnrichmentDigest; FieldEnrichmentObservation; FieldEnrichmentOptions; FieldEnrichmentResult; FieldProfileScope; FieldProfileWriter; HasDatasetGeneration; InvalidRelationship; InvalidateAll; InvalidateProject; InventoryRebuilder; LegacySemanticBindingIDV1; Merge; MergeRelationshipCounts; NewCache; NewCacheWithOptions; NewProfilerForGenerationWithLimits; NewResourceInventoryBuild; NewSemanticInventoryBuild; NewSemanticInventoryEmitter; NewShapePlanCacheWithLimit; NewShapePlanCacheWithLimits; NormalizeDatasetGeneration; NormalizeProfileLimits; ObservePayload; ObservePayloadWithCoverage; ObservePayloadWithInventory; PopulatedField; PopulatedFieldOptions; PopulatedReference; PopulatedReferenceOptions; ProfileLimits; Profiler; ProjectRelationshipObservation; ReadCapabilityEvidence; ReconcileSemanticCoverage; RelationshipAuditOptions; RelationshipAuditSummary; RelationshipCatalogDocument; RelationshipCatalogDocuments; RelationshipCountsFromRawEdges; RelationshipDiscoverer; RelationshipEvidenceKey; RelationshipEvidenceKeyFor; RelationshipEvidenceLess; RelationshipKey; RelationshipObservation; RelationshipObservationDigest; RelationshipObservationFromKey; RelationshipObservationOptions; RelationshipObservationResult; RelationshipRebuildOptions; RelationshipRebuildSummary; RepeatedShapeDigest; ResourceInventoryBuild; ResourceInventoryBuildKey; ResourceInventoryCount; ResourceInventoryCoverageDigest; ResourceInventoryDigest; ResourceInventoryObservation; ResourceInventoryOptions; ResourceInventoryRebuildOptions; ResourceInventoryRebuildSummary; ResourceInventoryResult; RetainedSemanticInventoryPage; RetainedSemanticInventoryRow; RetentionBytes; SemanticCodeableConceptTemplate; SemanticContributor; SemanticCoverageBuildState; SemanticCoverageCandidatePath; SemanticCoverageCandidateResult; SemanticCoverageCandidateStatus; SemanticCoverageCensus; SemanticCoverageCensusGap; SemanticCoverageCensusStatus; SemanticCoverageCounts; SemanticCoverageDiagnostic; SemanticCoverageDispositionKind; SemanticCoverageEmissionEvidence; SemanticCoverageEmissionSink; SemanticCoverageFacetAggregate; SemanticCoverageLookupOptions; SemanticCoverageLookupResult; SemanticCoverageOccurrenceKind; SemanticCoverageOwnerLocator; SemanticCoveragePathAggregate; SemanticCoveragePresence; SemanticCoverageReason; SemanticCoverageReference; SemanticCoverageReferenceKind; SemanticCoverageRole; SemanticCoverageSourceContribution; SemanticCoverageSourceIdentity; SemanticCoverageSourceSink; SemanticInventoryBuild; SemanticInventoryBuildID; SemanticInventoryBuildKey; SemanticInventoryCheckpoint; SemanticInventoryContribution; SemanticInventoryEmitter; SemanticInventoryEntry; SemanticInventoryFamily; SemanticInventoryFamilyID; SemanticInventoryFamilyReference; SemanticInventoryObservationSink; SemanticInventoryPage; SemanticInventoryPageDigest; SemanticInventoryPageOptions; SemanticInventoryReference; SemanticInventoryResolveOptions; SemanticInventoryResolveResult; SemanticInventorySelectionMode; SemanticInventoryState; SemanticObservation; SemanticObservationCompleteness; SemanticObservationKey; SemanticObservationSource; SemanticObservationValue; ShapePlanCache; Stats; Truncated; ValidateSemanticInventoryBuild,internal/fhir/schema; internal/fhir/semantic,generated/fhirschema; internal/fhir/schema; internal/fhir/semantic,cmd/arango-fhir-proto; cmd/loom-data; internal/api/graphql/graph/query; internal/api/recipe; internal/authscope; internal/backends/arango/internal/catalog; internal/backends/postgres/control; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/explorer/authoringv2; internal/explorer/lifecycle; internal/ingest; internal/server,internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/resolver; internal/dataframe/benchmark; internal/dataframe/compiler; internal/explorer/capability; internal/explorer/compilation,github.com/bytedance/sonic,5 exported interface(s),legacy=13; migration=1; compatibility=1,broad surface (19 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/catalog,inventory complete; semantic disposition pending internal/ci,github.com/calypr/loom/internal/ci,ci,domain/library,"Exercise repository-wide Go formatting and vet gates, including injected fault detection.",—,gofmt_gate_test.go; vet_gate_test.go,—,—,0,283,0,—,—,—,—,—,—,—,—,no in-repo production importer; verify runtime/tooling reachability,go test ./internal/ci,inventory complete; semantic disposition pending +internal/dataframe/benchmark,github.com/calypr/loom/internal/dataframe/benchmark,benchmark,domain/library,"Run the Arango-versus-PostgreSQL matched dataframe benchmark: deterministic fixtures, canonical bags and correctness gates evaluated before any timing is claimed, control runs, and report artifacts that a re-computing verifier must accept, with ClickHouse-backed publication legs disclosed as planned/not_run when the service is absent.",armplan.go; authoring.go; canonical.go; controls.go; driver.go; fixtures.go; gates.go; harness.go; matrix.go; measure.go; oracle.go; provision.go; report.go; report_md.go,armplan_test.go; authoring_test.go; canonical_test.go; controls_test.go; driver_test.go; fixtures_fhir_test.go; fixtures_test.go; gates_test.go; harness_scope_test.go; harness_test.go; matrix_test.go; measure_test.go; oracle_test.go; publication_test.go; report_test.go; scratch_authrepro_test.go; scratch_liveupload_test.go,README.md,—,5506,3534,154,AgeBandFor; AppendWorkloadDisclosures; Apply; ApplyExcludedWorkloadGateReports; ArangoBackendYAML; Arm; ArmConfig; ArmEvidence; AuthorWorkload; AuthoringStatus; AuthoringVerification; BackendResult; Bag; BothArmsPlan; BuildAllWorkspaces; BuildEmptyFloorWorkspace; BuildReport; BuildServerConfigYAML; BuildWorkspace; BuilderState; CBool; CList; CMissing; CNull; CNumber; CObject; CString; CacheLabel; CanonicalBag; CanonicalPair; CanonicalRow; CanonicalValue; CanonicalizeRows; CanonicalizeValue; Claim; ClaimableTimings; Clock; Close; ConditionFacts; ControlEmptyFloorMillis; ControlRuns; ControlSelfRatio; ControlUnstableCells; CreateExplorer; DatasetParams; DatasetSize; Digest; DriverOptions; ElapsedMillis; EvaluateGates; EvaluateGatesSingleArm; EvaluateNegativeAuth; EvaluateNegativeAuthSingleArm; ExcludedWorkloadGateReports; ExecuteOutput; ExpectedOutcomeForScope; Facts; FetchBuilderState; FirstDifference; FixtureFiles; FormatExclusionList; FreePort; GateFunc; GateName; GateResult; GenerateFacts; GenerateFiles; Grant; LegPlan; LiveBatteryExclusions; LoadReport; LogPath; MakeBearerToken; MarshalReport; MeasureArms; MeasureEmptyFloor; MockFence; NegativeAuthControl; NegativeAuthControlSingleArm; NewMockFence; NewMonotonicClock; NewSpeedRatio; NewStorageIDMap; NotRunBackends; Now; ObservationFacts; OracleBackedCount; OracleFor; Outcome; OutputIDFor; PatientFacts; PlannedGateReports; PlannedNotRun; PlannedNotRunForBackends; PlannedPlan; PostgresBackendYAML; PostgresOnlyPlan; PreviewResult; Publication; PublicationDisclosures; PublicationExcluded; PublicationGateReports; PublicationPlan; PublicationTimed; PublicationZeroEvidence; PublishWorkspace; Reconcile; RenderMarkdown; Report; Requests; Restart; RunBenchmark; RunConfig; RunIdentity; RunOptions; RunPlan; RunStatus; RunStatusScoped; Sample; ServerProcess; ServerSpec; Since; SingleArmGateFunc; SnapshotToken; SortedBattery; SpawnFunc; SpawnServer; SpecimenFacts; SpecsByPreset; SpeedRatio; Stop; StorageIDMap; Summarize; Summary; SummaryRow; ThroughputSummary; URL; UnsupportedCases; UploadGeneration; ValidBackend; ValidScope; Validate; ValidateBattery; ValidateLegPlan; ValidatePlannedNotRun; ValidatePublicationPlan; VerifyReport; VersionProbeFunc; WorkloadCategory; WorkloadDisclosures; WorkloadGateReport; WorkloadOutcome; WorkloadSpec; Write,internal/explorer/authoringv2,generated/fhir; internal/authscope; internal/backends/postgres/control; internal/catalog; internal/dataframe/benchmark; internal/explorer/authoringv2,internal/dataframe/benchmark/cmd/benchcli,internal/dataframe/benchmark,—,2 exported interface(s),migration=1,one production importer (internal/dataframe/benchmark/cmd/benchcli); inspect ownership/inline potential; broad surface (14 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/benchmark,inventory complete; semantic disposition pending +internal/dataframe/benchmark/cmd/benchcli,github.com/calypr/loom/internal/dataframe/benchmark/cmd/benchcli,main,domain/library,"Own the benchmark command-line entry point: fail-closed live-run preflight, planned disclosure artifacts, report verification authority, and the loom_bench PostgreSQL database lifecycle.",deployer.go; main.go; probes.go; setup.go,guards_test.go,—,—,1016,374,0,—,internal/backends/postgres/control; internal/dataframe/benchmark,internal/dataframe/benchmark,—,—,—,—,migration=1,no in-repo production importer; verify runtime/tooling reachability,go test ./internal/dataframe/benchmark/cmd/benchcli,inventory complete; semantic disposition pending internal/dataframe/columntransform,github.com/calypr/loom/internal/dataframe/columntransform,columntransform,domain/library,"Define closed, backend-neutral transformations over one selected output column value.",types.go,types_test.go,—,—,79,61,7,CategoryMapping; Clone; ExactCategoryRecode; Kind; UnknownPolicy; Validate; ValueTransformation,—,—,internal/dataframe/compiler/lower; internal/dataframe/recipe; internal/explorer/authoringv2,internal/dataframe/compiler; internal/explorer/compilation,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/columntransform,inventory complete; semantic disposition pending internal/dataframe/compiler,github.com/calypr/loom/internal/dataframe/compiler,compiler,domain/library,"Orchestrate semantic planning, physical lowering, optimization, rendering, and compiled-query assembly.",api.go; category_scan.go; category_scan_filter_pushdown.go; category_scan_related.go; cell_trace.go; clone_helpers.go; compiled_query.go; construction_preview_root_id_filter.go; graph_execution.go; ir_helpers.go; optimizer.go; output.go; population_mapping.go; postgres_target.go; preview_covering_index.go; recipe_execution.go; recipe_paging.go; row_lineage.go; semantic_group_projection_preview.go; table_shape_exclusions.go; target.go,backend_leaf_guard_test.go; backend_ownership_guard_contract_test.go; category_scan_arango_test.go; category_scan_parent_presence_arango_test.go; category_scan_related_collection_identity_test.go; category_scan_related_presence_arango_test.go; category_scan_related_test.go; category_scan_test.go; cell_trace_test.go; cohort_drop_boundary_arango_test.go; command_helpers_external_test.go; command_helpers_test.go; compiled_command_test.go; construction_arango_test.go; construction_coded_group_arango_test.go; construction_coded_group_execution_test.go; construction_coded_pivot_test.go; construction_execution_test.go; construction_group_expand_arango_test.go; construction_group_preview_liveness_arango_test.go; construction_group_related_summary_arango_test.go; construction_group_source_streaming_test.go; construction_pivot_lineage_arango_test.go; construction_preview_root_id_filter_test.go; construction_related_count_arango_test.go; construction_related_expand_arango_test.go; construction_related_group_preview_arango_test.go; correlated_arango_test.go; correlated_legacy_compat_test.go; correlated_physical_test.go; correlated_selection_test.go; correlated_system_version_arango_test.go; explicit_cohort_values_arango_test.go; filter_semantics_test.go; filter_test.go; generic_physical_plan_test.go; grain_test.go; group_connected_field_source_arango_test.go; group_field_source_arango_test.go; nested_expansion_arango_test.go; operator_oracles_arango_test.go; physical_diagnostics_test.go; physical_lowering_test.go; physical_optimize_test.go; physical_plan_test.go; physical_projection_reserved_name_arango_test.go; physical_render_test.go; physical_required_match_test.go; physical_scope_test.go; pivot_presence_arango_audit_test.go; pivot_presence_audit_test.go; plan_test_helpers_test.go; population_frontier_arango_test.go; population_mapping_filter_exists_test.go; population_mapping_identity_guard_test.go; population_mapping_routed_group_arango_test.go; population_mapping_test.go; postgres_coded_group_compile_test.go; postgres_parity_fixture_test.go; postgres_target_fingerprint_golden_test.go; public_columns_freeze_test.go; recipe_execution_pg18_test.go; recipe_execution_test.go; recipe_paging_related_filter_sequence_test.go; recipe_paging_routed_exists_arango_test.go; recipe_paging_test.go; root_page_related_seed_arango_test.go; row_expansion_test.go; row_item_projection_arango_test.go; row_lineage_cohort_filter_arango_test.go; row_lineage_composed_arango_test.go; row_lineage_group_pivot_arango_test.go; row_lineage_group_rows_arango_test.go; row_lineage_test.go; selected_source_contract_test.go; selection_semantics_test.go; selector_first_limit_test.go; semantic_group_arango_test.go; semantic_group_preview_arango_test.go; semantic_group_projection_factorization_arango_test.go; semantic_validation_test.go; source_group_preview_arango_test.go; streaming_missing_category_pivot_test.go; table_reshape_arango_test.go; table_reshape_execution_window_test.go; table_shape_exclusions_test.go; workspace_output_capture_test.go,—,—,8120,33173,44,ArangoTarget; Backend; CategoryOverflowWitness; CategoryScanProof; CategoryScanRefusal; CategoryScanRefusalCode; CategoryScanRefusalCodeOf; CompileCategoryScanOutput; CompileCategoryScanStage; CompileCellTraceOutput; CompileClickHousePrefix; CompilePopulationMappingOutput; CompileRecipeOutput; CompileRecipeOutputPage; CompileResolvedGraphQuery; CompileResolvedRecipePlan; CompileRowLineageOutput; CompileTableShapeExclusions; CompiledCategoryScanQuery; CompiledCellTraceQuery; CompiledClickHousePrefix; CompiledOutputPage; CompiledPopulationMappingQuery; CompiledQuery; CompiledRowLineageQuery; CompiledTableShapeExclusionQuery; Error; PostgreSQLTarget; PreviewCoveringIndexReplacement; PreviewCoveringIndexSpec; PreviewGroupProjectionIndexSpec; PreviewGroupProjectionSpec; PreviewGroupScanSpec; PublicOutputColumns; RenderPreviewGroupProjectionAssessment; RenderPreviewGroupProjectionFactorization; RenderPreviewPhysicalPlanWithRootIndexHint; RowLineageCapability; RowLineageCapabilityForOutput; RowLineageUnsupportedError; TableShapeExclusionRefusal; TableShapeExclusionRefusalCode; Target; Valid,internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec,internal/authscope; internal/backends/arango; internal/backends/postgres/control; internal/catalog; internal/dataframe/columntransform; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/explorer/capability; internal/fhir/schema; internal/projectid; internal/source; scripts/packagepolicy,cmd/dataframe-profile; cmd/preview-covering-index-artifact; internal/api/graphql/graph/query; internal/dataframe/execution; internal/explorer/lifecycle; internal/server,conformance/compiler; internal/api/graphql/graph; internal/api/graphql/graph/resolver; internal/backends/postgres; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/render/aql; internal/explorer/compilation,—,—,legacy=2; compatibility=1,broad surface (21 production files); inspect cohesion,go test ./internal/dataframe/compiler,inventory complete; semantic disposition pending internal/dataframe/compiler/capability,github.com/calypr/loom/internal/dataframe/compiler/capability,capability,domain/library,Probe which dataframe language capabilities the compiler can prove and support.,probe.go; schema_fields.go,probe_test.go; schema_fields_test.go,—,—,803,313,22,CandidateCapability; CandidateRequest; Chart; Filter; Options; OwnerRecordsCapability; OwnerRecordsRequest; ProbeCandidate; ProbeOwnerRecords; ProbeProjection; ProbeRoot; ProbeTraversal; Rendered; Result; RootCapability; RootRequest; SchemaFieldDescriptor; SchemaFieldDescriptors; Scope; Traversal; TraversalCapability; TraversalRequest,internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema,internal/authscope; internal/dataframe/recipe; internal/dataframe/spec; internal/fhir/schema,internal/explorer/lifecycle; internal/server,—,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/compiler/capability,inventory complete; semantic disposition pending -internal/dataframe/compiler/command,github.com/calypr/loom/internal/dataframe/compiler/command,command,domain/library,"Own the closed dispatch-tagged executable command types, rendered PostgreSQL plan validation, and constructor guarantees shared by compilers and row adapters.",command.go; rendered_postgres.go,compiled_command_test.go; imports_guard_test.go; rendered_postgres_test.go,—,—,433,526,15,AQLCommand; AQLParameters; Backend; CompiledBackend; CompiledCommand; Dispatch; NewAQLCommand; NewPostgreSQLCommand; NewRenderedPostgresPhysicalPlan; PostgreSQLCommand; PostgreSQLParameter; PostgreSQLParameterType; PostgreSQLParameters; RenderedPostgresPhysicalPlan; WithAQLBinds,—,internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/render/aql; internal/dataframe/recipe; internal/dataframe/semantic,internal/backends/postgres; internal/backends/postgres/internal/rows; internal/dataframe/compiler; internal/dataframe/compiler/render/postgres; internal/dataframe/execution; internal/server,conformance/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/render/aql; internal/explorer/compilation,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/compiler/command,inventory complete; semantic disposition pending +internal/dataframe/compiler/command,github.com/calypr/loom/internal/dataframe/compiler/command,command,domain/library,"Own the closed dispatch-tagged executable command types, rendered PostgreSQL plan validation, and constructor guarantees shared by compilers and row adapters.",command.go; rendered_postgres.go,compiled_command_test.go; imports_guard_test.go; rendered_postgres_test.go,—,—,433,526,15,AQLCommand; AQLParameters; Backend; CompiledBackend; CompiledCommand; Dispatch; NewAQLCommand; NewPostgreSQLCommand; NewRenderedPostgresPhysicalPlan; PostgreSQLCommand; PostgreSQLParameter; PostgreSQLParameterType; PostgreSQLParameters; RenderedPostgresPhysicalPlan; WithAQLBinds,—,internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/render/aql; internal/dataframe/recipe; internal/dataframe/semantic,internal/api/graphql/graph/resolver; internal/backends/postgres; internal/backends/postgres/internal/rows; internal/dataframe/compiler; internal/dataframe/compiler/render/postgres; internal/dataframe/execution; internal/server,conformance/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/render/aql; internal/explorer/compilation,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/compiler/command,inventory complete; semantic disposition pending internal/dataframe/compiler/ir,github.com/calypr/loom/internal/dataframe/compiler/ir,ir,domain/library,"Define typed physical operations, values, scope proofs, policies, and validation.",clone.go; dataset_generation.go; diagnostics.go; doc.go; navigation.go; physical_combine.go; physical_combine_scalar.go; physical_operations.go; physical_stage_sequence.go; physical_stage_validation.go; physical_table_shape.go; physical_types.go; physical_validation.go; physical_validation_expr.go; physical_validation_refs.go; physical_validation_shapes.go; physical_values.go; policy.go; postgres_storage.go; prefix.go; scope.go; selector_helpers.go; whole_relation_scope_evidence.go; workspace_combine_scope_operand.go,group_related_field_source_validation_test.go; physical_combine_scalar_test.go; physical_combine_test.go; physical_group_pivot_lineage_validation_test.go; physical_root_page_candidate_seed_test.go; physical_scope_cycle_test.go; physical_stage_validation_test.go; population_mapping_test.go; population_root_test.go; related_coding_group_lineage_validation_test.go; row_lineage_trace_validation_test.go; subplan_test.go; unit_validation_test.go; unnest_test.go; workspace_output_validation_test.go,—,—,12283,1804,215,AddDecision; CanonicalExecutionPhysicalPlan; ClickHouseArtifactIdentity; ClickHouseCombineScalarBaseType; ClonePhysicalExpression; ClonePhysicalOperation; ClonePhysicalOperations; ClonePhysicalPlan; ClonePhysicalPredicateExpression; ClonePhysicalSubplan; Column; CompilerPlanDiagnostics; DatasetGenerationBindValue; DecomposePhysicalTraversalPrefixAt; DefaultPhysicalOptimizationPolicy; Digest; Error; EstimatePreparedSelectorWork; EstimateTraversalSharingWork; Identifier; IsRegistered; Kind; Matches; MustColumn; Name; NewPhysicalOptimizationReport; NewPostgresStorageBinding; NewPostgresTableIdentifier; NewPrivateAQLPrefixScopeOperand; NewPublishedWorkspaceScopeOperand; NewWholeRelationScopeEvidence; NewWorkspaceCombineScopeEvidence; NewWorkspaceOutputScopeOperand; NormalizeDatasetGeneration; PhysicalAggregate; PhysicalAggregateOperation; PhysicalCall; PhysicalCardinality; PhysicalCellTraceConstruction; PhysicalCellTraceConstructionInput; PhysicalCellTraceContribution; PhysicalCellTraceRelatedSource; PhysicalCellTraceReshape; PhysicalCellTraceReshapeSource; PhysicalCellTraceReshapeSourceKind; PhysicalCellTraceReturn; PhysicalClickHouseCombine; PhysicalClickHousePrefix; PhysicalCodedGroupPathSegment; PhysicalCohortRootSource; PhysicalCollectionScan; PhysicalCombineInputRef; PhysicalCombineKey; PhysicalCombineKind; PhysicalCombineOutputColumn; PhysicalCombineProjection; PhysicalConstructionStage; PhysicalContributorWindow; PhysicalCorrelation; PhysicalDerivedLet; PhysicalDocumentLookup; PhysicalEngine; PhysicalExpression; PhysicalExpressionKind; PhysicalExpressionLet; PhysicalExpressionProjection; PhysicalExtract; PhysicalFilter; PhysicalGraphReturn; PhysicalGroupMemberValue; PhysicalGroupRows; PhysicalGroupRowsCellTrace; PhysicalGroupedCodedPivotCategory; PhysicalGroupedPivot; PhysicalGroupedPivotCategory; PhysicalGroupedPivotKey; PhysicalKeySet; PhysicalKeySetLookup; PhysicalKeyedMap; PhysicalLimit; PhysicalLiteral; PhysicalMapReduction; PhysicalNullBehavior; PhysicalObject; PhysicalObjectKeys; PhysicalObjectLookup; PhysicalOperation; PhysicalOperationKind; PhysicalOptimizationDecision; PhysicalOptimizationPolicy; PhysicalOptimizationReport; PhysicalOptimizationRule; PhysicalOptimizationRuleState; PhysicalOwnerRecords; PhysicalPathExtend; PhysicalPathNode; PhysicalPathRelationship; PhysicalPathSeed; PhysicalPivotCategoryMatchKind; PhysicalPivotMap; PhysicalPlan; PhysicalPlanDiagnostics; PhysicalPopulationMappingIdentityPart; PhysicalPopulationMappingReturn; PhysicalPopulationRootSource; PhysicalPredicate; PhysicalPredicateExpression; PhysicalPredicateKind; PhysicalPrefixSourceBackend; PhysicalPreparedField; PhysicalPreparedReference; PhysicalPreparedSet; PhysicalProjection; PhysicalProjectionPresence; PhysicalRelatedField; PhysicalReturn; PhysicalRootPageCandidateSeed; PhysicalRootScan; PhysicalRowLineageReturn; PhysicalRowLineageStageMatch; PhysicalRowLineageTrace; PhysicalScopeWindowEnd; PhysicalSelectorExecutionMode; PhysicalSelectorFallback; PhysicalSelectorValueDemand; PhysicalSet; PhysicalSetKind; PhysicalSetOutput; PhysicalSetOutputField; PhysicalSetProjection; PhysicalSetProjectionField; PhysicalSetReduction; PhysicalSetReductionField; PhysicalSetReductionMode; PhysicalSlice; PhysicalSort; PhysicalSource; PhysicalStageCodedGroup; PhysicalStageCohortGroup; PhysicalStageColumn; PhysicalStageExpand; PhysicalStageGroup; PhysicalStageGroupAggregate; PhysicalStageGroupCodingKey; PhysicalStageGroupCurrentStageFieldSource; PhysicalStageGroupFieldSource; PhysicalStageGroupFieldSourceKind; PhysicalStageGroupKey; PhysicalStageGroupMissingKeyPolicy; PhysicalStageGroupRelatedOccurrenceAdapter; PhysicalStageGroupRelatedOccurrenceFieldSource; PhysicalStageGroupRelatedProjection; PhysicalStageGroupRelatedRouteStep; PhysicalStageGroupSemanticKey; PhysicalStageOperationKind; PhysicalStagePopulationMappingReturn; PhysicalStageRelatedExpand; PhysicalStageRelatedField; PhysicalStageRelatedRecordAnchor; PhysicalStageRelatedRouteStep; PhysicalStageRelatedSource; PhysicalStageRowValue; PhysicalStageSequence; PhysicalSubplan; PhysicalTableShapeExclusionReturn; PhysicalTemporalOrdering; PhysicalTraversal; PhysicalTraversalDecision; PhysicalTraversalDirection; PhysicalTraversalPrefix; PhysicalTraversalPrefixDecomposition; PhysicalTraversalPrefixError; PhysicalTraversalPrefixRejectionReason; PhysicalTraversalStrategy; PhysicalTraversalSubset; PhysicalUnitNormalization; PhysicalUnnest; PhysicalUnnestAncestor; PhysicalUnnestEmptyPolicy; PhysicalUnnestExactGroupShapeEqual; PhysicalUnnestOwner; PhysicalUnnestRouteStep; PhysicalUnnestSortKeys; PhysicalUnpivot; PhysicalUnpivotIdentityPart; PhysicalUnpivotInput; PhysicalUnpivotOutput; PhysicalValue; PostgresColumnIdentifier; PostgresLogicalTable; PostgresLogicalTableKind; PostgresStorageBinding; PostgresTableIdentifier; RegisteredPostgresColumnNames; ResolvedClickHouseColumn; ResolvedClickHousePrivateArtifact; ResolvedClickHouseTable; RichConsumerGroup; RichSourceReuse; RuleEnabled; SQLText; Schema; Table; TableKind; TotalConsumers; Validate; ValidateForWorkspaceCompilation; ValidateGenericPhysicalPlanScope; ValidateScope; ValidateWithPrivateStage; ValidateWithWorkspaceArtifacts; WholeRelationScopeEvidence; WholeRelationScopeIdentity; WithRule; WorkspaceCombineScopeOperand,internal/dataframe/spec; internal/dataframe/unit; internal/fhir/schema; internal/projectid,internal/dataframe/spec; internal/dataframe/unit,cmd/dataframe-profile; cmd/preview-covering-index-artifact; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/clickhouse; internal/dataframe/compiler/render/postgres; internal/dataframe/execution; internal/explorer/lifecycle; internal/server,conformance/compiler; internal/backends/postgres; internal/dataframe/compiler/command; internal/explorer/compilation; internal/store/clickhouse,—,—,legacy=9; migration=7; compatibility=4,broad surface (24 production files); inspect cohesion,go test ./internal/dataframe/compiler/ir,inventory complete; semantic disposition pending internal/dataframe/compiler/lower,github.com/calypr/loom/internal/dataframe/compiler/lower,lower,domain/library,Lower logical FHIR dataframe plans into storage-aware physical IR.,auth_scope.go; child_sets.go; column_naming.go; correlated_binding.go; doc.go; filter_literal.go; graph_plan.go; group_field_source_context.go; group_field_source_related.go; group_related_occurrence_lower.go; group_rows.go; helpers.go; plan_orchestration.go; population.go; recipe_column_transformations.go; recipe_compile.go; recipe_construction.go; recipe_construction_coded_group.go; recipe_construction_coded_pivot.go; recipe_construction_combine.go; recipe_construction_combine_metadata.go; recipe_construction_composite.go; recipe_construction_group_sources.go; recipe_construction_related_eligibility.go; recipe_construction_related_expand.go; recipe_construction_related_field.go; recipe_construction_reshape.go; recipe_derived_columns.go; recipe_dynamic_columns.go; recipe_expression.go; recipe_expression_lowering.go; recipe_schema.go; recipe_table_reshape.go; required_match.go; root_projection.go; row_expansion.go; storage_route.go; traversal.go; typed_predicate.go; whole_relation_scope.go; workspace_output_dependencies.go,bound_scalar_coding_group_test.go; child_set_selector_demand_test.go; construction_group_streaming_test.go; correlated_binding_test.go; empty_bound_group_source_test.go; graph_plan_test.go; group_field_source_context_test.go; group_related_occurrence_lower_test.go; group_rows_test.go; group_specimen_inbound_coding_test.go; owner_records_test.go; physical_nested_scope_test.go; population_test.go; recipe_construction_coded_group_test.go; recipe_construction_coded_pivot_display_test.go; recipe_construction_coded_pivot_test.go; recipe_construction_combine_test.go; recipe_construction_composite_test.go; recipe_construction_related_count_test.go; recipe_construction_reshape_test.go; recipe_construction_row_values_test.go; recipe_construction_test.go; recipe_derived_columns_test.go; recipe_expression_test.go; recipe_output_test.go; recipe_schema_cardinality_test.go; recipe_semantic_path_test.go; recipe_table_reshape_test.go; recipe_unpivot_capability_test.go; root_id_filter_test.go; row_expansion_test.go; traversal_test.go; whole_relation_scope_evidence_test.go; workspace_combine_scope_evidence_test.go; workspace_output_compile_test.go,—,—,12219,10802,40,BindGroupRelatedOccurrenceSource; BuildGenericPhysicalPlanWithPolicy; BuildGraphPhysicalPlan; BuildPhysicalTraversal; CloneCompiledOutputSchema; CompileResolvedGraphPlan; CompileResolvedRecipePlan; CompiledActiveRelatedRecordStage; CompiledGroupFieldOwner; CompiledGroupFieldSourceAncestor; CompiledGroupFieldSourceContext; CompiledGroupFieldSourceExpansion; CompiledGroupFieldSourceRouteStep; CompiledOutputColumn; CompiledRecipe; CompiledRecipeOutput; CompiledRelatedExpandAnchor; CompiledRelatedExpandStage; CompiledRelatedRecordAnchor; CompiledStageDescriptor; ComposeClickHouseCombine; ConstructionCombineColumnType; DescribeConstructionSourceStage; DynamicColumnMetadata; Error; LowerCorrelatedBinding; LowerCorrelatedBindingWithVersion; LowerCorrelatedPredicate; LowerCorrelatedPredicateWithIdentity; LowerExtensionBinding; LowerRecipeExpression; PivotReducerTypeError; RelatedEligibilityAnchorError; RelatedSourceAnchorError; ResolveGroupRelatedOccurrenceSource; StageOperationCapability; TableReshapeScalarKind; TraversalLoweringRequest; TraversalLoweringResult; WorkspaceOutputSource,internal/authscope; internal/dataframe/columntransform; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/expression; internal/dataframe/lineage; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/fhir/schema; internal/projectid,internal/authscope; internal/dataframe/columntransform; internal/dataframe/compiler/ir; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/errors; internal/dataframe/expression; internal/dataframe/lineage; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/fhir/schema,internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/explorer/compilation; internal/explorer/lifecycle; internal/server,internal/backends/postgres; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres,—,—,legacy=3; compatibility=3,broad surface (41 production files); inspect cohesion,go test ./internal/dataframe/compiler/lower,inventory complete; semantic disposition pending internal/dataframe/compiler/optimize,github.com/calypr/loom/internal/dataframe/compiler/optimize,optimize,domain/library,Apply explainable semantics-preserving rewrites to validated physical IR.,doc.go; optimize.go,optimize_test.go,—,—,438,124,1,OptimizePhysicalPlanWithPolicy,internal/dataframe/compiler/ir,internal/dataframe/compiler/ir,internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution,internal/dataframe/compiler/lower,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/compiler/optimize,inventory complete; semantic disposition pending @@ -61,7 +63,7 @@ internal/dataframe/compiler/render/aql,github.com/calypr/loom/internal/dataframe internal/dataframe/compiler/render/clickhouse,github.com/calypr/loom/internal/dataframe/compiler/render/clickhouse,clickhouse,storage adapter,Render typed Combine plans and resolved published or private table inputs as deterministic ClickHouse SQL.,combine.go; workspace_combine.go,combine_test.go; membership_nullable_integration_test.go; workspace_combine_test.go,—,—,930,1093,6,RenderCombine; RenderCombineWithLimit; RenderCompositeClickHouseCombine; RenderWorkspaceClickHouseCombine; RenderedCombine; WorkspaceArtifact,internal/dataframe/compiler/ir,internal/dataframe/compiler/ir; internal/store/clickhouse,internal/dataframe/execution,internal/store/clickhouse,—,—,—,one production importer (internal/dataframe/execution); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts,go test ./internal/dataframe/compiler/render/clickhouse,inventory complete; semantic disposition pending internal/dataframe/compiler/render/postgres,github.com/calypr/loom/internal/dataframe/compiler/render/postgres,postgres,storage adapter,"Render the supported physical IR read spine and typed selector extractions with two-valued JSONB path-presence evidence onto allowlisted PostgreSQL storage bindings as deterministic parameterized SQL with ordered typed arguments, failing closed on every unsupported plan shape.",calls.go; category_scan.go; cell_trace.go; coded_group.go; graph.go; group.go; null_semantics.go; objects.go; pivot.go; population.go; population_mapping.go; predicates.go; render.go; root_scan.go; row_lineage.go; scope.go; scope_ops.go; selectors.go; sets.go; stage_related_occurrence.go; stage_semantic.go; stage_sequence.go; table_shape_exclusions.go; traversal.go; unit_normalization.go; unnest.go; unpivot.go; window.go,calls_live_test.go; calls_test.go; category_scan_test.go; cell_trace_test.go; coded_group_test.go; coded_pivot_test.go; graph_test.go; group_test.go; main_test.go; null_semantics_test.go; obj11_production_test.go; objects_test.go; pivot14_noncoded_test.go; pivot_test.go; population_live_test.go; population_mapping_test.go; population_test.go; predicates_pg18_test.go; predicates_test.go; render_pg18_test.go; render_test.go; root_scan_test.go; row_lineage_test.go; scope_ops_witness_test.go; scope_test.go; selectors_test.go; sets_live_test.go; sets_test.go; stage_related_occurrence_p4_test.go; stage_related_occurrence_production_test.go; stage_related_occurrence_test.go; stage_semantic_production_test.go; stage_semantic_test.go; stage_sequence_production_test.go; stage_sequence_test.go; table_shape_exclusions_live_test.go; table_shape_exclusions_test.go; traversal_test.go; unit_normalization_production_test.go; unit_normalization_test.go; unit_normalization_tie_test.go; unnest_test.go; unpivot_test.go; window_test.go,—,—,23444,42018,18,NewPostgresPopulationRootBinding; PostgresPopulationRootBinding; RenderCategoryScan; RenderGraphPhysicalPlan; RenderGroupedCodedPivot; RenderGroupedPivot; RenderPhysicalPlan; RenderPopulationMappingPlan; RenderPopulationRootScan; RenderRootScan; RenderSelectorExtraction; RenderTableShapeExclusions; RenderUnnest; RenderUnpivot; RenderedCategoryScan; Validate; ValidatePopulationMappingSource; ValidatePopulationRootSource,internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/spec; internal/dataframe/unit; internal/fhir/schema,internal/backends/postgres/control; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/explorer; internal/fhir/schema; internal/source,internal/dataframe/compiler; internal/dataframe/execution,—,—,—,legacy=13; migration=20; compatibility=2,broad surface (28 production files); inspect cohesion; storage adapter; preserve interface and persisted-data contracts,go test ./internal/dataframe/compiler/render/postgres,inventory complete; semantic disposition pending internal/dataframe/errors,github.com/calypr/loom/internal/dataframe/errors,dataframeerrors,domain/library,Define and normalize stable structured dataframe errors for transport presentation.,errors.go,errors_test.go,—,—,488,172,21,AsUserError; Code; Details; Error; ErrorCode; ErrorOption; FieldPath; IsFeatureResolutionCode; IsRetryableCode; NewError; Normalize; PublicMessage; Retryable; SanitizePersistedFailure; Unwrap; UserError; WithCause; WithDetails; WithFieldPath; WithRetryable; Wrap,—,—,internal/api/authpolicy; internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/backends/arango/internal/recipeexec; internal/backends/arango/internal/store; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/store; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer/lifecycle; internal/server,internal/backends/arango/internal/publication; internal/backends/postgres; internal/backends/postgres/internal/rows; internal/dataframe/compiler,—,1 exported interface(s),legacy=1,small shared semantic contract; compare direct callers before inlining; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/errors,inventory complete; semantic disposition pending -internal/dataframe/execution,github.com/calypr/loom/internal/dataframe/execution,execution,domain/library,"Prepare, authorize, execute, explain, and post-process compiled dataframe requests.",category_scan.go; cell_trace.go; clickhouse_artifact.go; control.go; control_explain_types.go; control_types.go; engine.go; execution_service.go; explain.go; explain_postgres.go; group_preview_cache.go; group_preview_scan.go; helpers.go; named_uuid.go; population_mapping.go; postquery.go; preview_capabilities.go; query_budget.go; related_preview_paging.go; row_lineage.go; service.go; table_shape_exclusions.go; types.go,auth_scope_test.go; boundary_helpers_test.go; category_scan_postgres_test.go; category_scan_test.go; cell_trace_postgres_test.go; cell_trace_row_not_found_class_test.go; cell_trace_test.go; clickhouse_artifact_test.go; cohort_preview_paging_arango_test.go; command_boundary_contract_test.go; construction_cell_trace_arango_test.go; control_validate_digest_test.go; engine_blank_project_review_test.go; engine_clickhouse_test.go; engine_postgres_capture_pg18_test.go; engine_postgres_capture_test.go; engine_postgres_category_pg18_test.go; engine_postgres_cell_trace_pg18_test.go; engine_postgres_exclusions_pg18_test.go; engine_postgres_postquery_pg18_test.go; engine_postgres_preview_pg18_test.go; engine_postgres_test.go; engine_run_output_class_test.go; engine_test.go; engine_workspace_artifact_test.go; execution_test.go; explain_test.go; group_preview_cache_test.go; group_preview_scan_test.go; helpers_test.go; identity_test.go; named_uuid_test.go; population_group_preview_test.go; population_mapping_postgres_pg18_test.go; population_mapping_test.go; postgres_explain_parser_internal_test.go; postgres_target_fixture_test.go; postquery_test.go; preview_capability_boundary_test.go; recipe_paging_postgres_adapter_test.go; recipe_paging_postgres_test.go; recipe_preview_paging_postgres_internal_test.go; row_lineage_postgres_pg18_test.go; row_lineage_test.go; selected_source_contract_test.go; table_shape_exclusions_test.go; vendor_import_guard_test.go; verify_arch01_pins_test.go,—,—,7088,27056,109,BuildPostgreSQLExplainCommand; CategoryScanRequest; CategoryScanResult; CategoryValue; CellTrace; CellTraceCompiled; CellTraceContribution; CellTraceRequest; CellTraceResult; CellTraceStatus; ClickHouseQueryRows; CombinePortsConfigured; CompileResolvedBundle; CompiledQueryFingerprint; CompilerPlanDiagnostics; Config; Control; DynamicColumnCheck; DynamicDriftError; Engine; Error; Explain; ExplainAssessment; ExplainCollectionScan; ExplainIndexLocation; ExplainIndexSummary; ExplainPhysical; ExplainPlanEstimate; ExplainResolved; ExplainWarning; Materialize; MaterializeBundle; MaterializeClickHouseArtifact; MaterializeResolvedBundle; MaterializeVersion; New; NewService; OutputRows; OutputStream; PhysicalExplanation; PhysicalOutputExplanation; PopulationMapping; PopulationMappingCompiled; PopulationMappingDiagnostic; PopulationMappingRequest; PopulationMappingResult; PopulationMappingStatus; PopulationMemberReader; PopulationMemberReaderFunc; PostgreSQLExplainPlan; PostgreSQLPlanAssessment; PostgreSQLPlanEstimate; PostgreSQLPlanJSONExecutor; PostgreSQLPlanNode; PostgreSQLRootPageSteps; PostgreSQLScanEvidence; Preview; PreviewBundle; PreviewCollectionIndex; PreviewCollectionScanEvidence; PreviewCollectionScans; PreviewIndexCoverage; PreviewIndexNodeEvidence; PreviewOutput; PreviewRequest; PreviewResolvedBundle; PreviewSummary; PrivateClickHouseArtifactInput; PrivateClickHouseArtifactSource; QueryDiagnostics; QueryMemoryBudgetFromContext; ResolutionError; Resolve; ResolveBundle; ResolveClickHouseInputs; ResolveVersion; Resolved; Result; RowIdentity; RowLineage; RowLineageContributor; RowLineageRequest; RowLineageResult; RowVisitor; RowsRequest; Run; RunCompiled; ScanCategories; ScanCategoriesCompiled; Service; ServiceConfig; SourceRows; Stream; StreamResult; Streams; TableShapeExclusion; TableShapeExclusionRequest; TableShapeExclusionResult; TableShapeExclusionStatus; TableShapeExclusions; TableShapeExclusionsCompiled; TableShapeSourceIdentity; Unwrap; Validate; Validation; VisitMembers; WithExecutionReadPins; WithPrivateClickHouseArtifacts; WithQueryMemoryBudget,internal/authscope; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/clickhouse; internal/dataframe/compiler/render/postgres; internal/dataframe/errors; internal/dataframe/execution/chartifact; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/projectid,internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer; internal/explorer/capability; internal/explorer/lifecycle; internal/projectid; internal/source; internal/store/clickhouse,internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/postgres; internal/backends/postgres/internal/rows; internal/explorer/lifecycle; internal/server,internal/api/graphql/graph; internal/dataframe/compiler; internal/dataframe/execution,github.com/google/uuid,1 exported interface(s),legacy=4; deprecated=1; compatibility=22,broad surface (23 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/execution,inventory complete; semantic disposition pending +internal/dataframe/execution,github.com/calypr/loom/internal/dataframe/execution,execution,domain/library,"Prepare, authorize, execute, explain, and post-process compiled dataframe requests.",category_scan.go; cell_trace.go; clickhouse_artifact.go; control.go; control_explain_types.go; control_types.go; engine.go; execution_service.go; explain.go; explain_postgres.go; group_preview_cache.go; group_preview_scan.go; helpers.go; named_uuid.go; population_mapping.go; postquery.go; preview_capabilities.go; query_budget.go; related_preview_paging.go; row_lineage.go; service.go; table_shape_exclusions.go; types.go,auth_scope_test.go; backend_neutral_diagnostics_test.go; boundary_helpers_test.go; category_scan_postgres_test.go; category_scan_test.go; cell_trace_postgres_test.go; cell_trace_row_not_found_class_test.go; cell_trace_test.go; clickhouse_artifact_test.go; cohort_preview_paging_arango_test.go; command_boundary_contract_test.go; construction_cell_trace_arango_test.go; control_validate_digest_test.go; engine_blank_project_review_test.go; engine_clickhouse_test.go; engine_postgres_capture_pg18_test.go; engine_postgres_capture_test.go; engine_postgres_category_pg18_test.go; engine_postgres_cell_trace_pg18_test.go; engine_postgres_exclusions_pg18_test.go; engine_postgres_postquery_pg18_test.go; engine_postgres_preview_pg18_test.go; engine_postgres_test.go; engine_run_output_class_test.go; engine_test.go; engine_workspace_artifact_test.go; execution_test.go; explain_test.go; group_preview_cache_test.go; group_preview_scan_test.go; helpers_test.go; identity_test.go; named_uuid_test.go; population_group_preview_test.go; population_mapping_postgres_pg18_test.go; population_mapping_test.go; postgres_explain_parser_internal_test.go; postgres_target_fixture_test.go; postquery_test.go; preview_capability_boundary_test.go; recipe_paging_postgres_adapter_test.go; recipe_paging_postgres_test.go; recipe_preview_paging_postgres_internal_test.go; row_lineage_postgres_pg18_test.go; row_lineage_test.go; selected_source_contract_test.go; table_shape_exclusions_test.go; vendor_import_guard_test.go; verify_arch01_pins_test.go,—,—,7241,27582,110,BuildPostgreSQLExplainCommand; CategoryScanRequest; CategoryScanResult; CategoryValue; CellTrace; CellTraceCompiled; CellTraceContribution; CellTraceRequest; CellTraceResult; CellTraceStatus; ClickHouseQueryRows; CombinePortsConfigured; CompileResolvedBundle; CompiledQueryFingerprint; CompilerPlanDiagnostics; Config; Control; DynamicColumnCheck; DynamicDriftError; Engine; Error; Explain; ExplainAssessment; ExplainCollectionScan; ExplainIndexLocation; ExplainIndexSummary; ExplainPhysical; ExplainPlanEstimate; ExplainResolved; ExplainWarning; Materialize; MaterializeBundle; MaterializeClickHouseArtifact; MaterializeResolvedBundle; MaterializeVersion; New; NewService; OutputRows; OutputStream; PhysicalExplanation; PhysicalOutputExplanation; PopulationMapping; PopulationMappingCompiled; PopulationMappingDiagnostic; PopulationMappingRequest; PopulationMappingResult; PopulationMappingStatus; PopulationMemberReader; PopulationMemberReaderFunc; PostgreSQLExplainPlan; PostgreSQLPlanAssessment; PostgreSQLPlanEstimate; PostgreSQLPlanJSONExecutor; PostgreSQLPlanNode; PostgreSQLRootPageSteps; PostgreSQLScanEvidence; Preview; PreviewBundle; PreviewCollectionIndex; PreviewCollectionScanEvidence; PreviewCollectionScans; PreviewIndexCoverage; PreviewIndexNodeEvidence; PreviewOutput; PreviewRequest; PreviewResolvedBundle; PreviewSummary; PrivateClickHouseArtifactInput; PrivateClickHouseArtifactSource; QueryDiagnostics; QueryMemoryBudgetFromContext; ResolutionError; Resolve; ResolveBundle; ResolveClickHouseInputs; ResolveVersion; Resolved; Result; RowIdentity; RowLineage; RowLineageContributor; RowLineageRequest; RowLineageResult; RowVisitor; RowsRequest; Run; RunCompiled; ScanCategories; ScanCategoriesCompiled; Service; ServiceConfig; SourcePlanFingerprint; SourceRows; Stream; StreamResult; Streams; TableShapeExclusion; TableShapeExclusionRequest; TableShapeExclusionResult; TableShapeExclusionStatus; TableShapeExclusions; TableShapeExclusionsCompiled; TableShapeSourceIdentity; Unwrap; Validate; Validation; VisitMembers; WithExecutionReadPins; WithPrivateClickHouseArtifacts; WithQueryMemoryBudget,internal/authscope; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/clickhouse; internal/dataframe/compiler/render/postgres; internal/dataframe/errors; internal/dataframe/execution/chartifact; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/projectid,internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer; internal/explorer/capability; internal/explorer/lifecycle; internal/projectid; internal/source; internal/store/clickhouse,internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/postgres; internal/backends/postgres/internal/rows; internal/explorer/lifecycle; internal/server,internal/api/graphql/graph; internal/dataframe/compiler; internal/dataframe/execution,github.com/google/uuid,1 exported interface(s),legacy=5; deprecated=1; compatibility=22,broad surface (23 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/execution,inventory complete; semantic disposition pending internal/dataframe/execution/chartifact,github.com/calypr/loom/internal/dataframe/execution/chartifact,chartifact,domain/library,Manage private ClickHouse staging tables and leases used to cross execution boundaries without publishing outputs.,artifact.go,artifact_test.go,—,—,1038,490,24,Abort; Artifact; Begin; Catalog; CheckLease; ClickHouse; Column; ColumnsFromCompiledOutput; Config; Finalize; Identity; LeaseContext; Manager; Manifest; New; NormalizeSchema; Progress; Reconcile; Release; SchemaDigest; ScopeDigest; State; Write; Writer,internal/authscope; internal/dataframe/compiler/lower; internal/dataframe/expression; internal/store/clickhouse,internal/authscope; internal/dataframe/compiler/lower; internal/dataframe/expression; internal/store/clickhouse,internal/backends/arango/internal/chartifact; internal/backends/postgres/internal/chartifact; internal/dataframe/execution; internal/server,internal/backends/postgres/internal/store,github.com/google/uuid,2 exported interface(s),legacy=1,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/execution/chartifact,inventory complete; semantic disposition pending internal/dataframe/expression,github.com/calypr/loom/internal/dataframe/expression,expression,domain/library,Define and validate the typed backend-neutral expression AST used by recipes.,expression_checking.go; expression_types.go; recipe_adapter.go,expression_test.go; recipe_adapter_test.go,—,—,856,235,26,AllowsMany; Call; Cardinality; Check; CheckedExpression; Constant; Document; DocumentRef; Expression; FromRecipe; FromRecipeInContexts; Function; Literal; NodeKind; NullBehavior; NullType; Optional; Select; SelectorRef; SelectorResolver; String; Type; TypeContext; Valid; Validate; ValueKind,internal/dataframe/recipe,internal/dataframe/recipe,internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution/chartifact; internal/dataframe/semantic; internal/explorer; internal/explorer/lifecycle; internal/server,internal/dataframe/execution,—,—,compatibility=3,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/expression,inventory complete; semantic disposition pending internal/dataframe/lineage,github.com/calypr/loom/internal/dataframe/lineage,lineage,domain/library,Define stable identities and provenance for columns expanded from one authored source slot.,source_child.go,source_child_test.go,—,—,163,77,7,CloneSourceChild; Coordinate; IsSourceChildID; ParseSourceChildID; SourceChild; SourceChildKind; StableSourceChildID,—,—,internal/dataframe/compiler/lower; internal/dataframe/recipe; internal/explorer/authoringv2; internal/explorer/compilation,internal/server,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/lineage,inventory complete; semantic disposition pending @@ -77,19 +79,19 @@ internal/dataframe/unit,github.com/calypr/loom/internal/dataframe/unit,unit,doma internal/dataset,github.com/calypr/loom/internal/dataset,dataset,domain/library,"Define immutable dataset generation manifests, releases, active pointers, and lifecycle validation.",active_resolver.go; dataframe_selector.go; doc.go; manifest.go; manifest_port.go; ref.go; release.go; schema.go; validation.go,active_resolver_test.go; manifest_test.go; memory_store_test.go; ref_test.go; release_id_framing_test.go; release_idempotency_test.go; release_requirements_e2e_test.go; release_test.go; release_verifier_fault_classification_test.go; schema_test.go,README.md,—,802,2253,34,Activate; ActivationRequest; Active; ActiveRelease; ActiveReleaseResolver; ActiveResolver; ContractVerification; Create; DataframeSelector; Error; IsStaged; Key; Manifest; ManifestLifecycle; ManifestReader; NewManifest; NewRef; NewSchemaSnapshot; ProjectRelease; PublicationVerification; PublicationVerifier; Ref; ReleasePublication; ReleaseRepository; ReleaseRequirementsError; ReleaseService; ResolveActive; SchemaSnapshot; State; Transition; Unwrap; Valid; Validate; ValidateGeneration,—,internal/backends/arango; internal/dataset,cmd/arango-fhir-proto; cmd/loom-data; internal/api/bulk/load; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/arango/internal/dataset; internal/backends/arango/internal/explorer; internal/backends/postgres/control; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/explorer; internal/explorer/lifecycle; internal/ingest; internal/server,internal/api/graphql/graph; internal/backends/arango/internal/publication; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/dataset,—,6 exported interface(s),legacy=1; compatibility=2,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataset,inventory complete; semantic disposition pending internal/explorer,github.com/calypr/loom/internal/explorer,explorer,domain/library,"Define Explorer aggregate identities, configs, revisions, receipts, output contracts, stores, and core service operations.",artifact.go; authoring_errors.go; canonical_json.go; compilation.go; compilation_receipt.go; config_v2.go; explicit_group_repository.go; explicit_groups.go; identity.go; interpretation.go; output_contract.go; selection.go; service.go; state_v1.go; store.go; types.go; viewer_projection.go,artifact_test.go; compilation_receipt_test.go; explicit_groups_test.go; interpretation_test.go; output_contract_test.go; repository_revision_id_test.go; selection_test.go; types_test.go; viewer_projection_order_test.go,—,—,4335,1587,165,AbortSelection; ActivateRepositoryGeneration; ActiveRevision; AppendSelectionMembers; ApplyWorkspaceCommands; ApplyWorkspaceCommandsChecked; ArtifactCompletion; ArtifactID; ArtifactRecord; ArtifactStage; ArtifactState; ArtifactStore; AuthoringDiagnostic; AuthoringError; BeginSelection; BuildViewerProjection; Canonical; CanonicalContent; CanonicalExplicitGroupDefinitions; CanonicalExplicitGroupMemberships; CanonicalInterpretationProject; CleanupSelectionStaging; CompilationArtifactDigest; CompilationKey; CompilationReceipt; CompilationReceiptForExplorer; CompilationWarning; CompleteSelection; ComputeContentDigest; ComputeRevisionID; ConfigAction; ConfigChart; ConfigColumn; ConfigExplorer; ConfigFilter; ConfigManagementForID; ConfigTable; ConfigV2; ConfigView; ConstructionProposalBinding; CreateInteractiveFrom; DatasetMetadata; DatasetOutput; DecodePublicOutputContracts; Diagnostic; DigestSelectionMembers; DraftRevision; DraftRevisionReader; EmittedColumn; Error; ExplicitGroupDefinition; ExplicitGroupDefinitionDigest; ExplicitGroupID; ExplicitGroupMembership; ExplicitGroupMembershipCursor; ExplicitGroupMembershipDigest; ExplicitGroupRepository; ExplicitGroupRevision; ExplicitGroupRevisionID; ExplicitGroupRevisionIDFor; ExplicitGroupRevisionState; Explorer; ExplorerRuntimeBindingV1; ExplorerRuntimeColumnV1; ExplorerRuntimeOutputV1; ExplorerRuntimeSchemaV1; ExplorerRuntimeTableColumnV1; ExplorerRuntimeTableV1; ExplorerRuntimeV1; ExplorerStateV1; ExplorerStateV1Active; ExplorerStateV1Draft; ExplorerStateV1Generated; ExplorerSummaryV1; FailRevision; FileActions; Get; GetDraftRevision; GetRevision; GetSelection; IdentityMapping; InterpretationApplicability; InterpretationContentDigest; InterpretationFeatureDefinition; InterpretationLibrary; InterpretationLibraryID; InterpretationPriority; InterpretationRepository; InterpretationRevision; InterpretationRevisionID; InterpretationRule; InterpretationRuleID; InterpretationStructuralCandidate; InterpretationStructuralMatch; Is; List; LoadExplorerState; ManagementMode; Matches; Materialization; MembershipDigest; MigrateLegacyConstructionSteps; NewInterpretationContentDigest; NewInterpretationLibraryID; NewInterpretationRevisionID; NewInterpretationRuleID; NewService; Output; PopulationMemberRemovalProposalBinding; PrepareInterpretationRevision; PublicOutputColumn; PublicOutputContract; PublicOutputContracts; PublicUnitNormalization; PublicUnitRuleIdentity; PublicationMetadata; PublishAuthoring; ReceiptCompiledOutputColumn; ReceiptConstructionActiveRelatedRecord; ReceiptConstructionCodedGroupChoice; ReceiptConstructionGroupFieldOwner; ReceiptConstructionGroupFieldSourceAncestor; ReceiptConstructionGroupFieldSourceContext; ReceiptConstructionGroupFieldSourceExpansion; ReceiptConstructionGroupFieldSourceRouteStep; ReceiptConstructionOperationChoice; ReceiptConstructionRelatedExpand; ReceiptConstructionRelatedExpandAnchor; ReceiptConstructionStage; ReceiptConstructionStageColumn; ReceiptContractSupportedForExecution; ReceiptID; RepositoryRevisionID; ResolvedInterpretation; ResourceRef; Revision; RevisionIdentityEnvelope; RevisionStatus; RowDefinitionProposalBinding; SelectRule; SelectionFilter; SelectionMember; SelectionRevision; SelectionRule; SelectionRuleDigest; SelectionRuleDigestWithMembers; SelectionSource; Service; SharedFilter; StableExplorerID; StartExplicitGroupMembershipDigest; Store; StoreCompilationReceipt; TableShapeProposalBinding; Unwrap; UpsertRepositoryV2; Validate; ValidateAgainst; ValidateID; ValidateSource; ValidateSourceSelection; ViewerProjectionIntegrityError; VisitSelectionMembers; WriteExplicitGroupMembershipFrame; WriteMembershipFrame,internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/unit; internal/dataset; internal/explorer/authoringv2; internal/explorer/capability; internal/projectid,internal/dataframe/publication; internal/dataframe/recipe; internal/dataframe/unit; internal/explorer/authoringv2,cmd/explorer-config-v2-convert; cmd/loom-acceptance; internal/backends/arango/internal/explorer; internal/backends/postgres/internal/explorer; internal/explorer/artifactfs; internal/explorer/compilation; internal/explorer/lifecycle; internal/server,internal/backends/postgres/internal/store; internal/dataframe/compiler/render/postgres; internal/dataframe/execution,—,6 exported interface(s),legacy=17; migration=2; compatibility=4,broad surface (17 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/explorer,inventory complete; semantic disposition pending internal/explorer/artifactfs,github.com/calypr/loom/internal/explorer/artifactfs,artifactfs,domain/library,Persist and serve immutable Explorer training artifacts from a dedicated filesystem root.,store.go,reconcile_test.go; store_test.go,—,—,379,621,10,Abort; Begin; Close; Commit; Get; New; Open; Reconcile; Store; Write,internal/dataframe/published; internal/explorer,internal/dataframe/published; internal/explorer,internal/server,—,—,—,—,one production importer (internal/server); inspect ownership/inline potential,go test ./internal/explorer/artifactfs,inventory complete; semantic disposition pending -internal/explorer/authoringv2,github.com/calypr/loom/internal/explorer/authoringv2,authoringv2,domain/library,"Define, validate, canonicalize, and mutate the V2 Explorer Builder authoring workspace.",availability.go; canonical.go; commands.go; construction.go; construction_change.go; construction_choice.go; construction_migration.go; construction_row_values.go; construction_source_children.go; construction_validation.go; framing.go; migration.go; route_identity.go; row_change.go; row_definition.go; semantic_command.go; semantic_types.go; table_shape.go; transformations.go; types.go; validation.go; value_transformations.go; workspace_output_dependencies.go,commands_test.go; compile_preparation_test.go; construction_choice_test.go; construction_combine_test.go; construction_expand_test.go; construction_formal_replay_test.go; construction_group_coded_pivot_test.go; construction_group_field_source_test.go; construction_projection_replay_test.go; construction_proposal_test.go; construction_related_contributor_test.go; construction_row_values_test.go; construction_source_children_test.go; construction_test.go; correlated_test.go; framing_test.go; interpretation_test.go; migration_test.go; population_member_proposal_test.go; row_change_test.go; row_definition_test.go; row_proposal_test.go; semantic_codeable_concept_fallback_test.go; semantic_coded_category_test.go; semantic_command_test.go; table_shape_proposal_test.go; table_shape_test.go; transformations_test.go; types_test.go; value_transformations_test.go; workspace_output_dependencies_test.go; workspace_test.go,—,—,13260,8548,231,Action; ActionColumn; Addable; AggregateSource; AggregateTransformationCapabilities; AggregateTransformationCapabilitiesForCapability; AggregateTransformationCapabilitiesForCatalog; AnalyzeConstructionCandidate; AnalyzeStepEdit; ApplyCommands; ApplyCommandsRequest; ApplyCommandsResponse; ApplyInterpretationCandidate; ApplyRowChange; ApplyRowReplacement; ArithmeticOperand; AssessRowChange; AssessRowReplacement; BuilderState; CanPopulateConstructionRows; CanonicalJSON; CatalogAvailability; CatalogAvailabilityReadiness; CatalogAvailabilitySourceReadiness; CatalogCandidate; CatalogDiagnostic; CatalogEdge; CatalogNode; CatalogObservedField; CatalogObservedRelationship; CatalogObservedResource; CatalogSnapshot; ChartPresentation; CodeValue; Column; ColumnOutput; ColumnSource; ColumnTransformationChange; ColumnValueTransformationCapabilities; ColumnValueTransformationCapabilitiesForCatalog; Command; CommandResult; ConceptCandidate; Construction; ConstructionCanCarrySourceProjection; ConstructionChoiceSelection; ConstructionCodedGroup; ConstructionCodedGroupSource; ConstructionCodedPivot; ConstructionCodedPivotCategory; ConstructionCodedPivotSource; ConstructionCombine; ConstructionCombineJoinType; ConstructionCombineKey; ConstructionCombineKind; ConstructionCombineMembershipMode; ConstructionCombineProjection; ConstructionCombineRightMatchPolicy; ConstructionDependencyIssue; ConstructionDerive; ConstructionDerivedOperation; ConstructionDivisionByZeroPolicy; ConstructionExpand; ConstructionExpandEmptyPolicy; ConstructionExpandedFieldSource; ConstructionFilter; ConstructionFilterOperator; ConstructionFilterValueKind; ConstructionGroup; ConstructionGroupAggregate; ConstructionGroupAggregateOp; ConstructionGroupFieldCurrentStage; ConstructionGroupFieldRelatedOccurrence; ConstructionGroupFieldRelatedRouteStep; ConstructionGroupFieldSource; ConstructionGroupFieldSourceInput; ConstructionGroupKey; ConstructionGroupMissingKeyPolicy; ConstructionGroupSemanticCategory; ConstructionID; ConstructionImpact; ConstructionInputKind; ConstructionInputRef; ConstructionLiteral; ConstructionMissingInputPolicy; ConstructionNumericKind; ConstructionOperand; ConstructionOperandKind; ConstructionOperation; ConstructionOperationKind; ConstructionOutputPresentation; ConstructionPivot; ConstructionPivotCategory; ConstructionPivotDuplicatePolicy; ConstructionPivotMissingCellPolicy; ConstructionPivotUnlistedCategoryPolicy; ConstructionRelatedContributorRule; ConstructionRelatedEligibility; ConstructionRelatedEligibilityMatch; ConstructionRelatedExpand; ConstructionRelatedField; ConstructionRelatedFieldSource; ConstructionRelatedSource; ConstructionRowValue; ConstructionRowValuePolicy; ConstructionSourceProjection; ConstructionSourceProjectionName; ConstructionStep; ConstructionUnpivot; ConstructionUnpivotInput; ConstructionUnpivotNullPolicy; ContributorCode; ContributorOperator; ContributorPredicate; ContributorQuantifier; ContributorValue; ContributorValueKind; ContributorWindowSource; DecodeWorkspace; DerivedConstruction; Digest; Document; DraftSourceContext; EmptyCollectionPolicy; ExpandedRows; ExplicitGroupRowValue; ExplicitGroupSource; ExplorerMetadata; FeatureInterpretation; FeatureInterpretationKind; FieldGroupSource; FieldPath; FieldSource; FileActions; FilterPresentation; FilterValue; FixedFilter; FrameDefinition; FrameManyPolicy; FrameZeroPolicy; GroupSource; GroupSourceKind; GroupedRows; HasMissingInputs; LegacyNilConstructionStepsDigest; LookupMatch; LookupSource; MarshalJSON; MigrateAggregateTemporalPolicy; MigrateLegacyContributors; MigrateLosslessDefaults; MissingKeyPolicy; NewFrameDefinition; NormalizePresentationOrders; Normalized; Output; OutputDependencyOrder; OwnerRecordsSource; PinnedInterpretation; PivotCategory; PivotConstruction; Population; PopulationRouteStep; PrepareWorkspaceForCompilation; ProjectionMode; ProposeStepRemoval; RecordRows; RecordsRowDefinition; RelatedSelection; RepeatedBoundary; ResolveConstructionProposal; ResolveDraftRevision; ResolveDraftSourceContext; ResolveFrameSource; ResolvePopulationMemberProposal; ResolveRowDefinitionProposal; ResolveSemanticSelectionPlan; ResolveTableShapeProposal; ResolvedConstructionChoice; ResolvedDraftSourceContext; RouteMatchMode; RouteNode; RoutePolicy; RouteRebaseChoice; RowChangeAssessment; RowChangeProposal; RowChangeRequest; RowChangeUnresolvedReference; RowDefinition; RowDefinitionKind; SemanticReadinessStatus; SemanticSelection; SemanticSelectionPlan; SemanticSelectionReadiness; SemanticSelectionResult; SharedFilterBinding; SharedFilterBindingRef; SourceWhere; StageColumn; SupportsAnchor; SupportsTimestamp; Tab; TablePresentation; TableReshape; TableScalar; TableScalarKind; TableShape; TemporalFieldChoice; TemporalOrderingSource; TemporalReductionCapabilities; UnassignedMemberPolicy; UnitNormalizationCapabilities; UnitNormalizationPolicy; UnitNormalizationPresetCapability; UnmarshalJSON; UnpivotConstruction; UnpivotInput; UpgradeDocumentToConstruction; Valid; Validate; ValidateConcreteValue; ValidateConstructionExpandedFieldSource; ValidateContributorForCatalog; ValidateForPublication; ValidatePivotCategoryKey; ValidateStructure; ValidateTableShapeOutput; ValidateWithExplicitGroups; ValueTransformationCapability; Workspace; WorkspaceOutputConsumer,internal/catalog; internal/dataframe/columntransform; internal/dataframe/lineage; internal/dataframe/unit; internal/explorer/capability; internal/fhir/schema,internal/catalog; internal/dataframe/columntransform; internal/dataframe/lineage; internal/explorer/capability; internal/fhir/schema,cmd/explorer-config-v2-convert; cmd/preview-covering-index-artifact; generated/loomapi; internal/acceptance; internal/backends/arango/internal/explorer; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/repositoryseed; internal/server,internal/backends/postgres/internal/explorer; internal/explorer/capability,—,—,legacy=50; migration=28; compatibility=3,broad surface (23 production files); inspect cohesion,go test ./internal/explorer/authoringv2,inventory complete; semantic disposition pending +internal/explorer/authoringv2,github.com/calypr/loom/internal/explorer/authoringv2,authoringv2,domain/library,"Define, validate, canonicalize, and mutate the V2 Explorer Builder authoring workspace.",availability.go; canonical.go; commands.go; construction.go; construction_change.go; construction_choice.go; construction_migration.go; construction_row_values.go; construction_source_children.go; construction_validation.go; framing.go; migration.go; route_identity.go; row_change.go; row_definition.go; semantic_command.go; semantic_types.go; table_shape.go; transformations.go; types.go; validation.go; value_transformations.go; workspace_output_dependencies.go,commands_test.go; compile_preparation_test.go; construction_choice_test.go; construction_combine_test.go; construction_expand_test.go; construction_formal_replay_test.go; construction_group_coded_pivot_test.go; construction_group_field_source_test.go; construction_projection_replay_test.go; construction_proposal_test.go; construction_related_contributor_test.go; construction_row_values_test.go; construction_source_children_test.go; construction_test.go; correlated_test.go; framing_test.go; interpretation_test.go; migration_test.go; population_member_proposal_test.go; row_change_test.go; row_definition_test.go; row_proposal_test.go; semantic_codeable_concept_fallback_test.go; semantic_coded_category_test.go; semantic_command_test.go; table_shape_proposal_test.go; table_shape_test.go; transformations_test.go; types_test.go; value_transformations_test.go; workspace_output_dependencies_test.go; workspace_test.go,—,—,13260,8548,231,Action; ActionColumn; Addable; AggregateSource; AggregateTransformationCapabilities; AggregateTransformationCapabilitiesForCapability; AggregateTransformationCapabilitiesForCatalog; AnalyzeConstructionCandidate; AnalyzeStepEdit; ApplyCommands; ApplyCommandsRequest; ApplyCommandsResponse; ApplyInterpretationCandidate; ApplyRowChange; ApplyRowReplacement; ArithmeticOperand; AssessRowChange; AssessRowReplacement; BuilderState; CanPopulateConstructionRows; CanonicalJSON; CatalogAvailability; CatalogAvailabilityReadiness; CatalogAvailabilitySourceReadiness; CatalogCandidate; CatalogDiagnostic; CatalogEdge; CatalogNode; CatalogObservedField; CatalogObservedRelationship; CatalogObservedResource; CatalogSnapshot; ChartPresentation; CodeValue; Column; ColumnOutput; ColumnSource; ColumnTransformationChange; ColumnValueTransformationCapabilities; ColumnValueTransformationCapabilitiesForCatalog; Command; CommandResult; ConceptCandidate; Construction; ConstructionCanCarrySourceProjection; ConstructionChoiceSelection; ConstructionCodedGroup; ConstructionCodedGroupSource; ConstructionCodedPivot; ConstructionCodedPivotCategory; ConstructionCodedPivotSource; ConstructionCombine; ConstructionCombineJoinType; ConstructionCombineKey; ConstructionCombineKind; ConstructionCombineMembershipMode; ConstructionCombineProjection; ConstructionCombineRightMatchPolicy; ConstructionDependencyIssue; ConstructionDerive; ConstructionDerivedOperation; ConstructionDivisionByZeroPolicy; ConstructionExpand; ConstructionExpandEmptyPolicy; ConstructionExpandedFieldSource; ConstructionFilter; ConstructionFilterOperator; ConstructionFilterValueKind; ConstructionGroup; ConstructionGroupAggregate; ConstructionGroupAggregateOp; ConstructionGroupFieldCurrentStage; ConstructionGroupFieldRelatedOccurrence; ConstructionGroupFieldRelatedRouteStep; ConstructionGroupFieldSource; ConstructionGroupFieldSourceInput; ConstructionGroupKey; ConstructionGroupMissingKeyPolicy; ConstructionGroupSemanticCategory; ConstructionID; ConstructionImpact; ConstructionInputKind; ConstructionInputRef; ConstructionLiteral; ConstructionMissingInputPolicy; ConstructionNumericKind; ConstructionOperand; ConstructionOperandKind; ConstructionOperation; ConstructionOperationKind; ConstructionOutputPresentation; ConstructionPivot; ConstructionPivotCategory; ConstructionPivotDuplicatePolicy; ConstructionPivotMissingCellPolicy; ConstructionPivotUnlistedCategoryPolicy; ConstructionRelatedContributorRule; ConstructionRelatedEligibility; ConstructionRelatedEligibilityMatch; ConstructionRelatedExpand; ConstructionRelatedField; ConstructionRelatedFieldSource; ConstructionRelatedSource; ConstructionRowValue; ConstructionRowValuePolicy; ConstructionSourceProjection; ConstructionSourceProjectionName; ConstructionStep; ConstructionUnpivot; ConstructionUnpivotInput; ConstructionUnpivotNullPolicy; ContributorCode; ContributorOperator; ContributorPredicate; ContributorQuantifier; ContributorValue; ContributorValueKind; ContributorWindowSource; DecodeWorkspace; DerivedConstruction; Digest; Document; DraftSourceContext; EmptyCollectionPolicy; ExpandedRows; ExplicitGroupRowValue; ExplicitGroupSource; ExplorerMetadata; FeatureInterpretation; FeatureInterpretationKind; FieldGroupSource; FieldPath; FieldSource; FileActions; FilterPresentation; FilterValue; FixedFilter; FrameDefinition; FrameManyPolicy; FrameZeroPolicy; GroupSource; GroupSourceKind; GroupedRows; HasMissingInputs; LegacyNilConstructionStepsDigest; LookupMatch; LookupSource; MarshalJSON; MigrateAggregateTemporalPolicy; MigrateLegacyContributors; MigrateLosslessDefaults; MissingKeyPolicy; NewFrameDefinition; NormalizePresentationOrders; Normalized; Output; OutputDependencyOrder; OwnerRecordsSource; PinnedInterpretation; PivotCategory; PivotConstruction; Population; PopulationRouteStep; PrepareWorkspaceForCompilation; ProjectionMode; ProposeStepRemoval; RecordRows; RecordsRowDefinition; RelatedSelection; RepeatedBoundary; ResolveConstructionProposal; ResolveDraftRevision; ResolveDraftSourceContext; ResolveFrameSource; ResolvePopulationMemberProposal; ResolveRowDefinitionProposal; ResolveSemanticSelectionPlan; ResolveTableShapeProposal; ResolvedConstructionChoice; ResolvedDraftSourceContext; RouteMatchMode; RouteNode; RoutePolicy; RouteRebaseChoice; RowChangeAssessment; RowChangeProposal; RowChangeRequest; RowChangeUnresolvedReference; RowDefinition; RowDefinitionKind; SemanticReadinessStatus; SemanticSelection; SemanticSelectionPlan; SemanticSelectionReadiness; SemanticSelectionResult; SharedFilterBinding; SharedFilterBindingRef; SourceWhere; StageColumn; SupportsAnchor; SupportsTimestamp; Tab; TablePresentation; TableReshape; TableScalar; TableScalarKind; TableShape; TemporalFieldChoice; TemporalOrderingSource; TemporalReductionCapabilities; UnassignedMemberPolicy; UnitNormalizationCapabilities; UnitNormalizationPolicy; UnitNormalizationPresetCapability; UnmarshalJSON; UnpivotConstruction; UnpivotInput; UpgradeDocumentToConstruction; Valid; Validate; ValidateConcreteValue; ValidateConstructionExpandedFieldSource; ValidateContributorForCatalog; ValidateForPublication; ValidatePivotCategoryKey; ValidateStructure; ValidateTableShapeOutput; ValidateWithExplicitGroups; ValueTransformationCapability; Workspace; WorkspaceOutputConsumer,internal/catalog; internal/dataframe/columntransform; internal/dataframe/lineage; internal/dataframe/unit; internal/explorer/capability; internal/fhir/schema,internal/catalog; internal/dataframe/columntransform; internal/dataframe/lineage; internal/explorer/capability; internal/fhir/schema,cmd/explorer-config-v2-convert; cmd/preview-covering-index-artifact; generated/loomapi; internal/acceptance; internal/backends/arango/internal/explorer; internal/dataframe/benchmark; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/repositoryseed; internal/server,internal/backends/postgres/internal/explorer; internal/explorer/capability,—,—,legacy=50; migration=28; compatibility=3,broad surface (23 production files); inspect cohesion,go test ./internal/explorer/authoringv2,inventory complete; semantic disposition pending internal/explorer/capability,github.com/calypr/loom/internal/explorer/capability,capability,domain/library,Build the immutable compiler-backed capability contract exposed to Explorer Builder.,builder.go; construction.go; construction_routes.go; domain.go; population_route_choice.go; repository.go; row_choice.go; value_operations.go,builder_test.go; construction_test.go; domain_test.go; row_choice_test.go; semantic_fallback_dto_test.go; value_operations_test.go,—,—,3950,1396,110,AggregateInput; AggregateOperation; AggregateOperationCapability; AggregateRowContext; Allows; Availability; AvailabilityReadiness; AvailabilitySourceReadiness; Build; Builder; Candidate; CandidateProof; CanonicalPayload; ChartAggregation; Clone; CodedGroupChoiceSource; CompilerCallbacks; ConceptCandidate; ConstructionChoice; ConstructionChoiceDecision; ConstructionChoiceFact; ConstructionChoiceForm; ConstructionChoiceIdentity; ConstructionChoiceOption; ConstructionChoicePredicateOperator; ConstructionChoicePresentation; ConstructionChoicePreservation; ConstructionChoiceRowEffect; ConstructionChoiceShape; ConstructionChoiceSource; ConstructionChoiceSourceKind; ConstructionChoiceSupport; ConstructionCodedGroupChoice; ConstructionRelatedFieldChoice; ConstructionRelatedResourceRouteChoice; ConstructionRoutePage; ConstructionRouteSearch; ConstructionRouteStep; DecodeConstructionChoiceID; DecodePopulationRouteChoiceID; DecodeRowChoiceID; DeriveAggregateOperationCapabilities; Diagnostic; Edge; EdgeProof; Evidence; FieldChoiceSource; FieldObservation; FilterOperator; GroupFieldChoiceInput; GroupFieldChoiceSource; GroupFieldCurrentStageInput; GroupFieldRelatedOccurrenceInput; GroupFieldRelatedRouteStep; IsRepeatedCardinality; IsSupportedConstructionSourceType; IsUsable; NewBuilder; NewConstructionCodedGroupChoice; NewConstructionRelatedFieldChoice; NewConstructionRelatedResourceRouteChoiceFromAnchor; NewFieldConstructionChoice; NewFieldConstructionChoiceForRoute; NewGroupCodingIdentityChoice; NewGroupFieldConstructionChoice; NewPopulationRouteChoiceID; NewRowChoice; NewSemanticConstructionChoice; NewSemanticConstructionChoiceForRoute; NewSemanticFrameConstructionChoice; NewSnapshot; Node; NodeProof; ObservedField; ObservedRelationship; ObservedResource; Operation; PlanConstructionRoutes; Policy; PopulationRouteChoiceIdentity; ProjectionMode; ProjectionPolicy; RelatedFieldChoiceSource; RelatedResourceChoiceSource; RelationshipObservation; RepeatedBoundary; RepeatedCoordinate; Repository; ResolveRowChoiceID; ResourceObservation; RoutePolicy; RowChoice; RowChoiceCardinality; RowChoiceFacts; RowChoiceIdentity; RowChoiceKind; RowChoiceOccurrence; RowChoicePathResolver; RowChoiceShape; SemanticBindingChoiceSource; SemanticFrameChoiceSource; SemanticFrameFamily; Snapshot; SnapshotIdentity; Status; UnmarshalJSON; Usable; ValidateConstructionSourceType; ValidateSnapshot; ValidateToken,internal/dataframe/recipe,generated/loomapi; internal/catalog; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/lifecycle,cmd/preview-covering-index-artifact; internal/backends/arango/internal/explorer; internal/backends/postgres/internal/explorer; internal/explorer; internal/explorer/authoringv2; internal/explorer/compilation; internal/explorer/lifecycle; internal/server,internal/dataframe/compiler; internal/dataframe/execution; internal/explorer/capability,—,2 exported interface(s),legacy=2,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/explorer/capability,inventory complete; semantic disposition pending internal/explorer/compilation,github.com/calypr/loom/internal/explorer/compilation,compilation,domain/library,Translate validated Explorer authoring intent into native recipe and compilation artifacts.,compile.go; construction.go; derived_columns.go; indexed_projection.go; interpretation.go; row_expansion.go; semantic_compile.go; source_row.go; table_reshape.go,acceptance_observation_aql_diagnostic_test.go; command_helpers_test.go; construction_combine_test.go; construction_related_contributor_test.go; construction_test.go; correlated_test.go; derived_columns_test.go; indexed_projection_test.go; interpretation_test.go; row_expansion_test.go; semantic_compile_expand_source_test.go; source_row_test.go; table_reshape_test.go; translate_test.go; value_transformations_test.go; workspace_output_mapping_test.go; workspace_test.go,—,—,3622,4536,24,Canonical; Compile; CompileWorkspace; Error; InterpretationCandidateMatch; InterpretationCandidateResolution; InterpretationCandidateResolutionState; Match; PopulationFor; PresentationColumn; PresentationConfig; Reason; ResolveAddressableSourceRow; ResolveInterpretationCandidate; ResolveWorkspaceInterpretations; ResolvedInputs; ResolvedInputsDigest; ResolvedInterpretation; ResolvedPopulation; Result; SourceRowDescriptor; State; Unwrap; WorkspaceResult,internal/dataframe/compiler/lower; internal/dataframe/lineage; internal/dataframe/recipe; internal/dataframe/spec; internal/dataframe/unit; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/fhir/schema; internal/projectid,internal/backends/arango; internal/catalog; internal/dataframe/columntransform; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/lineage; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/fhir/schema,cmd/preview-covering-index-artifact; internal/explorer/lifecycle; internal/server,—,—,1 exported interface(s),legacy=1,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/explorer/compilation,inventory complete; semantic disposition pending internal/explorer/lifecycle,github.com/calypr/loom/internal/explorer/lifecycle,lifecycle,domain/library,"Coordinate transport-neutral Explorer authoring, preview, publication, query, and policy operations.",artifact.go; authoring.go; cell_trace.go; column_source.go; construction_category_discovery.go; construction_choice.go; construction_choice_proposal.go; construction_coded_pivot.go; construction_group_field_choices.go; construction_group_related_field_choices.go; construction_group_source.go; construction_pivot_source.go; construction_proposal.go; construction_routes.go; construction_semantic_group.go; derive_source.go; draft_restore.go; errors.go; expand_source.go; explicit_group_resolver.go; explicit_groups.go; filter_source.go; frame_source.go; interpretation.go; interpretation_apply.go; interpretation_context.go; interpretation_from_column.go; interpretation_library.go; interpretation_preview.go; policy.go; population.go; population_cursor.go; population_member_proposal.go; population_report.go; population_routes.go; preview.go; proposal_receipt.go; publish.go; query.go; repository_publish.go; row_change.go; row_choices.go; row_comparison.go; row_lineage.go; row_proposal.go; row_value_policy.go; schema_field_candidate.go; schema_fields.go; selection.go; semantic_catalog.go; service.go; table_shape_capabilities.go; table_shape_comparison.go; table_shape_proposal.go; table_shape_proposal_composition.go; table_shape_resolution.go; table_shape_saved.go; types.go; unpivot_source.go,artifact_test.go; builder_full_ui_replay_test.go; cell_trace_test.go; construction_category_discovery_errors_test.go; construction_category_discovery_test.go; construction_category_presence_test.go; construction_choice_category_search_test.go; construction_choice_proposal_test.go; construction_choice_test.go; construction_coded_pivot_saved_source_test.go; construction_group_field_choices_test.go; construction_pivot_source_test.go; construction_proposal_test.go; construction_proposal_trace_test.go; construction_related_expand_removal_test.go; construction_routes_test.go; construction_semantic_group_test.go; construction_v9_receipt_test.go; derive_source_test.go; expand_source_test.go; explicit_group_resolver_recovery_test.go; explicit_groups_test.go; filter_source_pivot_clear_test.go; filter_source_test.go; frame_source_test.go; fs1_reverse_order_collision_test.go; interpretation_context_test.go; interpretation_from_column_test.go; interpretation_library_test.go; interpretation_preview_test.go; interpretation_test.go; population_attachment_formal_replay_test.go; population_direction_test.go; population_formal_replay_test.go; population_member_proposal_test.go; population_removal_formal_replay_test.go; population_report_test.go; publish_release_requirements_classification_test.go; repository_publish_activation_conflict_test.go; repository_publish_generation_activation_classification_test.go; repository_publish_release_cas_conflict_test.go; row_change_test.go; row_choices_test.go; row_lineage_test.go; row_proposal_test.go; row_value_policy_test.go; schema_field_candidate_test.go; schema_fields_test.go; selection_test.go; semantic_authoring_test.go; semantic_catalog_test.go; semantic_category_test.go; semantic_coded_concept_test.go; semantic_field_presentation_test.go; semantic_frame_compatibility_test.go; service_test.go; table_shape_capabilities_test.go; table_shape_comparison_evidence_test.go; table_shape_proposal_test.go; unpivot_source_test.go,—,—,25545,31520,319,AcceptedConstruction; ApplyCommands; ApplyCommandsWithConstructionPreview; ApplyCommandsWithConstructionPreviewResponse; ArtifactOpenRequest; ArtifactPublishedReader; ArtifactRequest; ArtifactResult; AssessRowChange; AssessRowChangeRequest; AssessRowChangeResult; AuthorizedCapability; BrowseFrameSourceOptions; BrowseFrameSourceOptionsRequest; BrowseFrameSourceOptionsResponse; BrowseSemanticInventory; BrowseSemanticInventoryRequest; BrowseSemanticInventoryResponse; Builder; BuilderRequest; CandidatePreviewCompleteness; CandidatePreviewCounts; CandidatePreviewState; Capability; CapabilityResolver; CategoryScanner; CellTrace; CellTraceBinding; CellTraceExecutor; CellTraceFeature; CellTraceRequest; CellTraceResult; Clone; ColumnSource; ColumnSourceRequest; ColumnSourceResponse; ColumnSourceRouteStep; CompileReceiptRequest; Config; ConfiguredColumnContext; ConfiguredColumnContextRequest; ConfiguredColumnContextResult; ConfiguredColumnReady; ConfiguredColumnResolution; ConstructionCapabilitiesRequest; ConstructionCapabilitiesResponse; ConstructionCategoryDiscoveryRequest; ConstructionCategoryDiscoveryResponse; ConstructionChoiceProposalRequest; ConstructionChoiceProposalResponse; ConstructionChoiceProposalSelection; ConstructionChoiceSearchRequest; ConstructionChoiceSearchResponse; ConstructionChoiceSearchSource; ConstructionDependencyImpact; ConstructionDependencyIssue; ConstructionDeriveSourceCapability; ConstructionDeriveSourceChoice; ConstructionDeriveSourceFacts; ConstructionDeriveSourceSelection; ConstructionDiscoveredCategory; ConstructionExpandSourceCapability; ConstructionExpandSourceChoice; ConstructionExpandSourceFacts; ConstructionExpandSourceSelection; ConstructionFilterSourceCapability; ConstructionFilterSourceChoice; ConstructionFilterSourceFacts; ConstructionFilterSourceSelection; ConstructionGroupFieldChoice; ConstructionGroupRelatedOccurrenceChoice; ConstructionGroupSourceCapability; ConstructionGroupSourceChoice; ConstructionGroupSourceSelection; ConstructionPivotSourceCapability; ConstructionPivotSourceChoice; ConstructionPivotSourceSelection; ConstructionProposalRequest; ConstructionProposalResponse; ConstructionSemanticGroupChoice; ConstructionSourceProjectionAvailability; ConstructionSourceStageCompiler; ConstructionSourceStageRequest; ConstructionUnpivotSourceCapability; ConstructionUnpivotSourceChoice; ConstructionUnpivotSourceFacts; ConstructionUnpivotSourceSelection; ConstructionWorkspaceInput; ConstructionWorkspaceInputColumn; Create; CreateExplicitGroupRevision; CreateInterpretationRevision; CreateInterpretationRevisionFromColumn; CreateInterpretationRevisionFromColumnRequest; CreateInterpretationRevisionRequest; CreateRequest; CreateSelection; Decode; DiscoverConstructionCategories; DiscoverTableShapeCategories; Encode; Error; ErrorClass; ExactExecutionMaterialization; ExactSemanticInventoryItemIdentity; Execution; ExecutionOutput; ExpandedSelection; ExplicitGroupCreateRequest; ExplicitGroupCreateResult; ExplicitGroupInput; ExplicitGroupRevisionChoice; ExplicitGroupRevisionListRequest; ExplicitGroupRevisionProof; ExplicitGroupRevisionResolveRequest; ExplicitGroupRevisionResolver; ExplicitGroupSelection; ExplicitGroupSummary; FamilySemanticInventoryItemIdentity; FieldGroupSelection; FieldPresentationAccess; FieldPresentationContext; FieldPresentationDecisionKind; FieldPresentationGroupUnderSemantic; FieldPresentationKeepRaw; FieldPresentationReady; FieldPresentationReason; FieldPresentationRequest; FieldPresentationResultKind; FieldPresentationUnavailable; FrameSourceForm; FrameSourceOption; GenerationValidator; Get; GetConstructionCapabilities; GetInterpretationRevision; GetInterpretationRevisionRequest; GetTableShapeCatalog; HMACPopulationMappingCursorCodec; InterpretationCandidatePreviewSample; InterpretationLibrarySummary; InterpretationLibraryView; InterpretationRevisionSummary; List; ListExplicitGroupRevisions; ListInterpretationLibraries; ListInterpretationLibrariesResult; ListResult; ListRowChoices; ListRowDefinitionChoices; New; NewHMACPopulationMappingCursorCodec; NewRepositoryExplicitGroupRevisionResolver; NewSchemaRowChoiceResolver; OpenArtifact; PopulationMapping; PopulationMappingBinding; PopulationMappingCounts; PopulationMappingCursor; PopulationMappingCursorCodec; PopulationMappingDiagnostic; PopulationMappingExecutor; PopulationMappingReport; PopulationMappingRequest; PopulationMappingResult; PopulationMemberRemovalProposalRequest; PopulationMemberRemovalProposalResponse; PopulationRouteChoice; PopulationRoutesRequest; PopulationRoutesResponse; PrepareArtifact; Preview; PreviewInterpretationCandidate; PreviewInterpretationCandidateRequest; PreviewInterpretationCandidateResult; PreviewRequest; PreviewResult; PreviewSinkFactory; ProposeConstruction; ProposeConstructionChoice; ProposePopulationMemberRemoval; ProposeRowDefinition; ProposeTableShape; Publish; PublishRepository; PublishRequest; PublishResult; PublishedOutputIntent; ReadSelection; Ready; Reason; ReceiptCompiler; ReceiptMaterializer; ReceiptPreviewer; ReceiptReader; ReceiptStreamValidator; Reconcile; ReconcileRequest; ReconcileResult; ReconcileWithWorkspace; RelatedExpandChoiceSearchRequest; RelatedExpandChoiceSearchResponse; RelatedExpandContributorChoice; RelatedExpandContributorChoiceSearchRequest; RelatedExpandContributorChoiceSearchResponse; RelatedExpandRouteChoice; RelatedFieldChoice; RelatedFieldChoiceSearchRequest; RelatedFieldChoiceSearchResponse; ReleaseActivator; ReleasePreparer; RepositoryPublishRequest; RepositoryPublishResult; ResolveExplicitGroupRevision; ResolveRowChoiceID; ResolveTableShape; ResolvedPopulationRouteChoice; ResolvedRowChoice; RowChoiceKind; RowChoicePlanner; RowChoiceResolveRequest; RowChoiceResolver; RowDefinitionChoice; RowDefinitionChoicePolicy; RowDefinitionChoicesRequest; RowDefinitionChoicesResponse; RowDefinitionComparison; RowDefinitionComparisonExample; RowDefinitionComparisonStatus; RowDefinitionPreviewSummary; RowDefinitionProposal; RowDefinitionProposalRequest; RowDefinitionSelection; RowDefinitionSelectionKind; RowLineage; RowLineageExecutor; RowLineageRequest; RowLineageResult; SchemaFieldOption; SchemaFieldSearchResponse; SchemaRowChoiceResolver; SearchConstructionChoices; SearchPopulationRoutes; SearchRelatedExpandChoices; SearchRelatedExpandContributorChoices; SearchRelatedFieldChoices; SearchSchemaFields; SearchSchemaFieldsRequest; SelectionCreateResult; SelectionIntentCreateRequest; SelectionPublishedReader; SelectionReadIntentRequest; SelectionReadResult; SelectionReferenceValidator; SelectionSourceIntent; SelectionSourceResolver; SemanticInventoryItem; SemanticInventoryItemIdentityKind; ServerTiming; Service; State; StreamExactExecution; Suggestions; SuggestionsRequest; SuggestionsResult; TableShapeCatalogChoice; TableShapeCatalogColumn; TableShapeCatalogRequest; TableShapeCatalogResult; TableShapeCategoryDiscoveryRequest; TableShapeCategoryDiscoveryResult; TableShapeCategoryValue; TableShapeCellContributor; TableShapeCellTraceEvidence; TableShapeCellTraceState; TableShapeCellValue; TableShapeChangedCell; TableShapeChangedRow; TableShapeColumnChoice; TableShapeComparison; TableShapeComparisonStatus; TableShapeContributor; TableShapeDeclaredInformationLoss; TableShapeDerivedReference; TableShapeDerivedSelection; TableShapeDiscoveredCategory; TableShapeEvidenceLimitation; TableShapeExcludedRecord; TableShapeExclusionEvidence; TableShapeExclusionEvidenceStatus; TableShapeExclusionExecutor; TableShapeInformationLoss; TableShapeInformationLossStatus; TableShapeOperandSelection; TableShapePivotCategorySelection; TableShapePivotSelection; TableShapePostPivotOutput; TableShapePreviewSummary; TableShapeProposal; TableShapeProposalMode; TableShapeProposalRequest; TableShapeReshapeModeChoice; TableShapeResolutionRequest; TableShapeResolutionResult; TableShapeResolvedCategory; TableShapeSavedDerived; TableShapeSavedOperand; TableShapeSavedPivot; TableShapeSavedPivotCategory; TableShapeSavedProposalIntent; TableShapeSavedSupport; TableShapeSavedUnpivot; TableShapeSavedUnpivotInput; TableShapeSourceIdentity; TableShapeUnpivotSelection; UnmarshalJSON; Unwrap; Validate; ValidateCompilationReceipt,internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/tableshapecap; internal/fhir/schema; internal/projectid,internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/tableshapecap; internal/fhir/schema; internal/projectid,internal/server,internal/backends/arango/internal/explorer; internal/dataframe/execution; internal/explorer/capability,github.com/google/uuid,8 exported interface(s),legacy=43; migration=2; compatibility=13,one production importer (internal/server); inspect ownership/inline potential; broad surface (59 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/explorer/lifecycle,inventory complete; semantic disposition pending internal/explorer/tableshapecap,github.com/calypr/loom/internal/explorer/tableshapecap,tableshapecap,domain/library,Define immutable table-shape capability and resolution artifacts plus their create-once repository contract.,identity.go; repository.go; types.go,artifacts_test.go,—,—,1971,503,65,AvailabilityState; Binding; BooleanScalar; CanonicalContent; CatalogChoices; CatalogLookup; CatalogReceipt; CategoryProof; CategoryScanReceipt; CategoryValuesDigest; Choice; ChoiceRole; ColumnChoice; DecimalScalar; DecodeStrict; DerivedResolution; DiscoveredCategory; FindCategoryChoice; FindColumn; FindOperand; FindOperator; FindPolicy; FrozenCategory; IntegerScalar; IntentKind; LogicalType; Matches; MissingScalar; NamedOutput; NewCatalogReceipt; NewCategoryScanReceipt; NewPivotDerivedOperandChoiceID; NewResolutionReceipt; NullScalar; OperandChoice; OperandKind; OperandRef; OperatorChoice; PivotDerivedOperand; PivotResolution; PolicyChoice; ProposalIntent; PublicColumn; RemoveIntent; Repository; ResolutionKind; ResolutionReceipt; ResolvedOperand; ResolvedOperandKind; ResolvedUnpivotInput; RoleAvailability; SavedShapeSummary; Scalar; ScalarKind; SetIntent; StringScalar; TypeFact; UnmarshalJSON; UnpivotResolution; Valid; Validate; ValidateAgainstCatalog; ValidateCategoryScanAgainstCatalog; ValidateResolutionAgainstCatalog; ValidateResolutionAgainstCategoryScan,internal/projectid,—,internal/backends/arango/internal/explorer; internal/backends/postgres/internal/explorer; internal/explorer/lifecycle; internal/server,—,—,1 exported interface(s),—,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/explorer/tableshapecap,inventory complete; semantic disposition pending internal/fhir/schema,github.com/calypr/loom/internal/fhir/schema,schema,domain/library,"Add handwritten selector, traversal, pivot, and compiler semantics to generated FHIR metadata.",compiler_semantics.go; correlated.go; fields.go; generated_metadata.go; identifier.go; index.go; pivots.go; quantity.go; row_resolution.go; selectors.go; structure.go; terminal_metadata.go; traversals.go,compiler_semantics_test.go; correlated_selection_test.go; correlated_test.go; fields_test.go; identifier_test.go; index_test.go; pivots_test.go; root_resources_test.go; row_resolution_test.go; selectors_test.go; structure_test.go; terminal_metadata_test.go; traversals_test.go,—,—,2446,1341,86,ArrayElementType; ArrayElements; At; BindingStrength; CanonicalPath; CanonicalResourceType; CanonicalizePath; ChoiceValueSelectorOptions; CompilerTraversal; ConcreteResourceType; ContainsFilter; CorrelatedBinding; CorrelatedBindingSpec; CorrelatedKey; CorrelatedSelectionScope; CorrelatedValueScope; DefaultPivotSpec; Definition; DefinitionExists; DefinitionName; DefinitionStructure; DefinitionView; Definitions; Element; ElementListView; ElementView; Elements; ExtensionBinding; ExtensionBindingSpec; FieldKind; FieldPredicateSpec; FieldSelectorSpec; FieldSelectorSpecFromPath; FieldSemantics; FieldSpec; FieldsForResource; GeneratedIndex; HasRepeatedCodingPath; HasResource; IdentifierBinding; IdentifierBindingSpec; Index; ItemJSONType; JSONType; Len; LookupField; LookupTraversal; Name; NewIndex; ParseSelector; PivotSpec; PrimitiveKind; QuantityIdentityPaths; ReadDefinition; ReferencedType; RepeatedCodingPaths; RequiredElements; ResolveCompilerTraversal; ResolveFieldSemantics; ResolvePath; ResolveRowPath; ResolveTerminalScalarMetadata; ResolvedPath; ResolvesToCodeableConcept; ResourceSpec; ResourceType; ResourceTypes; RowCardinality; RowPathFacts; RowPathShape; Selector; SelectorExpression; SelectorFromField; SelectorStep; Snapshot; Structure; StructureMember; TerminalScalarMetadata; TraversalDirection; TraversalMultiplicity; TraversalSpec; ValidateCorrelatedBinding; ValidateCorrelatedKey; ValidateExtensionBinding; ValidateIdentifierBinding; ValidatePivotSelectors,generated/fhirschema,—,cmd/arango-fhir-proto; generated/fhir; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/arango/internal/catalog; internal/backends/postgres/internal/catalog; internal/catalog; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer/authoringv2; internal/explorer/compilation; internal/explorer/lifecycle; internal/fhir/semantic; internal/ingest; internal/server; scripts,conformance/compiler; internal/dataframe/compiler,—,—,legacy=1; compatibility=2,broad surface (13 production files); inspect cohesion,go test ./internal/fhir/schema,inventory complete; semantic disposition pending internal/fhir/semantic,github.com/calypr/loom/internal/fhir/semantic,semantic,domain/library,Walk generated FHIR definition indexes to enumerate typed referenced datatype occurrences.,member_walk.go; walk.go,member_walk_resource_dispatch_test.go; member_walk_test.go; walk_test.go,—,—,829,949,17,Error; Len; Less; MemberFact; Occurrence; Pop; Push; RepeatedBoundary; ResourceBoundary; Swap; Walk; WalkDiagnostic; WalkDiagnosticCode; WalkLimitError; WalkLimitKind; WalkMembers; WalkMembersWithResourceTypes,internal/fhir/schema,internal/fhir/schema,internal/catalog,—,—,—,—,one production importer (internal/catalog); inspect ownership/inline potential,go test ./internal/fhir/semantic,inventory complete; semantic disposition pending -internal/ingest,github.com/calypr/loom/internal/ingest,ingest,domain/library,"Preflight and load FHIR NDJSON into generation-scoped Arango vertices, edges, and catalog facts, and commit built resource-row and built-edge-occurrence batches to the PostgreSQL source store through the ledgered batch writer with scope verification, canonical payload encoding, and payload-backed path/ordinal recovery for repeated references.",backend.go; doc.go; documents.go; files.go; generated_load.go; generation_identity.go; generation_load.go; generation_orchestration.go; generation_workers.go; load_options.go; postgres_writer.go; preflight.go; progress.go; resource_load.go; row_builder.go; semantic_inventory_backfill.go; source_schema.go,backend_test.go; basic_observation_fixture_validation_test.go; bench_test.go; documents_test.go; file_pipeline_test.go; files_test.go; generated_load_test.go; generation_cleanup_test.go; generation_failed_cleanup_e2e_test.go; generation_identity_test.go; generation_load_postgres_durability_test.go; generation_load_postgres_test.go; generation_load_test.go; integration_test.go; keycollide_generated_e2e_test.go; load_options_test.go; postgres_writer_test.go; preflight_test.go; related_medication_fixture_test.go; resource_load_test.go; row_builder_test.go; semantic_inventory_backfill_integration_test.go; semantic_inventory_backfill_test.go; semantic_inventory_test.go; source_schema_test.go; viewer_row_file_download_fixture_test.go,—,—,5650,10567,63,ActivatePostgresGeneration; ActivationOutcomeError; BackfillSemanticInventory; Build; Complete; ConnectionOptions; DiscoverNDJSON; EdgeDocument; EdgeFromGrip; Error; EventSink; GeneratedRowBuilder; GenerationCatalogWriters; GenerationFieldProfileWriter; GenerationInventoryRebuilder; GenerationLoadIncompleteError; GenerationRelationshipDiscoverer; GenerationSemanticContributor; GenericRowBuilder; IngestionMode; Load; LoadIncompleteError; LoadOptions; LoadPostgresGeneration; LoadSummary; NewGeneratedRowBuilder; NewGenericRowBuilder; NewPostgresEdgeWriter; NewPostgresResourceWriter; OpenLineScanner; PGEdgeBatchScope; PGEdgeRowIdentityError; PGEdgeWriteResult; PGEdgeWriteScopeError; PGLoadConfig; PGResourceBatchScope; PGResourceRowIdentityError; PGResourceWriteResult; PGResourceWriteScopeError; PGSourceOptions; PostgresEdgeWriter; PostgresResourceWriter; PreflightError; PreflightFiles; PreflightIssue; PreflightReport; PreflightResource; ResourceTypeFromPath; RowBuilder; RowErrorSample; SanitizeKey; SecondsSince; SemanticInventoryBackfillCollectionResult; SemanticInventoryBackfillOptions; SemanticInventoryBackfillProgress; SemanticInventoryBackfillReport; SplitCollectionID; Unwrap; Valid; VertexDocument; VertexFromFHIRWithExtra; WriteEdgeBatch; WriteResourceBatch,generated/fhir; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/fhir/schema; internal/source,generated/fhir; internal/backends/arango; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/fhir/schema; internal/source,cmd/arango-fhir-proto; cmd/loom-data; internal/api/bulk/load; internal/server,internal/api/graphql/graph/query; internal/backends/postgres/internal/catalog,github.com/bmeg/jsonschema/v6; github.com/bmeg/jsonschemagraph/graph; github.com/bmeg/jsonschemagraph/model; github.com/bmeg/jsonschemagraph/util; github.com/bytedance/sonic,5 exported interface(s),legacy=5; migration=9; compatibility=1,broad surface (17 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/ingest,inventory complete; semantic disposition pending +internal/ingest,github.com/calypr/loom/internal/ingest,ingest,domain/library,"Preflight and load FHIR NDJSON into generation-scoped Arango vertices, edges, and catalog facts, and commit built resource-row and built-edge-occurrence batches to the PostgreSQL source store through the ledgered batch writer with scope verification, canonical payload encoding, and payload-backed path/ordinal recovery for repeated references.",backend.go; doc.go; documents.go; files.go; generated_load.go; generation_identity.go; generation_load.go; generation_orchestration.go; generation_workers.go; load_options.go; postgres_writer.go; preflight.go; progress.go; resource_load.go; row_builder.go; semantic_inventory_backfill.go; source_schema.go,backend_test.go; basic_observation_fixture_validation_test.go; bench_test.go; documents_test.go; file_pipeline_test.go; files_test.go; generated_load_test.go; generation_cleanup_test.go; generation_failed_cleanup_e2e_test.go; generation_identity_test.go; generation_load_postgres_durability_test.go; generation_load_postgres_test.go; generation_load_test.go; integration_test.go; keycollide_generated_e2e_test.go; load_options_test.go; postgres_writer_test.go; preflight_test.go; related_medication_fixture_test.go; resource_load_test.go; row_builder_test.go; semantic_inventory_backfill_integration_test.go; semantic_inventory_backfill_test.go; semantic_inventory_test.go; source_schema_test.go; viewer_row_file_download_fixture_test.go,—,—,5650,10571,63,ActivatePostgresGeneration; ActivationOutcomeError; BackfillSemanticInventory; Build; Complete; ConnectionOptions; DiscoverNDJSON; EdgeDocument; EdgeFromGrip; Error; EventSink; GeneratedRowBuilder; GenerationCatalogWriters; GenerationFieldProfileWriter; GenerationInventoryRebuilder; GenerationLoadIncompleteError; GenerationRelationshipDiscoverer; GenerationSemanticContributor; GenericRowBuilder; IngestionMode; Load; LoadIncompleteError; LoadOptions; LoadPostgresGeneration; LoadSummary; NewGeneratedRowBuilder; NewGenericRowBuilder; NewPostgresEdgeWriter; NewPostgresResourceWriter; OpenLineScanner; PGEdgeBatchScope; PGEdgeRowIdentityError; PGEdgeWriteResult; PGEdgeWriteScopeError; PGLoadConfig; PGResourceBatchScope; PGResourceRowIdentityError; PGResourceWriteResult; PGResourceWriteScopeError; PGSourceOptions; PostgresEdgeWriter; PostgresResourceWriter; PreflightError; PreflightFiles; PreflightIssue; PreflightReport; PreflightResource; ResourceTypeFromPath; RowBuilder; RowErrorSample; SanitizeKey; SecondsSince; SemanticInventoryBackfillCollectionResult; SemanticInventoryBackfillOptions; SemanticInventoryBackfillProgress; SemanticInventoryBackfillReport; SplitCollectionID; Unwrap; Valid; VertexDocument; VertexFromFHIRWithExtra; WriteEdgeBatch; WriteResourceBatch,generated/fhir; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/fhir/schema; internal/source,generated/fhir; internal/backends/arango; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/fhir/schema; internal/source,cmd/arango-fhir-proto; cmd/loom-data; internal/api/bulk/load; internal/server,internal/api/graphql/graph/query; internal/backends/postgres/internal/catalog,github.com/bmeg/jsonschema/v6; github.com/bmeg/jsonschemagraph/graph; github.com/bmeg/jsonschemagraph/model; github.com/bmeg/jsonschemagraph/util; github.com/bytedance/sonic,5 exported interface(s),legacy=5; migration=9; compatibility=1,broad surface (17 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/ingest,inventory complete; semantic disposition pending internal/projectid,github.com/calypr/loom/internal/projectid,projectid,domain/library,Define canonical and legacy-compatible public project identity normalization.,projectid.go,projectid_test.go,—,—,51,18,2,Canonical; Legacy,—,—,cmd/loom-repository; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/backends/arango/internal/explorer; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/execution; internal/dataframe/published; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/explorer/tableshapecap; internal/repositoryseed; internal/server,internal/dataframe/compiler,—,—,legacy=5; compatibility=1,small shared semantic contract; compare direct callers before inlining,go test ./internal/projectid,inventory complete; semantic disposition pending internal/repositoryseed,github.com/calypr/loom/internal/repositoryseed,repositoryseed,domain/library,"Validate repository metadata and configuration, produce a source manifest, and refuse unmaterialized inputs before launch.",preflight.go,preflight_test.go,—,—,281,105,6,Error; File; Manifest; Options; Prepare; UnmaterializedError,internal/explorer/authoringv2; internal/projectid,—,cmd/loom-repository,—,—,—,—,one production importer (cmd/loom-repository); inspect ownership/inline potential,go test ./internal/repositoryseed,inventory complete; semantic disposition pending -internal/server,github.com/calypr/loom/internal/server,server,domain/library,"Compose storage, domain services, generated OpenAPI handlers, GraphQL adapters, and HTTP routes.",artifact_dev.go; auth_wiring.go; compose.go; construction_inputs_catalog.go; explain_wiring.go; explorer_authoring_http.go; explorer_capability.go; explorer_combine_output_metadata.go; explorer_config_route.go; explorer_lifecycle_helpers.go; explorer_lifecycle_wire.go; explorer_output_reconcile.go; explorer_preview_response.go; explorer_preview_security.go; explorer_publication.go; explorer_receipt_contract.go; explorer_receipt_validation.go; local_workspace_writeback.go; openapi_data_routes.go; openapi_explorer_artifacts.go; openapi_explorer_authoring_direct.go; openapi_explorer_construction.go; openapi_explorer_construction_choice_proposal.go; openapi_explorer_construction_inputs.go; openapi_explorer_construction_proposal.go; openapi_explorer_conversion.go; openapi_explorer_direct.go; openapi_explorer_explicit_groups.go; openapi_explorer_population_member_proposal.go; openapi_explorer_routes.go; openapi_explorer_row_definition.go; openapi_explorer_selections.go; openapi_explorer_table_shape.go; openapi_frame_source_options.go; openapi_graphql_routes.go; openapi_recipe_execution.go; openapi_routes.go; openapi_semantic_inventory.go; options.go; private_artifact_reconciler.go; receipt_preview_diagnostic.go; recipe_authorization.go; recipe_combine_resolution.go; recipe_discovery.go; recipe_publication.go; release_verifier.go; routes.go; row_sources.go; runtime.go; server.go,accepted_construction_contract_test.go; append_workspace_rows_integration_test.go; artifact_reconciler_wiring_test.go; auth_wiring_trusted_issuer_test.go; backend_composition_contract_test.go; backend_error_boundary_contract_test.go; case008_row_definition_preview_route_test.go; command_helpers_test.go; config_test.go; construction_category_discovery_openapi_error_test.go; construction_inputs_catalog_test.go; construction_proposal_zero_column_test.go; core_authoring_protocol_arango_conformance_test.go; core_authoring_protocol_formal_replay_test.go; core_authoring_protocol_receipts_test.go; core_authoring_protocol_restore_test.go; core_authoring_protocol_rowschema_test.go; core_authoring_protocol_scoped_compile_test.go; core_authoring_protocol_test.go; dataframe_query_error_test.go; derived_policy_rows_integration_test.go; explain_wiring_pg18_test.go; explain_wiring_preview_test.go; explorer_capability_test.go; explorer_coded_value_capability_test.go; explorer_concept_evidence_test.go; explorer_config_route_test.go; explorer_diagnostic_logging_test.go; explorer_output_reconcile_derive_source_test.go; explorer_output_reconcile_source_child_test.go; explorer_output_reconcile_unpivot_source_test.go; explorer_preview_response_test.go; explorer_preview_security_test.go; explorer_receipt_contract_error_test.go; explorer_receipt_contract_test.go; explorer_reconcile_scope_test.go; explorer_row_change_route_test.go; explorer_state_route_test.go; fs1_signed_slot_collision_test.go; health_readiness_wiring_test.go; http_test_helpers_test.go; local_workspace_writeback_test.go; openapi_column_id_contract_test.go; openapi_explorer_artifacts_download_test.go; openapi_explorer_authoring_write_fault_test.go; openapi_explorer_cell_trace_test.go; openapi_explorer_commands_notfound_test.go; openapi_explorer_construction_apply_preview_test.go; openapi_explorer_construction_derive_source_test.go; openapi_explorer_construction_expand_source_test.go; openapi_explorer_construction_filter_source_test.go; openapi_explorer_construction_inputs_test.go; openapi_explorer_construction_proposal_test.go; openapi_explorer_construction_unpivot_source_test.go; openapi_explorer_conversion_test.go; openapi_explorer_direct_test.go; openapi_explorer_draft_restore_test.go; openapi_explorer_legacy_details_control_test.go; openapi_explorer_population_mapping_construction_test.go; openapi_explorer_population_member_proposal_test.go; openapi_explorer_restricted_starting_collection_integration_test.go; openapi_explorer_route_roundtrip_test.go; openapi_explorer_route_rows_arango_test.go; openapi_explorer_row_definition_test.go; openapi_explorer_write_scope_test.go; openapi_field_presentation_fixture_test.go; openapi_graphql_status_test.go; openapi_graphql_transport_review_test.go; openapi_interpretation_routes_test.go; openapi_recipe_execution_test.go; openapi_route_inventory_test.go; openapi_routes_test.go; openapi_semantic_inventory_test.go; openapi_status_coverage_test.go; openapi_test_helpers_test.go; options_schemas_test.go; patient_one_construction_choice_route_test.go; population_member_receipt_test.go; postgres_query_test.go; postgres_refusal_contract_test.go; preview_postgres_wiring_pg18_test.go; private_artifact_reconciler_test.go; publication_rows_integration_test.go; publication_viewer_agreement_test.go; receipt_preview_diagnostic_test.go; recipe_authorization_test.go; recipe_blank_project_test.go; recipe_combine_resolution_test.go; recipe_discovery_test.go; recipe_execution_etl_failure_fields_e2e_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_header_probe_test.go; recipe_execution_status_twin_e2e_test.go; recipe_execution_twin_authz_repair_test.go; recipe_project_keying_test.go; recipe_publication_test.go; release_verifier_fault_mapping_test.go; release_verifier_test.go; repository_activation_conflict_dev_test.go; route_composition_contract_test.go; routes_test.go; row_tag_guard_contract_test.go; row_value_policy_stream_test.go; runtime_postgres_live_test.go; runtime_test.go; wiring_test.go; zz_readiness_contract_guard_test.go,—,—,14261,37094,103,Activate; ActivateDatasetGeneration; ActivateRepositoryGeneration; ApplyExplorerBuilderCommands; AssessExplorerRowChange; AuthConfig; AuthorizeRead; AuthorizeWrite; BasicAuthConfig; BrowseExplorerFrameSourceOptions; BrowseExplorerSemanticInventory; CalyprAuthConfig; CheckExplorerPopulationMapping; ClickHouseConfig; Config; CreateDatasetGeneration; CreateExplorer; CreateExplorerExplicitGroupRevision; CreateExplorerSelection; CreateInterpretationRevision; CreateInterpretationRevisionFromColumn; DataframerConfig; DefaultConfig; DiscoverExplorerConstructionCategories; DiscoverExplorerTableShapeCategories; DownloadExplorerArtifact; Error; ExactExecutionMaterialization; ExecuteDataframeGraphQL; ExecuteGraphQL; Explain; ExplainPhysical; Finish; GetApolloSandbox; GetDatasetGenerationStatus; GetExecution; GetExplorer; GetExplorerAuthoringCapability; GetExplorerBuilder; GetExplorerColumnSource; GetExplorerConstructionCapabilities; GetExplorerConstructionInputs; GetExplorerSelection; GetExplorerTableShapeCapabilities; GetGraphQLPlayground; GetHealth; GetInterpretationRevision; GetLiveness; GetPointer; GetReadiness; GetRecipeExecution; HTTPRoutes; ListExplorerRowDefinitionChoices; ListExplorers; ListInterpretationLibraries; LoadConfig; PostgreSQLConfig; PrepareExplorerArtifact; Preview; PreviewCollectionScanEvidenceFromExplain; PreviewExplorer; PreviewInterpretationCandidate; ProbeCandidate; ProbeEdge; ProbeNode; ProposeExplorerConstruction; ProposeExplorerConstructionChoice; ProposeExplorerPopulationMemberRemoval; ProposeExplorerRowDefinition; ProposeExplorerTableShape; PublishAuthoring; PublishExplorer; PublishRepositoryExplorerConfig; ReadManifest; ReconcileExplorerBuilder; Resolve; ResolveConfiguredColumnContext; ResolveExplorerTableShape; ResolveForCompilation; ResolveForExecution; ResolveToken; Run; RunGeneration; RunWithConfig; SearchExplorerCandidates; SearchExplorerConstructionChoices; SearchExplorerPopulationRoutes; SearchExplorerRelatedExpandChoices; SearchExplorerRelatedExpandContributors; SearchExplorerRelatedFieldChoices; SearchExplorerSchemaFields; ServerConfig; StreamExactExport; String; TraceExplorerCell; TraceExplorerRowLineage; Unwrap; Validate; ValidateSelectionReferences; VerifyPublication; Visit; VisitProposeExplorerTableShapeResponse; Write,generated/loomapi; internal/api/authpolicy; internal/api/bulk/load; internal/api/columncapabilities; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/artifactfs; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/explorer/tableshapecap; internal/fhir/schema; internal/ingest; internal/projectid; internal/store/clickhouse,generated/graphql/graph/model; generated/loomapi; internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/lineage; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/fhir/schema; internal/ingest; internal/projectid; internal/source; internal/store/clickhouse,.; cmd/arango-fhir-server,internal/backends/postgres,github.com/gofiber/fiber/v3; go.yaml.in/yaml/v3; golang.org/x/sync/singleflight,—,legacy=41; migration=11; compatibility=3,broad surface (50 production files); inspect cohesion,go test ./internal/server,inventory complete; semantic disposition pending +internal/server,github.com/calypr/loom/internal/server,server,domain/library,"Compose storage, domain services, generated OpenAPI handlers, GraphQL adapters, and HTTP routes.",artifact_dev.go; auth_wiring.go; compose.go; construction_inputs_catalog.go; explain_wiring.go; explorer_authoring_http.go; explorer_capability.go; explorer_combine_output_metadata.go; explorer_config_route.go; explorer_lifecycle_helpers.go; explorer_lifecycle_wire.go; explorer_output_reconcile.go; explorer_preview_response.go; explorer_preview_security.go; explorer_publication.go; explorer_receipt_contract.go; explorer_receipt_validation.go; local_workspace_writeback.go; openapi_data_routes.go; openapi_explorer_artifacts.go; openapi_explorer_authoring_direct.go; openapi_explorer_construction.go; openapi_explorer_construction_choice_proposal.go; openapi_explorer_construction_inputs.go; openapi_explorer_construction_proposal.go; openapi_explorer_conversion.go; openapi_explorer_direct.go; openapi_explorer_explicit_groups.go; openapi_explorer_population_member_proposal.go; openapi_explorer_routes.go; openapi_explorer_row_definition.go; openapi_explorer_selections.go; openapi_explorer_table_shape.go; openapi_frame_source_options.go; openapi_graphql_routes.go; openapi_recipe_execution.go; openapi_routes.go; openapi_semantic_inventory.go; options.go; private_artifact_reconciler.go; receipt_preview_diagnostic.go; recipe_authorization.go; recipe_combine_resolution.go; recipe_discovery.go; recipe_publication.go; release_verifier.go; routes.go; row_sources.go; runtime.go; server.go,accepted_construction_contract_test.go; append_workspace_rows_integration_test.go; artifact_reconciler_wiring_test.go; auth_wiring_trusted_issuer_test.go; backend_architecture_conformance_test.go; backend_composition_contract_test.go; backend_error_boundary_contract_test.go; backend_neutral_diagnostics_live_test.go; case008_row_definition_preview_route_test.go; command_helpers_test.go; config_test.go; construction_category_discovery_openapi_error_test.go; construction_inputs_catalog_test.go; construction_proposal_zero_column_test.go; core_authoring_protocol_arango_conformance_test.go; core_authoring_protocol_formal_replay_test.go; core_authoring_protocol_receipts_test.go; core_authoring_protocol_restore_test.go; core_authoring_protocol_rowschema_test.go; core_authoring_protocol_scoped_compile_test.go; core_authoring_protocol_test.go; dataframe_query_error_test.go; derived_policy_rows_integration_test.go; explain_wiring_pg18_test.go; explain_wiring_preview_test.go; explorer_capability_test.go; explorer_coded_value_capability_test.go; explorer_concept_evidence_test.go; explorer_config_route_test.go; explorer_diagnostic_logging_test.go; explorer_output_reconcile_derive_source_test.go; explorer_output_reconcile_source_child_test.go; explorer_output_reconcile_unpivot_source_test.go; explorer_preview_response_test.go; explorer_preview_security_test.go; explorer_receipt_contract_error_test.go; explorer_receipt_contract_test.go; explorer_reconcile_scope_test.go; explorer_row_change_route_test.go; explorer_state_route_test.go; fs1_signed_slot_collision_test.go; health_readiness_wiring_test.go; http_test_helpers_test.go; local_workspace_writeback_test.go; openapi_column_id_contract_test.go; openapi_explorer_artifacts_download_test.go; openapi_explorer_authoring_write_fault_test.go; openapi_explorer_cell_trace_test.go; openapi_explorer_commands_notfound_test.go; openapi_explorer_construction_apply_preview_test.go; openapi_explorer_construction_derive_source_test.go; openapi_explorer_construction_expand_source_test.go; openapi_explorer_construction_filter_source_test.go; openapi_explorer_construction_inputs_test.go; openapi_explorer_construction_proposal_test.go; openapi_explorer_construction_unpivot_source_test.go; openapi_explorer_conversion_test.go; openapi_explorer_direct_test.go; openapi_explorer_draft_restore_test.go; openapi_explorer_legacy_details_control_test.go; openapi_explorer_population_mapping_construction_test.go; openapi_explorer_population_member_proposal_test.go; openapi_explorer_restricted_starting_collection_integration_test.go; openapi_explorer_route_roundtrip_test.go; openapi_explorer_route_rows_arango_test.go; openapi_explorer_row_definition_test.go; openapi_explorer_write_scope_test.go; openapi_field_presentation_fixture_test.go; openapi_graphql_status_test.go; openapi_graphql_transport_review_test.go; openapi_interpretation_routes_test.go; openapi_recipe_execution_test.go; openapi_route_inventory_test.go; openapi_routes_test.go; openapi_semantic_inventory_test.go; openapi_status_coverage_test.go; openapi_test_helpers_test.go; options_schemas_test.go; patient_one_construction_choice_route_test.go; population_member_receipt_test.go; postgres_query_test.go; postgres_refusal_contract_test.go; preview_postgres_wiring_pg18_test.go; private_artifact_reconciler_test.go; publication_rows_integration_test.go; publication_viewer_agreement_test.go; receipt_preview_diagnostic_test.go; recipe_authorization_test.go; recipe_blank_project_test.go; recipe_combine_resolution_test.go; recipe_discovery_test.go; recipe_execution_etl_failure_fields_e2e_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_header_probe_test.go; recipe_execution_status_twin_e2e_test.go; recipe_execution_twin_authz_repair_test.go; recipe_project_keying_test.go; recipe_publication_test.go; release_verifier_fault_mapping_test.go; release_verifier_test.go; repository_activation_conflict_dev_test.go; route_composition_contract_test.go; routes_test.go; row_tag_guard_contract_test.go; row_value_policy_stream_test.go; runtime_postgres_live_test.go; runtime_test.go; wiring_test.go; zz_readiness_contract_guard_test.go,—,—,14261,40179,103,Activate; ActivateDatasetGeneration; ActivateRepositoryGeneration; ApplyExplorerBuilderCommands; AssessExplorerRowChange; AuthConfig; AuthorizeRead; AuthorizeWrite; BasicAuthConfig; BrowseExplorerFrameSourceOptions; BrowseExplorerSemanticInventory; CalyprAuthConfig; CheckExplorerPopulationMapping; ClickHouseConfig; Config; CreateDatasetGeneration; CreateExplorer; CreateExplorerExplicitGroupRevision; CreateExplorerSelection; CreateInterpretationRevision; CreateInterpretationRevisionFromColumn; DataframerConfig; DefaultConfig; DiscoverExplorerConstructionCategories; DiscoverExplorerTableShapeCategories; DownloadExplorerArtifact; Error; ExactExecutionMaterialization; ExecuteDataframeGraphQL; ExecuteGraphQL; Explain; ExplainPhysical; Finish; GetApolloSandbox; GetDatasetGenerationStatus; GetExecution; GetExplorer; GetExplorerAuthoringCapability; GetExplorerBuilder; GetExplorerColumnSource; GetExplorerConstructionCapabilities; GetExplorerConstructionInputs; GetExplorerSelection; GetExplorerTableShapeCapabilities; GetGraphQLPlayground; GetHealth; GetInterpretationRevision; GetLiveness; GetPointer; GetReadiness; GetRecipeExecution; HTTPRoutes; ListExplorerRowDefinitionChoices; ListExplorers; ListInterpretationLibraries; LoadConfig; PostgreSQLConfig; PrepareExplorerArtifact; Preview; PreviewCollectionScanEvidenceFromExplain; PreviewExplorer; PreviewInterpretationCandidate; ProbeCandidate; ProbeEdge; ProbeNode; ProposeExplorerConstruction; ProposeExplorerConstructionChoice; ProposeExplorerPopulationMemberRemoval; ProposeExplorerRowDefinition; ProposeExplorerTableShape; PublishAuthoring; PublishExplorer; PublishRepositoryExplorerConfig; ReadManifest; ReconcileExplorerBuilder; Resolve; ResolveConfiguredColumnContext; ResolveExplorerTableShape; ResolveForCompilation; ResolveForExecution; ResolveToken; Run; RunGeneration; RunWithConfig; SearchExplorerCandidates; SearchExplorerConstructionChoices; SearchExplorerPopulationRoutes; SearchExplorerRelatedExpandChoices; SearchExplorerRelatedExpandContributors; SearchExplorerRelatedFieldChoices; SearchExplorerSchemaFields; ServerConfig; StreamExactExport; String; TraceExplorerCell; TraceExplorerRowLineage; Unwrap; Validate; ValidateSelectionReferences; VerifyPublication; Visit; VisitProposeExplorerTableShapeResponse; Write,generated/loomapi; internal/api/authpolicy; internal/api/bulk/load; internal/api/columncapabilities; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/artifactfs; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/explorer/tableshapecap; internal/fhir/schema; internal/ingest; internal/projectid; internal/store/clickhouse,generated/graphql/graph/model; generated/loomapi; internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/lineage; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/fhir/schema; internal/ingest; internal/projectid; internal/source; internal/store/clickhouse,.; cmd/arango-fhir-server,internal/backends/postgres,github.com/gofiber/fiber/v3; go.yaml.in/yaml/v3; golang.org/x/sync/singleflight,—,legacy=41; migration=11; compatibility=3,broad surface (50 production files); inspect cohesion,go test ./internal/server,inventory complete; semantic disposition pending internal/source,github.com/calypr/loom/internal/source,source,domain/library,"Define the backend-neutral logical source identity, payload, and authorization-scope contracts shared by ingestion and store read/write paths, free of any storage or driver detail.",doc.go; resource.go,ownership_contract_test.go,—,—,80,403,2,Resource; ResourceScope,—,—,internal/backends/postgres/internal/store; internal/ingest,internal/backends/postgres; internal/dataframe/compiler; internal/dataframe/compiler/render/postgres; internal/dataframe/execution; internal/server,—,—,legacy=4,small shared semantic contract; compare direct callers before inlining,go test ./internal/source,inventory complete; semantic disposition pending internal/store/clickhouse,github.com/calypr/loom/internal/store/clickhouse,clickhouse,storage adapter,Provide the low-level typed ClickHouse connection and database-management boundary.,client.go,client_test.go; integration_test.go,—,—,676,712,15,AddColumn; Client; Close; Column; CreateTable; DropColumns; DropTable; EnsureDatabase; InsertRows; New; Options; Ping; QueryRowsArgs; QueryRowsArgsVisit; VerifyOutput,—,internal/dataframe/compiler/ir; internal/dataframe/compiler/render/clickhouse; internal/dataframe/published,internal/acceptance; internal/dataframe/execution/chartifact; internal/dataframe/publication/clickhouse; internal/server,internal/backends/arango/internal/chartifact; internal/backends/arango/internal/publication; internal/backends/postgres/internal/chartifact; internal/dataframe/compiler/render/clickhouse; internal/dataframe/execution; internal/dataframe/published,github.com/ClickHouse/clickhouse-go/v2; github.com/ClickHouse/clickhouse-go/v2/lib/driver,—,—,storage adapter; preserve interface and persisted-data contracts,go test ./internal/store/clickhouse,inventory complete; semantic disposition pending -scripts,github.com/calypr/loom/scripts,main,domain/library,"Hold repository developer, verification, migration, and release utilities that are not part of the Go module runtime packages.",check_fhir_semantic_resource_types.go; local-preview-index-migration.go,check_fhir_semantic_resource_types_test.go; local-preview-index-migration_test.go,README.md; package-lock.json; package.json; pg-exec-03-skipset-capture.json,—,769,532,0,—,internal/fhir/schema,—,—,—,github.com/arangodb/go-driver/v2/arangodb; github.com/arangodb/go-driver/v2/arangodb/shared; github.com/arangodb/go-driver/v2/connection; github.com/arangodb/go-driver/v2/utils,build tags,legacy=8; migration=9,no in-repo production importer; verify runtime/tooling reachability; runtime/build signals present; static importer counts are insufficient for deletion,go test ./scripts,inventory complete; semantic disposition pending +scripts,github.com/calypr/loom/scripts,main,domain/library,"Hold repository developer, verification, migration, and release utilities that are not part of the Go module runtime packages.",check_fhir_semantic_resource_types.go; local-preview-index-migration.go,check_fhir_semantic_resource_types_test.go; local-preview-index-migration_test.go,README.md; backend-conformance-skipset-capture.json; package-lock.json; package.json; pg-exec-03-skipset-capture.json,—,769,532,0,—,internal/fhir/schema,—,—,—,github.com/arangodb/go-driver/v2/arangodb; github.com/arangodb/go-driver/v2/arangodb/shared; github.com/arangodb/go-driver/v2/connection; github.com/arangodb/go-driver/v2/utils,build tags,legacy=8; migration=9,no in-repo production importer; verify runtime/tooling reachability; runtime/build signals present; static importer counts are insufficient for deletion,go test ./scripts,inventory complete; semantic disposition pending scripts/check-boundaries,github.com/calypr/loom/scripts/check-boundaries,main,domain/library,"Run the authoritative package-boundary policy over the repository and prove it can go RED: the --self-test negative fixtures plant forbidden imports, removed facades, re-exports, cycle leaks, and vacuous scans, each failing closed, with removal converging to green (arch-backend-09).",main.go,—,—,—,435,0,0,—,scripts/packagepolicy,—,—,—,—,—,—,no in-repo production importer; verify runtime/tooling reachability,go test ./scripts/check-boundaries,inventory complete; semantic disposition pending scripts/packagepolicy,github.com/calypr/loom/scripts/packagepolicy,packagepolicy,domain/library,"Hold the single authoritative parsed-import ownership policy table, scanner, identifier-absence and no-re-export checks shared by the check-boundaries CLI, the shell entrypoint, and the compiler guard tests, separating import findings from verification failures and refusing vacuous scans.",policy.go; rules.go,policy_test.go,old_concrete_paths.txt,—,1055,318,24,AbsenceRule; ByLabel; CheckAbsences; CheckNoReExport; Clean; ErrorKind; FindRepoRoot; Finding; Forbidden; HasImportFindings; ImportFindings; NoReExportRule; OldConcreteImportPaths; RepoAbsences; RepoNoReExport; RepoRules; Result; Rule; ScanAll; ScanError; ScanRule; ScanTarget; Scope; String,—,—,scripts/check-boundaries,internal/dataframe/compiler,—,go:embed,compatibility=2,one production importer (scripts/check-boundaries); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./scripts/packagepolicy,inventory complete; semantic disposition pending diff --git a/docs/PACKAGE_AUDIT.md b/docs/PACKAGE_AUDIT.md index f62aa9fe1..b7f47c093 100644 --- a/docs/PACKAGE_AUDIT.md +++ b/docs/PACKAGE_AUDIT.md @@ -6,14 +6,14 @@ A row marked as a consolidation candidate is **not** deletion authorization. Bef ## Snapshot -- Packages: 94 -- Production Go files: 695 -- Test Go files: 844 -- Production Go lines: 553,642 -- Test Go lines: 347,644 -- Package kinds: conformance=1, domain/library=61, executable=12, generated=5, storage adapter=6, transport adapter=9 -- Non-entrypoint handwritten packages with no production importer: internal/ci; scripts; scripts/check-boundaries -- Non-entrypoint handwritten packages with one production importer: internal/acceptance; internal/api/bulk/load; internal/api/graphql/graph; internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/arango/internal/explorer; internal/backends/arango/internal/recipeexec; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/redact; internal/backends/postgres/internal/rows; internal/dataframe/compiler/render/clickhouse; internal/dataframe/publication/clickhouse; internal/explorer/artifactfs; internal/explorer/lifecycle; internal/fhir/semantic; internal/repositoryseed; scripts/packagepolicy +- Packages: 96 +- Production Go files: 713 +- Test Go files: 873 +- Production Go lines: 560,599 +- Test Go lines: 357,011 +- Package kinds: conformance=1, domain/library=63, executable=12, generated=5, storage adapter=6, transport adapter=9 +- Non-entrypoint handwritten packages with no production importer: internal/ci; internal/dataframe/benchmark/cmd/benchcli; scripts; scripts/check-boundaries +- Non-entrypoint handwritten packages with one production importer: internal/acceptance; internal/api/bulk/load; internal/api/graphql/graph; internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/arango/internal/explorer; internal/backends/arango/internal/recipeexec; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/redact; internal/backends/postgres/internal/rows; internal/dataframe/benchmark; internal/dataframe/compiler/render/clickhouse; internal/dataframe/publication/clickhouse; internal/explorer/artifactfs; internal/explorer/lifecycle; internal/fhir/semantic; internal/repositoryseed; scripts/packagepolicy ## How to use the table @@ -53,19 +53,19 @@ These records preserve verified package-boundary decisions for both live package | `cmd/arango-fhir-proto` | executable | Provide operator commands for FHIR loading, catalog diagnostics, generation repair, and activation. | P: main.go; T: main_test.go; resource_inventory_test.go; Other: — | 842/470 | 0: — | internal/backends/arango; internal/catalog; internal/dataset; internal/fhir/schema; internal/ingest | — | executable boundary; compare orchestration with sibling commands before combining | | `cmd/arango-fhir-server` | executable | Start the explicit Loom HTTP server binary. | P: main.go; T: —; Other: — | 7/0 | 0: — | internal/server | — | executable boundary; compare orchestration with sibling commands before combining | | `cmd/dataframe-profile` | executable | Run and compare developer-facing dataframe profiling workloads. | P: main.go; T: —; Other: — | 331/0 | 0: — | conformance/compiler; generated/graphql/graph/model; internal/api/graphql/graph/query; internal/backends/arango; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/recipe; internal/dataframe/semantic | — | executable boundary; compare orchestration with sibling commands before combining | -| `cmd/dataframe-query` | executable | Execute a checked-in GraphQL dataframe request and report response timing and diagnostics. | P: main.go; T: main_test.go; Other: — | 459/432 | 3: Error; String; Unwrap | — | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/dataframe-query` | executable | Execute a checked-in GraphQL dataframe request and report response timing and diagnostics. | P: main.go; T: backend_neutral_diagnostics_test.go; main_test.go; Other: — | 475/508 | 3: Error; String; Unwrap | — | — | executable boundary; compare orchestration with sibling commands before combining | | `cmd/explorer-config-v2-convert` | executable | Convert legacy Explorer configuration input into the V2 authoring representation offline. | P: main.go; T: main_test.go; Other: — | 377/62 | 0: — | internal/dataframe/recipe; internal/explorer; internal/explorer/authoringv2 | — | executable boundary; compare orchestration with sibling commands before combining | | `cmd/generate` | executable | Generate FHIR models, schema metadata, validation, extraction, and GraphQL artifacts from source schemas. | P: extraction.go; graphql.go; helpers.go; main.go; metadata.go; model.go; names.go; resources.go; schema.go; validation.go; T: extraction_test.go; graphql_test.go; metadata_test.go; model_test.go; resources_test.go; root_test.go; schema_fixture_test.go; Other: fhir_helpers.go.tmpl | 1,750/522 | 10: BindingStrength; Definition; EdgeDocument; Link; Property; Result; Schema; TargetHints; TargetSchema; TemplatePointers | — | — | executable boundary; compare orchestration with sibling commands before combining; runtime/build signals present; static importer counts are insufficient for deletion | | `cmd/gqlgenfix` | executable | Post-process gqlgen output to enforce Loom-specific generated-code invariants. | P: main.go; T: main_test.go; Other: — | 173/67 | 1: MarshalJSON | — | — | executable boundary; compare orchestration with sibling commands before combining | | `cmd/loom-acceptance` | executable | Run the locked real-data acceptance scenario and emit machine-readable verification artifacts. | P: main.go; T: main_test.go; Other: — | 225/46 | 0: — | internal/acceptance; internal/explorer | — | executable boundary; compare orchestration with sibling commands before combining | -| `cmd/loom-data` | executable | Load one immutable generation into the backend chosen only by an explicit arango or postgres tag, routing to the Arango loader or the PostgreSQL lifecycle loader without inference or fallback. | P: load.go; main.go; T: load_test.go; main_test.go; Other: — | 964/1,703 | 2: Error; Unwrap | internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/ingest | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/loom-data` | executable | Load one immutable generation into the backend chosen only by an explicit arango or postgres tag, routing to the Arango loader or the PostgreSQL lifecycle loader without inference or fallback. | P: load.go; main.go; T: load_test.go; main_test.go; Other: — | 964/1,735 | 2: Error; Unwrap | internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/ingest | — | executable boundary; compare orchestration with sibling commands before combining | | `cmd/loom-repository` | executable | Preflight a repository checkout and launch Loom against its META and CONFIG data with an owned Compose project. | P: main.go; T: main_test.go; Other: — | 426/231 | 0: — | internal/projectid; internal/repositoryseed | — | executable boundary; compare orchestration with sibling commands before combining | | `cmd/preview-covering-index-artifact` | executable | Recreate a captured Builder proposal offline and emit the exact compiler artifact for index-coverage analysis. | P: main.go; T: main_test.go; Other: — | 1,449/34 | 0: — | internal/authscope; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation | — | executable boundary; compare orchestration with sibling commands before combining | | `conformance/compiler` | conformance | Load and exercise the canonical dataframe compiler oracle corpus for conformance and performance checks. | P: types.go; T: benchmark_test.go; command_helpers_test.go; corpus_coverage_test.go; gdc_fixture_test.go; generated_semantics_coverage_test.go; physical_plan_cost_test.go; policy_ablation_test.go; run_test.go; schema_roots_test.go; types_test.go; Other: — | 119/1,279 | 4: Expected; Fixture; LoadDir; Validate | internal/dataframe/recipe | cmd/dataframe-profile | non-runtime conformance boundary | | `generated/fhir` | generated | Expose generated FHIR resource models and their extraction, validation, and GraphQL helpers. | P: extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go; T: —; Other: — | 72,020/0 | 155: Address; Age; Annotation; Attachment; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; CodeableConcept; … (+143) | internal/fhir/schema | generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/dataframe/recipe; internal/dataframe/spec; internal/ingest | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | | `generated/fhirschema` | generated | Expose generated raw FHIR schema metadata tables. | P: generated.go; T: —; Other: — | 55,277/0 | 3: Definition; Property; Traversal | — | internal/fhir/schema | generated boundary; change its source/generator, not emitted code | -| `generated/graphql/graph/executor` | generated | Provide gqlgen-generated GraphQL executable schema and execution plumbing. | P: fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go; T: —; Other: — | 155,289/0 | 328: Address; AddressResolver; Age; AgeResolver; Annotation; AnnotationResolver; Attachment; AttachmentResolver; Availability; AvailabilityAvailableTime; AvailabilityAvailableTimeResolver; AvailabilityNotAvailableTime; … (+316) | generated/fhir; generated/graphql/graph/model | internal/api/graphql/graph; internal/api/graphql/graph/resolver | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | -| `generated/graphql/graph/model` | generated | Provide gqlgen-generated GraphQL transport model types. | P: models.go; T: —; Other: — | 1,333/0 | 95: DataframeAggregateInput; DataframeAggregateResult; DataframeAggregationSpecInput; DataframeAggregationsInput; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeDatasetInput; DataframeFilterInput; DataframeMaterialization; DataframeMaterializationState; DataframeOptimizationDecision; … (+83) | — | cmd/dataframe-profile; generated/graphql/graph/executor; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | +| `generated/graphql/graph/executor` | generated | Provide gqlgen-generated GraphQL executable schema and execution plumbing. | P: fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go; T: —; Other: — | 155,331/0 | 328: Address; AddressResolver; Age; AgeResolver; Annotation; AnnotationResolver; Attachment; AttachmentResolver; Availability; AvailabilityAvailableTime; AvailabilityAvailableTimeResolver; AvailabilityNotAvailableTime; … (+316) | generated/fhir; generated/graphql/graph/model | internal/api/graphql/graph; internal/api/graphql/graph/resolver | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | +| `generated/graphql/graph/model` | generated | Provide gqlgen-generated GraphQL transport model types. | P: models.go; T: —; Other: — | 1,334/0 | 95: DataframeAggregateInput; DataframeAggregateResult; DataframeAggregationSpecInput; DataframeAggregationsInput; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeDatasetInput; DataframeFilterInput; DataframeMaterialization; DataframeMaterializationState; DataframeOptimizationDecision; … (+83) | — | cmd/dataframe-profile; generated/graphql/graph/executor; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | | `generated/loomapi` | generated | Provide OpenAPI-generated HTTP request, response, and strict-server contracts. | P: api.gen.go; strict_json.go; T: construction_json_test.go; correlated_json_test.go; request_boundary_test.go; selection_json_test.go; strict_json_test.go; Other: — | 23,408/512 | 1620: APIVersion; AcceptedConstruction; AcceptedConstructionReceipt; Action; ActionColumn; ActivateDatasetGeneration; ActivateDatasetGeneration200JSONResponse; ActivateDatasetGeneration400JSONResponse; ActivateDatasetGeneration401JSONResponse; ActivateDatasetGeneration403JSONResponse; ActivateDatasetGeneration409JSONResponse; ActivateDatasetGeneration500JSONResponse; … (+1608) | internal/explorer/authoringv2 | internal/server | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/acceptance` | domain/library | Fetch and cache the locked FHIR fixture, drive the real-data acceptance scenario, and verify its outputs and performance evidence. | P: fixture.go; performance.go; runner.go; scenario.go; T: fixture_test.go; performance_test.go; runner_locking_test.go; runner_status_test.go; runner_zero_value_test.go; scenario_test.go; Other: — | 2,283/1,570 | 41: Acquire; Close; ComparePerformance; ComparePerformanceReportFiles; ComparePerformanceRuns; ComputedDigest; Config; Connections; DefaultLimits; Fetcher; FixtureDigest; FixtureLock; … (+29) | internal/backends/arango; internal/dataframe/publication; internal/explorer/authoringv2; internal/store/clickhouse | cmd/loom-acceptance | one production importer (cmd/loom-acceptance); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/api/authpolicy` | transport adapter | Centralize authorization-sentinel classification for transport operations while preserving operation-specific concealment. | P: policy.go; T: policy_test.go; Other: — | 56/44 | 2: Classify; Operation | internal/authscope; internal/dataframe/errors | internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/server | no automatic consolidation signal; perform semantic review | @@ -73,22 +73,22 @@ These records preserve verified package-boundary decisions for both live package | `internal/api/columncapabilities` | transport adapter | Derive transport-neutral filter, sort, and aggregate capabilities from published ClickHouse column types. | P: capabilities.go; T: capabilities_test.go; Other: — | 56/23 | 2: FromClickHouse; Tuple | — | internal/api/graphql/graph/dataframe; internal/server | no automatic consolidation signal; perform semantic review | | `internal/api/graphql/graph` | transport adapter | Own GraphQL HTTP handler construction, error presentation, and generated executor binding. | P: doc.go; errors.go; handler.go; T: aggregate_http_integration_test.go; errors_test.go; handler_test.go; http_integration_test.go; Other: — | 511/944 | 8: Flush; Header; NewApolloSandboxHandler; NewHandler; NewPlaygroundHandler; RouteConfig; Write; WriteHeader | generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/errors | internal/server | one production importer (internal/server); inspect ownership/inline potential | | `internal/api/graphql/graph/dataframe` | transport adapter | Adapt published ClickHouse dataframe discovery, row, aggregate, and export operations to GraphQL. | P: aggregates.go; export.go; model.go; rows.go; service.go; T: aggregates_test.go; export_test.go; fb22_principal_auth_path_control_test.go; filter_test.go; model_test.go; service_test.go; Other: — | 1,072/506 | 17: AggregateInput; AggregateRowsResult; AggregationsInput; AggregationsJSON; ColumnFromPhysical; ColumnModel; Config; Dataset; ExportDataframe; Model; NewService; PersistedFailure; … (+5) | generated/graphql/graph/model; internal/api/authpolicy; internal/api/columncapabilities; internal/api/http; internal/authscope; internal/dataframe/errors; internal/dataframe/publication; internal/dataframe/published; internal/projectid | internal/api/graphql/graph/resolver; internal/server | no automatic consolidation signal; perform semantic review | -| `internal/api/graphql/graph/query` | transport adapter | Resolve and authorize Arango-backed FHIR dataframe and graph query requests for GraphQL. | P: active_generation.go; doc.go; errors.go; fhir_service.go; fieldrefs.go; graph_service.go; helpers.go; input_resolution.go; introspection.go; recipe_mapping.go; recipe_resolve.go; service.go; types.go; T: active_generation_test.go; auth_scope_test.go; errors_test.go; fhir_service_test.go; fieldrefs_test.go; graph_service_test.go; introspection_test.go; postgres_query_test.go; project_identity_test.go; read_scope_ingress_control_chars_e2e_test.go; recipe_mapping_test.go; service_test.go; Other: — | 1,888/2,361 | 28: Config; ExplainFHIRGraph; FHIRGraphExplanation; FHIRGraphNode; FHIRGraphPageInfo; FHIRGraphPath; FHIRGraphQuery; FHIRGraphRelationship; FHIRGraphResult; FHIRGraphTraversal; FHIRListRequest; FHIRListResult; … (+16) | generated/graphql/graph/model; internal/api/authpolicy; internal/api/recipe; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataset; internal/fhir/schema; internal/projectid | cmd/dataframe-profile; internal/api/graphql/graph/resolver; internal/server | broad surface (13 production files); inspect cohesion | -| `internal/api/graphql/graph/resolver` | transport adapter | Implement gqlgen field resolvers and map GraphQL values to query and lifecycle services. | P: dataframe_diagnostics.go; fhir_reference_loader.go; fhir_resolver_helpers.go; fhir_schema.resolvers.go; graph_resolvers.go; graphql_output_helpers.go; operation_context.go; recipe_control_graphql.go; recipe_execution_reader.go; recipe_revision_model.go; resolver.go; schema.resolvers.go; T: fhir_reference_loader_test.go; output_mapping_test.go; recipe_control_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_oracle_probe_test.go; recipe_execution_reader_authz_e2e_test.go; recipe_execution_twin_wire_parity_test.go; recipe_revision_register_test.go; Other: — | 5,459/2,334 | 362: AbatementDateTime; AbatementString; Active; Actual; ActualComparisonGroup; ActualNumber; Address; Age; AgeString; Alias; AllDay; AllowedBoolean; … (+350) | generated/fhir; generated/graphql/graph/executor; generated/graphql/graph/model; internal/api/authpolicy; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataset; internal/fhir/schema | internal/api/graphql/graph; internal/server | broad surface (12 production files); inspect cohesion; mixed handwritten/generated package (2 generated file(s)); preserve regeneration boundary; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/api/graphql/graph/query` | transport adapter | Resolve and authorize Arango-backed FHIR dataframe and graph query requests for GraphQL. | P: active_generation.go; doc.go; errors.go; fhir_service.go; fieldrefs.go; graph_service.go; helpers.go; input_resolution.go; introspection.go; recipe_mapping.go; recipe_resolve.go; service.go; types.go; T: active_generation_test.go; auth_scope_test.go; errors_test.go; fhir_service_test.go; fieldrefs_test.go; graph_service_test.go; introspection_test.go; postgres_query_test.go; project_identity_test.go; read_scope_ingress_control_chars_e2e_test.go; recipe_mapping_test.go; service_test.go; Other: — | 1,888/2,364 | 28: Config; ExplainFHIRGraph; FHIRGraphExplanation; FHIRGraphNode; FHIRGraphPageInfo; FHIRGraphPath; FHIRGraphQuery; FHIRGraphRelationship; FHIRGraphResult; FHIRGraphTraversal; FHIRListRequest; FHIRListResult; … (+16) | generated/graphql/graph/model; internal/api/authpolicy; internal/api/recipe; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataset; internal/fhir/schema; internal/projectid | cmd/dataframe-profile; internal/api/graphql/graph/resolver; internal/server | broad surface (13 production files); inspect cohesion | +| `internal/api/graphql/graph/resolver` | transport adapter | Implement gqlgen field resolvers and map GraphQL values to query and lifecycle services. | P: dataframe_diagnostics.go; fhir_reference_loader.go; fhir_resolver_helpers.go; fhir_schema.resolvers.go; graph_resolvers.go; graphql_output_helpers.go; operation_context.go; recipe_control_graphql.go; recipe_execution_reader.go; recipe_revision_model.go; resolver.go; schema.resolvers.go; T: dataframe_diagnostics_test.go; fhir_reference_loader_test.go; output_mapping_test.go; recipe_control_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_oracle_probe_test.go; recipe_execution_reader_authz_e2e_test.go; recipe_execution_twin_wire_parity_test.go; recipe_revision_register_test.go; Other: — | 5,469/2,376 | 362: AbatementDateTime; AbatementString; Active; Actual; ActualComparisonGroup; ActualNumber; Address; Age; AgeString; Alias; AllDay; AllowedBoolean; … (+350) | generated/fhir; generated/graphql/graph/executor; generated/graphql/graph/model; internal/api/authpolicy; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataset; internal/fhir/schema | internal/api/graphql/graph; internal/server | broad surface (12 production files); inspect cohesion; mixed handwritten/generated package (2 generated file(s)); preserve regeneration boundary; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/api/http` | transport adapter | Own shared Fiber server setup, middleware, request IDs, errors, health, and readiness behavior. | P: doc.go; errors.go; middleware.go; request_id.go; routes.go; server.go; T: backend_error_boundary_contract_test.go; errors_test.go; health_readiness_test.go; http_test.go; request_id_test.go; Other: — | 642/1,145 | 15: App; ContextWithRequestID; ErrorResponse; HTTPConfig; HTTPErrorBody; HTTPServer; Health; Liveness; Logger; MapDataframeError; MappedError; NewHTTPServer; … (+3) | internal/authscope; internal/dataframe/errors | internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/server | no automatic consolidation signal; perform semantic review | | `internal/api/recipe` | transport adapter | Adapt scoped catalog field discovery to recipe schema resolution for one-shot and stored recipes. | P: discovery.go; T: discovery_auth_mode_test.go; discovery_test.go; Other: — | 110/126 | 3: FieldDiscovery; Fields; NewFieldDiscovery | internal/authscope; internal/catalog; internal/dataframe/errors; internal/dataframe/recipe/schema | internal/api/graphql/graph/query; internal/server | no automatic consolidation signal; perform semantic review | | `internal/authscope` | domain/library | Represent request principals and resolve authorized project, generation, and resource scopes. | P: authentication.go; doc.go; fence_resource_access.go; principal.go; scope_resolver.go; T: all_generations_cache_key_test.go; auth_path_control_ingress_test.go; auth_resource_path_options_conflict_test.go; authentication_test.go; covers_execution_paths_control_test.go; persisted_scope_fails_closed_test.go; probe_double_normalize_test.go; scope_cache_key_namespace_test.go; scope_test.go; Other: — | 1,175/2,104 | 36: AllowAllAuthorizer; Authenticate; Authenticator; AuthorizeProject; AuthorizeReadProject; AuthorizeWrite; Authorizer; BasicAuthenticator; BearerTokenAuthenticator; CalyprAuthenticator; Clone; ContextWithPrincipal; … (+24) | internal/catalog; internal/projectid | cmd/preview-covering-index-artifact; internal/api/authpolicy; internal/api/bulk/load; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/backends/arango/internal/explorer; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/backends/arango` | storage adapter | Expose the only public entry to the ArangoDB 3.12 backend: a single Open factory validates explicit config, owns exactly one lifecycle client with the required collection bootstrap policy, and returns the typed runtime carrying the catalog, dataset lifecycle, Explorer, recipe registry/revisions, and published-dataframe ports assembled from the private implementation tree, with alias re-exports for witness-side reassembly. | P: aliases.go; doc.go; open.go; T: ownership_contract_test.go; Other: — | 336/800 | 4: Close; Config; Open; Runtime | internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/arango/internal/dataset; internal/backends/arango/internal/explorer; internal/backends/arango/internal/publication; internal/backends/arango/internal/recipeexec; internal/backends/arango/internal/store | cmd/arango-fhir-proto; cmd/dataframe-profile; cmd/loom-data; internal/acceptance; internal/ingest; internal/server | storage adapter; preserve interface and persisted-data contracts | -| `internal/backends/arango/internal/catalog` | domain/library | Persist and retrieve catalog facts in ArangoDB. | P: codec.go; evidence.go; fields.go; relationships.go; resource_inventory.go; resources.go; semantic_coverage.go; semantic_coverage_envelope.go; semantic_inventory.go; store.go; T: all_generations_auth_path_conflict_test.go; all_generations_project_binding_integration_test.go; resource_inventory_integration_test.go; resource_inventory_unit_test.go; semantic_coded_category_integration_test.go; semantic_coverage_envelope_test.go; semantic_coverage_resource_integration_test.go; semantic_coverage_test.go; semantic_inventory_category_test.go; semantic_inventory_integration_test.go; semantic_inventory_selection_test.go; semantic_inventory_source_availability_test.go; store_test.go; Other: — | 5,267/5,466 | 54: AccumulateRelationshipCatalog; AddSource; AdvanceSemanticInventoryBackfill; AdvanceSemanticInventoryBuild; AuditRelationshipEdges; BeginSemanticCoverageBuild; BeginSemanticCoverageResource; BeginSemanticInventoryBuild; ClaimSemanticInventoryBackfill; CompleteRetainedSemanticCoverageResource; CompleteSemanticCoverageResource; CompleteSemanticInventoryBackfill; … (+42) | internal/backends/arango/internal/store; internal/catalog; internal/fhir/schema | internal/backends/arango | one production importer (internal/backends/arango); inspect ownership/inline potential | +| `internal/backends/arango/internal/catalog` | domain/library | Persist and retrieve catalog facts in ArangoDB. | P: codec.go; evidence.go; fields.go; relationships.go; resource_inventory.go; resources.go; semantic_coverage.go; semantic_coverage_envelope.go; semantic_inventory.go; store.go; T: all_generations_auth_path_conflict_test.go; all_generations_project_binding_integration_test.go; missing_field_catalog_integration_test.go; resource_inventory_integration_test.go; resource_inventory_unit_test.go; semantic_coded_category_integration_test.go; semantic_coverage_envelope_test.go; semantic_coverage_resource_integration_test.go; semantic_coverage_test.go; semantic_inventory_category_test.go; semantic_inventory_integration_test.go; semantic_inventory_selection_test.go; semantic_inventory_source_availability_test.go; store_test.go; Other: — | 5,283/5,594 | 54: AccumulateRelationshipCatalog; AddSource; AdvanceSemanticInventoryBackfill; AdvanceSemanticInventoryBuild; AuditRelationshipEdges; BeginSemanticCoverageBuild; BeginSemanticCoverageResource; BeginSemanticInventoryBuild; ClaimSemanticInventoryBackfill; CompleteRetainedSemanticCoverageResource; CompleteSemanticCoverageResource; CompleteSemanticInventoryBackfill; … (+42) | internal/backends/arango/internal/store; internal/catalog; internal/fhir/schema | internal/backends/arango | one production importer (internal/backends/arango); inspect ownership/inline potential | | `internal/backends/arango/internal/chartifact` | domain/library | Persist private ClickHouse artifact manifests and leases in a dedicated Arango collection. | P: catalog.go; T: catalog_arango_test.go; catalog_test.go; Other: — | 379/723 | 10: BootstrapSpec; Catalog; ClaimCleanup; Create; Delete; ListExpired; NewCatalog; ReleaseCleanup; Renew; Update | internal/backends/arango/internal/store; internal/dataframe/execution/chartifact | internal/backends/arango | one production importer (internal/backends/arango); inspect ownership/inline potential | -| `internal/backends/arango/internal/dataset` | domain/library | Persist immutable dataset generation lifecycle state in ArangoDB. | P: collections.go; doc.go; document_codec.go; manifest_lifecycle.go; release_store.go; T: generation_activation_cas_e2e_test.go; release_policy_change_e2e_test.go; revision_inflation_e2e_test.go; store_test.go; Other: — | 764/1,652 | 15: Activate; ActiveDocumentKey; BootstrapSpec; CollectionSpecs; CompareAndSwapActivateRelease; CompareAndSwapPublishRelease; CreateManifest; ManifestDocumentKey; New; ReadActiveRelease; ReadManifest; ResolveActiveManifest; … (+3) | internal/backends/arango/internal/store; internal/dataset | internal/backends/arango; internal/backends/arango/internal/explorer | no automatic consolidation signal; perform semantic review | +| `internal/backends/arango/internal/dataset` | domain/library | Persist immutable dataset generation lifecycle state in ArangoDB. | P: collections.go; doc.go; document_codec.go; manifest_lifecycle.go; release_store.go; T: generation_activation_cas_e2e_test.go; release_policy_change_e2e_test.go; release_store_cas_retry_test.go; revision_inflation_e2e_test.go; store_test.go; Other: — | 791/1,783 | 15: Activate; ActiveDocumentKey; BootstrapSpec; CollectionSpecs; CompareAndSwapActivateRelease; CompareAndSwapPublishRelease; CreateManifest; ManifestDocumentKey; New; ReadActiveRelease; ReadManifest; ResolveActiveManifest; … (+3) | internal/backends/arango/internal/store; internal/dataset | internal/backends/arango; internal/backends/arango/internal/explorer | no automatic consolidation signal; perform semantic review | | `internal/backends/arango/internal/explorer` | domain/library | Persist Explorer drafts, revisions, receipts, publications, capabilities, and repository state in ArangoDB. | P: activation.go; capability_snapshot_store.go; codec.go; collections.go; drafts.go; explicit_groups.go; explorers.go; interpretations.go; publication.go; receipts.go; repository.go; revisions.go; selection_references.go; selections.go; store.go; table_shape_capabilities.go; T: capability_snapshot_store_test.go; collections_test.go; cross_generation_publish_e2e_test.go; draft_conflict_test.go; draft_transaction_formal_replay_test.go; drafts_integration_test.go; drafts_test.go; explicit_group_storage_integration_test.go; explicit_groups_test.go; interpretations_test.go; receipt_store_test.go; repository_test.go; selection_references_test.go; selection_scale_integration_test.go; selection_storage_integration_test.go; selections_cleanup_e2e_test.go; store_test.go; table_shape_capabilities_test.go; Other: — | 3,366/4,552 | 56: AbortExplicitGroupRevision; AbortSelection; ActivateRepositoryGeneration; AppendExplicitGroupMemberships; AppendSelectionMembers; BeginExplicitGroupRevision; BeginSelection; BootstrapSpec; CapabilitySnapshotCollectionSpec; CapabilitySnapshotStore; CleanupExplicitGroupStaging; CleanupSelectionStaging; … (+44) | internal/authscope; internal/backends/arango/internal/dataset; internal/backends/arango/internal/publication; internal/backends/arango/internal/store; internal/dataframe/publication; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/tableshapecap; internal/projectid | internal/backends/arango | one production importer (internal/backends/arango); inspect ownership/inline potential; broad surface (16 production files); inspect cohesion | | `internal/backends/arango/internal/publication` | domain/library | Persist dataframe publication registry and bundle metadata in ArangoDB. | P: bundle_registry.go; registry.go; T: bundle_registry_test.go; dangling_binding_e2e_test.go; execution_read_pin_integration_test.go; name_latest_binding_e2e_test.go; publish_atomicity_e2e_test.go; release_dangle_reconcile_e2e_test.go; Other: — | 533/2,990 | 22: AcquireBundleLease; AcquireExecutionReadPin; BootstrapSpec; ClaimExecutionCleanup; Client; CompareAndSwapPointer; FindExecutionByKey; FindExecutionBySelector; GetExecution; GetPointer; ListExecutions; New; … (+10) | internal/backends/arango/internal/store; internal/dataframe/publication | internal/backends/arango; internal/backends/arango/internal/explorer | no automatic consolidation signal; perform semantic review | | `internal/backends/arango/internal/recipeexec` | domain/library | Persist recipe versions and execution-use records in ArangoDB. | P: registry.go; revisions.go; T: registry_test.go; revisions_test.go; revisions_user_faults_test.go; Other: — | 294/555 | 15: BootstrapSpec; Client; Get; List; LoadRecipe; LoadRecipeVersion; New; NewRevisionRegistry; RecipeVersionUsed; Register; Registry; ReplaceRecipeVersion; … (+3) | internal/backends/arango/internal/store; internal/dataframe/errors; internal/dataframe/recipe; internal/dataframe/recipe/exec | internal/backends/arango | one production importer (internal/backends/arango); inspect ownership/inline potential | | `internal/backends/arango/internal/store` | domain/library | Provide the low-level typed ArangoDB client, query streaming, profiling, and Explain assessment boundary. | P: client.go; errors.go; explain.go; explain_assessment.go; explain_client.go; options.go; preview_index.go; profile.go; profile_client.go; types.go; T: client_query_memory_limit_test.go; client_queryrows_test.go; client_revision_test.go; errors_test.go; explain_assessment_test.go; explain_client_test.go; explain_test.go; population_profile_integration_test.go; preview_index_test.go; profile_client_test.go; profile_integration_test.go; profile_test.go; Other: — | 1,950/2,210 | 70: AQLExecutor; AssessExplainResult; BatchInserter; Bootstrap; BootstrapSpec; Client; Close; CollectionCount; CollectionExists; CollectionRevision; CollectionSpec; ConnectionOptions; … (+58) | internal/dataframe/errors | internal/backends/arango; internal/backends/arango/internal/catalog; internal/backends/arango/internal/chartifact; internal/backends/arango/internal/dataset; internal/backends/arango/internal/explorer; internal/backends/arango/internal/publication; internal/backends/arango/internal/recipeexec | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/backends/postgres` | storage adapter | Own the PostgreSQL 18 factory: a single Open validates explicit config, opens exactly one readiness-gated pool with own-prefix connection tagging, composes the control runtime, and returns the typed runtime carrying the SourceRows and PlanJSON data-plane ports over tagged compiled commands through the typed Querier bridge; the sole legal importer of the private rows adapter, and no re-export surface - control-plane consumers import internal/backends/postgres/control directly (arch-backend-09 deleted aliases.go). | P: doc.go; open.go; plan_executor.go; T: engine_postgres_command_boundary_pg18_test.go; engine_postgres_pg18_test.go; explain_postgres_test.go; open_live_test.go; ownership_contract_test.go; plan_executor_test.go; sentinel_identity_test.go; Other: — | 178/4,408 | 5: NewPlanJSONExecutor; NewSourceRows; Open; Querier; Runtime | internal/backends/postgres/control; internal/backends/postgres/internal/rows; internal/backends/postgres/internal/store; internal/dataframe/compiler/command; internal/dataframe/execution | cmd/loom-data; internal/ingest; internal/server | storage adapter; preserve interface and persisted-data contracts | -| `internal/backends/postgres/control` | domain/library | Own the cycle-free public control plane of the PostgreSQL backend as its single definition: configuration and series-assignment validation, the bare-pool client and store-owned batch vocabulary, the dataset manifest lifecycle and catalog evidence port assembly, and the structural query surface; production consumers and backend-adjacent witnesses import it directly for control-plane symbols, and it must never reach the factory root, the private rows adapter, or dataframe/execution (machine-enforced by scripts/packagepolicy). | P: catalog_read.go; config.go; errors.go; runtime.go; vocabulary.go; vocabulary_control.go; T: catalog_read_test.go; Other: — | 1,386/284 | 65: AllSeries; Begin; CatalogStore; Close; Commit; Config; ConnSource; DiscoverExistingAuthResourcePaths; DiscoverFields; Error; Exec; FieldProfileScope; … (+53) | internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/redact; internal/backends/postgres/internal/store; internal/catalog; internal/dataset | cmd/loom-data; internal/backends/postgres; internal/ingest; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | -| `internal/backends/postgres/internal/catalog` | domain/library | Persist and retrieve catalog facts in PostgreSQL 18: publish-last generation-scoped resource inventory builds, bounded field-profile records with completeness and truncation metadata, leased compare-and-swap-guarded semantic inventory build state, deterministic semantic-inventory contribution rows with content-addressed replay and conflict classification, byte-order-stable keyset pages over retained resources, and authorization-scoped semantic-inventory pages and selection resolution served directly from contribution aggregates. | P: field_discovery.go; field_profile.go; relationships.go; resource_inventory.go; semantic_build.go; semantic_contribution.go; semantic_page.go; store.go; T: field_discovery_test.go; field_profile_test.go; relationships_pg_test.go; relationships_test.go; resource_inventory_test.go; semantic_build_test.go; semantic_contribution_test.go; semantic_page_test.go; Other: — | 4,340/11,088 | 54: AdvanceSemanticInventoryBackfill; ClaimSemanticInventoryBackfill; CompleteSemanticInventoryBackfill; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverRelationshipObservations; DiscoverResourceInventory; EnsureSemanticInventoryEntries; Error; FailSemanticInventoryBackfill; FieldProfileStore; … (+42) | internal/backends/postgres/internal/store; internal/catalog; internal/fhir/schema | internal/backends/postgres/control | one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/backends/postgres/control` | domain/library | Own the cycle-free public control plane of the PostgreSQL backend as its single definition: configuration and series-assignment validation, the bare-pool client and store-owned batch vocabulary, the dataset manifest lifecycle and catalog evidence port assembly, and the structural query surface; production consumers and backend-adjacent witnesses import it directly for control-plane symbols, and it must never reach the factory root, the private rows adapter, or dataframe/execution (machine-enforced by scripts/packagepolicy). | P: catalog_read.go; config.go; errors.go; runtime.go; vocabulary.go; vocabulary_control.go; T: capability_compose_test.go; capability_evidence_assembly_test.go; catalog_read_test.go; Other: — | 1,393/796 | 65: AllSeries; Begin; CatalogStore; Close; Commit; Config; ConnSource; DiscoverExistingAuthResourcePaths; DiscoverFields; Error; Exec; FieldProfileScope; … (+53) | internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/chartifact; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/redact; internal/backends/postgres/internal/store; internal/catalog; internal/dataset | cmd/loom-data; internal/backends/postgres; internal/dataframe/benchmark/cmd/benchcli; internal/ingest; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/backends/postgres/internal/catalog` | domain/library | Persist and retrieve catalog facts in PostgreSQL 18: publish-last generation-scoped resource inventory builds, bounded field-profile records with completeness and truncation metadata, leased compare-and-swap-guarded semantic inventory build state, deterministic semantic-inventory contribution rows with content-addressed replay and conflict classification, byte-order-stable keyset pages over retained resources, and authorization-scoped semantic-inventory pages and selection resolution served directly from contribution aggregates. | P: field_discovery.go; field_profile.go; relationships.go; resource_inventory.go; semantic_build.go; semantic_contribution.go; semantic_page.go; store.go; T: capability_compose_pg_test.go; field_discovery_test.go; field_profile_test.go; relationships_pg_test.go; relationships_test.go; resource_inventory_test.go; semantic_build_test.go; semantic_contribution_test.go; semantic_page_test.go; Other: — | 4,430/11,834 | 55: AdvanceSemanticInventoryBackfill; ClaimSemanticInventoryBackfill; CompleteSemanticInventoryBackfill; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverRelationshipObservations; DiscoverResourceInventory; EnsureSemanticInventoryEntries; Error; FailSemanticInventoryBackfill; FieldProfileStore; … (+43) | internal/backends/postgres/internal/store; internal/catalog; internal/fhir/schema | internal/backends/postgres/control | one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/backends/postgres/internal/chartifact` | domain/library | Persist private ClickHouse artifact manifests and cleanup leases in the PostgreSQL 18 control plane with CAS progress writes, bounded expired scans, and one-winner cleanup claims while physical tables stay in ClickHouse. | P: catalog.go; T: catalog_manager_test.go; catalog_test.go; Other: — | 535/1,605 | 10: Catalog; ClaimCleanup; Create; DB; Delete; ListExpired; NewCatalog; ReleaseCleanup; Renew; Update | internal/dataframe/execution/chartifact | internal/backends/postgres/control | one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/backends/postgres/internal/dataset` | domain/library | Persist dataset generation manifests, the active-generation compare-and-swap pointer, and immutable project release candidates with the active-release compare-and-swap pointer in PostgreSQL 18. | P: commands.go; manifest_lifecycle.go; migrate.go; release_store.go; store.go; T: commands_test.go; manifest_lifecycle_pg_test.go; release_store_pg_test.go; Other: — | 1,725/2,488 | 28: ActivateGeneration; Backend; Command; CommandBackend; CompareAndSwapActivateRelease; ConnSource; CreateManifest; EnsureGenerationPrerequisites; GenerationOwnerAdapter; MigrateSchema; New; NewPostgreSQLCommand; … (+16) | internal/backends/postgres/internal/store; internal/dataset | internal/backends/postgres/control; internal/backends/postgres/internal/explorer | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/backends/postgres/internal/explorer` | domain/library | Persist Explorer owner records, draft saves with atomic version/digest CAS and exact preimage history, insert-once compilation receipts, immutable Explorer revisions, create-once immutable capability snapshots and table-shape capability receipts, project-scoped interpretation libraries advancing heads over immutable content-addressed revisions by transactional parent CAS, the staged selection lifecycle with project-scoped idempotency, an expiring writer owner, and insert-once membership staged in bounded idempotent batches with the digest/count finalized under the current writer fence and bounded cancellation-aware keyset membership paging, atomic publish authoring that commits a verified compilation receipt, the immutable Explorer revision, the active Explorer owner pointer, and the project release activation CAS in one PostgreSQL transaction, and staging explicit-group revision headers with ordered definition rows and insert-once group membership (one row per revision/group/reference so an overlap is several rows, bounded keyset paging, and a fenced digest/count-verified STAGING to COMPLETE transition) under guarded writer-lease, abort, and expired-cleanup lifecycle in PostgreSQL 18 with unique project/Explorer identity, typed columns, lossless JSONB payloads, DB-enforced row freezing and content-digest collision checks, guard-triggered immutability, and CAS-guarded revision lifecycle on the shared pg-store pool and migration runner. | P: capabilities.go; drafts.go; explicit_group_memberships.go; explicit_groups.go; explorers.go; interpretations.go; publication.go; receipts.go; revisions.go; selection_members.go; selections.go; T: capabilities_test.go; drafts_test.go; explicit_groups_readpath_test.go; explicit_groups_test.go; explorers_test.go; interpretations_test.go; pgstate02_fixture_test.go; pubauth_fixture_test.go; publication_golden_test.go; publication_guarddrop_test.go; publication_race_test.go; publication_retry_test.go; publication_test.go; publication_zz_test.go; receipts_test.go; revisions_test.go; selection_members_test.go; selections_test.go; Other: — | 7,425/21,659 | 60: AbortExplicitGroupRevision; AbortSelection; AppendExplicitGroupMemberships; AppendSelectionMembers; BeginExplicitGroupRevision; BeginSelection; CapabilitySnapshotStore; CleanupExplicitGroupStaging; CleanupSelectionStaging; CompleteExplicitGroupRevision; CompleteSelection; Create; … (+48) | internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/store; internal/dataframe/publication; internal/dataframe/recipe; internal/dataset; internal/explorer; internal/explorer/capability; internal/explorer/tableshapecap | internal/backends/postgres/control | one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | @@ -97,13 +97,15 @@ These records preserve verified package-boundary decisions for both live package | `internal/backends/postgres/internal/recipes` | domain/library | Persist project recipe revision chains with CAS head tracking in the PostgreSQL 18 control plane. | P: revisions.go; T: revisions_test.go; Other: — | 409/1,072 | 9: ChainMigrations; DB; Get; List; MigrateSchema; New; Register; RevisionRegistry; VerifySchema | internal/backends/postgres/internal/store; internal/dataframe/errors; internal/dataframe/recipe | internal/backends/postgres/control | one production importer (internal/backends/postgres/control); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/backends/postgres/internal/redact` | domain/library | Scrub DSN passwords and secret parameter values from pool-open diagnostics so credential material never reaches rendered error text while the cause chain stays intact. | P: redact.go; T: redact_internal_test.go; Other: — | 79/84 | 3: ExtractDSNPassword; RedactDSN; ScrubSecretText | — | internal/backends/postgres/control | one production importer (internal/backends/postgres/control); inspect ownership/inline potential | | `internal/backends/postgres/internal/rows` | domain/library | Stream typed rows from tagged compiled PostgreSQL commands over pgx connections, preserving bound-value integrity and three-state typed decoding, enforcing output column contracts, and classifying cancellation, visitor stops, contract violations, and driver failures with identifiable wrapped causes. | P: rows.go; T: rows_boundary_pg18_ab03_test.go; rows_postgres_test.go; rows_postquery_postgres_test.go; rows_test.go; Other: — | 483/3,537 | 8: Error; Executor; NewExecutor; Querier; QueryError; RowVisitor; SourceRows; Unwrap | internal/backends/postgres/internal/store; internal/dataframe/compiler/command; internal/dataframe/execution | internal/backends/postgres | one production importer (internal/backends/postgres); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | -| `internal/backends/postgres/internal/store` | domain/library | Provide the bounded pgx/v5-backed PostgreSQL 18 source store: readiness-pinged pool, advisory-lock serialized migration runner, scoped FHIR resource repository that accepts the logical internal/source contract on writes and answers reads with its store-owned physical row under the server-assigned keyset sequence, scoped typed edge-occurrence repository, publish-last inventory repository, scoped bounded field-profile record persistence, the ledgered idempotent COPY batch writer for resource and edge rows, and resource-only and edge-only batch facades over that writer. | P: advisory.go; batch_writer.go; client.go; commit_outcome.go; edge.go; edge_writer.go; errors.go; field_profile.go; inventory.go; migrate.go; migration_ownership.go; resource.go; resource_writer.go; T: batch_writer_test.go; client_test.go; edge_test.go; edge_writer_test.go; errors_test.go; field_profile_ddl_test.go; field_profile_filter_golden_test.go; inventory_test.go; migrate_test.go; ownership_contract_test.go; resource_test.go; resource_writer_test.go; verify_classify_test.go; verify_readonly_test.go; Other: — | 5,898/13,789 | 112: Acquire; AsUniqueConflict; Assignment; AssignmentConflict; Batch; BatchAccountingError; BatchEdgeConflictError; BatchIdentityConflictError; BatchResourceConflictError; BatchResult; BatchWriteError; BatchWriter; … (+100) | internal/catalog; internal/dataframe/errors; internal/source | internal/backends/postgres; internal/backends/postgres/control; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/rows | broad surface (13 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/backends/postgres/internal/store` | domain/library | Provide the bounded pgx/v5-backed PostgreSQL 18 source store: readiness-pinged pool, advisory-lock serialized migration runner, scoped FHIR resource repository that accepts the logical internal/source contract on writes and answers reads with its store-owned physical row under the server-assigned keyset sequence, scoped typed edge-occurrence repository, publish-last inventory repository, scoped bounded field-profile record persistence, the ledgered idempotent COPY batch writer for resource and edge rows, and resource-only and edge-only batch facades over that writer. | P: advisory.go; batch_writer.go; client.go; commit_outcome.go; edge.go; edge_writer.go; errors.go; field_profile.go; inventory.go; migrate.go; migration_ownership.go; resource.go; resource_writer.go; T: batch_writer_test.go; client_test.go; edge_test.go; edge_writer_test.go; errors_test.go; field_profile_auth_paths_test.go; field_profile_ddl_test.go; field_profile_filter_golden_test.go; inventory_test.go; migrate_test.go; ownership_contract_test.go; resource_test.go; resource_writer_test.go; verify_classify_test.go; verify_readonly_test.go; Other: — | 5,971/13,963 | 113: Acquire; AsUniqueConflict; Assignment; AssignmentConflict; Batch; BatchAccountingError; BatchEdgeConflictError; BatchIdentityConflictError; BatchResourceConflictError; BatchResult; BatchWriteError; BatchWriter; … (+101) | internal/catalog; internal/dataframe/errors; internal/source | internal/backends/postgres; internal/backends/postgres/control; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipeexec; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/rows | broad surface (13 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/catalog` | domain/library | Own persistence-neutral observed FHIR field, pivot, relationship, and capability facts. | P: auth_scope.go; backend_contract.go; cache.go; capability_evidence.go; generation.go; helpers.go; limits.go; rebuild.go; relationship_evidence.go; relationships.go; resource_inventory.go; retained_semantic_page.go; semantic.go; semantic_coverage.go; semantic_coverage_contract.go; semantic_inventory.go; semantic_schema.go; types.go; write_profiler.go; T: auth_scope_test.go; cache_test.go; capability_evidence_test.go; generation_test.go; relationship_evidence_census_test.go; relationship_evidence_test.go; relationships_test.go; semantic_coding_display_projection_test.go; semantic_coverage_diagnostics_test.go; semantic_coverage_test.go; semantic_inventory_buildid_cache_test.go; semantic_inventory_category_test.go; semantic_inventory_emitter_anchor_test.go; semantic_inventory_test.go; semantic_schema_identifier_type_test.go; semantic_schema_test.go; semantic_test.go; write_profiler_coverage_test.go; write_profiler_test.go; Other: — | 6,471/3,946 | 146: AuthResourcePathOptions; Cache; CacheOptions; CacheStats; CapabilityEvidence; CapabilityEvidenceOptions; CapabilityEvidenceReader; CensusSemanticCoverage; ConceptCandidate; DecodeSemanticInventoryCursor; DefaultProfileLimits; DiscoverFields; … (+134) | internal/fhir/schema; internal/fhir/semantic | cmd/arango-fhir-proto; cmd/loom-data; internal/api/graphql/graph/query; internal/api/recipe; internal/authscope; internal/backends/arango/internal/catalog; internal/backends/postgres/control; internal/backends/postgres/internal/catalog; internal/backends/postgres/internal/store; internal/explorer/authoringv2; internal/explorer/lifecycle; internal/ingest; internal/server | broad surface (19 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/ci` | domain/library | Exercise repository-wide Go formatting and vet gates, including injected fault detection. | P: —; T: gofmt_gate_test.go; vet_gate_test.go; Other: — | 0/283 | 0: — | — | — | no in-repo production importer; verify runtime/tooling reachability | +| `internal/dataframe/benchmark` | domain/library | Run the Arango-versus-PostgreSQL matched dataframe benchmark: deterministic fixtures, canonical bags and correctness gates evaluated before any timing is claimed, control runs, and report artifacts that a re-computing verifier must accept, with ClickHouse-backed publication legs disclosed as planned/not_run when the service is absent. | P: armplan.go; authoring.go; canonical.go; controls.go; driver.go; fixtures.go; gates.go; harness.go; matrix.go; measure.go; oracle.go; provision.go; report.go; report_md.go; T: armplan_test.go; authoring_test.go; canonical_test.go; controls_test.go; driver_test.go; fixtures_fhir_test.go; fixtures_test.go; gates_test.go; harness_scope_test.go; harness_test.go; matrix_test.go; measure_test.go; oracle_test.go; publication_test.go; report_test.go; scratch_authrepro_test.go; scratch_liveupload_test.go; Other: README.md | 5,506/3,534 | 154: AgeBandFor; AppendWorkloadDisclosures; Apply; ApplyExcludedWorkloadGateReports; ArangoBackendYAML; Arm; ArmConfig; ArmEvidence; AuthorWorkload; AuthoringStatus; AuthoringVerification; BackendResult; … (+142) | internal/explorer/authoringv2 | internal/dataframe/benchmark/cmd/benchcli | one production importer (internal/dataframe/benchmark/cmd/benchcli); inspect ownership/inline potential; broad surface (14 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/benchmark/cmd/benchcli` | domain/library | Own the benchmark command-line entry point: fail-closed live-run preflight, planned disclosure artifacts, report verification authority, and the loom_bench PostgreSQL database lifecycle. | P: deployer.go; main.go; probes.go; setup.go; T: guards_test.go; Other: — | 1,016/374 | 0: — | internal/backends/postgres/control; internal/dataframe/benchmark | — | no in-repo production importer; verify runtime/tooling reachability | | `internal/dataframe/columntransform` | domain/library | Define closed, backend-neutral transformations over one selected output column value. | P: types.go; T: types_test.go; Other: — | 79/61 | 7: CategoryMapping; Clone; ExactCategoryRecode; Kind; UnknownPolicy; Validate; ValueTransformation | — | internal/dataframe/compiler/lower; internal/dataframe/recipe; internal/explorer/authoringv2 | no automatic consolidation signal; perform semantic review | | `internal/dataframe/compiler` | domain/library | Orchestrate semantic planning, physical lowering, optimization, rendering, and compiled-query assembly. | P: api.go; category_scan.go; category_scan_filter_pushdown.go; category_scan_related.go; cell_trace.go; clone_helpers.go; compiled_query.go; construction_preview_root_id_filter.go; graph_execution.go; ir_helpers.go; optimizer.go; output.go; population_mapping.go; postgres_target.go; preview_covering_index.go; recipe_execution.go; recipe_paging.go; row_lineage.go; semantic_group_projection_preview.go; table_shape_exclusions.go; target.go; T: backend_leaf_guard_test.go; backend_ownership_guard_contract_test.go; category_scan_arango_test.go; category_scan_parent_presence_arango_test.go; category_scan_related_collection_identity_test.go; category_scan_related_presence_arango_test.go; category_scan_related_test.go; category_scan_test.go; cell_trace_test.go; cohort_drop_boundary_arango_test.go; command_helpers_external_test.go; command_helpers_test.go; compiled_command_test.go; construction_arango_test.go; construction_coded_group_arango_test.go; construction_coded_group_execution_test.go; construction_coded_pivot_test.go; construction_execution_test.go; construction_group_expand_arango_test.go; construction_group_preview_liveness_arango_test.go; construction_group_related_summary_arango_test.go; construction_group_source_streaming_test.go; construction_pivot_lineage_arango_test.go; construction_preview_root_id_filter_test.go; construction_related_count_arango_test.go; construction_related_expand_arango_test.go; construction_related_group_preview_arango_test.go; correlated_arango_test.go; correlated_legacy_compat_test.go; correlated_physical_test.go; correlated_selection_test.go; correlated_system_version_arango_test.go; explicit_cohort_values_arango_test.go; filter_semantics_test.go; filter_test.go; generic_physical_plan_test.go; grain_test.go; group_connected_field_source_arango_test.go; group_field_source_arango_test.go; nested_expansion_arango_test.go; operator_oracles_arango_test.go; physical_diagnostics_test.go; physical_lowering_test.go; physical_optimize_test.go; physical_plan_test.go; physical_projection_reserved_name_arango_test.go; physical_render_test.go; physical_required_match_test.go; physical_scope_test.go; pivot_presence_arango_audit_test.go; pivot_presence_audit_test.go; plan_test_helpers_test.go; population_frontier_arango_test.go; population_mapping_filter_exists_test.go; population_mapping_identity_guard_test.go; population_mapping_routed_group_arango_test.go; population_mapping_test.go; postgres_coded_group_compile_test.go; postgres_parity_fixture_test.go; postgres_target_fingerprint_golden_test.go; public_columns_freeze_test.go; recipe_execution_pg18_test.go; recipe_execution_test.go; recipe_paging_related_filter_sequence_test.go; recipe_paging_routed_exists_arango_test.go; recipe_paging_test.go; root_page_related_seed_arango_test.go; row_expansion_test.go; row_item_projection_arango_test.go; row_lineage_cohort_filter_arango_test.go; row_lineage_composed_arango_test.go; row_lineage_group_pivot_arango_test.go; row_lineage_group_rows_arango_test.go; row_lineage_test.go; selected_source_contract_test.go; selection_semantics_test.go; selector_first_limit_test.go; semantic_group_arango_test.go; semantic_group_preview_arango_test.go; semantic_group_projection_factorization_arango_test.go; semantic_validation_test.go; source_group_preview_arango_test.go; streaming_missing_category_pivot_test.go; table_reshape_arango_test.go; table_reshape_execution_window_test.go; table_shape_exclusions_test.go; workspace_output_capture_test.go; Other: — | 8,120/33,173 | 44: ArangoTarget; Backend; CategoryOverflowWitness; CategoryScanProof; CategoryScanRefusal; CategoryScanRefusalCode; CategoryScanRefusalCodeOf; CompileCategoryScanOutput; CompileCategoryScanStage; CompileCellTraceOutput; CompileClickHousePrefix; CompilePopulationMappingOutput; … (+32) | internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec | cmd/dataframe-profile; cmd/preview-covering-index-artifact; internal/api/graphql/graph/query; internal/dataframe/execution; internal/explorer/lifecycle; internal/server | broad surface (21 production files); inspect cohesion | | `internal/dataframe/compiler/capability` | domain/library | Probe which dataframe language capabilities the compiler can prove and support. | P: probe.go; schema_fields.go; T: probe_test.go; schema_fields_test.go; Other: — | 803/313 | 22: CandidateCapability; CandidateRequest; Chart; Filter; Options; OwnerRecordsCapability; OwnerRecordsRequest; ProbeCandidate; ProbeOwnerRecords; ProbeProjection; ProbeRoot; ProbeTraversal; … (+10) | internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema | internal/explorer/lifecycle; internal/server | no automatic consolidation signal; perform semantic review | -| `internal/dataframe/compiler/command` | domain/library | Own the closed dispatch-tagged executable command types, rendered PostgreSQL plan validation, and constructor guarantees shared by compilers and row adapters. | P: command.go; rendered_postgres.go; T: compiled_command_test.go; imports_guard_test.go; rendered_postgres_test.go; Other: — | 433/526 | 15: AQLCommand; AQLParameters; Backend; CompiledBackend; CompiledCommand; Dispatch; NewAQLCommand; NewPostgreSQLCommand; NewRenderedPostgresPhysicalPlan; PostgreSQLCommand; PostgreSQLParameter; PostgreSQLParameterType; … (+3) | — | internal/backends/postgres; internal/backends/postgres/internal/rows; internal/dataframe/compiler; internal/dataframe/compiler/render/postgres; internal/dataframe/execution; internal/server | no automatic consolidation signal; perform semantic review | +| `internal/dataframe/compiler/command` | domain/library | Own the closed dispatch-tagged executable command types, rendered PostgreSQL plan validation, and constructor guarantees shared by compilers and row adapters. | P: command.go; rendered_postgres.go; T: compiled_command_test.go; imports_guard_test.go; rendered_postgres_test.go; Other: — | 433/526 | 15: AQLCommand; AQLParameters; Backend; CompiledBackend; CompiledCommand; Dispatch; NewAQLCommand; NewPostgreSQLCommand; NewRenderedPostgresPhysicalPlan; PostgreSQLCommand; PostgreSQLParameter; PostgreSQLParameterType; … (+3) | — | internal/api/graphql/graph/resolver; internal/backends/postgres; internal/backends/postgres/internal/rows; internal/dataframe/compiler; internal/dataframe/compiler/render/postgres; internal/dataframe/execution; internal/server | no automatic consolidation signal; perform semantic review | | `internal/dataframe/compiler/ir` | domain/library | Define typed physical operations, values, scope proofs, policies, and validation. | P: clone.go; dataset_generation.go; diagnostics.go; doc.go; navigation.go; physical_combine.go; physical_combine_scalar.go; physical_operations.go; physical_stage_sequence.go; physical_stage_validation.go; physical_table_shape.go; physical_types.go; physical_validation.go; physical_validation_expr.go; physical_validation_refs.go; physical_validation_shapes.go; physical_values.go; policy.go; postgres_storage.go; prefix.go; scope.go; selector_helpers.go; whole_relation_scope_evidence.go; workspace_combine_scope_operand.go; T: group_related_field_source_validation_test.go; physical_combine_scalar_test.go; physical_combine_test.go; physical_group_pivot_lineage_validation_test.go; physical_root_page_candidate_seed_test.go; physical_scope_cycle_test.go; physical_stage_validation_test.go; population_mapping_test.go; population_root_test.go; related_coding_group_lineage_validation_test.go; row_lineage_trace_validation_test.go; subplan_test.go; unit_validation_test.go; unnest_test.go; workspace_output_validation_test.go; Other: — | 12,283/1,804 | 215: AddDecision; CanonicalExecutionPhysicalPlan; ClickHouseArtifactIdentity; ClickHouseCombineScalarBaseType; ClonePhysicalExpression; ClonePhysicalOperation; ClonePhysicalOperations; ClonePhysicalPlan; ClonePhysicalPredicateExpression; ClonePhysicalSubplan; Column; CompilerPlanDiagnostics; … (+203) | internal/dataframe/spec; internal/dataframe/unit; internal/fhir/schema; internal/projectid | cmd/dataframe-profile; cmd/preview-covering-index-artifact; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/clickhouse; internal/dataframe/compiler/render/postgres; internal/dataframe/execution; internal/explorer/lifecycle; internal/server | broad surface (24 production files); inspect cohesion | | `internal/dataframe/compiler/lower` | domain/library | Lower logical FHIR dataframe plans into storage-aware physical IR. | P: auth_scope.go; child_sets.go; column_naming.go; correlated_binding.go; doc.go; filter_literal.go; graph_plan.go; group_field_source_context.go; group_field_source_related.go; group_related_occurrence_lower.go; group_rows.go; helpers.go; plan_orchestration.go; population.go; recipe_column_transformations.go; recipe_compile.go; recipe_construction.go; recipe_construction_coded_group.go; recipe_construction_coded_pivot.go; recipe_construction_combine.go; recipe_construction_combine_metadata.go; recipe_construction_composite.go; recipe_construction_group_sources.go; recipe_construction_related_eligibility.go; recipe_construction_related_expand.go; recipe_construction_related_field.go; recipe_construction_reshape.go; recipe_derived_columns.go; recipe_dynamic_columns.go; recipe_expression.go; recipe_expression_lowering.go; recipe_schema.go; recipe_table_reshape.go; required_match.go; root_projection.go; row_expansion.go; storage_route.go; traversal.go; typed_predicate.go; whole_relation_scope.go; workspace_output_dependencies.go; T: bound_scalar_coding_group_test.go; child_set_selector_demand_test.go; construction_group_streaming_test.go; correlated_binding_test.go; empty_bound_group_source_test.go; graph_plan_test.go; group_field_source_context_test.go; group_related_occurrence_lower_test.go; group_rows_test.go; group_specimen_inbound_coding_test.go; owner_records_test.go; physical_nested_scope_test.go; population_test.go; recipe_construction_coded_group_test.go; recipe_construction_coded_pivot_display_test.go; recipe_construction_coded_pivot_test.go; recipe_construction_combine_test.go; recipe_construction_composite_test.go; recipe_construction_related_count_test.go; recipe_construction_reshape_test.go; recipe_construction_row_values_test.go; recipe_construction_test.go; recipe_derived_columns_test.go; recipe_expression_test.go; recipe_output_test.go; recipe_schema_cardinality_test.go; recipe_semantic_path_test.go; recipe_table_reshape_test.go; recipe_unpivot_capability_test.go; root_id_filter_test.go; row_expansion_test.go; traversal_test.go; whole_relation_scope_evidence_test.go; workspace_combine_scope_evidence_test.go; workspace_output_compile_test.go; Other: — | 12,219/10,802 | 40: BindGroupRelatedOccurrenceSource; BuildGenericPhysicalPlanWithPolicy; BuildGraphPhysicalPlan; BuildPhysicalTraversal; CloneCompiledOutputSchema; CompileResolvedGraphPlan; CompileResolvedRecipePlan; CompiledActiveRelatedRecordStage; CompiledGroupFieldOwner; CompiledGroupFieldSourceAncestor; CompiledGroupFieldSourceContext; CompiledGroupFieldSourceExpansion; … (+28) | internal/authscope; internal/dataframe/columntransform; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/expression; internal/dataframe/lineage; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/fhir/schema; internal/projectid | internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/explorer/compilation; internal/explorer/lifecycle; internal/server | broad surface (41 production files); inspect cohesion | | `internal/dataframe/compiler/optimize` | domain/library | Apply explainable semantics-preserving rewrites to validated physical IR. | P: doc.go; optimize.go; T: optimize_test.go; Other: — | 438/124 | 1: OptimizePhysicalPlanWithPolicy | internal/dataframe/compiler/ir | internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution | no automatic consolidation signal; perform semantic review | @@ -111,7 +113,7 @@ These records preserve verified package-boundary decisions for both live package | `internal/dataframe/compiler/render/clickhouse` | storage adapter | Render typed Combine plans and resolved published or private table inputs as deterministic ClickHouse SQL. | P: combine.go; workspace_combine.go; T: combine_test.go; membership_nullable_integration_test.go; workspace_combine_test.go; Other: — | 930/1,093 | 6: RenderCombine; RenderCombineWithLimit; RenderCompositeClickHouseCombine; RenderWorkspaceClickHouseCombine; RenderedCombine; WorkspaceArtifact | internal/dataframe/compiler/ir | internal/dataframe/execution | one production importer (internal/dataframe/execution); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts | | `internal/dataframe/compiler/render/postgres` | storage adapter | Render the supported physical IR read spine and typed selector extractions with two-valued JSONB path-presence evidence onto allowlisted PostgreSQL storage bindings as deterministic parameterized SQL with ordered typed arguments, failing closed on every unsupported plan shape. | P: calls.go; category_scan.go; cell_trace.go; coded_group.go; graph.go; group.go; null_semantics.go; objects.go; pivot.go; population.go; population_mapping.go; predicates.go; render.go; root_scan.go; row_lineage.go; scope.go; scope_ops.go; selectors.go; sets.go; stage_related_occurrence.go; stage_semantic.go; stage_sequence.go; table_shape_exclusions.go; traversal.go; unit_normalization.go; unnest.go; unpivot.go; window.go; T: calls_live_test.go; calls_test.go; category_scan_test.go; cell_trace_test.go; coded_group_test.go; coded_pivot_test.go; graph_test.go; group_test.go; main_test.go; null_semantics_test.go; obj11_production_test.go; objects_test.go; pivot14_noncoded_test.go; pivot_test.go; population_live_test.go; population_mapping_test.go; population_test.go; predicates_pg18_test.go; predicates_test.go; render_pg18_test.go; render_test.go; root_scan_test.go; row_lineage_test.go; scope_ops_witness_test.go; scope_test.go; selectors_test.go; sets_live_test.go; sets_test.go; stage_related_occurrence_p4_test.go; stage_related_occurrence_production_test.go; stage_related_occurrence_test.go; stage_semantic_production_test.go; stage_semantic_test.go; stage_sequence_production_test.go; stage_sequence_test.go; table_shape_exclusions_live_test.go; table_shape_exclusions_test.go; traversal_test.go; unit_normalization_production_test.go; unit_normalization_test.go; unit_normalization_tie_test.go; unnest_test.go; unpivot_test.go; window_test.go; Other: — | 23,444/42,018 | 18: NewPostgresPopulationRootBinding; PostgresPopulationRootBinding; RenderCategoryScan; RenderGraphPhysicalPlan; RenderGroupedCodedPivot; RenderGroupedPivot; RenderPhysicalPlan; RenderPopulationMappingPlan; RenderPopulationRootScan; RenderRootScan; RenderSelectorExtraction; RenderTableShapeExclusions; … (+6) | internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/spec; internal/dataframe/unit; internal/fhir/schema | internal/dataframe/compiler; internal/dataframe/execution | broad surface (28 production files); inspect cohesion; storage adapter; preserve interface and persisted-data contracts | | `internal/dataframe/errors` | domain/library | Define and normalize stable structured dataframe errors for transport presentation. | P: errors.go; T: errors_test.go; Other: — | 488/172 | 21: AsUserError; Code; Details; Error; ErrorCode; ErrorOption; FieldPath; IsFeatureResolutionCode; IsRetryableCode; NewError; Normalize; PublicMessage; … (+9) | — | internal/api/authpolicy; internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/backends/arango/internal/recipeexec; internal/backends/arango/internal/store; internal/backends/postgres/internal/publication; internal/backends/postgres/internal/recipes; internal/backends/postgres/internal/store; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer/lifecycle; internal/server | small shared semantic contract; compare direct callers before inlining; runtime/build signals present; static importer counts are insufficient for deletion | -| `internal/dataframe/execution` | domain/library | Prepare, authorize, execute, explain, and post-process compiled dataframe requests. | P: category_scan.go; cell_trace.go; clickhouse_artifact.go; control.go; control_explain_types.go; control_types.go; engine.go; execution_service.go; explain.go; explain_postgres.go; group_preview_cache.go; group_preview_scan.go; helpers.go; named_uuid.go; population_mapping.go; postquery.go; preview_capabilities.go; query_budget.go; related_preview_paging.go; row_lineage.go; service.go; table_shape_exclusions.go; types.go; T: auth_scope_test.go; boundary_helpers_test.go; category_scan_postgres_test.go; category_scan_test.go; cell_trace_postgres_test.go; cell_trace_row_not_found_class_test.go; cell_trace_test.go; clickhouse_artifact_test.go; cohort_preview_paging_arango_test.go; command_boundary_contract_test.go; construction_cell_trace_arango_test.go; control_validate_digest_test.go; engine_blank_project_review_test.go; engine_clickhouse_test.go; engine_postgres_capture_pg18_test.go; engine_postgres_capture_test.go; engine_postgres_category_pg18_test.go; engine_postgres_cell_trace_pg18_test.go; engine_postgres_exclusions_pg18_test.go; engine_postgres_postquery_pg18_test.go; engine_postgres_preview_pg18_test.go; engine_postgres_test.go; engine_run_output_class_test.go; engine_test.go; engine_workspace_artifact_test.go; execution_test.go; explain_test.go; group_preview_cache_test.go; group_preview_scan_test.go; helpers_test.go; identity_test.go; named_uuid_test.go; population_group_preview_test.go; population_mapping_postgres_pg18_test.go; population_mapping_test.go; postgres_explain_parser_internal_test.go; postgres_target_fixture_test.go; postquery_test.go; preview_capability_boundary_test.go; recipe_paging_postgres_adapter_test.go; recipe_paging_postgres_test.go; recipe_preview_paging_postgres_internal_test.go; row_lineage_postgres_pg18_test.go; row_lineage_test.go; selected_source_contract_test.go; table_shape_exclusions_test.go; vendor_import_guard_test.go; verify_arch01_pins_test.go; Other: — | 7,088/27,056 | 109: BuildPostgreSQLExplainCommand; CategoryScanRequest; CategoryScanResult; CategoryValue; CellTrace; CellTraceCompiled; CellTraceContribution; CellTraceRequest; CellTraceResult; CellTraceStatus; ClickHouseQueryRows; CombinePortsConfigured; … (+97) | internal/authscope; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/clickhouse; internal/dataframe/compiler/render/postgres; internal/dataframe/errors; internal/dataframe/execution/chartifact; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/projectid | internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/postgres; internal/backends/postgres/internal/rows; internal/explorer/lifecycle; internal/server | broad surface (23 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/execution` | domain/library | Prepare, authorize, execute, explain, and post-process compiled dataframe requests. | P: category_scan.go; cell_trace.go; clickhouse_artifact.go; control.go; control_explain_types.go; control_types.go; engine.go; execution_service.go; explain.go; explain_postgres.go; group_preview_cache.go; group_preview_scan.go; helpers.go; named_uuid.go; population_mapping.go; postquery.go; preview_capabilities.go; query_budget.go; related_preview_paging.go; row_lineage.go; service.go; table_shape_exclusions.go; types.go; T: auth_scope_test.go; backend_neutral_diagnostics_test.go; boundary_helpers_test.go; category_scan_postgres_test.go; category_scan_test.go; cell_trace_postgres_test.go; cell_trace_row_not_found_class_test.go; cell_trace_test.go; clickhouse_artifact_test.go; cohort_preview_paging_arango_test.go; command_boundary_contract_test.go; construction_cell_trace_arango_test.go; control_validate_digest_test.go; engine_blank_project_review_test.go; engine_clickhouse_test.go; engine_postgres_capture_pg18_test.go; engine_postgres_capture_test.go; engine_postgres_category_pg18_test.go; engine_postgres_cell_trace_pg18_test.go; engine_postgres_exclusions_pg18_test.go; engine_postgres_postquery_pg18_test.go; engine_postgres_preview_pg18_test.go; engine_postgres_test.go; engine_run_output_class_test.go; engine_test.go; engine_workspace_artifact_test.go; execution_test.go; explain_test.go; group_preview_cache_test.go; group_preview_scan_test.go; helpers_test.go; identity_test.go; named_uuid_test.go; population_group_preview_test.go; population_mapping_postgres_pg18_test.go; population_mapping_test.go; postgres_explain_parser_internal_test.go; postgres_target_fixture_test.go; postquery_test.go; preview_capability_boundary_test.go; recipe_paging_postgres_adapter_test.go; recipe_paging_postgres_test.go; recipe_preview_paging_postgres_internal_test.go; row_lineage_postgres_pg18_test.go; row_lineage_test.go; selected_source_contract_test.go; table_shape_exclusions_test.go; vendor_import_guard_test.go; verify_arch01_pins_test.go; Other: — | 7,241/27,582 | 110: BuildPostgreSQLExplainCommand; CategoryScanRequest; CategoryScanResult; CategoryValue; CellTrace; CellTraceCompiled; CellTraceContribution; CellTraceRequest; CellTraceResult; CellTraceStatus; ClickHouseQueryRows; CombinePortsConfigured; … (+98) | internal/authscope; internal/dataframe/compiler; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/clickhouse; internal/dataframe/compiler/render/postgres; internal/dataframe/errors; internal/dataframe/execution/chartifact; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/projectid | internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/postgres; internal/backends/postgres/internal/rows; internal/explorer/lifecycle; internal/server | broad surface (23 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/dataframe/execution/chartifact` | domain/library | Manage private ClickHouse staging tables and leases used to cross execution boundaries without publishing outputs. | P: artifact.go; T: artifact_test.go; Other: — | 1,038/490 | 24: Abort; Artifact; Begin; Catalog; CheckLease; ClickHouse; Column; ColumnsFromCompiledOutput; Config; Finalize; Identity; LeaseContext; … (+12) | internal/authscope; internal/dataframe/compiler/lower; internal/dataframe/expression; internal/store/clickhouse | internal/backends/arango/internal/chartifact; internal/backends/postgres/internal/chartifact; internal/dataframe/execution; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/dataframe/expression` | domain/library | Define and validate the typed backend-neutral expression AST used by recipes. | P: expression_checking.go; expression_types.go; recipe_adapter.go; T: expression_test.go; recipe_adapter_test.go; Other: — | 856/235 | 26: AllowsMany; Call; Cardinality; Check; CheckedExpression; Constant; Document; DocumentRef; Expression; FromRecipe; FromRecipeInContexts; Function; … (+14) | internal/dataframe/recipe | internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution/chartifact; internal/dataframe/semantic; internal/explorer; internal/explorer/lifecycle; internal/server | no automatic consolidation signal; perform semantic review | | `internal/dataframe/lineage` | domain/library | Define stable identities and provenance for columns expanded from one authored source slot. | P: source_child.go; T: source_child_test.go; Other: — | 163/77 | 7: CloneSourceChild; Coordinate; IsSourceChildID; ParseSourceChildID; SourceChild; SourceChildKind; StableSourceChildID | — | internal/dataframe/compiler/lower; internal/dataframe/recipe; internal/explorer/authoringv2; internal/explorer/compilation | no automatic consolidation signal; perform semantic review | @@ -127,20 +129,20 @@ These records preserve verified package-boundary decisions for both live package | `internal/dataset` | domain/library | Define immutable dataset generation manifests, releases, active pointers, and lifecycle validation. | P: active_resolver.go; dataframe_selector.go; doc.go; manifest.go; manifest_port.go; ref.go; release.go; schema.go; validation.go; T: active_resolver_test.go; manifest_test.go; memory_store_test.go; ref_test.go; release_id_framing_test.go; release_idempotency_test.go; release_requirements_e2e_test.go; release_test.go; release_verifier_fault_classification_test.go; schema_test.go; Other: README.md | 802/2,253 | 34: Activate; ActivationRequest; Active; ActiveRelease; ActiveReleaseResolver; ActiveResolver; ContractVerification; Create; DataframeSelector; Error; IsStaged; Key; … (+22) | — | cmd/arango-fhir-proto; cmd/loom-data; internal/api/bulk/load; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/arango/internal/dataset; internal/backends/arango/internal/explorer; internal/backends/postgres/control; internal/backends/postgres/internal/dataset; internal/backends/postgres/internal/explorer; internal/backends/postgres/internal/publication; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/explorer; internal/explorer/lifecycle; internal/ingest; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/explorer` | domain/library | Define Explorer aggregate identities, configs, revisions, receipts, output contracts, stores, and core service operations. | P: artifact.go; authoring_errors.go; canonical_json.go; compilation.go; compilation_receipt.go; config_v2.go; explicit_group_repository.go; explicit_groups.go; identity.go; interpretation.go; output_contract.go; selection.go; service.go; state_v1.go; store.go; types.go; viewer_projection.go; T: artifact_test.go; compilation_receipt_test.go; explicit_groups_test.go; interpretation_test.go; output_contract_test.go; repository_revision_id_test.go; selection_test.go; types_test.go; viewer_projection_order_test.go; Other: — | 4,335/1,587 | 165: AbortSelection; ActivateRepositoryGeneration; ActiveRevision; AppendSelectionMembers; ApplyWorkspaceCommands; ApplyWorkspaceCommandsChecked; ArtifactCompletion; ArtifactID; ArtifactRecord; ArtifactStage; ArtifactState; ArtifactStore; … (+153) | internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/unit; internal/dataset; internal/explorer/authoringv2; internal/explorer/capability; internal/projectid | cmd/explorer-config-v2-convert; cmd/loom-acceptance; internal/backends/arango/internal/explorer; internal/backends/postgres/internal/explorer; internal/explorer/artifactfs; internal/explorer/compilation; internal/explorer/lifecycle; internal/server | broad surface (17 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/explorer/artifactfs` | domain/library | Persist and serve immutable Explorer training artifacts from a dedicated filesystem root. | P: store.go; T: reconcile_test.go; store_test.go; Other: — | 379/621 | 10: Abort; Begin; Close; Commit; Get; New; Open; Reconcile; Store; Write | internal/dataframe/published; internal/explorer | internal/server | one production importer (internal/server); inspect ownership/inline potential | -| `internal/explorer/authoringv2` | domain/library | Define, validate, canonicalize, and mutate the V2 Explorer Builder authoring workspace. | P: availability.go; canonical.go; commands.go; construction.go; construction_change.go; construction_choice.go; construction_migration.go; construction_row_values.go; construction_source_children.go; construction_validation.go; framing.go; migration.go; route_identity.go; row_change.go; row_definition.go; semantic_command.go; semantic_types.go; table_shape.go; transformations.go; types.go; validation.go; value_transformations.go; workspace_output_dependencies.go; T: commands_test.go; compile_preparation_test.go; construction_choice_test.go; construction_combine_test.go; construction_expand_test.go; construction_formal_replay_test.go; construction_group_coded_pivot_test.go; construction_group_field_source_test.go; construction_projection_replay_test.go; construction_proposal_test.go; construction_related_contributor_test.go; construction_row_values_test.go; construction_source_children_test.go; construction_test.go; correlated_test.go; framing_test.go; interpretation_test.go; migration_test.go; population_member_proposal_test.go; row_change_test.go; row_definition_test.go; row_proposal_test.go; semantic_codeable_concept_fallback_test.go; semantic_coded_category_test.go; semantic_command_test.go; table_shape_proposal_test.go; table_shape_test.go; transformations_test.go; types_test.go; value_transformations_test.go; workspace_output_dependencies_test.go; workspace_test.go; Other: — | 13,260/8,548 | 231: Action; ActionColumn; Addable; AggregateSource; AggregateTransformationCapabilities; AggregateTransformationCapabilitiesForCapability; AggregateTransformationCapabilitiesForCatalog; AnalyzeConstructionCandidate; AnalyzeStepEdit; ApplyCommands; ApplyCommandsRequest; ApplyCommandsResponse; … (+219) | internal/catalog; internal/dataframe/columntransform; internal/dataframe/lineage; internal/dataframe/unit; internal/explorer/capability; internal/fhir/schema | cmd/explorer-config-v2-convert; cmd/preview-covering-index-artifact; generated/loomapi; internal/acceptance; internal/backends/arango/internal/explorer; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/repositoryseed; internal/server | broad surface (23 production files); inspect cohesion | +| `internal/explorer/authoringv2` | domain/library | Define, validate, canonicalize, and mutate the V2 Explorer Builder authoring workspace. | P: availability.go; canonical.go; commands.go; construction.go; construction_change.go; construction_choice.go; construction_migration.go; construction_row_values.go; construction_source_children.go; construction_validation.go; framing.go; migration.go; route_identity.go; row_change.go; row_definition.go; semantic_command.go; semantic_types.go; table_shape.go; transformations.go; types.go; validation.go; value_transformations.go; workspace_output_dependencies.go; T: commands_test.go; compile_preparation_test.go; construction_choice_test.go; construction_combine_test.go; construction_expand_test.go; construction_formal_replay_test.go; construction_group_coded_pivot_test.go; construction_group_field_source_test.go; construction_projection_replay_test.go; construction_proposal_test.go; construction_related_contributor_test.go; construction_row_values_test.go; construction_source_children_test.go; construction_test.go; correlated_test.go; framing_test.go; interpretation_test.go; migration_test.go; population_member_proposal_test.go; row_change_test.go; row_definition_test.go; row_proposal_test.go; semantic_codeable_concept_fallback_test.go; semantic_coded_category_test.go; semantic_command_test.go; table_shape_proposal_test.go; table_shape_test.go; transformations_test.go; types_test.go; value_transformations_test.go; workspace_output_dependencies_test.go; workspace_test.go; Other: — | 13,260/8,548 | 231: Action; ActionColumn; Addable; AggregateSource; AggregateTransformationCapabilities; AggregateTransformationCapabilitiesForCapability; AggregateTransformationCapabilitiesForCatalog; AnalyzeConstructionCandidate; AnalyzeStepEdit; ApplyCommands; ApplyCommandsRequest; ApplyCommandsResponse; … (+219) | internal/catalog; internal/dataframe/columntransform; internal/dataframe/lineage; internal/dataframe/unit; internal/explorer/capability; internal/fhir/schema | cmd/explorer-config-v2-convert; cmd/preview-covering-index-artifact; generated/loomapi; internal/acceptance; internal/backends/arango/internal/explorer; internal/dataframe/benchmark; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/repositoryseed; internal/server | broad surface (23 production files); inspect cohesion | | `internal/explorer/capability` | domain/library | Build the immutable compiler-backed capability contract exposed to Explorer Builder. | P: builder.go; construction.go; construction_routes.go; domain.go; population_route_choice.go; repository.go; row_choice.go; value_operations.go; T: builder_test.go; construction_test.go; domain_test.go; row_choice_test.go; semantic_fallback_dto_test.go; value_operations_test.go; Other: — | 3,950/1,396 | 110: AggregateInput; AggregateOperation; AggregateOperationCapability; AggregateRowContext; Allows; Availability; AvailabilityReadiness; AvailabilitySourceReadiness; Build; Builder; Candidate; CandidateProof; … (+98) | internal/dataframe/recipe | cmd/preview-covering-index-artifact; internal/backends/arango/internal/explorer; internal/backends/postgres/internal/explorer; internal/explorer; internal/explorer/authoringv2; internal/explorer/compilation; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/explorer/compilation` | domain/library | Translate validated Explorer authoring intent into native recipe and compilation artifacts. | P: compile.go; construction.go; derived_columns.go; indexed_projection.go; interpretation.go; row_expansion.go; semantic_compile.go; source_row.go; table_reshape.go; T: acceptance_observation_aql_diagnostic_test.go; command_helpers_test.go; construction_combine_test.go; construction_related_contributor_test.go; construction_test.go; correlated_test.go; derived_columns_test.go; indexed_projection_test.go; interpretation_test.go; row_expansion_test.go; semantic_compile_expand_source_test.go; source_row_test.go; table_reshape_test.go; translate_test.go; value_transformations_test.go; workspace_output_mapping_test.go; workspace_test.go; Other: — | 3,622/4,536 | 24: Canonical; Compile; CompileWorkspace; Error; InterpretationCandidateMatch; InterpretationCandidateResolution; InterpretationCandidateResolutionState; Match; PopulationFor; PresentationColumn; PresentationConfig; Reason; … (+12) | internal/dataframe/compiler/lower; internal/dataframe/lineage; internal/dataframe/recipe; internal/dataframe/spec; internal/dataframe/unit; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/fhir/schema; internal/projectid | cmd/preview-covering-index-artifact; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/explorer/lifecycle` | domain/library | Coordinate transport-neutral Explorer authoring, preview, publication, query, and policy operations. | P: artifact.go; authoring.go; cell_trace.go; column_source.go; construction_category_discovery.go; construction_choice.go; construction_choice_proposal.go; construction_coded_pivot.go; construction_group_field_choices.go; construction_group_related_field_choices.go; construction_group_source.go; construction_pivot_source.go; construction_proposal.go; construction_routes.go; construction_semantic_group.go; derive_source.go; draft_restore.go; errors.go; expand_source.go; explicit_group_resolver.go; explicit_groups.go; filter_source.go; frame_source.go; interpretation.go; interpretation_apply.go; interpretation_context.go; interpretation_from_column.go; interpretation_library.go; interpretation_preview.go; policy.go; population.go; population_cursor.go; population_member_proposal.go; population_report.go; population_routes.go; preview.go; proposal_receipt.go; publish.go; query.go; repository_publish.go; row_change.go; row_choices.go; row_comparison.go; row_lineage.go; row_proposal.go; row_value_policy.go; schema_field_candidate.go; schema_fields.go; selection.go; semantic_catalog.go; service.go; table_shape_capabilities.go; table_shape_comparison.go; table_shape_proposal.go; table_shape_proposal_composition.go; table_shape_resolution.go; table_shape_saved.go; types.go; unpivot_source.go; T: artifact_test.go; builder_full_ui_replay_test.go; cell_trace_test.go; construction_category_discovery_errors_test.go; construction_category_discovery_test.go; construction_category_presence_test.go; construction_choice_category_search_test.go; construction_choice_proposal_test.go; construction_choice_test.go; construction_coded_pivot_saved_source_test.go; construction_group_field_choices_test.go; construction_pivot_source_test.go; construction_proposal_test.go; construction_proposal_trace_test.go; construction_related_expand_removal_test.go; construction_routes_test.go; construction_semantic_group_test.go; construction_v9_receipt_test.go; derive_source_test.go; expand_source_test.go; explicit_group_resolver_recovery_test.go; explicit_groups_test.go; filter_source_pivot_clear_test.go; filter_source_test.go; frame_source_test.go; fs1_reverse_order_collision_test.go; interpretation_context_test.go; interpretation_from_column_test.go; interpretation_library_test.go; interpretation_preview_test.go; interpretation_test.go; population_attachment_formal_replay_test.go; population_direction_test.go; population_formal_replay_test.go; population_member_proposal_test.go; population_removal_formal_replay_test.go; population_report_test.go; publish_release_requirements_classification_test.go; repository_publish_activation_conflict_test.go; repository_publish_generation_activation_classification_test.go; repository_publish_release_cas_conflict_test.go; row_change_test.go; row_choices_test.go; row_lineage_test.go; row_proposal_test.go; row_value_policy_test.go; schema_field_candidate_test.go; schema_fields_test.go; selection_test.go; semantic_authoring_test.go; semantic_catalog_test.go; semantic_category_test.go; semantic_coded_concept_test.go; semantic_field_presentation_test.go; semantic_frame_compatibility_test.go; service_test.go; table_shape_capabilities_test.go; table_shape_comparison_evidence_test.go; table_shape_proposal_test.go; unpivot_source_test.go; Other: — | 25,545/31,520 | 319: AcceptedConstruction; ApplyCommands; ApplyCommandsWithConstructionPreview; ApplyCommandsWithConstructionPreviewResponse; ArtifactOpenRequest; ArtifactPublishedReader; ArtifactRequest; ArtifactResult; AssessRowChange; AssessRowChangeRequest; AssessRowChangeResult; AuthorizedCapability; … (+307) | internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/tableshapecap; internal/fhir/schema; internal/projectid | internal/server | one production importer (internal/server); inspect ownership/inline potential; broad surface (59 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/explorer/tableshapecap` | domain/library | Define immutable table-shape capability and resolution artifacts plus their create-once repository contract. | P: identity.go; repository.go; types.go; T: artifacts_test.go; Other: — | 1,971/503 | 65: AvailabilityState; Binding; BooleanScalar; CanonicalContent; CatalogChoices; CatalogLookup; CatalogReceipt; CategoryProof; CategoryScanReceipt; CategoryValuesDigest; Choice; ChoiceRole; … (+53) | internal/projectid | internal/backends/arango/internal/explorer; internal/backends/postgres/internal/explorer; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | | `internal/fhir/schema` | domain/library | Add handwritten selector, traversal, pivot, and compiler semantics to generated FHIR metadata. | P: compiler_semantics.go; correlated.go; fields.go; generated_metadata.go; identifier.go; index.go; pivots.go; quantity.go; row_resolution.go; selectors.go; structure.go; terminal_metadata.go; traversals.go; T: compiler_semantics_test.go; correlated_selection_test.go; correlated_test.go; fields_test.go; identifier_test.go; index_test.go; pivots_test.go; root_resources_test.go; row_resolution_test.go; selectors_test.go; structure_test.go; terminal_metadata_test.go; traversals_test.go; Other: — | 2,446/1,341 | 86: ArrayElementType; ArrayElements; At; BindingStrength; CanonicalPath; CanonicalResourceType; CanonicalizePath; ChoiceValueSelectorOptions; CompilerTraversal; ConcreteResourceType; ContainsFilter; CorrelatedBinding; … (+74) | generated/fhirschema | cmd/arango-fhir-proto; generated/fhir; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/backends/arango/internal/catalog; internal/backends/postgres/internal/catalog; internal/catalog; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/compiler/render/postgres; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer/authoringv2; internal/explorer/compilation; internal/explorer/lifecycle; internal/fhir/semantic; internal/ingest; internal/server; scripts | broad surface (13 production files); inspect cohesion | | `internal/fhir/semantic` | domain/library | Walk generated FHIR definition indexes to enumerate typed referenced datatype occurrences. | P: member_walk.go; walk.go; T: member_walk_resource_dispatch_test.go; member_walk_test.go; walk_test.go; Other: — | 829/949 | 17: Error; Len; Less; MemberFact; Occurrence; Pop; Push; RepeatedBoundary; ResourceBoundary; Swap; Walk; WalkDiagnostic; … (+5) | internal/fhir/schema | internal/catalog | one production importer (internal/catalog); inspect ownership/inline potential | -| `internal/ingest` | domain/library | Preflight and load FHIR NDJSON into generation-scoped Arango vertices, edges, and catalog facts, and commit built resource-row and built-edge-occurrence batches to the PostgreSQL source store through the ledgered batch writer with scope verification, canonical payload encoding, and payload-backed path/ordinal recovery for repeated references. | P: backend.go; doc.go; documents.go; files.go; generated_load.go; generation_identity.go; generation_load.go; generation_orchestration.go; generation_workers.go; load_options.go; postgres_writer.go; preflight.go; progress.go; resource_load.go; row_builder.go; semantic_inventory_backfill.go; source_schema.go; T: backend_test.go; basic_observation_fixture_validation_test.go; bench_test.go; documents_test.go; file_pipeline_test.go; files_test.go; generated_load_test.go; generation_cleanup_test.go; generation_failed_cleanup_e2e_test.go; generation_identity_test.go; generation_load_postgres_durability_test.go; generation_load_postgres_test.go; generation_load_test.go; integration_test.go; keycollide_generated_e2e_test.go; load_options_test.go; postgres_writer_test.go; preflight_test.go; related_medication_fixture_test.go; resource_load_test.go; row_builder_test.go; semantic_inventory_backfill_integration_test.go; semantic_inventory_backfill_test.go; semantic_inventory_test.go; source_schema_test.go; viewer_row_file_download_fixture_test.go; Other: — | 5,650/10,567 | 63: ActivatePostgresGeneration; ActivationOutcomeError; BackfillSemanticInventory; Build; Complete; ConnectionOptions; DiscoverNDJSON; EdgeDocument; EdgeFromGrip; Error; EventSink; GeneratedRowBuilder; … (+51) | generated/fhir; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/fhir/schema; internal/source | cmd/arango-fhir-proto; cmd/loom-data; internal/api/bulk/load; internal/server | broad surface (17 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/ingest` | domain/library | Preflight and load FHIR NDJSON into generation-scoped Arango vertices, edges, and catalog facts, and commit built resource-row and built-edge-occurrence batches to the PostgreSQL source store through the ledgered batch writer with scope verification, canonical payload encoding, and payload-backed path/ordinal recovery for repeated references. | P: backend.go; doc.go; documents.go; files.go; generated_load.go; generation_identity.go; generation_load.go; generation_orchestration.go; generation_workers.go; load_options.go; postgres_writer.go; preflight.go; progress.go; resource_load.go; row_builder.go; semantic_inventory_backfill.go; source_schema.go; T: backend_test.go; basic_observation_fixture_validation_test.go; bench_test.go; documents_test.go; file_pipeline_test.go; files_test.go; generated_load_test.go; generation_cleanup_test.go; generation_failed_cleanup_e2e_test.go; generation_identity_test.go; generation_load_postgres_durability_test.go; generation_load_postgres_test.go; generation_load_test.go; integration_test.go; keycollide_generated_e2e_test.go; load_options_test.go; postgres_writer_test.go; preflight_test.go; related_medication_fixture_test.go; resource_load_test.go; row_builder_test.go; semantic_inventory_backfill_integration_test.go; semantic_inventory_backfill_test.go; semantic_inventory_test.go; source_schema_test.go; viewer_row_file_download_fixture_test.go; Other: — | 5,650/10,571 | 63: ActivatePostgresGeneration; ActivationOutcomeError; BackfillSemanticInventory; Build; Complete; ConnectionOptions; DiscoverNDJSON; EdgeDocument; EdgeFromGrip; Error; EventSink; GeneratedRowBuilder; … (+51) | generated/fhir; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataset; internal/fhir/schema; internal/source | cmd/arango-fhir-proto; cmd/loom-data; internal/api/bulk/load; internal/server | broad surface (17 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | | `internal/projectid` | domain/library | Define canonical and legacy-compatible public project identity normalization. | P: projectid.go; T: projectid_test.go; Other: — | 51/18 | 2: Canonical; Legacy | — | cmd/loom-repository; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/backends/arango/internal/explorer; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/execution; internal/dataframe/published; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/explorer/tableshapecap; internal/repositoryseed; internal/server | small shared semantic contract; compare direct callers before inlining | | `internal/repositoryseed` | domain/library | Validate repository metadata and configuration, produce a source manifest, and refuse unmaterialized inputs before launch. | P: preflight.go; T: preflight_test.go; Other: — | 281/105 | 6: Error; File; Manifest; Options; Prepare; UnmaterializedError | internal/explorer/authoringv2; internal/projectid | cmd/loom-repository | one production importer (cmd/loom-repository); inspect ownership/inline potential | -| `internal/server` | domain/library | Compose storage, domain services, generated OpenAPI handlers, GraphQL adapters, and HTTP routes. | P: artifact_dev.go; auth_wiring.go; compose.go; construction_inputs_catalog.go; explain_wiring.go; explorer_authoring_http.go; explorer_capability.go; explorer_combine_output_metadata.go; explorer_config_route.go; explorer_lifecycle_helpers.go; explorer_lifecycle_wire.go; explorer_output_reconcile.go; explorer_preview_response.go; explorer_preview_security.go; explorer_publication.go; explorer_receipt_contract.go; explorer_receipt_validation.go; local_workspace_writeback.go; openapi_data_routes.go; openapi_explorer_artifacts.go; openapi_explorer_authoring_direct.go; openapi_explorer_construction.go; openapi_explorer_construction_choice_proposal.go; openapi_explorer_construction_inputs.go; openapi_explorer_construction_proposal.go; openapi_explorer_conversion.go; openapi_explorer_direct.go; openapi_explorer_explicit_groups.go; openapi_explorer_population_member_proposal.go; openapi_explorer_routes.go; openapi_explorer_row_definition.go; openapi_explorer_selections.go; openapi_explorer_table_shape.go; openapi_frame_source_options.go; openapi_graphql_routes.go; openapi_recipe_execution.go; openapi_routes.go; openapi_semantic_inventory.go; options.go; private_artifact_reconciler.go; receipt_preview_diagnostic.go; recipe_authorization.go; recipe_combine_resolution.go; recipe_discovery.go; recipe_publication.go; release_verifier.go; routes.go; row_sources.go; runtime.go; server.go; T: accepted_construction_contract_test.go; append_workspace_rows_integration_test.go; artifact_reconciler_wiring_test.go; auth_wiring_trusted_issuer_test.go; backend_composition_contract_test.go; backend_error_boundary_contract_test.go; case008_row_definition_preview_route_test.go; command_helpers_test.go; config_test.go; construction_category_discovery_openapi_error_test.go; construction_inputs_catalog_test.go; construction_proposal_zero_column_test.go; core_authoring_protocol_arango_conformance_test.go; core_authoring_protocol_formal_replay_test.go; core_authoring_protocol_receipts_test.go; core_authoring_protocol_restore_test.go; core_authoring_protocol_rowschema_test.go; core_authoring_protocol_scoped_compile_test.go; core_authoring_protocol_test.go; dataframe_query_error_test.go; derived_policy_rows_integration_test.go; explain_wiring_pg18_test.go; explain_wiring_preview_test.go; explorer_capability_test.go; explorer_coded_value_capability_test.go; explorer_concept_evidence_test.go; explorer_config_route_test.go; explorer_diagnostic_logging_test.go; explorer_output_reconcile_derive_source_test.go; explorer_output_reconcile_source_child_test.go; explorer_output_reconcile_unpivot_source_test.go; explorer_preview_response_test.go; explorer_preview_security_test.go; explorer_receipt_contract_error_test.go; explorer_receipt_contract_test.go; explorer_reconcile_scope_test.go; explorer_row_change_route_test.go; explorer_state_route_test.go; fs1_signed_slot_collision_test.go; health_readiness_wiring_test.go; http_test_helpers_test.go; local_workspace_writeback_test.go; openapi_column_id_contract_test.go; openapi_explorer_artifacts_download_test.go; openapi_explorer_authoring_write_fault_test.go; openapi_explorer_cell_trace_test.go; openapi_explorer_commands_notfound_test.go; openapi_explorer_construction_apply_preview_test.go; openapi_explorer_construction_derive_source_test.go; openapi_explorer_construction_expand_source_test.go; openapi_explorer_construction_filter_source_test.go; openapi_explorer_construction_inputs_test.go; openapi_explorer_construction_proposal_test.go; openapi_explorer_construction_unpivot_source_test.go; openapi_explorer_conversion_test.go; openapi_explorer_direct_test.go; openapi_explorer_draft_restore_test.go; openapi_explorer_legacy_details_control_test.go; openapi_explorer_population_mapping_construction_test.go; openapi_explorer_population_member_proposal_test.go; openapi_explorer_restricted_starting_collection_integration_test.go; openapi_explorer_route_roundtrip_test.go; openapi_explorer_route_rows_arango_test.go; openapi_explorer_row_definition_test.go; openapi_explorer_write_scope_test.go; openapi_field_presentation_fixture_test.go; openapi_graphql_status_test.go; openapi_graphql_transport_review_test.go; openapi_interpretation_routes_test.go; openapi_recipe_execution_test.go; openapi_route_inventory_test.go; openapi_routes_test.go; openapi_semantic_inventory_test.go; openapi_status_coverage_test.go; openapi_test_helpers_test.go; options_schemas_test.go; patient_one_construction_choice_route_test.go; population_member_receipt_test.go; postgres_query_test.go; postgres_refusal_contract_test.go; preview_postgres_wiring_pg18_test.go; private_artifact_reconciler_test.go; publication_rows_integration_test.go; publication_viewer_agreement_test.go; receipt_preview_diagnostic_test.go; recipe_authorization_test.go; recipe_blank_project_test.go; recipe_combine_resolution_test.go; recipe_discovery_test.go; recipe_execution_etl_failure_fields_e2e_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_header_probe_test.go; recipe_execution_status_twin_e2e_test.go; recipe_execution_twin_authz_repair_test.go; recipe_project_keying_test.go; recipe_publication_test.go; release_verifier_fault_mapping_test.go; release_verifier_test.go; repository_activation_conflict_dev_test.go; route_composition_contract_test.go; routes_test.go; row_tag_guard_contract_test.go; row_value_policy_stream_test.go; runtime_postgres_live_test.go; runtime_test.go; wiring_test.go; zz_readiness_contract_guard_test.go; Other: — | 14,261/37,094 | 103: Activate; ActivateDatasetGeneration; ActivateRepositoryGeneration; ApplyExplorerBuilderCommands; AssessExplorerRowChange; AuthConfig; AuthorizeRead; AuthorizeWrite; BasicAuthConfig; BrowseExplorerFrameSourceOptions; BrowseExplorerSemanticInventory; CalyprAuthConfig; … (+91) | generated/loomapi; internal/api/authpolicy; internal/api/bulk/load; internal/api/columncapabilities; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/artifactfs; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/explorer/tableshapecap; internal/fhir/schema; internal/ingest; internal/projectid; internal/store/clickhouse | .; cmd/arango-fhir-server | broad surface (50 production files); inspect cohesion | +| `internal/server` | domain/library | Compose storage, domain services, generated OpenAPI handlers, GraphQL adapters, and HTTP routes. | P: artifact_dev.go; auth_wiring.go; compose.go; construction_inputs_catalog.go; explain_wiring.go; explorer_authoring_http.go; explorer_capability.go; explorer_combine_output_metadata.go; explorer_config_route.go; explorer_lifecycle_helpers.go; explorer_lifecycle_wire.go; explorer_output_reconcile.go; explorer_preview_response.go; explorer_preview_security.go; explorer_publication.go; explorer_receipt_contract.go; explorer_receipt_validation.go; local_workspace_writeback.go; openapi_data_routes.go; openapi_explorer_artifacts.go; openapi_explorer_authoring_direct.go; openapi_explorer_construction.go; openapi_explorer_construction_choice_proposal.go; openapi_explorer_construction_inputs.go; openapi_explorer_construction_proposal.go; openapi_explorer_conversion.go; openapi_explorer_direct.go; openapi_explorer_explicit_groups.go; openapi_explorer_population_member_proposal.go; openapi_explorer_routes.go; openapi_explorer_row_definition.go; openapi_explorer_selections.go; openapi_explorer_table_shape.go; openapi_frame_source_options.go; openapi_graphql_routes.go; openapi_recipe_execution.go; openapi_routes.go; openapi_semantic_inventory.go; options.go; private_artifact_reconciler.go; receipt_preview_diagnostic.go; recipe_authorization.go; recipe_combine_resolution.go; recipe_discovery.go; recipe_publication.go; release_verifier.go; routes.go; row_sources.go; runtime.go; server.go; T: accepted_construction_contract_test.go; append_workspace_rows_integration_test.go; artifact_reconciler_wiring_test.go; auth_wiring_trusted_issuer_test.go; backend_architecture_conformance_test.go; backend_composition_contract_test.go; backend_error_boundary_contract_test.go; backend_neutral_diagnostics_live_test.go; case008_row_definition_preview_route_test.go; command_helpers_test.go; config_test.go; construction_category_discovery_openapi_error_test.go; construction_inputs_catalog_test.go; construction_proposal_zero_column_test.go; core_authoring_protocol_arango_conformance_test.go; core_authoring_protocol_formal_replay_test.go; core_authoring_protocol_receipts_test.go; core_authoring_protocol_restore_test.go; core_authoring_protocol_rowschema_test.go; core_authoring_protocol_scoped_compile_test.go; core_authoring_protocol_test.go; dataframe_query_error_test.go; derived_policy_rows_integration_test.go; explain_wiring_pg18_test.go; explain_wiring_preview_test.go; explorer_capability_test.go; explorer_coded_value_capability_test.go; explorer_concept_evidence_test.go; explorer_config_route_test.go; explorer_diagnostic_logging_test.go; explorer_output_reconcile_derive_source_test.go; explorer_output_reconcile_source_child_test.go; explorer_output_reconcile_unpivot_source_test.go; explorer_preview_response_test.go; explorer_preview_security_test.go; explorer_receipt_contract_error_test.go; explorer_receipt_contract_test.go; explorer_reconcile_scope_test.go; explorer_row_change_route_test.go; explorer_state_route_test.go; fs1_signed_slot_collision_test.go; health_readiness_wiring_test.go; http_test_helpers_test.go; local_workspace_writeback_test.go; openapi_column_id_contract_test.go; openapi_explorer_artifacts_download_test.go; openapi_explorer_authoring_write_fault_test.go; openapi_explorer_cell_trace_test.go; openapi_explorer_commands_notfound_test.go; openapi_explorer_construction_apply_preview_test.go; openapi_explorer_construction_derive_source_test.go; openapi_explorer_construction_expand_source_test.go; openapi_explorer_construction_filter_source_test.go; openapi_explorer_construction_inputs_test.go; openapi_explorer_construction_proposal_test.go; openapi_explorer_construction_unpivot_source_test.go; openapi_explorer_conversion_test.go; openapi_explorer_direct_test.go; openapi_explorer_draft_restore_test.go; openapi_explorer_legacy_details_control_test.go; openapi_explorer_population_mapping_construction_test.go; openapi_explorer_population_member_proposal_test.go; openapi_explorer_restricted_starting_collection_integration_test.go; openapi_explorer_route_roundtrip_test.go; openapi_explorer_route_rows_arango_test.go; openapi_explorer_row_definition_test.go; openapi_explorer_write_scope_test.go; openapi_field_presentation_fixture_test.go; openapi_graphql_status_test.go; openapi_graphql_transport_review_test.go; openapi_interpretation_routes_test.go; openapi_recipe_execution_test.go; openapi_route_inventory_test.go; openapi_routes_test.go; openapi_semantic_inventory_test.go; openapi_status_coverage_test.go; openapi_test_helpers_test.go; options_schemas_test.go; patient_one_construction_choice_route_test.go; population_member_receipt_test.go; postgres_query_test.go; postgres_refusal_contract_test.go; preview_postgres_wiring_pg18_test.go; private_artifact_reconciler_test.go; publication_rows_integration_test.go; publication_viewer_agreement_test.go; receipt_preview_diagnostic_test.go; recipe_authorization_test.go; recipe_blank_project_test.go; recipe_combine_resolution_test.go; recipe_discovery_test.go; recipe_execution_etl_failure_fields_e2e_test.go; recipe_execution_malformed_persisted_scope_test.go; recipe_execution_malformed_scope_header_probe_test.go; recipe_execution_status_twin_e2e_test.go; recipe_execution_twin_authz_repair_test.go; recipe_project_keying_test.go; recipe_publication_test.go; release_verifier_fault_mapping_test.go; release_verifier_test.go; repository_activation_conflict_dev_test.go; route_composition_contract_test.go; routes_test.go; row_tag_guard_contract_test.go; row_value_policy_stream_test.go; runtime_postgres_live_test.go; runtime_test.go; wiring_test.go; zz_readiness_contract_guard_test.go; Other: — | 14,261/40,179 | 103: Activate; ActivateDatasetGeneration; ActivateRepositoryGeneration; ApplyExplorerBuilderCommands; AssessExplorerRowChange; AuthConfig; AuthorizeRead; AuthorizeWrite; BasicAuthConfig; BrowseExplorerFrameSourceOptions; BrowseExplorerSemanticInventory; CalyprAuthConfig; … (+91) | generated/loomapi; internal/api/authpolicy; internal/api/bulk/load; internal/api/columncapabilities; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/api/recipe; internal/authscope; internal/backends/arango; internal/backends/postgres; internal/backends/postgres/control; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/command; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/execution/chartifact; internal/dataframe/expression; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataframe/unit; internal/dataset; internal/explorer; internal/explorer/artifactfs; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/explorer/tableshapecap; internal/fhir/schema; internal/ingest; internal/projectid; internal/store/clickhouse | .; cmd/arango-fhir-server | broad surface (50 production files); inspect cohesion | | `internal/source` | domain/library | Define the backend-neutral logical source identity, payload, and authorization-scope contracts shared by ingestion and store read/write paths, free of any storage or driver detail. | P: doc.go; resource.go; T: ownership_contract_test.go; Other: — | 80/403 | 2: Resource; ResourceScope | — | internal/backends/postgres/internal/store; internal/ingest | small shared semantic contract; compare direct callers before inlining | | `internal/store/clickhouse` | storage adapter | Provide the low-level typed ClickHouse connection and database-management boundary. | P: client.go; T: client_test.go; integration_test.go; Other: — | 676/712 | 15: AddColumn; Client; Close; Column; CreateTable; DropColumns; DropTable; EnsureDatabase; InsertRows; New; Options; Ping; … (+3) | — | internal/acceptance; internal/dataframe/execution/chartifact; internal/dataframe/publication/clickhouse; internal/server | storage adapter; preserve interface and persisted-data contracts | -| `scripts` | domain/library | Hold repository developer, verification, migration, and release utilities that are not part of the Go module runtime packages. | P: check_fhir_semantic_resource_types.go; local-preview-index-migration.go; T: check_fhir_semantic_resource_types_test.go; local-preview-index-migration_test.go; Other: README.md; package-lock.json; package.json; pg-exec-03-skipset-capture.json | 769/532 | 0: — | internal/fhir/schema | — | no in-repo production importer; verify runtime/tooling reachability; runtime/build signals present; static importer counts are insufficient for deletion | +| `scripts` | domain/library | Hold repository developer, verification, migration, and release utilities that are not part of the Go module runtime packages. | P: check_fhir_semantic_resource_types.go; local-preview-index-migration.go; T: check_fhir_semantic_resource_types_test.go; local-preview-index-migration_test.go; Other: README.md; backend-conformance-skipset-capture.json; package-lock.json; package.json; pg-exec-03-skipset-capture.json | 769/532 | 0: — | internal/fhir/schema | — | no in-repo production importer; verify runtime/tooling reachability; runtime/build signals present; static importer counts are insufficient for deletion | | `scripts/check-boundaries` | domain/library | Run the authoritative package-boundary policy over the repository and prove it can go RED: the --self-test negative fixtures plant forbidden imports, removed facades, re-exports, cycle leaks, and vacuous scans, each failing closed, with removal converging to green (arch-backend-09). | P: main.go; T: —; Other: — | 435/0 | 0: — | scripts/packagepolicy | — | no in-repo production importer; verify runtime/tooling reachability | | `scripts/packagepolicy` | domain/library | Hold the single authoritative parsed-import ownership policy table, scanner, identifier-absence and no-re-export checks shared by the check-boundaries CLI, the shell entrypoint, and the compiler guard tests, separating import findings from verification failures and refusing vacuous scans. | P: policy.go; rules.go; T: policy_test.go; Other: old_concrete_paths.txt | 1,055/318 | 24: AbsenceRule; ByLabel; CheckAbsences; CheckNoReExport; Clean; ErrorKind; FindRepoRoot; Finding; Forbidden; HasImportFindings; ImportFindings; NoReExportRule; … (+12) | — | scripts/check-boundaries | one production importer (scripts/check-boundaries); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | diff --git a/internal/backends/arango/internal/catalog/all_generations_auth_path_conflict_test.go b/internal/backends/arango/internal/catalog/all_generations_auth_path_conflict_test.go index c421871f9..e4ed49bcb 100644 --- a/internal/backends/arango/internal/catalog/all_generations_auth_path_conflict_test.go +++ b/internal/backends/arango/internal/catalog/all_generations_auth_path_conflict_test.go @@ -12,8 +12,10 @@ import ( ) type fb27ctlArangoClient struct { - queries []string - vars []map[string]any + queries []string + vars []map[string]any + existsCalls int + collectionGone bool // simulates a database that has never ingested } func (c *fb27ctlArangoClient) QueryRows(_ context.Context, query string, _ int, vars map[string]any, _ store.RowVisitor) error { @@ -35,7 +37,8 @@ func (c *fb27ctlArangoClient) ExecuteAQL(context.Context, string, map[string]any } func (c *fb27ctlArangoClient) CollectionExists(context.Context, string) (bool, error) { - panic("unused by the auth path discovery controls") + c.existsCalls++ + return !c.collectionGone, nil } func (c *fb27ctlArangoClient) Bootstrap(context.Context, store.BootstrapSpec) error { @@ -51,11 +54,14 @@ func fb27ctlDiscover(t *testing.T, opts catalog.AuthResourcePathOptions) ([]stri } paths, err := s.DiscoverExistingAuthResourcePaths(context.Background(), opts) if err != nil { - if len(client.queries) != 0 { - t.Fatalf("VERDICT=UNEXPECTED rejected request reached Arango (%d queries) for %+v: %v", len(client.queries), opts, err) + if len(client.queries) != 0 || client.existsCalls != 0 { + t.Fatalf("VERDICT=UNEXPECTED rejected request reached Arango (%d probes, %d queries) for %+v: %v", client.existsCalls, len(client.queries), opts, err) } return paths, err } + if client.existsCalls != 1 { + t.Fatalf("VERDICT=UNEXPECTED accepted request issued %d existence probes for %+v, want 1", client.existsCalls, opts) + } if len(client.queries) != 1 { t.Fatalf("VERDICT=UNEXPECTED accepted request issued %d queries for %+v, want 1", len(client.queries), opts) } @@ -105,3 +111,34 @@ func TestFB27SupportedAuthResourcePathOptionShapesStillDiscover(t *testing.T) { }) } } + +// TestFB27MissingFieldCatalogIsNoRecordedPaths pins the first-authorized- +// upload guarantee: on a database that has never ingested, the collection +// does not exist, discovery answers "no recorded paths" (the posture the +// authscope F-B27 zero-path bootstrap branch is built for), and no AQL +// reaches Arango. Surfacing the missing-collection fault instead would +// 403 every first restricted-scope upload, because the project-binding +// gate consults discovery before ingest can bootstrap the collection. +func TestFB27MissingFieldCatalogIsNoRecordedPaths(t *testing.T) { + for _, opts := range []catalog.AuthResourcePathOptions{ + {Project: "projA", DatasetGeneration: "gen-1"}, + {Project: "projA", AllGenerations: true}, + } { + client := &fb27ctlArangoClient{collectionGone: true} + s, err := New(client) + if err != nil { + t.Fatalf("VERDICT=UNEXPECTED store construction = %v", err) + } + paths, err := s.DiscoverExistingAuthResourcePaths(context.Background(), opts) + if err != nil { + t.Fatalf("VERDICT=CONFIRMED missing fhir_field_catalog surfaced as a discovery fault for %+v: %v", opts, err) + } + if paths == nil || len(paths) != 0 { + t.Fatalf("VERDICT=UNEXPECTED missing collection must answer empty non-nil paths, got %#v", paths) + } + if len(client.queries) != 0 { + t.Fatalf("VERDICT=UNEXPECTED missing collection must not query (%d issued) for %+v", len(client.queries), opts) + } + } + t.Logf("VERDICT=REPAIRED a never-ingested database discovers zero paths instead of a storage fault") +} diff --git a/internal/backends/arango/internal/catalog/missing_field_catalog_integration_test.go b/internal/backends/arango/internal/catalog/missing_field_catalog_integration_test.go new file mode 100644 index 000000000..78f573413 --- /dev/null +++ b/internal/backends/arango/internal/catalog/missing_field_catalog_integration_test.go @@ -0,0 +1,90 @@ +package arango + +import ( + "context" + "net/http" + "net/url" + "os" + "strings" + "testing" + "time" + + storearango "github.com/calypr/loom/internal/backends/arango/internal/store" + "github.com/calypr/loom/internal/catalog" + "github.com/google/uuid" +) + +// Durable control for the first-authorized-upload deadlock: the authscope +// project-binding gate (F-B27) asks this discovery which auth paths a project +// has recorded BEFORE ingest can bootstrap fhir_field_catalog. On a fresh +// database the collection does not exist, so a storage fault there is +// surfaced as 403 FORBIDDEN on the very first restricted-scope upload and the +// database can never receive its first generation. Discovery must answer +// "no recorded paths", the same posture the zero-path bootstrap branch of +// AuthorizeWrite is built for and the posture the postgres backend has +// because SetupMigrate pre-creates the table. + +const missingCatalogTestDatabasePrefix = "loom_catalog_missing_" + +func openMissingCatalogHarness(t *testing.T, budget time.Duration) (context.Context, *storearango.Client) { + t.Helper() + endpoint := strings.TrimSpace(os.Getenv("LOOM_TEST_ARANGO_URL")) + if endpoint == "" { + t.Skip("set LOOM_TEST_ARANGO_URL to run the missing-field-catalog Arango proof") + } + database := missingCatalogTestDatabasePrefix + strings.ReplaceAll(uuid.NewString(), "-", "") + if !strings.HasPrefix(database, missingCatalogTestDatabasePrefix) { + t.Fatalf("verification database %q escapes the %s prefix", database, missingCatalogTestDatabasePrefix) + } + ctx, cancel := context.WithTimeout(context.Background(), budget) + t.Cleanup(cancel) + client, err := storearango.Open(ctx, endpoint, database) + if err != nil { + t.Fatalf("open owned database %s: %v", database, err) + } + t.Logf("MISSINGCATALOG_DATABASE=%s", database) + t.Cleanup(func() { + _ = client.Close(context.Background()) + cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cleanupCancel() + request, err := http.NewRequestWithContext(cleanupCtx, http.MethodDelete, strings.TrimRight(endpoint, "/")+"/_api/database/"+url.PathEscape(database), nil) + if err != nil { + t.Errorf("build cleanup request for %s: %v", database, err) + return + } + response, err := http.DefaultClient.Do(request) + if err != nil { + t.Errorf("cleanup %s: %v", database, err) + return + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound { + t.Errorf("cleanup %s: HTTP %d", database, response.StatusCode) + } + }) + // Deliberately NO bootstrap: the whole point is a database that has + // never ingested. + return ctx, client +} + +func TestDiscoverExistingAuthResourcePathsMissingCollectionIsNoPaths(t *testing.T) { + ctx, client := openMissingCatalogHarness(t, 60*time.Second) + store, err := New(client) + if err != nil { + t.Fatal(err) + } + for name, opts := range map[string]catalog.AuthResourcePathOptions{ + "generation scoped": {Project: "loom-bench", DatasetGeneration: "bench-first"}, + "all generations": {Project: "loom-bench", AllGenerations: true}, + } { + t.Run(name, func(t *testing.T) { + paths, err := store.DiscoverExistingAuthResourcePaths(ctx, opts) + if err != nil { + t.Fatalf("missing fhir_field_catalog must answer no paths, got error: %v", err) + } + if len(paths) != 0 { + t.Fatalf("fresh database must report zero recorded paths, got %v", paths) + } + }) + } +} diff --git a/internal/backends/arango/internal/catalog/resources.go b/internal/backends/arango/internal/catalog/resources.go index f2535cae2..eb83dfeb0 100644 --- a/internal/backends/arango/internal/catalog/resources.go +++ b/internal/backends/arango/internal/catalog/resources.go @@ -16,6 +16,22 @@ func (s *Store) DiscoverExistingAuthResourcePaths(ctx context.Context, opts cata if err := ValidateAuthResourcePathOptions(opts); err != nil { return nil, err } + // A database that has never ingested has no field catalog collection at + // all. Discovery answers "no recorded paths" there, exactly as the + // retained-profile read treats a missing collection as no data: the + // alternative is a storage fault on every first authorized write, + // because the authscope project-binding gate consults this discovery + // BEFORE ingest can bootstrap the collection (its zero-path branch is + // built for precisely this first-generation bootstrap), and surfacing + // the fault as 403 would deadlock a fresh database against itself. + // A collection deleted mid-request still fails the query honestly. + exists, err := s.client.CollectionExists(ctx, catalog.FieldCatalogCollection) + if err != nil { + return nil, err + } + if !exists { + return []string{}, nil + } if opts.CursorBatch <= 0 { opts.CursorBatch = 1000 } @@ -26,7 +42,7 @@ func (s *Store) DiscoverExistingAuthResourcePaths(ctx context.Context, opts cata query = existingAuthResourcePathsAllGenerationsAQL delete(bindVars, "dataset_generation") } - err := s.client.QueryRows(ctx, query, opts.CursorBatch, bindVars, func(row map[string]any) error { + err = s.client.QueryRows(ctx, query, opts.CursorBatch, bindVars, func(row map[string]any) error { if value := stringValue(row["auth_resource_path"]); value != "" { result = append(result, value) } diff --git a/internal/dataframe/benchmark/README.md b/internal/dataframe/benchmark/README.md new file mode 100644 index 000000000..830bed531 --- /dev/null +++ b/internal/dataframe/benchmark/README.md @@ -0,0 +1,166 @@ +# Backend benchmark harness (Arango/AQL vs PostgreSQL/SQL) + +Matched dataframe-task benchmark. Correctness gates run before any timing +is claimed; every derived number in an artifact is recomputed by the Go +verifier, so a hand-edited report never verifies. + +## Layout + +- `fixtures.go` — deterministic NDJSON dataset (preset × seed → digest). +- `authoring.go` — battery workloads in acceptance-proven workspace shapes. +- `canonical.go`, `oracle.go`, `gates.go` — bag canonicalization, expected + results, correctness gates (bag equality, shape, auth, truncation). +- `measure.go`, `controls.go` — sampling, summaries, stability CV, + self-ratio, empty-result floor. +- `matrix.go`, `armplan.go` — battery matrix; run scopes and leg plans. +- `report.go`, `report_md.go` — artifact schema, `VerifyReport`, Markdown. +- `harness.go`, `provision.go`, `driver.go` — live arms, MockFence, server + spawn, RunPlan-checked driver. +- `cmd/benchcli` — CLI below. `scripts/benchmark-backends.mjs` — node + orchestrator (evidence lines only; Go owns verification authority). + +## Scopes + +| scope | arango leg | postgres leg | use | +| -------------- | --------------- | --------------- | ---------------------------------------- | +| `both-arms` | run | run | CI target (needs both services) | +| `postgres-only`| not_run + capability | run | worker sessions with a granted window | +| `planned` | not_run + capability | not_run + capability | pre-window disclosure artifact | + +A not_run leg must name its missing capability; the artifact carries it per +workload, and `VerifyReport` refuses samples, summaries, claims, versions, +or control values for legs that did not run. A planned report can never be +`passed`: it is `failed` with zero claims, and the node orchestrator +refuses a planned artifact that captured any timing. + +## Commands + +```sh +go run ./internal/dataframe/benchmark/cmd/benchcli verify --report OUT/report.json +go run ./internal/dataframe/benchmark/cmd/benchcli run --scope planned \ + --source-commit <40-hex> --out OUT +go run ./internal/dataframe/benchmark/cmd/benchcli run --scope postgres-only \ + --source-commit <40-hex> --window-note "granter + time" --postgres-dsn --binary --out OUT +# database lifecycle (admin DSN via LOOM_BENCH_ADMIN_DSN env, never argv): +... setup-pg --database loom_bench_x # six-series ladder via SetupMigrate +... drop-pg --database loom_bench_x +``` + +Artifacts per run: `report.json` (verified schema), `report.md`, +`arms.json` (operational evidence: arm URLs, log paths, fence requests or +planned capabilities). + +## Exit codes (`run`) + + - `0` — the live legs ran and the report status is `passed`. + - `1` — the run measured something and failed (a correctness gate failed, + or any leg failed after real timings were captured). + - `3` — disclosed not_run, never a pass: preflight refused (missing + `LOOM_BENCH_LIVE=1`, window note, source commit, namespace guard, + existing output), the planned disclosure artifact was written, or a + live run ended non-passed with zero measured samples (for example the + default `--publication=excluded` path, where every gate arm is + blocked). A run that timed nothing must not look like a success. + +## Publication chain (ClickHouse-backed) and the two modes + +The publication *chain* — dataset → bundle → +`publication.Target`/`ClickHouseBundleStore`, the chain's only production +implementation — is ClickHouse-backed, and the battery rides it: every +workload begins with the explorer-config publish, which materializes +through the chain. The first publish against a fresh generation answers +422 `MATERIALIZATION_FAILED` when no materialization target is configured +(2026-11-13 rehearsal), so there is no chain-less battery and the authoring +battery requires a reachable ClickHouse. `--publication` accepts exactly +two modes; anything else is refused at preflight +(`unknown --publication ... want excluded | run`). + +- `--publication=excluded` (default): spawn, upload, and version probes run + for real on every ran leg; the whole publication chain ships as + planned/not_run under the capability `CH--unavailable-in-container` + (`PublicationCapabilityCH`, derived from `ClickHouseCIVersion`, which + must equal the `clickhouse/clickhouse-server` tag in + `.github/workflows/tests.yaml` — currently 25.3). `VerifyReport` then + refuses any samples, summaries, claims, or control values, requires every + gate arm blocked, and one disclosure row per battery workload per ran leg. + This mode measures nothing and can never satisfy a benchmark check. +- `--publication=run`: the paired timing lane. The battery executes for + real on every ran leg — publish, materialization, preview, timing, + controls — with `server.clickhouse.enabled=true` and + `--clickhouse-enabled` on the harness identity. Each arm materializes + into its own `loom_bench_*` database. Credentials resolve flags + (`--clickhouse-user`/`--clickhouse-password`) over + `LOOM_BENCH_CH_USER`/`LOOM_BENCH_CH_PASSWORD` over the server default. + Reports from this lane carry real samples, summaries, controls, and + claims. A report whose identity says ClickHouse is disabled while + claiming the chain ran fails verification, and a run-mode report must + not name a publication capability. + +## Paired speed comparison (operator acceptance, 2026-11-13) + +`node scripts/benchmark-backends.mjs --paired --binary --benchcli + --out [--profile tiny|small|representative] +[--iterations N>=2*rounds --warmup N>=1 --rounds N>=5] [--source-commit <40-hex>] +--clickhouse-url ` +runs the same matched battery against BOTH real drivers discovered from +`LOOM_TEST_ARANGO_URL` + `LOOM_TEST_POSTGRES_URL` (the same variables the +`backend-conformance` job exports; the CI job +`factory/paired-driver-benchmark` in `.github/workflows/tests.yaml` runs +this exact command with services `arangodb:3.12` + `postgres:18` + +`clickhouse/clickhouse-server:25.3`, invoking benchcli with +`--publication=run --clickhouse-enabled`). With +`LOOM_BENCH_ADMIN_DSN` set it first provisions its own fresh +`loom_bench_*` database (name must match `loom_bench_[a-z0-9_]{4,48}`, +the rule benchcli enforces) via `benchcli drop-pg`/`setup-pg` (database, +six-series schema ladder, migrated tables) — the PostgreSQL backend +readiness answers `schema_missing ... the caller owns schema lifecycle`, +so the lane, not the server, bootstraps. A failed `setup-pg` fails the +check before any timing attempt. Before any timing starts, each arm +publishes its reconciled receipt through the authoring route (untimed +setup; every output must answer READY with a materialization identity) and +the arms must prove matched state — equal content-derived draft digest, +equal output set, equal setup-preview rows — or the run aborts with no +report. Activation-style refusals (`GENERATION_ACTIVATION_FAILED`, the +typed `BACKEND_UNAVAILABLE` + `stage=adapter` posture, and siblings) are +fatal centrally in `doArmRequest` and are never tolerated, retried, or +special-cased on any route. +Correctness gates decide first; then >= 5 interleaved measured rounds per +driver are required, each round contributing the post-restart +process-cold sample plus at least one warm sample (`--iterations >= +2*--rounds`; spawned arms restart per round, and claims cite the warm +cell, so a one-iteration round could never produce a claimable regime). +The check FAILS — never skips, never passes — when a +service is missing, an arm is planned/not_run/excluded (the exclusions +`LiveBatteryExclusions()` names — currently six workloads — are tolerated +through the exclusion marker), a driver produced no usable +timing samples, or the Go verifier rejects the artifact. On success it +writes `comparison.md` (median/min/max/range in milliseconds per workload +per driver, plus versions, source SHA, fixture digest, warmup/cache/ +rounds/concurrency conditions) beside the raw `report.json`. No speedup is +claimed unless the claim clears correctness and the stability control; +either driver may win. + +## Live-run guards (all fail closed, exit 3) + +`LOOM_BENCH_LIVE=1` (coordinator-granted exclusive window), `--window-note`, +full 40-hex `--source-commit`, no overwrite of `--out/report.json`, +PostgreSQL database/schema and Arango database under `loom_bench_*`, arms +uniformly attached or spawned (never mixed), spawned arms get tokens only +from the MockFence. + +## Current blockers (cited in planned artifacts) + +- ClickHouse service: needed by the publication chain, which the battery + itself requires (`--publication=run`), on + `clickhouse/clickhouse-server:25.3` as provisioned by the CI lane. The + paired lane depends on that service; there is no chain-less fallback. + Owner: CI lane. +- `PG-PG-AUTH-PATH-DISCOVERY-ALLGENS` — FIXED in PR #156 (verified live: + upload accepted, fence hit once, no 403); the branch base predates the + library-side fix, so live legs go green when it merges. Evidence: + `pg-pg-auth-path-discovery-allgens-evidence.md` (session artifacts). +- `PG-PG-CAPABILITY-EVIDENCE-INVENTORY-ONLY-STORE` — FIXED in PR #158 + (`NewFullStore` composition); verified live: no panic, reaches + materialization. Evidence: `pg-pg-capability-evidence-inventory-only-store-evidence.md`. +- Arango leg: no ArangoDB service in the worker container; go-arango CI + lane targets it. diff --git a/internal/dataframe/benchmark/armplan.go b/internal/dataframe/benchmark/armplan.go new file mode 100644 index 000000000..dce2ff4e3 --- /dev/null +++ b/internal/dataframe/benchmark/armplan.go @@ -0,0 +1,572 @@ +package benchmark + +import ( + "fmt" + "sort" + "strings" +) + +// Run scope and leg plans make "we could only run the PostgreSQL arm" an +// explicit, verified property of the artifact instead of a silent absence. +// The Arango legs are CI-targeted per the coordinator ACK: in worker +// sessions they ship as planned/not_run rows with the named missing +// capability, never as skipped-to-green rows. + +const ( + RunScopeBothArms = "both-arms" + RunScopePostgresOnly = "postgres-only" + // RunScopePlanned is the fully-disclosed pre-window artifact: no leg + // ran, every leg is not_run with its missing capability named, and the + // report can carry no claims. It exists so a blocked session (e.g. + // PG-PG-AUTH-PATH-DISCOVERY-ALLGENS) publishes an honest planned + // report instead of staying silent or dressing an empty matrix up as + // a pass. + RunScopePlanned = "planned" +) + +func ValidScope(scope string) bool { + return scope == RunScopeBothArms || scope == RunScopePostgresOnly || scope == RunScopePlanned +} + +// LegPlan declares one backend arm's participation in the run. +type LegPlan struct { + Backend string `json:"backend"` + Mode string `json:"mode"` // run | not_run + // Capability names what was missing for not_run rows, e.g. + // "no ArangoDB service in worker container; go-arango CI lane targets + // this leg" — required and non-empty for not_run. + Capability string `json:"capability,omitempty"` +} + +// PlannedNotRun is a per-workload disclosure row. +type PlannedNotRun struct { + WorkloadID string `json:"workloadId"` + Backend string `json:"backend"` + Reason string `json:"reason"` +} + +// ClickHouseCIVersion is the single source of truth for the ClickHouse +// line the benchmark's publication stages target. It must equal the image +// tag pinned by the repository CI services (currently +// clickhouse/clickhouse-server:25.3 in .github/workflows/tests.yaml); when +// CI bumps the pin, this constant, ClickHouseCIImage, and +// PublicationCapabilityCH follow automatically. One constant keeps the +// harness, the named capability, and the CI lane describing one substrate. +const ClickHouseCIVersion = "25.3" + +// ClickHouseCIImage is the container image the CI lanes run the +// publication-dependent stages against. No container engine exists in the +// worker container, so publication runs are planned/not_run there +// (coordinator ruling 2026-11-13: no ClickHouse chasing); the paired CI +// lane provisions the service and runs the whole chain for real. +const ClickHouseCIImage = "clickhouse/clickhouse-server:" + ClickHouseCIVersion + +// PublicationCapabilityCH names the missing capability when the +// publication chain cannot execute because ClickHouse is not deployed. +// Publication is structurally ClickHouse-backed: publication.Target has +// exactly one production implementation (ClickHouseBundleStore), and the +// repository publish that seeds every Explorer draft materializes through +// it. The 2026-11-13 local rehearsal disproved the earlier premise that +// the authoring battery could run with clickhouse.enabled=false: the +// first explorer-config publish answers 422 MATERIALIZATION_FAILED +// without a target, so there is no chain-less battery — a run either +// deploys ClickHouse (mode run) or discloses everything downstream of +// upload as not_run (mode excluded). +const PublicationCapabilityCH = "CH-" + ClickHouseCIVersion + "-unavailable-in-container" + +// PublicationPlan declares whether the publication chain ran. It is a +// plan-level property so VerifyReport, not the driver's self-description, +// decides whether a report may carry timing evidence. +type PublicationPlan struct { + // Run is true when publication executed (CI lane with ClickHouse); + // the timing battery only ever runs alongside it, because authoring + // publishes through the chain. + Run bool `json:"run"` + // Capability names why the chain did not run when Run is false; + // PublicationCapabilityCH is the canonical value for a ClickHouse-less + // deployment. A run-mode plan must leave it empty. + Capability string `json:"capability,omitempty"` +} + +// PublicationZeroEvidence reports the excluded mode: the chain and +// everything downstream of upload were excluded, so the run measured +// nothing and a report carrying samples, summaries, claims, control +// values, or non-blocked arms would be forged. This is what VerifyReport +// pins. +func (p RunPlan) PublicationZeroEvidence() bool { + return p.PublicationExcluded() +} + +// ApplyExcludedWorkloadGateReports replaces any gate report for an +// excluded workload with its disclosed blocked row, so no construction +// path can leave a marker-less (or worse, claiming) row for a workload +// that was never allowed to run. +func ApplyExcludedWorkloadGateReports(gates, excludedReports []WorkloadGateReport) []WorkloadGateReport { + excludedIDs := map[string]bool{} + for _, g := range excludedReports { + excludedIDs[g.WorkloadID] = true + } + out := make([]WorkloadGateReport, 0, len(gates)+len(excludedReports)) + for _, g := range gates { + if !excludedIDs[g.WorkloadID] { + out = append(out, g) + } + } + return append(out, excludedReports...) +} + +// PublicationExcluded reports whether the plan declares the publication +// chain not run. A nil plan means the field predates the exclusion model +// and is treated as run for leg/scope checks; VerifyReport still rejects a +// ClickHouse-disabled report claiming it. +func (p RunPlan) PublicationExcluded() bool { + return p.Publication != nil && !p.Publication.Run +} + +// PublicationChainReason prefixes disclosure rows created by excluding +// the publication chain; VerifyReport recognizes the exclusion by it. +const PublicationChainReason = "publication chain not run: " + +// WorkloadNotRunReason prefixes disclosure rows for a single workload +// excluded from live legs (disproven or unproven oracle/output agreement); +// VerifyReport recognizes the exclusion by it. +const WorkloadNotRunReason = "workload not run: " + +// LegNotRunReason prefixes disclosure rows and blocked-arm details for a +// leg that did not execute at all. +const LegNotRunReason = "leg not run: " + +// LiveBatteryExclusionMarker prefixes the ExclusionReason of gate reports +// built by ExcludedWorkloadGateReports. RunStatusScoped skips these rows +// (they are disclosed absences, not failures); VerifyReport is what ties +// the marker back to LiveBatteryExclusions() so it cannot be forged onto +// a workload the harness could have run. +const LiveBatteryExclusionMarker = "live battery exclusion: " + +// AppendWorkloadDisclosures adds the disclosure rows for excluded +// workloads on every running leg, skipping rows already present. Both +// benchcli (preflight) and the driver (final plan copy) call it, so it +// must be idempotent. +func AppendWorkloadDisclosures(plan *RunPlan, excluded map[string]string) { + seen := map[string]bool{} + for _, row := range plan.PlannedNotRun { + seen[row.WorkloadID+"|"+row.Backend+"|"+row.Reason] = true + } + for _, row := range WorkloadDisclosures(plan.Legs, excluded) { + if !seen[row.WorkloadID+"|"+row.Backend+"|"+row.Reason] { + plan.PlannedNotRun = append(plan.PlannedNotRun, row) + } + } +} + +// WorkloadDisclosures expands the per-workload exclusions across every +// running leg, so an excluded workload cannot quietly keep gate or timing +// rows on a leg that was never allowed to run it. +func WorkloadDisclosures(legs []LegPlan, excluded map[string]string) []PlannedNotRun { + var rows []PlannedNotRun + for _, leg := range legs { + if leg.Mode != "run" { + continue + } + for _, spec := range Battery { + if reason, ok := excluded[spec.ID]; ok { + rows = append(rows, PlannedNotRun{ + WorkloadID: spec.ID, Backend: leg.Backend, + Reason: WorkloadNotRunReason + reason, + }) + } + } + } + return rows +} + +// ExcludedWorkloadGateReports builds the blocked gate reports for +// excluded workloads: running legs blocked by the workload reason, +// not-run legs by their leg capability. Claimable stays false. +func ExcludedWorkloadGateReports(legs []LegPlan, excluded map[string]string) []WorkloadGateReport { + legCapability := map[string]string{} + for _, leg := range legs { + if leg.Mode != "run" { + legCapability[leg.Backend] = leg.Capability + } + } + detail := func(backend, workloadID string) string { + if cap, blocked := legCapability[backend]; blocked { + return LegNotRunReason + cap + } + return WorkloadNotRunReason + excluded[workloadID] + } + var reports []WorkloadGateReport + for _, spec := range Battery { + if _, ok := excluded[spec.ID]; !ok { + continue + } + reports = append(reports, WorkloadGateReport{ + WorkloadID: spec.ID, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelBlocked, Detail: detail(BackendArango, spec.ID)}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelBlocked, Detail: detail(BackendPostgres, spec.ID)}, + ExclusionReason: LiveBatteryExclusionMarker + detail(BackendArango, spec.ID) + " | " + detail(BackendPostgres, spec.ID), + }) + } + return reports +} + +// PublicationDisclosures expands the publication-chain exclusion across +// every battery workload of every running leg, so "publish excluded" +// cannot quietly leave workload rows claiming measurement on a chain that +// never executed. Legs that did not run are covered by their own leg +// capability rows instead. +func PublicationDisclosures(legs []LegPlan, capability string) []PlannedNotRun { + var rows []PlannedNotRun + for _, leg := range legs { + if leg.Mode != "run" { + continue + } + for _, spec := range Battery { + rows = append(rows, PlannedNotRun{ + WorkloadID: spec.ID, Backend: leg.Backend, + Reason: PublicationChainReason + capability, + }) + } + } + return rows +} + +// PublicationGateReports builds the honest per-workload gate reports for +// a run whose publication chain was excluded: every arm of every workload +// blocked — not-run legs by their leg capability, run legs by the +// publication capability — nothing claimable. +func PublicationGateReports(legs []LegPlan, capability string) []WorkloadGateReport { + legCapability := map[string]string{} + for _, leg := range legs { + if leg.Mode != "run" { + legCapability[leg.Backend] = leg.Capability + } + } + detail := func(backend string) string { + if cap, blocked := legCapability[backend]; blocked { + return LegNotRunReason + cap + } + return PublicationChainReason + capability + } + var reports []WorkloadGateReport + for _, spec := range Battery { + reports = append(reports, WorkloadGateReport{ + WorkloadID: spec.ID, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelBlocked, Detail: detail(BackendArango)}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelBlocked, Detail: detail(BackendPostgres)}, + ExclusionReason: "publication chain excluded: " + detail(BackendArango) + " | " + detail(BackendPostgres), + }) + } + return reports +} + +// ValidatePublicationPlan checks the publication declaration is honest in +// shape: an excluded chain must name its missing capability, and a +// run-mode chain must not carry one (a capability names what did not run). +func ValidatePublicationPlan(plan RunPlan) error { + if plan.Publication == nil || plan.Publication.Run { + if plan.Publication != nil && plan.Publication.Run && strings.TrimSpace(plan.Publication.Capability) != "" { + return fmt.Errorf("benchmark: publication plan claims the chain ran while naming a missing capability %q", plan.Publication.Capability) + } + return nil + } + if strings.TrimSpace(plan.Publication.Capability) == "" { + return fmt.Errorf("benchmark: publication chain excluded without a named capability") + } + return nil +} + +// RunPlan is the declared participation of each arm for a run, stored in +// the report and re-checked by VerifyReport: "we could only run the +// PostgreSQL arm" must be a verified property, never a silent absence. +type RunPlan struct { + Scope string `json:"scope"` + Legs []LegPlan `json:"legs"` + PlannedNotRun []PlannedNotRun `json:"plannedNotRun,omitempty"` + // Publication declares whether the ClickHouse-backed publication chain + // executed. nil predates the exclusion model: treated as run for + // leg/scope checks, but VerifyReport still refuses a publication-run + // claim from a ClickHouse-disabled identity. + Publication *PublicationPlan `json:"publication,omitempty"` +} + +// BothArmsPlan is the CI-target plan: both services execute every leg. +func BothArmsPlan() RunPlan { + return RunPlan{ + Scope: RunScopeBothArms, + Legs: []LegPlan{ + {Backend: BackendArango, Mode: "run"}, + {Backend: BackendPostgres, Mode: "run"}, + }, + } +} + +// PostgresOnlyPlan is the worker-session plan: the Arango leg ships as +// not_run with its missing capability named verbatim in every disclosure +// row, per the coordinator ACK (Arango rows stay CI-targeted). +func PostgresOnlyPlan(arangoCapability string) RunPlan { + return RunPlan{ + Scope: RunScopePostgresOnly, + Legs: []LegPlan{ + {Backend: BackendArango, Mode: "not_run", Capability: arangoCapability}, + {Backend: BackendPostgres, Mode: "run"}, + }, + } +} + +// PlannedPlan is the pre-window disclosure plan: both legs not_run, each +// with its missing capability named verbatim (finding ids welcome, e.g. +// "blocked by PG-PG-AUTH-PATH-DISCOVERY-ALLGENS: ..."). A planned run +// measures nothing and claims nothing; VerifyReport enforces that. +func PlannedPlan(arangoCapability, postgresCapability string) RunPlan { + return RunPlan{ + Scope: RunScopePlanned, + Legs: []LegPlan{ + {Backend: BackendArango, Mode: "not_run", Capability: arangoCapability}, + {Backend: BackendPostgres, Mode: "not_run", Capability: postgresCapability}, + }, + } +} + +// PlannedNotRunForBackends expands one disclosure reason across every +// battery workload for each not-run leg, so no row can be quietly dropped. +func PlannedNotRunForBackends(backends []string) []PlannedNotRun { + var rows []PlannedNotRun + for _, backend := range backends { + for _, spec := range Battery { + rows = append(rows, PlannedNotRun{WorkloadID: spec.ID, Backend: backend, Reason: "leg not run"}) + } + } + return rows +} + +// PlannedGateReports builds the honest per-workload gate reports for a +// planned run: every arm blocked with its leg capability in the detail, +// nothing claimable. VerifyReport's not-run rules (blocked arm, disclosure +// row, zero samples/claims) then bind the artifact. +func PlannedGateReports(legs []LegPlan) []WorkloadGateReport { + capability := map[string]string{} + for _, leg := range legs { + capability[leg.Backend] = leg.Capability + } + detail := func(backend string) string { return LegNotRunReason + capability[backend] } + var reports []WorkloadGateReport + for _, spec := range Battery { + reports = append(reports, WorkloadGateReport{ + WorkloadID: spec.ID, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelBlocked, Detail: detail(BackendArango)}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelBlocked, Detail: detail(BackendPostgres)}, + // Claimable stays false: a planned run measured nothing. + ExclusionReason: "planned run: " + detail(BackendArango) + " | " + detail(BackendPostgres), + }) + } + return reports +} + +// ValidateLegPlan checks the plan is total, closed, and honest. +func ValidateLegPlan(scope string, legs []LegPlan) error { + if !ValidScope(scope) { + return fmt.Errorf("benchmark: unknown run scope %q", scope) + } + byBackend := map[string]LegPlan{} + for _, leg := range legs { + if !ValidBackend(leg.Backend) { + return fmt.Errorf("benchmark: leg plan has unknown backend %q", leg.Backend) + } + if leg.Mode != "run" && leg.Mode != "not_run" { + return fmt.Errorf("benchmark: leg %s has unknown mode %q", leg.Backend, leg.Mode) + } + if leg.Mode == "not_run" && strings.TrimSpace(leg.Capability) == "" { + return fmt.Errorf("benchmark: leg %s is not_run without a named capability", leg.Backend) + } + if _, dup := byBackend[leg.Backend]; dup { + return fmt.Errorf("benchmark: duplicate leg for %s", leg.Backend) + } + byBackend[leg.Backend] = leg + } + for _, backend := range []string{BackendArango, BackendPostgres} { + if _, ok := byBackend[backend]; !ok { + return fmt.Errorf("benchmark: leg plan missing %s", backend) + } + } + switch scope { + case RunScopePlanned: + for _, backend := range []string{BackendArango, BackendPostgres} { + if byBackend[backend].Mode != "not_run" || strings.TrimSpace(byBackend[backend].Capability) == "" { + return fmt.Errorf("benchmark: planned scope requires %s not_run WITH a named capability", backend) + } + } + case RunScopePostgresOnly: + if byBackend[BackendPostgres].Mode != "run" { + return fmt.Errorf("benchmark: postgres-only scope requires the postgres leg to run") + } + if byBackend[BackendArango].Mode != "not_run" || strings.TrimSpace(byBackend[BackendArango].Capability) == "" { + return fmt.Errorf("benchmark: postgres-only scope requires the arango leg not_run WITH a named capability") + } + default: + for _, backend := range []string{BackendArango, BackendPostgres} { + if byBackend[backend].Mode != "run" { + return fmt.Errorf("benchmark: both-arms scope requires %s to run", backend) + } + } + } + return nil +} + +// ValidatePlannedNotRun checks disclosure rows reference real workloads +// and backends with non-empty reasons. +func ValidatePlannedNotRun(rows []PlannedNotRun) error { + for _, row := range rows { + if _, ok := batterySpec(row.WorkloadID); !ok { + return fmt.Errorf("benchmark: planned/not_run references unknown workload %q", row.WorkloadID) + } + if !ValidBackend(row.Backend) { + return fmt.Errorf("benchmark: planned/not_run references unknown backend %q", row.Backend) + } + if strings.TrimSpace(row.Reason) == "" { + return fmt.Errorf("benchmark: planned/not_run for %s/%s has no reason", row.WorkloadID, row.Backend) + } + } + return nil +} + +// NotRunBackends lists the backends whose legs did not execute. +func (p RunPlan) NotRunBackends() []string { + var out []string + for _, leg := range p.Legs { + if leg.Mode == "not_run" { + out = append(out, leg.Backend) + } + } + return out +} + +// ExpectedOutcomeForScope returns the outcome an arm must show when its leg +// did not run, so gates can distinguish "expectedly absent" from "failed". +func ExpectedOutcomeForScope(leg LegPlan) (WorkloadOutcome, string) { + if leg.Mode == "not_run" { + return OutcomeLabelBlocked, LegNotRunReason + leg.Capability + } + return "", "" +} + +// RunStatusScoped aggregates gate reports under the run scope. In +// postgres-only scope the arango arm's blocked outcome is the EXPECTED +// posture (its capability is named in the leg plan) and does not fail the +// run; everything else behaves as in both-arms scope. +func RunStatusScoped(reports []WorkloadGateReport, scope string) (string, []string) { + if len(reports) == 0 { + return "failed", []string{"no workloads were evaluated; an empty matrix is not a pass"} + } + claimable := 0 + byID := append([]WorkloadGateReport(nil), reports...) + sort.Slice(byID, func(i, j int) bool { return byID[i].WorkloadID < byID[j].WorkloadID }) + var reasons []string + for _, report := range byID { + if scope == RunScopePlanned { + // Nothing ran. Both arms must carry the expected blocked + // posture with their capability in the detail; anything else + // (a passed arm, a measured-looking row) is a forgery signal. + if report.Arango.Outcome != OutcomeLabelBlocked || report.Postgres.Outcome != OutcomeLabelBlocked { + reasons = append(reasons, fmt.Sprintf("%s: planned run must show both arms blocked, got arango=%s postgres=%s", + report.WorkloadID, report.Arango.Outcome, report.Postgres.Outcome)) + } + continue + } + armOutcomes := []WorkloadOutcome{report.Arango.Outcome, report.Postgres.Outcome} + if scope == RunScopePostgresOnly { + // Only the PostgreSQL arm decides; the Arango arm must be the + // expected blocked posture, anything else is a forgery signal. + if report.Arango.Outcome != OutcomeLabelBlocked { + reasons = append(reasons, report.WorkloadID+": postgres-only run must show the arango arm blocked, got "+string(report.Arango.Outcome)) + continue + } + armOutcomes = []WorkloadOutcome{report.Postgres.Outcome} + } + if strings.HasPrefix(report.ExclusionReason, LiveBatteryExclusionMarker) { + // A disclosed live-battery exclusion is neither a pass nor + // a failure: it is a named absence. VerifyReport ties the + // marker to LiveBatteryExclusions() and requires the + // blocked posture, a disclosure row, and zero samples for + // every such row; a marker on anything else is refused + // there, so skipping here cannot launder a blocked run. + continue + } + blocked := false + for _, outcome := range armOutcomes { + if outcome == OutcomeLabelBlocked { + blocked = true + } + } + if blocked { + reasons = append(reasons, report.WorkloadID+": blocked") + continue + } + if !report.Claimable { + reasons = append(reasons, report.WorkloadID+": "+report.ExclusionReason) + continue + } + claimable++ + } + if len(reasons) > 0 { + return "failed", reasons + } + if claimable == 0 { + return "failed", []string{"no workload completed its expected arms; unsupported-only matrices are not passes"} + } + return "passed", nil +} + +// EvaluateGatesSingleArm evaluates the scope-applicable gates for one arm +// with an independent oracle (postgres-only sessions). Cross-backend gates +// cannot apply; that is recorded in the gate detail, not hidden. The other +// arm must already be published as blocked by the caller. +func EvaluateGatesSingleArm(workloadID, backend string, columns []string, expected *CanonicalBag, arm BackendResult, rows []map[string]any, idMap *StorageIDMap, blockedDetail string) WorkloadGateReport { + blocked := BackendResult{Backend: otherBackend(backend), Outcome: OutcomeLabelBlocked, Detail: blockedDetail} + report := WorkloadGateReport{WorkloadID: workloadID, Claimable: true} + if backend == BackendArango { + report.Arango, report.Postgres = arm, blocked + } else { + report.Arango, report.Postgres = blocked, arm + } + add := func(gate GateName, pass bool, format string, args ...any) { + result := GateResult{Gate: gate, Pass: pass} + if !pass { + result.Detail = fmt.Sprintf(format, args...) + report.Claimable = false + if report.ExclusionReason == "" { + report.ExclusionReason = string(gate) + ": " + result.Detail + } + } + report.Gates = append(report.Gates, result) + } + if arm.Outcome != OutcomeLabelPassed { + add(GateArmOutcome, false, "%s arm outcome=%s (error=%s)", arm.Backend, arm.Outcome, arm.ErrorCode) + return report + } + add(GateColumnContract, equalStrings(arm.Columns, columns), "%s columns deviate from the authored contract", arm.Backend) + mapped := idMap.Apply(arm.Columns, rows) + bag := Bag(CanonicalizeRows(arm.Columns, mapped)) + add(GateBagEquality, arm.BagDigest == bag.Digest(), "%s bag digest does not reproduce from its rows", arm.Backend) + if arm.Truncated { + add(GateCompleteness, false, "%s arm consumed a truncated output", arm.Backend) + } + if expected != nil { + if bag.Digest() != expected.Digest() { + firstA, firstB, _ := FirstDifference(bag, *expected) + add(GateOracleRows, false, "%s bag differs from independent oracle at arm=%s oracle=%s", arm.Backend, firstA, firstB) + } + } else { + report.Gates = append(report.Gates, GateResult{Gate: GateOracleRows, Pass: true, Detail: "profile has no hand oracle; single-arm scope publishes timings only"}) + } + report.Gates = append(report.Gates, GateResult{Gate: GateBagEquality, Pass: true, Detail: "cross-backend equality is CI-targeted under " + RunScopePostgresOnly + " scope"}) + return report +} + +func otherBackend(backend string) string { + if backend == BackendArango { + return BackendPostgres + } + return BackendArango +} diff --git a/internal/dataframe/benchmark/armplan_test.go b/internal/dataframe/benchmark/armplan_test.go new file mode 100644 index 000000000..00c6b1b9d --- /dev/null +++ b/internal/dataframe/benchmark/armplan_test.go @@ -0,0 +1,339 @@ +package benchmark + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + "time" +) + +// The postgres-only scope is where "we only ran PostgreSQL" lives. It must +// be a verified property: the un-run arm stays blocked, disclosed per +// workload, sample-free, claim-free, version-free — every deception path +// below has to RED. + +const testArangoCapability = "no ArangoDB service in worker container; go-arango CI lane targets this leg" + +func postgresOnlyGate(workloadID string, bagDigest string) WorkloadGateReport { + return WorkloadGateReport{ + WorkloadID: workloadID, Claimable: true, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelBlocked, Detail: "leg not run: " + testArangoCapability}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelPassed, RowCount: 5, BagDigest: bagDigest}, + Gates: []GateResult{{Gate: GateOracleRows, Pass: true}, {Gate: GateBagEquality, Pass: true, Detail: "cross-backend equality is CI-targeted"}}, + } +} + +func validPostgresOnlyReport(t *testing.T) *Report { + t.Helper() + identity := RunIdentity{ + RunID: "bench-2026-11-13-pg-a", StartedAt: time.Unix(1700000000, 0).UTC(), + Preset: PresetTiny, SourceCommit: strings.Repeat("a", 40), + DatasetDigest: strings.Repeat("b", 64), + PostgresVersion: "PostgreSQL 18.x", + // Timing needs the publication chain, which is ClickHouse- + // backed: this fixture describes the CI lane with ClickHouse. + ClickHouseDisabled: false, + Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}, + } + plan := PostgresOnlyPlan(testArangoCapability) + plan.Publication = &PublicationPlan{Run: true} + plan.PlannedNotRun = PlannedNotRunForBackends([]string{BackendArango}) + for i := range plan.PlannedNotRun { + plan.PlannedNotRun[i].Reason = "leg not run: " + testArangoCapability + } + liveExcluded := LiveBatteryExclusions() + var gates []WorkloadGateReport + var samples []Sample + for _, spec := range Battery { + if _, excluded := liveExcluded[spec.ID]; excluded { + continue + } + gates = append(gates, postgresOnlyGate(spec.ID, strings.Repeat("c", 64))) + samples = append(samples, sample(BackendPostgres, spec.ID, OutcomePassed, 5, true)) + // Halves [10, 10, 10.3, 10.3]: honest self-ratio 1.03. The + // verifier recomputes controls from raw samples (review + // r4561644049), so the fixture states what the samples show. + for i := 0; i < 4; i++ { + offset := 0.0 + if i >= 2 { + offset = 0.3 + } + samples = append(samples, sample(BackendPostgres, spec.ID, OutcomePassed, 10+offset, false)) + } + } + controls := ControlRuns{ + StabilityCVLimit: StabilityCVLimit, + UnstableCells: ControlUnstableCells(samples, StabilityCVLimit), + // the arango self-ratio cannot exist without the leg: 0 = absent, + // never a forged 1.0 + SelfRatioArango: 0, EmptyFloorMillis: 0.4, RatioPolicy: "ratio-only-both-passed", + } + if value, ok := ControlSelfRatio(samples, BackendPostgres); ok { + controls.SelfRatioPostgres = value + } else { + t.Fatal("postgres self-ratio not computable from fixture samples") + } + gates = ApplyExcludedWorkloadGateReports(gates, ExcludedWorkloadGateReports(plan.Legs, liveExcluded)) + AppendWorkloadDisclosures(&plan, liveExcluded) + report, err := BuildReport(identity, plan, controls, gates, samples) + if err != nil { + t.Fatalf("honest postgres-only report must build: %v", err) + } + return report +} + +func TestPostgresOnlyReportRoundTrips(t *testing.T) { + report := validPostgresOnlyReport(t) + loaded, err := LoadReport(mustMarshal(t, report)) + if err != nil { + t.Fatalf("honest postgres-only report must verify: %v", err) + } + if loaded.Status != "passed" { + t.Fatalf("postgres-only pass must be a pass: %+v", loaded.StatusReasons) + } + if len(loaded.Claims) != 0 { + t.Fatalf("no speed claims may exist without the arango leg: %+v", loaded.Claims) + } +} + +func TestPostgresOnlyRunStatusIgnoresExpectedBlockedArm(t *testing.T) { + status, reasons := RunStatusScoped([]WorkloadGateReport{postgresOnlyGate(WorkloadFilterActiveGender, strings.Repeat("c", 64))}, RunScopePostgresOnly) + if status != "passed" || len(reasons) != 0 { + t.Fatalf("expected pass, got %s %v", status, reasons) + } + // An unexpected arango outcome under postgres-only scope is a forgery + // signal, not a pass. + gate := postgresOnlyGate(WorkloadFilterActiveGender, strings.Repeat("c", 64)) + gate.Arango.Outcome = OutcomeLabelPassed + if status, _ := RunStatusScoped([]WorkloadGateReport{gate}, RunScopePostgresOnly); status != "failed" { + t.Fatal("arango arm passed under postgres-only scope must fail the run") + } + // Empty matrix is never a pass, even scoped. + if status, _ := RunStatusScoped(nil, RunScopePostgresOnly); status != "failed" { + t.Fatal("empty scoped matrix must not pass") + } +} + +func TestLegPlanValidation(t *testing.T) { + if err := ValidateLegPlan(RunScopePostgresOnly, []LegPlan{ + {Backend: BackendArango, Mode: "not_run"}, + {Backend: BackendPostgres, Mode: "run"}, + }); err == nil { + t.Fatal("not_run leg without capability must be refused") + } + if err := ValidateLegPlan(RunScopeBothArms, BothArmsPlan().Legs); err != nil { + t.Fatalf("both-arms plan must validate: %v", err) + } + if err := ValidateLegPlan(RunScopePostgresOnly, BothArmsPlan().Legs); err == nil { + t.Fatal("postgres-only scope must refuse an executed arango leg") + } + if err := ValidateLegPlan("everything", BothArmsPlan().Legs); err == nil { + t.Fatal("unknown scope must be refused") + } +} + +func forgeWith(t *testing.T, base func(*testing.T) *Report, mutate func(*Report)) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + report := base(t) + mutate(report) + data, err := json.Marshal(report) + if err != nil { + t.Fatal(err) + } + if _, err := LoadReport(data); err == nil { + t.Fatal("forged report survived verification") + } + } +} + +func TestPostgresOnlyForgerySuite(t *testing.T) { + cases := map[string]func(*Report){ + "arango-arm-faked-passed": func(r *Report) { + r.GateReports[0].Arango.Outcome = OutcomeLabelPassed + }, + "arango-samples-smuggled": func(r *Report) { + r.Samples = append(r.Samples, sample(BackendArango, primaryTiming(), OutcomePassed, 20, false)) + r.Summaries = append(r.Summaries, SummaryRow{ + WorkloadID: primaryTiming(), Backend: BackendArango, Cache: CacheWarm, + Summary: Summarize([]Sample{sample(BackendArango, primaryTiming(), OutcomePassed, 20, false)}), + }) + }, + "disclosure-rows-dropped": func(r *Report) { + r.Plan.PlannedNotRun = nil + }, + "capability-blanked": func(r *Report) { + r.Plan.Legs[0].Capability = " " + }, + "arango-version-claimed-for-unrun-leg": func(r *Report) { + r.Identity.ArangoVersion = "3.12.x" + }, + "postgres-version-hidden": func(r *Report) { + r.Identity.PostgresVersion = "" + }, + "speed-claim-injected-without-arango": func(r *Report) { + r.Claims = append(r.Claims, Claim{ + WorkloadID: primaryTiming(), + Arango: Summarize([]Sample{sample(BackendArango, primaryTiming(), OutcomePassed, 40, false)}), + Postgres: Summarize([]Sample{sample(BackendPostgres, primaryTiming(), OutcomePassed, 10, false)}), + Ratio: SpeedRatio{ArangoOverPostgres: 4.0}, + }) + }, + "unrun-selfratio-forged-to-one": func(r *Report) { + r.Controls.SelfRatioArango = 1.0 + }, + "scope-lie-arango-leg-marked-run": func(r *Report) { + r.Plan.Legs[0].Mode = "run" + }, + } + for name, mutate := range cases { + t.Run(name, forgeWith(t, validPostgresOnlyReport, mutate)) + } + // The converse: disclosure rows for a leg that DID run are lies too. + t.Run("disclosure-for-ran-leg", forgeWith(t, validReport, func(r *Report) { + r.Plan.PlannedNotRun = []PlannedNotRun{{WorkloadID: primaryTiming(), Backend: BackendArango, Reason: WorkloadNotRunReason + LiveBatteryExclusions()[WorkloadDeriveAgeBand]}} + })) +} + +func TestRenderMarkdown(t *testing.T) { + bothReport := validReport(t) + selfRatioLine := fmt.Sprintf("arango=%.3f, postgres=%.3f", + bothReport.Controls.SelfRatioArango, bothReport.Controls.SelfRatioPostgres) + both := RenderMarkdown(bothReport) + for _, want := range []string{ + "Status: passed", "Scope: `both-arms`", "ratio-only-both-passed", + "arango/postgres", "Empty-result floor", selfRatioLine, + } { + if !strings.Contains(both, want) { + t.Fatalf("both-arms markdown missing %q", want) + } + } + only := RenderMarkdown(validPostgresOnlyReport(t)) + for _, want := range []string{ + "Scope: `postgres-only`", "not_run", testArangoCapability, + "not observed (leg not run", "no cross-backend speed claims", + "never silently skipped", + } { + if !strings.Contains(only, want) { + t.Fatalf("postgres-only markdown missing %q", want) + } + } + if strings.Contains(only, "arango/postgres |") { + t.Fatal("postgres-only markdown must not contain a claims table") + } +} + +// The planned scope is the pre-window disclosure artifact: nothing ran, so +// nothing may be claimed — not a sample, not a summary, not a control, not +// a version. These tests pin the honest shape and every deception path. + +func plannedValidReport(t *testing.T) *Report { + t.Helper() + identity := RunIdentity{ + RunID: "bench-planned-a", StartedAt: time.Unix(1700000000, 0).UTC(), + Preset: PresetTiny, SourceCommit: strings.Repeat("a", 40), + DatasetDigest: strings.Repeat("b", 64), + ClickHouseDisabled: true, + Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}, + } + plan := PlannedPlan( + "no ArangoDB service in this execution environment; go-arango CI lane targets this leg", + "blocked by PG-PG-AUTH-PATH-DISCOVERY-ALLGENS: the scoped calypr lane cannot ingest on postgres") + plan.PlannedNotRun = PlannedNotRunForBackends(plan.NotRunBackends()) + report, err := BuildReport(identity, plan, ControlRuns{}, PlannedGateReports(plan.Legs), nil) + if err != nil { + t.Fatalf("valid planned report build failed: %v", err) + } + if report.Status != "failed" || len(report.Claims) != 0 { + t.Fatalf("planned report must be failed with zero claims, got %s / %d claims", report.Status, len(report.Claims)) + } + if want := 2 * len(Battery); len(report.Plan.PlannedNotRun) != want || len(report.GateReports) != len(Battery) { + t.Fatalf("planned disclosure must cover the whole battery both legs: %d rows, %d gates, want %d/%d", + len(report.Plan.PlannedNotRun), len(report.GateReports), want, len(Battery)) + } + return report +} + +func TestPlannedReportRoundTrips(t *testing.T) { + loaded := roundTrip(t, plannedValidReport(t)) + if loaded.Plan.Scope != RunScopePlanned || loaded.Status != "failed" || len(loaded.Claims) != 0 { + t.Fatalf("planned round trip drifted: %+v", loaded.Plan) + } + if strings.Contains(RenderMarkdown(loaded), "arango/postgres |") { + t.Fatal("planned markdown must not contain a claims table") + } +} + +func forgePlanned(t *testing.T, mutate func(*Report)) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + report := plannedValidReport(t) + mutate(report) + data, err := json.Marshal(report) + if err != nil { + t.Fatal(err) + } + if _, err := LoadReport(data); err == nil { + t.Fatal("forged planned report survived verification") + } + } +} + +func TestPlannedForgerySuite(t *testing.T) { + cases := map[string]func(*Report){ + "planned-leg-marked-run": func(r *Report) { r.Plan.Legs[1].Mode = "run" }, + "planned-capability-blanked": func(r *Report) { r.Plan.Legs[1].Capability = " " }, + "planned-arm-shown-passed": func(r *Report) { + r.GateReports[0].Postgres.Outcome = OutcomeLabelPassed + r.GateReports[0].Postgres.RowCount = 5 + r.GateReports[0].Claimable = true + r.Status = "passed" + r.StatusReasons = nil + }, + "planned-status-flipped-passed": func(r *Report) { r.Status = "passed" }, + "planned-stolen-control-limits": func(r *Report) { r.Controls.StabilityCVLimit = StabilityCVLimit }, + "planned-stolen-empty-floor": func(r *Report) { r.Controls.EmptyFloorMillis = 0.4 }, + "planned-forged-self-ratio": func(r *Report) { r.Controls.SelfRatioPostgres = 1.0 }, + "planned-forged-ratio-policy": func(r *Report) { r.Controls.RatioPolicy = "ratio-only-both-passed" }, + "planned-samples-injected": func(r *Report) { + r.Samples = append(r.Samples, sample(BackendPostgres, r.GateReports[0].WorkloadID, OutcomePassed, 10, false)) + }, + "planned-summaries-injected": func(r *Report) { + r.Summaries = append(r.Summaries, SummaryRow{WorkloadID: r.GateReports[0].WorkloadID, Backend: BackendPostgres, Cache: CacheWarm, Summary: Summary{Count: 4, MedianMillis: 9}}) + }, + "planned-claim-injected": func(r *Report) { + r.Claims = append(r.Claims, Claim{WorkloadID: r.GateReports[0].WorkloadID, Ratio: SpeedRatio{ArangoOverPostgres: 2.0}}) + }, + "planned-disclosure-row-dropped": func(r *Report) { r.Plan.PlannedNotRun = r.Plan.PlannedNotRun[1:] }, + "planned-version-claimed": func(r *Report) { r.Identity.PostgresVersion = "PostgreSQL 18.x" }, + "planned-arango-version-claimed": func(r *Report) { r.Identity.ArangoVersion = "3.12.x" }, + } + for name, mutate := range cases { + t.Run(name, forgePlanned(t, mutate)) + } +} + +func TestPlannedLegPlanValidation(t *testing.T) { + plan := PlannedPlan("capability a", "capability b") + if err := ValidateLegPlan(plan.Scope, plan.Legs); err != nil { + t.Fatalf("valid planned plan refused: %v", err) + } + if err := ValidateLegPlan(RunScopePlanned, []LegPlan{ + {Backend: BackendArango, Mode: "not_run", Capability: "a"}, + {Backend: BackendPostgres, Mode: "run"}, + }); err == nil { + t.Fatal("planned scope accepted a run leg") + } + if err := ValidateLegPlan(RunScopePlanned, []LegPlan{ + {Backend: BackendArango, Mode: "not_run", Capability: "a"}, + {Backend: BackendPostgres, Mode: "not_run"}, + }); err == nil { + t.Fatal("planned scope accepted an unnamed capability") + } + // The live driver must refuse to run a planned scope at all. + if _, _, err := RunBenchmark(nil, DriverOptions{Plan: PlannedPlan("a", "b")}); err == nil || + !strings.Contains(err.Error(), "planned scope") { + t.Fatalf("driver must refuse planned scope, got %v", err) + } +} diff --git a/internal/dataframe/benchmark/authoring.go b/internal/dataframe/benchmark/authoring.go new file mode 100644 index 000000000..48a83f02e --- /dev/null +++ b/internal/dataframe/benchmark/authoring.go @@ -0,0 +1,442 @@ +package benchmark + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/calypr/loom/internal/explorer/authoringv2" +) + +// Battery authoring: one workspace per workload in the exact wire shapes +// proven by testdata/acceptance/ncpi-tcga-brca/workspace.json. The live flow +// per workload is four public routes, no builder commands: +// +// POST .../generations/{g}/explorer-config (workspace JSON) +// POST .../explorers {sourceExplorerId:"default"} (draft = active revision) +// POST .../authoring/v2/reconcile (compile receipt) +// POST .../authoring/v2/preview {receiptId, outputId, limit} +// +// Every template is validated offline by the repository's own decoder +// (AuthoringVerification); anything the decoder rejects is published as +// planned/not_run with the decoder's error as the named capability, never a +// silent skip and never an approximated shape. + +// AuthoringStatus is the offline authoring verdict for one workload. +type AuthoringStatus string + +const ( + // AuthoringProven: shapes copied verbatim from the acceptance fixture. + AuthoringProven AuthoringStatus = "proven" + // AuthoringUnverified: decodes, but a shape or cast has no acceptance + // precedent; rehearsal classifies it before any claim. + AuthoringUnverified AuthoringStatus = "unverified-shape" + // AuthoringRejected: the decoder refuses it; the workload is + // planned/not_run with the decoder error as the capability note. + AuthoringRejected AuthoringStatus = "rejected" +) + +type authoringNote struct { + Status AuthoringStatus + Detail string +} + +// wire shapes, verbatim families from the acceptance fixture +type wsColumnDef struct { + Column string `json:"column"` + Label string `json:"label"` + LogicalType string `json:"logicalType"` + OccurrenceID string `json:"occurrenceId"` + Source map[string]any `json:"source"` + ValueTransformation map[string]any `json:"valueTransformation,omitempty"` + Table wsTable `json:"table"` +} + +type wsTable struct { + Visible bool `json:"visible"` + Order int `json:"order"` +} + +type wsChild struct { + OccurrenceID string `json:"occurrenceId"` + ResourceType string `json:"resourceType"` + Relationship string `json:"relationship"` + Children []wsChild `json:"children,omitempty"` +} + +type wsDoc struct { + Kind string `json:"kind"` + Output wsOutputDef `json:"output"` + RootResourceType string `json:"rootResourceType"` + Route wsRouteNode `json:"route"` + Columns []wsColumnDef `json:"columns"` + FixedFilters []wsFixed `json:"fixedFilters,omitempty"` +} + +type wsFixed struct { + Column string `json:"column"` + Values []string `json:"values"` +} + +type wsRouteNode struct { + OccurrenceID string `json:"occurrenceId"` + ResourceType string `json:"resourceType"` + Children []wsChild `json:"children,omitempty"` +} + +type wsOutputDef struct { + ID string `json:"id"` + Title string `json:"title"` + RowLabel string `json:"rowLabel,omitempty"` +} + +type wsWorkspaceDoc struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + SemanticsVersion int `json:"semanticsVersion"` + Explorer wsExplorer `json:"explorer"` + Documents []wsDoc `json:"documents"` + Tabs []wsTabDef `json:"tabs"` +} + +type wsTabDef struct { + ID string `json:"id"` + Title string `json:"title"` + OutputID string `json:"outputId"` + Order int `json:"order"` + Visible bool `json:"visible"` +} + +type wsExplorer struct { + Title string `json:"title"` +} + +const ( + wsAPIVersion = "loom.calypr.org/explorer-authoring/v2" + wsKind = "ExplorerBuilderWorkspace" + wsDocKind = "ExplorerBuilderDocument" + wsSemanticsVersion = 3 // the acceptance-proven version; DecodeWorkspace owns migration +) + +// OutputIDFor maps a workload id to a decoder-legal output id +// (letters/digits/underscores only). +func OutputIDFor(workloadID string) string { + return strings.ReplaceAll(workloadID, "-", "_") +} + +func fieldCol(column, occurrence, path, mode, logical string, order int) wsColumnDef { + return wsColumnDef{ + Column: column, Label: column, LogicalType: logical, OccurrenceID: occurrence, + Source: map[string]any{"kind": "field", "field": map[string]any{"path": path, "projectionMode": mode}}, + Table: wsTable{Visible: true, Order: order}, + } +} + +func aggregateCol(column, label, logical, occurrence, operation, path, wherePath, whereEquals string, order int) wsColumnDef { + aggregate := map[string]any{"operation": operation} + if path != "" { + aggregate["path"] = path + } + if wherePath != "" { + aggregate["where"] = map[string]any{"path": wherePath, "equals": whereEquals} + } + return wsColumnDef{ + Column: column, Label: label, LogicalType: logical, OccurrenceID: occurrence, + Source: map[string]any{"kind": "aggregate", "aggregate": aggregate}, + Table: wsTable{Visible: true, Order: order}, + } +} + +func extensionCol(column, occurrence, matchURL string, order int) wsColumnDef { + return wsColumnDef{ + Column: column, Label: column, LogicalType: "string", OccurrenceID: occurrence, + Source: map[string]any{"kind": "extensionByUrl", "lookup": map[string]any{ + "match": matchURL, "path": "extension[]", "projectionMode": "FIRST", + }}, + Table: wsTable{Visible: true, Order: order}, + } +} + +// identifierCol projects identifier[].value where system == matchSystem via +// the acceptance-proven identifierBySystem source shape. +func identifierCol(column, occurrence, matchSystem string, order int) wsColumnDef { + return wsColumnDef{ + Column: column, Label: column, LogicalType: "string", OccurrenceID: occurrence, + Source: map[string]any{"kind": "identifierBySystem", "lookup": map[string]any{ + "match": matchSystem, "path": "identifier[]", "projectionMode": "FIRST", + }}, + Table: wsTable{Visible: true, Order: order}, + } +} + +func observationChildren() []wsChild { + return []wsChild{ + {OccurrenceID: "observation", ResourceType: "Observation", Relationship: "subject_Patient"}, + {OccurrenceID: "specimen", ResourceType: "Specimen", Relationship: "subject_Patient"}, + {OccurrenceID: "condition", ResourceType: "Condition", Relationship: "subject_Patient"}, + } +} + +func patientWorkspace(outputID string, columns []wsColumnDef, children []wsChild, filters []wsFixed) []byte { + return rootWorkspace("Patient", outputID, columns, children, filters) +} + +// observationRootWorkspace authors an output whose rows are the Observation +// resources themselves (one row per observation, no child occurrences). +func observationRootWorkspace(outputID string, columns []wsColumnDef, filters []wsFixed) []byte { + return rootWorkspace("Observation", outputID, columns, nil, filters) +} + +func rootWorkspace(rootType, outputID string, columns []wsColumnDef, children []wsChild, filters []wsFixed) []byte { + if children == nil { + children = []wsChild{} + } + w := wsWorkspaceDoc{ + APIVersion: wsAPIVersion, Kind: wsKind, SemanticsVersion: wsSemanticsVersion, + Explorer: wsExplorer{Title: "Backend benchmark battery"}, + Documents: []wsDoc{{ + Kind: wsDocKind, + Output: wsOutputDef{ID: outputID, Title: outputID, RowLabel: "row"}, + RootResourceType: rootType, + Route: wsRouteNode{OccurrenceID: "base", ResourceType: rootType, Children: children}, + Columns: columns, + FixedFilters: filters, + }}, + Tabs: []wsTabDef{{ID: outputID + "_tab", Title: outputID, OutputID: outputID, Order: 0, Visible: true}}, + } + raw, err := json.MarshalIndent(w, "", " ") + if err != nil { // value types only; unreachable + panic(fmt.Sprintf("benchmark: workspace marshal: %v", err)) + } + return raw +} + +// BuildWorkspace returns the authored workspace JSON for one workload. +// Output ID equals workload ID: receipts, previews, gates, and samples all +// key on it. +func BuildWorkspace(workloadID string) ([]byte, error) { + switch workloadID { + case WorkloadFilterActiveGender: + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + fieldCol("patient_id", "base", "id", "VALUE", "string", 0), + fieldCol("gender", "base", "gender", "VALUE", "string", 1), + // hidden root filter column: the fixed filter over it keeps + // inactive patients out of the output + {Column: "active", Label: "Active", LogicalType: "boolean", OccurrenceID: "base", + Source: map[string]any{"kind": "field", "field": map[string]any{"path": "active", "projectionMode": "VALUE"}}, + Table: wsTable{Visible: false, Order: 2}}, + }, nil, []wsFixed{ + {Column: "gender", Values: []string{"female"}}, + {Column: "active", Values: []string{"true"}}, + }), nil + case WorkloadGroupCountByPatient: + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + fieldCol("patient_id", "base", "id", "VALUE", "string", 0), + aggregateCol("obs_count", "Observation count", "integer", "observation", "COUNT", "", "", "", 1), + }, observationChildren(), nil), nil + case WorkloadAggregateSumAgeByPatient: + ageWhere := []struct{ col, op, logical string }{ + {"sum_age", "SUM", "decimal"}, {"count_age", "COUNT", "integer"}, + {"min_age", "MIN", "decimal"}, {"max_age", "MAX", "decimal"}, + } + columns := []wsColumnDef{fieldCol("patient_id", "base", "id", "VALUE", "string", 0)} + for i, agg := range ageWhere { + columns = append(columns, aggregateCol(agg.col, agg.col, agg.logical, "observation", agg.op, + "valueQuantity.value", "code.coding[].code", ObservationCodeAge, i+1)) + } + return patientWorkspace(OutputIDFor(workloadID), columns, observationChildren(), nil), nil + case WorkloadStageCounts: + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + aggregateCol("stage_code", "Stage code", "string", "observation", "DISTINCT_VALUES", + "valueCodeableConcept.coding[].code", "code.coding[].code", ObservationCodeStage, 0), + aggregateCol("stage_count", "Stage count", "integer", "observation", "COUNT", "", + "code.coding[].code", ObservationCodeStage, 1), + }, observationChildren(), nil), nil + case WorkloadSpecimenTypeFilter: + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + aggregateCol("specimen_id", "Specimen id", "string", "specimen", "DISTINCT_VALUES", + "id", "type.coding[].code", SpecimenTypePrimary, 0), + fieldCol("patient_id", "base", "id", "VALUE", "string", 1), + aggregateCol("type_code", "Specimen type", "string", "specimen", "DISTINCT_VALUES", + "type.coding[].code", "", "", 2), + }, observationChildren(), nil), nil + case WorkloadConditionNullHandling: + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + aggregateCol("condition_id", "Condition id", "string", "condition", "DISTINCT_VALUES", "id", "", "", 0), + aggregateCol("verification_status", "Verification status", "string", "condition", "DISTINCT_VALUES", + "verificationStatus.coding[].code", "", "", 1), + }, observationChildren(), nil), nil + case WorkloadSortLimitTopN: + // Engine order is not comparable across backends; the workload is a + // full active-row pass compared as a bag, and the oracle checks the + // deterministic total order client-side. Sort knobs stay a diagnostic. + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + fieldCol("family", "base", "name[].family", "VALUE", "string", 0), + fieldCol("given", "base", "name[].given[]", "VALUE", "string", 1), + fieldCol("patient_id", "base", "id", "VALUE", "string", 2), + }, nil, nil), nil + case WorkloadDistinctFamily: + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + aggregateCol("family", "Family", "string", "base", "DISTINCT_VALUES", "name[].family", "", "", 0), + }, nil, nil), nil + case WorkloadStudyCorrelation: + return patientWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + fieldCol("patient_id", "base", "id", "VALUE", "string", 0), + extensionCol("study_ref", "base", partOfStudyURL, 1), + }, nil, nil), nil + case WorkloadDeriveAgeBand: + // Derivation intent, authored with the repository's closed value + // transformation: age_band recodes the scalar-string age identifier + // (fixtures.go) through the exhaustive exact-category map generated + // from the same AgeBandFor function the oracle thresholds + // independently. unknownPolicy ERROR: any value outside the pinned + // fixture domain is a contract violation, never silent misbanding. + // The fixed filter keeps exactly the age observations (missing-value + // observations have no identifier, so their null band is excluded + // and the oracle skips them too). The map itself has no + // acceptance-fixture precedent, so the workload stays + // AuthoringUnverified — planned/not_run with a named reason — until + // the ClickHouse-lane rehearsal; TestDeriveAgeBandOfflineWitness + // proves offline that its output bag equals the oracle's. + ageBandCol := identifierCol("age_band", "base", BenchAgeIdentifierSystem, 1) + ageBandCol.ValueTransformation = map[string]any{ + "kind": "EXACT_CATEGORY_RECODE", + "exactCategoryRecode": map[string]any{ + "mappings": ageRecodeMappings(), + "unknownPolicy": "ERROR", + }, + } + return observationRootWorkspace(OutputIDFor(workloadID), []wsColumnDef{ + fieldCol("observation_id", "base", "id", "VALUE", "string", 0), + ageBandCol, + }, []wsFixed{ + {Column: "age_band", Values: []string{"young", "middle", "senior"}}, + }), nil + } + return nil, fmt.Errorf("benchmark: no workspace template for workload %q", workloadID) +} + +// ageRecodeMappings enumerates the exhaustive exact-category map over the +// pinned fixture domain [1, AgeBandFixtureMax], deriving every label from +// AgeBandFor — the same function the oracle thresholds independently. A +// drift between workspace and oracle is unrepresentable by construction; +// TestDeriveAgeBandOfflineWitness re-checks coverage against the fixtures. +func ageRecodeMappings() []map[string]string { + mappings := make([]map[string]string, 0, AgeBandFixtureMax) + for v := 1; v <= AgeBandFixtureMax; v++ { + mappings = append(mappings, map[string]string{ + "from": strconv.Itoa(v), "to": AgeBandFor(float64(v)), + }) + } + return mappings +} + +// BuildEmptyFloorWorkspace authors the identical-request empty-result floor +// control: the same shape as filter-active-gender whose fixed filter cannot +// match any patient, so the expectation is provably zero rows. +func BuildEmptyFloorWorkspace() ([]byte, error) { + return patientWorkspace("empty_floor", []wsColumnDef{ + fieldCol("patient_id", "base", "id", "VALUE", "string", 0), + fieldCol("gender", "base", "gender", "VALUE", "string", 1), + }, nil, []wsFixed{ + {Column: "gender", Values: []string{"__bench_impossible_gender_value__"}}, + }), nil +} + +// AuthoringVerification asks the repository's own decoder about every +// battery workspace. It runs before any service is contacted and feeds the +// report's arm plan: rejected shapes are planned/not_run, named. +func AuthoringVerification() map[string]AuthoringStatus { + out := map[string]AuthoringStatus{} + for _, spec := range Battery { + raw, err := BuildWorkspace(spec.ID) + if err != nil { + out[spec.ID] = AuthoringRejected + continue + } + if _, err := authoringv2.DecodeWorkspace(raw); err != nil { + out[spec.ID] = AuthoringRejected + continue + } + if unverifiedWorkloads[spec.ID] { + out[spec.ID] = AuthoringUnverified + continue + } + out[spec.ID] = AuthoringProven + } + return out +} + +// unverifiedWorkloads lists shapes without acceptance-fixture precedent; +// rehearsal classifies them and the outcome is recorded in the report. +// Anything not proven ships planned/not_run (live battery law below), so +// the live legs can never fail a gate a shape could not pass by +// construction (review r4559430560). +var unverifiedWorkloads = map[string]bool{ + WorkloadFilterActiveGender: true, // fixedFilters over a boolean field column + WorkloadDistinctFamily: true, // DISTINCT_VALUES at the root occurrence + WorkloadDeriveAgeBand: true, // EXACT_CATEGORY_RECODE map: no acceptance precedent (passability witnessed offline) + WorkloadStudyCorrelation: true, // extensionByUrl returning a reference string + WorkloadStageCounts: true, // DISTINCT_VALUES + COUNT pair at one occurrence (per-row grain expectation) + WorkloadSortLimitTopN: true, // VALUE over multi-valued name[].family: row expansion unproven (review r4559430560) +} + +// workloadNotRunReasons names why each not-proven workload is excluded +// from live legs. Every non-proven verdict must carry an entry: an +// exclusion without a named reason is a silent coverage hole, which is +// the exact deception class the report verifier exists to refuse. +var workloadNotRunReasons = map[string]string{ + WorkloadFilterActiveGender: "shape has no acceptance-fixture precedent: fixedFilters over a boolean field column; live rehearsal (ClickHouse lane) must prove the oracle/output agreement before this leg runs", + WorkloadDistinctFamily: "shape has no acceptance-fixture precedent: DISTINCT_VALUES at the root occurrence; live rehearsal must prove the oracle/output agreement before this leg runs", + WorkloadDeriveAgeBand: "shape has no acceptance-fixture precedent: the EXACT_CATEGORY_RECODE map (identifierBySystem source, ERROR unknown policy, fixed filter on the derived column) has never run end to end; TestDeriveAgeBandOfflineWitness proves offline that its output bag equals the independently thresholded oracle over the pinned fixture domain [1,90], so the workload can pass; the ClickHouse-lane rehearsal must confirm the backends apply recode+filter as specified before this leg runs", + WorkloadStudyCorrelation: "shape has no acceptance-fixture precedent: extensionByUrl returning a reference string; live rehearsal must prove the oracle/output agreement before this leg runs", + WorkloadStageCounts: "shape has no acceptance-fixture precedent: DISTINCT_VALUES + COUNT pair at one occurrence with a per-row grain expectation; live rehearsal must prove the oracle/output agreement before this leg runs", + WorkloadSortLimitTopN: "oracle/output agreement unproven: VALUE columns over multi-valued name[].family with a second family entry on every 4th patient; the oracle assumes first-only while preview row expansion (first-only vs row-per-value) has no acceptance precedent; live rehearsal must pin the scalarization contract", +} + +// LiveBatteryExclusions returns the workloads that may not run in a live +// leg and the named reason for each: anything the decoder rejects, and +// anything without an acceptance-proven shape or disproven oracle/output +// agreement. The driver skips authoring, publication, preview, and timing +// for these; the report verifier requires one disclosure row per excluded +// workload per running leg and refuses any timing evidence for them. +func LiveBatteryExclusions() map[string]string { + out := map[string]string{} + for workloadID, status := range AuthoringVerification() { + if status == AuthoringProven { + continue + } + reason := workloadNotRunReasons[workloadID] + if reason == "" { + // Fail closed in the disclosure itself: an unproven verdict + // with no reason is a bug, not a licence to run silently. + reason = "verdict " + string(status) + " without a named reason (harness bug; refusing to run it)" + } + out[workloadID] = reason + } + return out +} + +// BuildAllWorkspaces returns every battery workspace keyed by workload id. +func BuildAllWorkspaces() (map[string][]byte, error) { + out := map[string][]byte{} + for _, spec := range Battery { + raw, err := BuildWorkspace(spec.ID) + if err != nil { + return nil, err + } + out[spec.ID] = raw + } + return out, nil +} + +// batterySpec resolves one workload's contract. +func batterySpec(workloadID string) (WorkloadSpec, bool) { + for _, spec := range Battery { + if spec.ID == workloadID { + return spec, true + } + } + return WorkloadSpec{}, false +} diff --git a/internal/dataframe/benchmark/authoring_test.go b/internal/dataframe/benchmark/authoring_test.go new file mode 100644 index 000000000..f25a6ade8 --- /dev/null +++ b/internal/dataframe/benchmark/authoring_test.go @@ -0,0 +1,301 @@ +package benchmark + +import ( + "encoding/json" + "strconv" + "strings" + "testing" + + "github.com/calypr/loom/internal/explorer/authoringv2" +) + +// Format fidelity is judged by the repository's own decoder: if +// authoringv2.DecodeWorkspace accepts a battery workspace and its validation +// passes, the live routes will see exactly this JSON. + +func TestBatteryWorkspacesDecode(t *testing.T) { + workspaces, err := BuildAllWorkspaces() + if err != nil { + t.Fatal(err) + } + if len(workspaces) != len(Battery) { + t.Fatalf("every battery workload must have a workspace template: %d/%d", len(workspaces), len(Battery)) + } + for _, spec := range Battery { + raw, ok := workspaces[spec.ID] + if !ok { + t.Fatalf("missing workspace for %s", spec.ID) + } + ws, err := authoringv2.DecodeWorkspace(raw) + if err != nil { + t.Fatalf("%s: DecodeWorkspace rejected the authored shape: %v", spec.ID, err) + } + if len(ws.Documents) != 1 { + t.Fatalf("%s: expected one document", spec.ID) + } + doc := ws.Documents[0] + if doc.Output.ID != OutputIDFor(spec.ID) { + t.Fatalf("%s: output id mismatch: %s", spec.ID, doc.Output.ID) + } + if len(doc.Columns) != len(spec.Columns) { + // hidden helper columns (e.g. a filter column excluded from the + // published contract) are allowed; the visible projection must + // equal the contract exactly. + var visible []string + for _, column := range doc.Columns { + if column.Table.Visible == nil || *column.Table.Visible { + visible = append(visible, column.Column) + } + } + if strings.Join(visible, ",") != strings.Join(spec.Columns, ",") { + t.Fatalf("%s: visible columns %v != contract %v", spec.ID, visible, spec.Columns) + } + } else { + for i, column := range spec.Columns { + if doc.Columns[i].Column != column { + t.Fatalf("%s: column %d = %q want %q", spec.ID, i, doc.Columns[i].Column, column) + } + } + } + } +} + +func TestBatteryWorkspaceRouteShape(t *testing.T) { + // Root is Patient with base occurrence for every workload except + // derive-age-band, whose rows are observations at an Observation root + // (one row per observation, acceptance-proven single-node route form); + // child occurrences use the relationship spellings proven by the + // acceptance fixture. + workspaces, err := BuildAllWorkspaces() + if err != nil { + t.Fatal(err) + } + allowed := map[string]bool{"subject_Patient": true, "partOfStudy_ResearchStudy": true} + for id, raw := range workspaces { + ws, err := authoringv2.DecodeWorkspace(raw) + if err != nil { + t.Fatalf("%s: %v", id, err) + } + route := ws.Documents[0].Route + wantRoot := "Patient" + if id == WorkloadDeriveAgeBand { + wantRoot = "Observation" + } + if route.OccurrenceID != "base" || route.ResourceType != wantRoot { + t.Fatalf("%s: route root: %+v", id, route) + } + var walk func(children []authoringv2.RouteNode) + walk = func(children []authoringv2.RouteNode) { + for _, child := range children { + if !allowed[child.Relationship] { + t.Fatalf("%s: unproven relationship %q (acceptance-proven set only)", id, child.Relationship) + } + walk(child.Children) + } + } + walk(route.Children) + } +} + +func TestAuthoringVerificationNoneRejected(t *testing.T) { + status := AuthoringVerification() + for _, spec := range Battery { + got, ok := status[spec.ID] + if !ok { + t.Fatalf("%s: no authoring verdict", spec.ID) + } + if got == AuthoringRejected { + // not a test failure per se: the driver would ship this row as + // planned/not_run — but during development we want to know. + t.Logf("workload %s authoring REJECTED by decoder", spec.ID) + } + } +} + +// TestDeriveAgeBandOfflineWitness proves the derive-age-band workload CAN +// reach passed, offline and without any server: the workspace decodes, its +// exhaustive recode map agrees with the independently thresholded oracle on +// every fixture value, and simulating the authored shape (identifier FIRST +// + recode + fixed filter) against the uploaded documents reproduces the +// oracle bag byte-for-byte. A backend that faithfully applies recode and +// filter therefore cannot fail this workload — the ClickHouse-lane +// rehearsal only has to confirm faithfulness, not discover a mismatch. +func TestDeriveAgeBandOfflineWitness(t *testing.T) { + params := DatasetParams{Preset: PresetTiny, Seed: 5, Project: "bench-proj", Generation: "gen-w", AuthResourcePath: "/lox/lox-w"} + files, err := GenerateFiles(params) + if err != nil { + t.Fatal(err) + } + raw, err := BuildWorkspace(WorkloadDeriveAgeBand) + if err != nil { + t.Fatal(err) + } + if _, err := authoringv2.DecodeWorkspace(raw); err != nil { + t.Fatalf("derive-age-band workspace must decode: %v", err) + } + + // Extract the recode map from the authored workspace, not from the + // helper, so the witness checks the actual wire artifact. + var doc struct { + Documents []struct { + Columns []struct { + Column string `json:"column"` + ValueTransformation struct { + Kind string `json:"kind"` + ExactCategoryRecode struct { + Mappings []map[string]string `json:"mappings"` + UnknownPolicy string `json:"unknownPolicy"` + } `json:"exactCategoryRecode"` + } `json:"valueTransformation"` + } `json:"columns"` + FixedFilters []struct { + Column string `json:"column"` + Values []string `json:"values"` + } `json:"fixedFilters"` + } `json:"documents"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatal(err) + } + recode := map[string]string{} + for _, col := range doc.Documents[0].Columns { + if col.Column != "age_band" { + continue + } + if col.ValueTransformation.Kind != "EXACT_CATEGORY_RECODE" || + col.ValueTransformation.ExactCategoryRecode.UnknownPolicy != "ERROR" { + t.Fatalf("age_band must recode with unknownPolicy ERROR: %+v", col.ValueTransformation) + } + for _, m := range col.ValueTransformation.ExactCategoryRecode.Mappings { + if _, dup := recode[m["from"]]; dup { + t.Fatalf("duplicate recode source %q", m["from"]) + } + recode[m["from"]] = m["to"] + } + } + if len(recode) != AgeBandFixtureMax { + t.Fatalf("recode map must cover [1,%d] exhaustively: %d entries", AgeBandFixtureMax, len(recode)) + } + for v := 1; v <= AgeBandFixtureMax; v++ { + if recode[strconv.Itoa(v)] != AgeBandFor(float64(v)) { + t.Fatalf("recode map disagrees with the oracle banding at %d: %q vs %q", + v, recode[strconv.Itoa(v)], AgeBandFor(float64(v))) + } + } + if got := doc.Documents[0].FixedFilters; len(got) != 1 || got[0].Column != "age_band" || + strings.Join(got[0].Values, ",") != "young,middle,senior" { + t.Fatalf("fixed filter must keep exactly the banded rows: %+v", got) + } + + // Simulate the authored shape against the uploaded documents: one row + // per observation with a bench age identifier, band = recode[value]. + var simulated []map[string]any + seenAge := 0 + for _, line := range strings.Split(strings.TrimRight(files.Files["Observation.ndjson"], "\n"), "\n") { + var o struct { + ID string `json:"id"` + Code struct { + Coding []struct { + Code string `json:"code"` + } `json:"coding"` + } `json:"code"` + Identifier []struct { + System string `json:"system"` + Value string `json:"value"` + } `json:"identifier"` + ValueQuantity *struct { + Value float64 `json:"value"` + } `json:"valueQuantity"` + } + if err := json.Unmarshal([]byte(line), &o); err != nil { + t.Fatal(err) + } + isAge := len(o.Code.Coding) == 1 && o.Code.Coding[0].Code == ObservationCodeAge + var bench []string + for _, id := range o.Identifier { + if id.System == BenchAgeIdentifierSystem { + bench = append(bench, id.Value) + } + } + switch { + case !isAge && len(bench) != 0: + t.Fatalf("%s: non-age observation must not carry the bench age identifier", o.ID) + case isAge && o.ValueQuantity == nil && len(bench) != 0: + t.Fatalf("%s: missing-value age observation must not carry the identifier either", o.ID) + case isAge && o.ValueQuantity != nil: + if len(bench) != 1 { + t.Fatalf("%s: age observation needs exactly one identifier value, got %d", o.ID, len(bench)) + } + if bench[0] != strconv.FormatFloat(o.ValueQuantity.Value, 'f', -1, 64) { + t.Fatalf("%s: identifier %q diverged from valueQuantity %v", o.ID, bench[0], o.ValueQuantity.Value) + } + band, ok := recode[bench[0]] + if !ok { + t.Fatalf("%s: fixture age %s outside the recode domain", o.ID, bench[0]) + } + simulated = append(simulated, map[string]any{"observation_id": o.ID, "age_band": band}) + seenAge++ + } + } + if seenAge == 0 { + t.Fatal("no valued age observations in the tiny preset") + } + columns, _ := workloadColumns(WorkloadDeriveAgeBand) + simulatedBag := Bag(CanonicalizeRows(columns, simulated)) + expected, err := OracleFor(WorkloadDeriveAgeBand, files.Facts) + if err != nil { + t.Fatal(err) + } + if expected == nil { + t.Fatal("derive-age-band must have an oracle") + } + if a, b, diff := FirstDifference(*expected, simulatedBag); diff { + t.Fatalf("workspace-simulated bag differs from the oracle bag: first divergence expected %q, simulated %q", a, b) + } + if expected.Digest() != simulatedBag.Digest() { + t.Fatalf("bag digests differ: oracle %s simulated %s", expected.Digest(), simulatedBag.Digest()) + } +} + +// TestLiveBatteryExclusionsRegistry pins the live battery law from review +// r4559430560: a live leg runs exactly the authoring-proven workloads, +// and every exclusion carries a named reason. A silent exclusion is the +// same deception class the report verifier exists to refuse. +func TestLiveBatteryExclusionsRegistry(t *testing.T) { + exclusions := LiveBatteryExclusions() + statuses := AuthoringVerification() + live := 0 + for _, spec := range Battery { + reason, excluded := exclusions[spec.ID] + if statuses[spec.ID] == AuthoringProven && !unverifiedWorkloads[spec.ID] { + if excluded { + t.Errorf("%s: proven workload excluded from live legs: %q", spec.ID, reason) + } + live++ + continue + } + if !excluded { + t.Errorf("%s: unproven verdict %q not excluded from live legs", spec.ID, statuses[spec.ID]) + continue + } + if strings.TrimSpace(reason) == "" { + t.Errorf("%s: exclusion without a named reason", spec.ID) + } + } + if live == 0 { + t.Fatal("live battery is empty: nothing would ever be timed") + } + if live == len(Battery) { + t.Fatal("nothing is excluded: the disproven oracle rows must stay out of live legs") + } + // The two review findings must be named in the registry with their + // actual defects, not generic prose. + if r := exclusions[WorkloadDeriveAgeBand]; !strings.Contains(r, "EXACT_CATEGORY_RECODE") || + !strings.Contains(r, "acceptance-fixture precedent") || !strings.Contains(r, "TestDeriveAgeBandOfflineWitness") { + t.Errorf("derive-age-band exclusion must name the unproven-recode shape and the offline witness: %q", r) + } + if r := exclusions[WorkloadSortLimitTopN]; !strings.Contains(r, "name[].family") || + !strings.Contains(r, "first-only") { + t.Errorf("sort-limit-topn exclusion must name the multi-family risk: %q", r) + } +} diff --git a/internal/dataframe/benchmark/authoringpublish.go b/internal/dataframe/benchmark/authoringpublish.go new file mode 100644 index 000000000..db7ef8543 --- /dev/null +++ b/internal/dataframe/benchmark/authoringpublish.go @@ -0,0 +1,246 @@ +package benchmark + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "sort" + "strings" +) + +// Authoring-plane publication primitives for the ruling setup conditions, +// wired into the live setup path by RunBenchmark (review r4561644037): per +// workload and per arm it calls SetupProofEvidence after authoring and +// gates all timing behind MatchedSetupProof, so the authoring publish, the +// materialization-identity verification, the active-state readback, and the +// pre-timing matched-state proof execute on every timed lane, not only in +// unit tests. They speak only public routes. No arm may reach the +// repository activation posture through them, and activation-style refusals +// are fatal wherever they appear: the benchmark never tolerates, retries, +// or special-cases one. + +// ErrActivationRefusal marks an activation-style refusal answered by the +// server (typed 409/503 stances whose message is that a revision was +// retained, or that activation itself failed). The verifier contract is +// fail-closed: such a refusal aborts the run with this error wrapped, so a +// green report can never sit on a tolerated activation gap. +var ErrActivationRefusal = errors.New("benchmark: activation-style refusal (fail-closed, never tolerated)") + +// activationRefusalMarkers are the server's own typed codes for +// activation-stage refusals; matching is on codes and the structured +// adapter posture, never on message prose (coordinator ruling 2026-11-13). +// Codes are matched bare so JSON formatting drift cannot hide a refusal +// from the fail-closed posture. PUBLICATION_ACTIVATION_CONFLICT and +// RELEASE_ACTIVATION_FAILED are deterministic contract failures: losing +// them means the harness setup violated a contract, so they are fatal, not +// retried. +var activationRefusalMarkers = []string{ + "GENERATION_ACTIVATION_FAILED", + "PUBLICATION_ACTIVATION_CONFLICT", + "RELEASE_ACTIVATION_FAILED", + "MATERIALIZATION_ACTIVATION_FAILED", +} + +// adapterRefusalMarkers together identify the typed named refusal the +// PostgreSQL runtime answers for capabilities with no adapter (postgreSQL +// RuntimeUnavailable: CodeBackendUnavailable + details stage=adapter). +// Both must appear: the code alone could in principle ride an unrelated +// envelope, and the stage detail alone is not an identity. +var adapterRefusalMarkers = []string{ + "BACKEND_UNAVAILABLE", + `"stage"`, + `"adapter"`, +} + +func activationRefusalIn(body string) bool { + for _, marker := range activationRefusalMarkers { + if strings.Contains(body, marker) { + return true + } + } + for _, marker := range adapterRefusalMarkers { + if !strings.Contains(body, marker) { + return false + } + } + return true +} + +// PublicationOutput is one materialized output as the authoring publish +// route answers it. +type PublicationOutput struct { + OutputID string `json:"outputId"` + State string `json:"state"` + MaterializationID string `json:"materializationId"` +} + +// AuthoringPublication is the verified response of the authoring publish +// route: identity fields must all be present, and every output must be +// READY with a materialization identity (condition ii: materialization +// evidence is consumed, never decorative). +type AuthoringPublication struct { + ReceiptID string `json:"receiptId"` + RevisionID string `json:"revisionId"` + State string `json:"state"` + Outputs []PublicationOutput `json:"outputs"` + Diagnostics []any `json:"diagnostics"` +} + +// PublishReceipt publishes a reconciled receipt through the authoring +// route (POST explorers/{id}/authoring/v2/publish) on this arm and +// verifies the answer: the receipt identity must round-trip, the revision +// identity must exist, and every output must carry a READY materialization +// identity. Any shortfall is an error before the caller can use the +// receipt for anything. +func (a *Arm) PublishReceipt(ctx context.Context, token, project, explorer, receiptID string) (AuthoringPublication, error) { + endpoint := a.scoped(a.authoringURL(project, explorer) + "/publish") + raw, err := json.Marshal(map[string]any{"receiptId": receiptID}) + if err != nil { + return AuthoringPublication{}, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(raw)) + if err != nil { + return AuthoringPublication{}, err + } + req.Header.Set("Content-Type", "application/json") + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish on %s: %w", a.Backend, err) + } + encoded, _ := json.Marshal(value) + var pub AuthoringPublication + if err := json.Unmarshal(encoded, &pub); err != nil { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish shape on %s: %w", a.Backend, err) + } + if pub.ReceiptID != receiptID { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish on %s answered receipt %q for reconcile %q", a.Backend, pub.ReceiptID, receiptID) + } + if strings.TrimSpace(pub.RevisionID) == "" { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish on %s omitted the revision identity", a.Backend) + } + if strings.TrimSpace(pub.State) == "" { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish on %s omitted the revision state", a.Backend) + } + if len(pub.Outputs) == 0 { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish on %s answered no materialized outputs", a.Backend) + } + for _, out := range pub.Outputs { + if strings.TrimSpace(out.OutputID) == "" || strings.TrimSpace(out.MaterializationID) == "" { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish on %s answered output %q without output/materialization identity", a.Backend, out.OutputID) + } + if out.State != "READY" { + return AuthoringPublication{}, fmt.Errorf("benchmark: authoring publish on %s left output %s in state %q, want READY", a.Backend, out.OutputID, out.State) + } + } + return pub, nil +} + +// ExplorerActiveState is the matched-state readback: the stable identity +// fields the explorer route answers for a published interactive explorer. +// Timestamps are excluded on purpose; digests are content-derived, so +// cross-arm equality is the setup-equivalence proof. +type ExplorerActiveState struct { + DraftDigest string + ActiveRevisionID string +} + +// FetchExplorerActiveState reads the explorer's active runtime projection +// for the matched-state proof. +func (a *Arm) FetchExplorerActiveState(ctx context.Context, token, project, explorerID string) (ExplorerActiveState, error) { + endpoint := a.scoped(a.BaseURL + "/api/v1/projects/" + url.PathEscape(project) + "/explorers/" + url.PathEscape(explorerID)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return ExplorerActiveState{}, err + } + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return ExplorerActiveState{}, fmt.Errorf("benchmark: explorer readback on %s: %w", a.Backend, err) + } + raw, _ := json.Marshal(value) + // The route answers ExplorerStateV1: the draft digest is at + // draft.digest and the revision identity at active.revisionId. + var wire struct { + Draft struct { + Digest string `json:"digest"` + } `json:"draft"` + Active struct { + RevisionID string `json:"revisionId"` + } `json:"active"` + } + if err := json.Unmarshal(raw, &wire); err != nil { + return ExplorerActiveState{}, fmt.Errorf("benchmark: explorer readback shape on %s: %w", a.Backend, err) + } + return ExplorerActiveState{DraftDigest: wire.Draft.Digest, ActiveRevisionID: wire.Active.RevisionID}, nil +} + +// SetupProof is everything the pre-timing matched-state proof compares for +// one workload on one arm. +type SetupProof struct { + DraftDigest string + RevisionID string + OutputIDs []string + Materializations int + PreviewRows int +} + +// SetupProofEvidence publishes the receipt, reads back the active state, +// and runs one untimed setup preview on this arm. Publish failures +// (including activation-style refusals) abort here; nothing downstream of +// a divergent setup may be timed. +func (a *Arm) SetupProofEvidence(ctx context.Context, token, project, explorerID, receiptID, outputID string) (SetupProof, error) { + pub, err := a.PublishReceipt(ctx, token, project, explorerID, receiptID) + if err != nil { + return SetupProof{}, err + } + state, err := a.FetchExplorerActiveState(ctx, token, project, explorerID) + if err != nil { + return SetupProof{}, err + } + if strings.TrimSpace(state.DraftDigest) == "" { + return SetupProof{}, fmt.Errorf("benchmark: explorer readback on %s omitted the draft digest", a.Backend) + } + preview, err := a.ExecuteOutput(ctx, token, project, explorerID, receiptID, outputID, 1) + if err != nil { + return SetupProof{}, fmt.Errorf("benchmark: setup preview on %s: %w", a.Backend, err) + } + proof := SetupProof{DraftDigest: state.DraftDigest, RevisionID: state.ActiveRevisionID, PreviewRows: preview.RowCount, Materializations: len(pub.Outputs)} + for _, out := range pub.Outputs { + proof.OutputIDs = append(proof.OutputIDs, out.OutputID) + } + sort.Strings(proof.OutputIDs) + return proof, nil +} + +// MatchedSetupProof compares the two arms' pre-timing evidence +// (condition i). The draft digest is the content-equivalence proof; the +// output set must match exactly and every arm must carry a materialization +// identity per output; the setup preview rows must agree. Revision ids are +// recorded by the caller but not compared (server-internal identity +// composition may differ across backends). +func MatchedSetupProof(workloadID string, arango, postgres SetupProof) error { + if arango.DraftDigest == "" || postgres.DraftDigest == "" { + return fmt.Errorf("benchmark: %s matched-state proof missing draft digest", workloadID) + } + if arango.DraftDigest != postgres.DraftDigest { + return fmt.Errorf("benchmark: %s matched-state proof diverged: draft digest %s != %s", workloadID, arango.DraftDigest, postgres.DraftDigest) + } + if len(arango.OutputIDs) != len(postgres.OutputIDs) || arango.Materializations != len(arango.OutputIDs) || postgres.Materializations != len(postgres.OutputIDs) { + return fmt.Errorf("benchmark: %s matched-state proof output/materialization counts differ: arango %d/%d, postgres %d/%d", + workloadID, len(arango.OutputIDs), arango.Materializations, len(postgres.OutputIDs), postgres.Materializations) + } + for i := range arango.OutputIDs { + if arango.OutputIDs[i] != postgres.OutputIDs[i] { + return fmt.Errorf("benchmark: %s matched-state proof output set diverged at %d: %s != %s", workloadID, i, arango.OutputIDs[i], postgres.OutputIDs[i]) + } + } + if arango.PreviewRows != postgres.PreviewRows { + return fmt.Errorf("benchmark: %s matched-state proof setup preview rows differ: arango %d, postgres %d", workloadID, arango.PreviewRows, postgres.PreviewRows) + } + return nil +} diff --git a/internal/dataframe/benchmark/authoringpublish_test.go b/internal/dataframe/benchmark/authoringpublish_test.go new file mode 100644 index 000000000..205c55d9b --- /dev/null +++ b/internal/dataframe/benchmark/authoringpublish_test.go @@ -0,0 +1,171 @@ +package benchmark + +import ( + "context" + "errors" + "strings" + "testing" +) + +// Ruling-B conditions: the authoring publish must consume its materialization +// evidence (ii), activation-style refusals are fatal everywhere (iii), and +// the two arms must prove matched state before any timing (i). + +func TestPublishReceiptVerifiesMaterializationIdentity(t *testing.T) { + f := newFakeArm(BackendArango) + defer f.close() + arm := f.arm(BackendArango) + pub, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out") + if err != nil { + t.Fatal(err) + } + if pub.ReceiptID != "rcpt-out" || pub.RevisionID == "" || len(pub.Outputs) != 1 || pub.Outputs[0].MaterializationID == "" { + t.Fatalf("verified publication lost identity: %+v", pub) + } + calls := f.calls + found := false + for _, c := range calls { + if c == BackendArango+":publish:rcpt-out" { + found = true + } + } + if !found { + t.Fatalf("publish route not called: %v", calls) + } + + // A READY-less output is a hard failure: materialization evidence must + // be consumed, never accepted decoratively. + f.mu.Lock() + f.publishOutputs = []PublicationOutput{{OutputID: "out", State: "PENDING", MaterializationID: "mat-1"}} + f.mu.Unlock() + if _, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out"); err == nil || !strings.Contains(err.Error(), "PENDING") { + t.Fatalf("non-READY output must fail: %v", err) + } + f.mu.Lock() + f.publishOutputs = []PublicationOutput{{OutputID: "out", State: "READY"}} + f.mu.Unlock() + if _, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out"); err == nil || !strings.Contains(err.Error(), "materialization identity") { + t.Fatalf("output without materialization id must fail: %v", err) + } + f.mu.Lock() + f.publishOutputs = nil + f.publishEchoBad = true + f.mu.Unlock() + if _, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out"); err == nil || !strings.Contains(err.Error(), "for reconcile") { + t.Fatalf("receipt identity mismatch must fail: %v", err) + } + f.mu.Lock() + f.publishEchoBad = false + f.publishNoOutputs = true + f.mu.Unlock() + if _, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out"); err == nil || !strings.Contains(err.Error(), "no materialized outputs") { + t.Fatalf("zero outputs must fail: %v", err) + } +} + +func TestActivationRefusalIsFatalOnEveryRoute(t *testing.T) { + activationBody := `{"error":{"code":"GENERATION_ACTIVATION_FAILED","message":"activate ExplorerConfigV2 failed; the prior Explorer revision was retained"}}` + f := newFakeArm(BackendPostgres) + defer f.close() + f.mu.Lock() + f.activationBody = activationBody + f.mu.Unlock() + arm := f.arm(BackendPostgres) + + _, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-1") + if !errors.Is(err, ErrActivationRefusal) { + t.Fatalf("authoring publish must fail closed on an activation-style refusal: %v", err) + } + // the repository workspace route wraps it too: no lane can tolerate one + workspace := []byte(`{"kind":"explorer-builder"}`) + if _, err := arm.PublishWorkspace(context.Background(), "Bearer x", "proj", "gen-1", workspace, "a"); !errors.Is(err, ErrActivationRefusal) { + t.Fatalf("repository publish must fail closed on the same refusal: %v", err) + } + + // the typed adapter posture (CodeBackendUnavailable + stage=adapter + // details) is the structural key (coordinator ruling: not prose) + f.mu.Lock() + f.activationBody = `{"error":{"code":"BACKEND_UNAVAILABLE","message":"Explicit-selection reference validation has no PostgreSQL adapter yet","retryable":false,"details":{"backend":"postgresql","stage":"adapter"}}}` + f.mu.Unlock() + if _, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-1"); !errors.Is(err, ErrActivationRefusal) { + t.Fatalf("adapter-posture refusal must fail closed: %v", err) + } + + // message prose alone is no longer a key: a body carrying the old + // retained-revision wording without any typed code is an ordinary + // failure, not an activation refusal (ruling: key on codes/structure) + f.mu.Lock() + f.activationBody = `{"error":{"code":"SCRIPTED_NOTHING","message":"the prior Explorer revision was retained"}}` + f.mu.Unlock() + if _, err := arm.PublishReceipt(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-1"); errors.Is(err, ErrActivationRefusal) { + t.Fatal("prose without a typed code must not be labeled an activation refusal") + } + + // a plain scripted failure (no markers at all) must NOT be labeled as + // an activation refusal either + f.mu.Lock() + f.activationBody = "" + f.failWith["anyout"] = 503 + f.mu.Unlock() + if _, err := arm.ExecuteOutput(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-1", "anyout", 10); errors.Is(err, ErrActivationRefusal) { + t.Fatal("generic 503 must not be mislabeled as an activation refusal") + } +} + +func TestMatchedSetupProofCatchesDivergence(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.rows["out"] = []map[string]any{{"patient_id": "p1"}} + fp.rows["out"] = []map[string]any{{"patient_id": "p1"}} + + proof := func(f *fakeArm, backend string) SetupProof { + t.Helper() + p, err := f.arm(backend).SetupProofEvidence(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out", "out") + if err != nil { + t.Fatal(err) + } + return p + } + pa, pp := proof(fa, BackendArango), proof(fp, BackendPostgres) + if err := MatchedSetupProof("w", pa, pp); err != nil { + t.Fatalf("matched arms must prove: %v", err) + } + if pa.PreviewRows != 1 || len(pa.OutputIDs) != 1 || pa.DraftDigest == "" { + t.Fatalf("proof lost content: %+v", pa) + } + + // draft digest divergence (the content-equivalence proof) + bad := pp + bad.DraftDigest = "sha256:other" + if err := MatchedSetupProof("w", pa, bad); err == nil || !strings.Contains(err.Error(), "draft digest") { + t.Fatalf("digest divergence must fail: %v", err) + } + // row divergence before timing + bad = pp + bad.PreviewRows = 2 + if err := MatchedSetupProof("w", pa, bad); err == nil || !strings.Contains(err.Error(), "preview rows") { + t.Fatalf("row divergence must fail: %v", err) + } + // output-set divergence + bad = pp + bad.OutputIDs = []string{"out-other"} + bad.Materializations = 1 + if err := MatchedSetupProof("w", pa, bad); err == nil || !strings.Contains(err.Error(), "output set") { + t.Fatalf("output set divergence must fail: %v", err) + } + // materialization count must match the output set (decorative evidence + // cannot pass) + bad = pp + bad.Materializations = 0 + if err := MatchedSetupProof("w", pa, bad); err == nil || !strings.Contains(err.Error(), "output/materialization counts") { + t.Fatalf("decorative materialization count must fail: %v", err) + } + // a real cross-arm readback divergence surfaces through SetupProofEvidence + fp.mu.Lock() + fp.activeDigest = "" + fp.mu.Unlock() + if _, err := fp.arm(BackendPostgres).SetupProofEvidence(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out", "out"); err != nil { + t.Fatalf("default digest script must answer: %v", err) + } +} diff --git a/internal/dataframe/benchmark/canonical.go b/internal/dataframe/benchmark/canonical.go new file mode 100644 index 000000000..c733f65d9 --- /dev/null +++ b/internal/dataframe/benchmark/canonical.go @@ -0,0 +1,300 @@ +package benchmark + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "math" + "sort" + "strconv" +) + +// Result canonicalization for cross-backend comparison. +// +// The two backends legitimately differ in storage identity (Arango `_key` +// materialization vs the PostgreSQL stored logical id) and in JSON numeric +// spelling. A comparison is only meaningful under an EXPLICIT contract, so +// canonicalization here is total: every value maps to exactly one canonical +// token, null and missing are distinct, multiplicity is preserved (bag +// semantics), and unordered comparison sorts canonical row encodings. + +// CanonicalValue is the closed canonical value algebra. +type CanonicalValue interface{ canonicalToken() string } + +type CNull struct{} // JSON null +type CMissing struct{} // key absent from the row object +type CString string // string, exact bytes +type CBool bool // boolean +type CNumber struct{ Text string } // numeric, canonical shortest spelling +type CList struct{ Items []CanonicalValue } // ordered list, multiplicity kept +type CObject struct{ Pairs []CanonicalPair } // object, keys sorted on encode + +type CanonicalPair struct { + Key string + Value CanonicalValue +} + +func (CNull) canonicalToken() string { return "N" } +func (CMissing) canonicalToken() string { return "A" } +func (CString) canonicalToken() string { return "" } +func (CBool) canonicalToken() string { return "" } +func (CNumber) canonicalToken() string { return "" } +func (CList) canonicalToken() string { return "" } +func (CObject) canonicalToken() string { return "" } + +// canonicalEncode renders one deterministic token for a value. Type tags +// keep 1, "1", true, and null mutually distinct. +func canonicalEncode(v CanonicalValue) string { + switch t := v.(type) { + case CNull: + return "N" + case CMissing: + return "A" + case CString: + return "s:" + strconv.Quote(string(t)) + case CBool: + if t { + return "b:1" + } + return "b:0" + case CNumber: + return "n:" + canonicalNumberText(t.Text) + case CList: + out := "l:[" + for i, item := range t.Items { + if i > 0 { + out += "," + } + out += canonicalEncode(item) + } + return out + "]" + case CObject: + pairs := append([]CanonicalPair(nil), t.Pairs...) + sort.Slice(pairs, func(i, j int) bool { return pairs[i].Key < pairs[j].Key }) + out := "o:[" + for i, pair := range pairs { + if i > 0 { + out += "," + } + out += strconv.Quote(pair.Key) + ":" + canonicalEncode(pair.Value) + } + return out + "]" + default: + return "u:" + fmt.Sprintf("%T", v) + } +} + +// canonicalNumberText renders the shortest round-tripping decimal for the +// numeric spelling, so 1, 1.0, and 1e0 canonicalize identically while 0.1 +// stays distinct from 0.10000000001. +func canonicalNumberText(text string) string { + f, err := strconv.ParseFloat(text, 64) + if err != nil { + return "raw:" + text + } + if math.IsInf(f, 0) || math.IsNaN(f) { + return "raw:" + text + } + return strconv.FormatFloat(f, 'g', -1, 64) +} + +// CanonicalizeValue converts one decoded JSON value. The caller distinguishes +// missing keys before calling (rows are walked with an explicit key set). +func CanonicalizeValue(value any) CanonicalValue { + switch t := value.(type) { + case nil: + return CNull{} + case bool: + return CBool(t) + case float64: + return CNumber{Text: strconv.FormatFloat(t, 'g', -1, 64)} + case json.Number: + return CNumber{Text: string(t)} + case string: + return CString(t) + case []any: + items := make([]CanonicalValue, 0, len(t)) + for _, item := range t { + items = append(items, CanonicalizeValue(item)) + } + return CList{Items: items} + case map[string]any: + pairs := make([]CanonicalPair, 0, len(t)) + for key, child := range t { + pairs = append(pairs, CanonicalPair{Key: key, Value: CanonicalizeValue(child)}) + } + return CObject{Pairs: pairs} + default: + return CString(fmt.Sprintf("%v", t)) + } +} + +// CanonicalRow is one row over the pinned column order. Missing keys become +// CMissing, so a backend that omits a null column cannot silently match one +// that emits null. +type CanonicalRow []CanonicalValue + +// CanonicalizeRows converts row objects using the exact column order. +func CanonicalizeRows(columns []string, rows []map[string]any) []CanonicalRow { + out := make([]CanonicalRow, 0, len(rows)) + for _, row := range rows { + cr := make(CanonicalRow, len(columns)) + for i, column := range columns { + value, ok := row[column] + if !ok { + cr[i] = CMissing{} + continue + } + cr[i] = CanonicalizeValue(value) + } + out = append(out, cr) + } + return out +} + +// CanonicalBag is a fully canonical unordered multiset of rows: the sorted +// canonical encodings. Equality of two bags is exactly cross-backend +// equality under this contract (types, multiplicity, and nested structure). +type CanonicalBag []string + +func Bag(rows []CanonicalRow) CanonicalBag { + bag := make(CanonicalBag, 0, len(rows)) + for _, row := range rows { + tokens := make([]string, len(row)) + for i, cell := range row { + tokens[i] = canonicalEncode(cell) + } + bag = append(bag, fmt.Sprintf("%q", tokens)) + } + sort.Strings(bag) + return bag +} + +func (b CanonicalBag) Digest() string { + h := sha256.New() + for _, row := range b { + h.Write([]byte(row)) + h.Write([]byte{0}) + } + return hex.EncodeToString(h.Sum(nil)) +} + +// FirstDifference reports the first row encoding where two equal-length +// bags diverge, for a human-readable mismatch report without dumping either +// full result set (bound values stay out of evidence). +func FirstDifference(a, b CanonicalBag) (string, string, bool) { + limit := len(a) + if len(b) < limit { + limit = len(b) + } + for i := 0; i < limit; i++ { + if a[i] != b[i] { + return truncateToken(a[i]), truncateToken(b[i]), true + } + } + if len(a) != len(b) { + return "(len " + strconv.Itoa(len(a)) + ")", "(len " + strconv.Itoa(len(b)) + ")", true + } + return "", "", false +} + +func truncateToken(token string) string { + const max = 160 + if len(token) <= max { + return token + } + return token[:max] + "…" +} + +// StorageIDMap is the explicit backend storage identity -> logical identity +// mapping the comparison uses. Backends must not be compared on storage +// identifiers; any column listed here is translated to logical identity on +// both sides before bagging. +// +// Arango rows carry storage keys of the form "Collection/key" or bare keys in +// row-identity columns; the PostgreSQL rows carry the stored logical id. The +// mapping is data-driven: the fixture manifest owns logical ids, and both +// spellings resolve through it, so a mismatch is a real mismatch. +type StorageIDMap struct { + // Columns are the row columns that carry storage identities. + Columns []string + // toLogical maps either spelling (storage or logical) to the logical id. + toLogical map[string]string +} + +// NewStorageIDMap builds the mapping from fixture facts. +func NewStorageIDMap(facts *Facts) *StorageIDMap { + toLogical := map[string]string{} + add := func(logical string) { + toLogical[logical] = logical + // Arango spellings: Collection/key and _id-like Type/key forms. + for _, collection := range []string{"Patient", "Observation", "Specimen", "Condition", "ResearchStudy"} { + toLogical[collection+"/"+logical] = logical + } + } + for _, p := range facts.Patients { + add(p.ID) + for _, id := range p.ObservationIDs { + add(id) + } + for _, id := range p.SpecimenIDs { + add(id) + } + for _, id := range p.ConditionIDs { + add(id) + } + } + for _, o := range facts.Observations { + add(o.ID) + } + for _, s := range facts.Specimens { + add(s.ID) + } + for _, c := range facts.Conditions { + add(c.ID) + } + for _, s := range facts.StudyIDs { + add(s) + } + return &StorageIDMap{ + Columns: []string{"_row_id", "row_id", "resource_id", "root_resource_id"}, + toLogical: toLogical, + } +} + +// Apply rewrites identity-carrying cells through the logical mapping. An +// unmapped identity string stays verbatim (a foreign identity must compare +// unequal rather than vanish), and non-strings pass through. +func (m *StorageIDMap) Apply(columns []string, rows []map[string]any) []map[string]any { + if m == nil || len(m.Columns) == 0 { + return rows + } + present := map[string]bool{} + for _, column := range columns { + present[column] = true + } + watched := map[string]bool{} + for _, column := range m.Columns { + if present[column] { + watched[column] = true + } + } + out := make([]map[string]any, 0, len(rows)) + for _, row := range rows { + clone := make(map[string]any, len(row)) + for key, value := range row { + if watched[key] { + if text, ok := value.(string); ok { + if logical, found := m.toLogical[text]; found { + clone[key] = logical + continue + } + } + } + clone[key] = value + } + out = append(out, clone) + } + return out +} diff --git a/internal/dataframe/benchmark/canonical_test.go b/internal/dataframe/benchmark/canonical_test.go new file mode 100644 index 000000000..59b8fa890 --- /dev/null +++ b/internal/dataframe/benchmark/canonical_test.go @@ -0,0 +1,143 @@ +package benchmark + +import ( + "encoding/json" + "strings" + "testing" +) + +func decode(t *testing.T, data string) map[string]any { + t.Helper() + var m map[string]any + dec := json.NewDecoder(strings.NewReader(data)) + dec.UseNumber() + if err := dec.Decode(&m); err != nil { + t.Fatalf("decode %q: %v", data, err) + } + return m +} + +func TestCanonicalTypeDistinctness(t *testing.T) { + // 1, "1", true, and null must all canonicalize apart. + a := CanonicalizeValue(decode(t, `{"v": 1}`)["v"]) + b := CanonicalizeValue(decode(t, `{"v": "1"}`)["v"]) + c := CanonicalizeValue(decode(t, `{"v": true}`)["v"]) + d := CanonicalizeValue(decode(t, `{"v": null}`)["v"]) + tokens := map[string]bool{} + for _, v := range []CanonicalValue{a, b, c, d} { + tok := canonicalEncode(v) + if tokens[tok] { + t.Fatalf("two distinct values share token %q", tok) + } + tokens[tok] = true + } +} + +func TestCanonicalNumericSpellingEquivalence(t *testing.T) { + same := []string{"1", "1.0", "1e0", "1.00"} + distinct := []string{"0.1", "0.10000000001", "2"} + bagOf := func(lits ...string) CanonicalBag { + var rows []CanonicalRow + for _, lit := range lits { + rows = append(rows, CanonicalRow{CanonicalizeValue(decode(t, `{"v": `+lit+`}`)["v"])}) + } + return Bag(rows) + } + if d := bagOf(same[0], same[1], same[2], same[3]); len(d) != 4 { + t.Fatal("bag lost multiplicity") + } + // All four spellings encode to ONE distinct token; bag equality of a + // single 1 vs a single 1.0 must hold. + if Bag([]CanonicalRow{{CanonicalizeValue(decode(t, `{"v": 1}`)["v"])}}).Digest() != + Bag([]CanonicalRow{{CanonicalizeValue(decode(t, `{"v": 1.0}`)["v"])}}).Digest() { + t.Fatal("1 and 1.0 must canonicalize identically") + } + if Bag([]CanonicalRow{{CanonicalizeValue(decode(t, `{"v": 0.1}`)["v"])}}).Digest() == + Bag([]CanonicalRow{{CanonicalizeValue(decode(t, `{"v": 0.10000000001}`)["v"])}}).Digest() { + t.Fatal("near-equal numbers must stay distinct") + } + _ = distinct +} + +func TestCanonicalNullVsMissing(t *testing.T) { + nullRow := CanonicalizeRows([]string{"v"}, []map[string]any{{"v": nil}}) + missingRow := CanonicalizeRows([]string{"v"}, []map[string]any{{}}) + if Bag(nullRow).Digest() == Bag(missingRow).Digest() { + t.Fatal("null must not equal missing") + } + if _, ok := missingRow[0][0].(CMissing); !ok { + t.Fatalf("absent key must canonicalize to CMissing, got %T", missingRow[0][0]) + } +} + +func TestCanonicalBagKeepsMultiplicity(t *testing.T) { + dup := []map[string]any{{"v": "x"}, {"v": "x"}} + single := []map[string]any{{"v": "x"}} + if Bag(CanonicalizeRows([]string{"v"}, dup)).Digest() == Bag(CanonicalizeRows([]string{"v"}, single)).Digest() { + t.Fatal("bag semantics must preserve multiplicity") + } +} + +func TestCanonicalBagIsUnordered(t *testing.T) { + a := []map[string]any{{"v": "x"}, {"v": "y"}} + b := []map[string]any{{"v": "y"}, {"v": "x"}} + if Bag(CanonicalizeRows([]string{"v"}, a)).Digest() != Bag(CanonicalizeRows([]string{"v"}, b)).Digest() { + t.Fatal("bag comparison must ignore row order") + } +} + +func TestCanonicalNestedStructure(t *testing.T) { + // Nested list identity: backend A gives ["a","a"], backend B ["a"]. + a := CanonicalizeValue(decode(t, `{"v": ["a","a"]}`)["v"]) + b := CanonicalizeValue(decode(t, `{"v": ["a"]}`)["v"]) + if canonicalEncode(a) == canonicalEncode(b) { + t.Fatal("nested multiplicity must be preserved") + } + // Object key ORDER must not matter, but key presence must. + c := CanonicalizeValue(decode(t, `{"v": {"a": 1, "b": 2}}`)["v"]) + d := CanonicalizeValue(decode(t, `{"v": {"b": 2, "a": 1}}`)["v"]) + if canonicalEncode(c) != canonicalEncode(d) { + t.Fatal("object key order must not affect encoding") + } + e := CanonicalizeValue(decode(t, `{"v": {"a": 1}}`)["v"]) + if canonicalEncode(c) == canonicalEncode(e) { + t.Fatal("object key presence must affect encoding") + } +} + +func TestStorageIDMapResolvesArangoSpellings(t *testing.T) { + facts, err := GenerateFacts(DatasetParams{Preset: PresetTiny, Seed: 7, Project: "p", Generation: "g", AuthResourcePath: "/bench"}) + if err != nil { + t.Fatal(err) + } + idMap := NewStorageIDMap(facts) + pid := facts.Patients[0].ID + // The engine's row-identity column carries the storage spelling on the + // Arango arm and the stored logical id on the PostgreSQL arm. + columns := []string{"_row_id", "gender"} + arangoRows := []map[string]any{{"_row_id": "Patient/" + pid, "gender": "female"}} + postgresRows := []map[string]any{{"_row_id": pid, "gender": "female"}} + aBag := Bag(CanonicalizeRows(columns, idMap.Apply(columns, arangoRows))) + pBag := Bag(CanonicalizeRows(columns, idMap.Apply(columns, postgresRows))) + if aBag.Digest() != pBag.Digest() { + t.Fatal("storage spelling must map to logical identity on both arms") + } + // A foreign identity stays verbatim and must not vanish into the map. + foreign := []map[string]any{{"_row_id": "Patient/not-a-real-id", "gender": "female"}} + fBag := Bag(CanonicalizeRows(columns, idMap.Apply(columns, foreign))) + if fBag.Digest() == pBag.Digest() { + t.Fatal("foreign identity must remain distinct") + } +} + +func TestFirstDifferenceReportsDivergence(t *testing.T) { + a := Bag(CanonicalizeRows([]string{"v"}, []map[string]any{{"v": "a"}, {"v": "b"}})) + b := Bag(CanonicalizeRows([]string{"v"}, []map[string]any{{"v": "a"}, {"v": "z"}})) + x, y, diff := FirstDifference(a, b) + if !diff || x == "" || y == "" { + t.Fatalf("expected divergence report, got %q %q %v", x, y, diff) + } + if _, _, diff := FirstDifference(a, a); diff { + t.Fatal("identical bags must report no difference") + } +} diff --git a/internal/dataframe/benchmark/cmd/benchcli/deployer.go b/internal/dataframe/benchmark/cmd/benchcli/deployer.go new file mode 100644 index 000000000..f9d466361 --- /dev/null +++ b/internal/dataframe/benchmark/cmd/benchcli/deployer.go @@ -0,0 +1,163 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/calypr/loom/internal/dataframe/benchmark" +) + +// deployer turns CLI flags into driver arm configs. All participating arms +// are either attached (running servers + caller tokens) or spawned +// (binary + benchmark fence); mixing is refused: the attached server's +// trusted issuer cannot be assumed to be the benchmark fence. +type deployer struct { + flags *runFlags + + fence *benchmark.MockFence + unrestrictedToken string + scopedToken string + + arango benchmark.ArmConfig + postgres benchmark.ArmConfig +} + +const ( + benchmarkAdminSubject = "bench-admin" + benchmarkScopedSubject = "bench-scoped" + // Same fence project as benchmarkDefaultAuthResourcePath: both admin + // grants normalize (authscope calypr-key rule) to the legacy key + // "loom-bench" that AuthorizeReadProject demands for the benchmark + // project id. See the comment on benchmarkDefaultAuthResourcePath. + benchmarkDecoyPath = "/programs/loom/projects/bench/decoy" +) + +func (d *deployer) prepare(ctx context.Context) error { + f := d.flags + type legSpec struct { + backend string + attachURL string + dsn string + arm *benchmark.ArmConfig + } + var legs []legSpec + if f.scope == benchmark.RunScopeBothArms { + legs = append(legs, legSpec{benchmark.BackendArango, f.arangoAttachURL, f.arangoDSN, &d.arango}) + } + legs = append(legs, legSpec{benchmark.BackendPostgres, f.postgresAttachURL, f.postgresDSN, &d.postgres}) + + allAttached, allSpawned := true, true + for _, l := range legs { + if strings.TrimSpace(l.attachURL) == "" { + allAttached = false + } + if strings.TrimSpace(l.attachURL) != "" { + allSpawned = false + } + } + if !allAttached && !allSpawned { + return fmt.Errorf("mixing attached and spawned arms is refused: the attached server's trusted issuer is not the benchmark fence") + } + if allAttached { + if strings.TrimSpace(f.tokenUnrestricted) == "" || strings.TrimSpace(f.tokenScoped) == "" { + return fmt.Errorf("attached arms require --token-unrestricted and --token-scoped") + } + d.unrestrictedToken = f.tokenUnrestricted + d.scopedToken = f.tokenScoped + for _, l := range legs { + *l.arm = benchmark.ArmConfig{BaseURL: l.attachURL} + if l.dsn == "" { + return fmt.Errorf("%s attached arm still needs --%s-dsn for the live version probe", l.backend, l.backend) + } + } + return nil + } + // Spawned: one benchmark fence is the single auth authority. + if strings.TrimSpace(f.binary) == "" { + return fmt.Errorf("spawned arms require --binary") + } + if strings.TrimSpace(f.tokenUnrestricted) != "" || strings.TrimSpace(f.tokenScoped) != "" { + return fmt.Errorf("spawned arms receive tokens from the benchmark fence; drop the token flags") + } + fence, err := benchmark.NewMockFence() + if err != nil { + return fmt.Errorf("benchmark fence: %w", err) + } + d.fence = fence + fence.Grant(benchmarkAdminSubject, f.authResourcePath, benchmarkDecoyPath) + fence.Grant(benchmarkScopedSubject, benchmarkDecoyPath) // dataset path EXCLUDED: production denial shape + d.unrestrictedToken = "Bearer " + benchmark.MakeBearerToken(fence.URL(), benchmarkAdminSubject) + d.scopedToken = "Bearer " + benchmark.MakeBearerToken(fence.URL(), benchmarkScopedSubject) + for _, l := range legs { + if strings.TrimSpace(l.dsn) == "" { + return fmt.Errorf("spawned %s arm requires --%s-dsn", l.backend, l.backend) + } + // One signing secret per arm run; scrubbed from arm logs. + cursorSecret := randomRunIDSuffix() + randomRunIDSuffix() + var backendYAML string + switch l.backend { + case benchmark.BackendPostgres: + host, port, database, user, password, sslmode, err := parsePostgresDSN(l.dsn) + if err != nil { + return fmt.Errorf("%s dsn: %w", l.backend, err) + } + backendYAML = benchmark.PostgresBackendYAML(host, port, database, user, password, sslmode, "loom_bench") + case benchmark.BackendArango: + rawURL, database, err := parseArangoDSN(l.dsn) + if err != nil { + return fmt.Errorf("%s dsn: %w", l.backend, err) + } + backendYAML = benchmark.ArangoBackendYAML(rawURL, database) + } + *l.arm = benchmark.ArmConfig{ + BinaryPath: f.binary, + ConfigYAML: benchmark.BuildServerConfigYAML(benchmark.ListenPlaceholder, l.backend, backendYAML, fence.URL(), cursorSecret, clickHouseSpecFor(f, l.backend)), + ScrubSecrets: scrubSecretsFor(f, l.dsn, cursorSecret), + } + } + return nil +} + +// clickHouseSpecFor returns the arm's publication-chain config, or nil +// when the arms run without ClickHouse (excluded mode). The database is +// benchmark-owned and arm-specific: both arms share the ClickHouse +// server, so sharing a database would let one driver's published rows +// answer the other's reads. +func clickHouseSpecFor(f *runFlags, backend string) *benchmark.ClickHouseSpec { + if !f.clickHouseEnabled { + return nil + } + database := "loom_bench_" + backend + return &benchmark.ClickHouseSpec{ + URL: f.clickHouseURL, + Database: database, + Username: f.clickHouseUser, + Password: f.clickHousePassword, + Recipe: f.clickHouseRecipe, + } +} + +func scrubSecretsFor(f *runFlags, dsn, cursorSecret string) []string { + secrets := []string{dsn, cursorSecret} + if f.clickHousePassword != "" { + secrets = append(secrets, f.clickHousePassword) + } + return secrets +} + +func (d *deployer) close() { + if d.fence != nil { + d.fence.Close() + } +} + +func (d *deployer) probe(ctx context.Context, cfg benchmark.ArmConfig, backend string) (string, error) { + switch backend { + case benchmark.BackendArango: + return probeArangoVersion(ctx, d.flags.arangoDSN) + case benchmark.BackendPostgres: + return probePostgresVersion(ctx, d.flags.postgresDSN) + } + return "", fmt.Errorf("unknown backend %q", backend) +} diff --git a/internal/dataframe/benchmark/cmd/benchcli/guards_test.go b/internal/dataframe/benchmark/cmd/benchcli/guards_test.go new file mode 100644 index 000000000..b75d88c57 --- /dev/null +++ b/internal/dataframe/benchmark/cmd/benchcli/guards_test.go @@ -0,0 +1,419 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/benchmark" + "github.com/calypr/loom/internal/projectid" +) + +// The benchcli guards are fail-closed surfaces: they keep a benchmark run +// inside loom_bench_* namespaces, keep credentials out of error text, and +// refuse incoherent arm topologies before anything spawns. Each refusal +// here is a production guarantee, so each is pinned. + +func TestGuardPostgresNamespace(t *testing.T) { + cases := []struct { + name string + dsn string + wantErr string + }{ + {"benchmark db passes", "postgresql://u:p@127.0.0.1:55432/loom_bench_x1?sslmode=disable", ""}, + {"shared test db refused", "postgresql://u:p@127.0.0.1:55432/loom_test?sslmode=disable", "outside the benchmark namespace"}, + {"postgres db refused", "postgresql://u:p@127.0.0.1:55432/postgres?sslmode=disable", "outside the benchmark namespace"}, + {"prefix lookalike refused", "postgresql://u:p@127.0.0.1:55432/loom_benchx?sslmode=disable", "outside the benchmark namespace"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := guardPostgresNamespace(tc.dsn) + if tc.wantErr == "" { + if err != nil { + t.Fatalf("valid benchmark dsn refused: %v", err) + } + return + } + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("want error containing %q, got %v", tc.wantErr, err) + } + }) + } +} + +func TestGuardErrorsNeverLeakCredentials(t *testing.T) { + const secret = "s3cr3t-pw-9f2a" + dsn := "postgresql://u:" + secret + "@127.0.0.1:55432/loom_test?sslmode=disable" + err := guardPostgresNamespace(dsn) + if err == nil { + t.Fatal("shared db accepted") + } + if strings.Contains(err.Error(), secret) { + t.Fatalf("guard error leaked the password: %v", err) + } + if _, _, _, _, _, _, perr := parsePostgresDSN("not a dsn at all " + secret); perr == nil || strings.Contains(perr.Error(), secret) { + t.Fatalf("parsePostgresDSN error missing or leaked: %v", perr) + } + if _, _, aerr := parseArangoDSN("arango://u:" + secret + "@127.0.0.1:8529/loom_bench_x"); aerr == nil || strings.Contains(aerr.Error(), secret) { + t.Fatalf("parseArangoDSN error missing or leaked: %v", aerr) + } +} + +func TestParsePostgresDSNFully(t *testing.T) { + host, port, database, user, password, sslmode, err := parsePostgresDSN("postgresql://loom_test:pw@127.0.0.1:55432/loom_bench_a?sslmode=disable") + if err != nil { + t.Fatalf("valid dsn refused: %v", err) + } + if host != "127.0.0.1" || port != 55432 || database != "loom_bench_a" || user != "loom_test" || password != "pw" || sslmode != "disable" { + t.Fatalf("unexpected parse: %s:%d/%s user=%s pw=%s ssl=%s", host, port, database, user, password, sslmode) + } + if _, port, _, _, _, _, err := parsePostgresDSN("postgresql://u@127.0.0.1/loom_bench_a?sslmode=disable"); err != nil || port != 5432 { + t.Fatalf("default port: %d, %v", port, err) + } +} + +func TestParseArangoDSN(t *testing.T) { + rawURL, database, err := parseArangoDSN("http://root:pw@127.0.0.1:8529/loom_bench_ar") + if err != nil { + t.Fatalf("valid arango dsn refused: %v", err) + } + if database != "loom_bench_ar" || !strings.HasPrefix(rawURL, "http://") || strings.Contains(rawURL, "loom_bench_ar") { + t.Fatalf("unexpected split: rawURL=%q db=%q", rawURL, database) + } + for _, bad := range []string{ + "arango://root@127.0.0.1:8529/loom_bench_ar", // scheme + "http://root@127.0.0.1:8529/", // no database + "http://root@127.0.0.1:8529/production", // outside namespace + } { + if _, _, err := parseArangoDSN(bad); err == nil { + t.Fatalf("accepted bad arango dsn %q", bad) + } + } +} + +func TestBenchDatabaseNameGuard(t *testing.T) { + for _, ok := range []string{"loom_bench_probe1", "loom_bench_abcd", "loom_bench_" + strings.Repeat("z", 48)} { + if !benchDatabaseRe.MatchString(ok) { + t.Fatalf("rejected valid database name %q", ok) + } + } + for _, bad := range []string{"", "loom_bench_abc", "loom_test", "postgres", "loom_bench_UPPER", "public", "loom_bench_", "xloom_bench_abcd"} { + if benchDatabaseRe.MatchString(bad) { + t.Fatalf("accepted out-of-namespace database name %q", bad) + } + } +} + +func TestSetupSchemaPrefixGuard(t *testing.T) { + // setupPG validates the prefix before any connect, so these refusals + // are hermetic and must not need a database. + if err := setupPG(context.Background(), "postgresql://no:such@127.0.0.1:1/postgres", "loom_bench_guarded", "public"); err == nil || + !strings.Contains(err.Error(), "outside the benchmark namespace") { + t.Fatalf("schema prefix public accepted: %v", err) + } + if err := setupPG(context.Background(), "postgresql://no:such@127.0.0.1:1/postgres", "loom_bench_guarded", "loom_bench"); err != nil && + strings.Contains(err.Error(), "outside the benchmark namespace") { + t.Fatalf("canonical loom_bench prefix refused: %v", err) + } +} + +func TestWithDatabase(t *testing.T) { + swapped, err := withDatabase("postgresql://u:p@127.0.0.1:55432/postgres?sslmode=disable", "loom_bench_t") + if err != nil || !strings.Contains(swapped, "/loom_bench_t") || !strings.Contains(swapped, "sslmode=disable") { + t.Fatalf("swap failed: %q %v", swapped, err) + } + if _, err := withDatabase("host=127.0.0.1 dbname=postgres", "loom_bench_t"); err == nil { + t.Fatal("keyword DSN form accepted for admin") + } + if _, err := withDatabase("mysql://u:p@127.0.0.1/db", "loom_bench_t"); err == nil { + t.Fatal("non-postgres scheme accepted for admin") + } +} + +func TestAdminDSNFromEnv(t *testing.T) { + t.Setenv("LOOM_BENCH_ADMIN_DSN", " ") + if _, err := adminDSNFromEnv(); err == nil { + t.Fatal("blank admin DSN accepted") + } + t.Setenv("LOOM_BENCH_ADMIN_DSN", "postgresql://u:p@127.0.0.1:55432/postgres") + if dsn, err := adminDSNFromEnv(); err != nil || !strings.HasPrefix(dsn, "postgresql://") { + t.Fatalf("admin DSN round trip: %q %v", dsn, err) + } +} + +func TestPlanNotRunMatchesBatteryBothWays(t *testing.T) { + planned := benchmark.PlannedPlan("cap a", "cap b") + planned.PlannedNotRun = benchmark.PlannedNotRunForBackends(planned.NotRunBackends()) + if !planNotRunMatchesBattery(planned) { + t.Fatal("complete two-leg disclosure rejected") + } + only := benchmark.PostgresOnlyPlan("cap a") + only.PlannedNotRun = benchmark.PlannedNotRunForBackends(only.NotRunBackends()) + // the ran leg must also carry one disclosure row per excluded workload + benchmark.AppendWorkloadDisclosures(&only, benchmark.LiveBatteryExclusions()) + if !planNotRunMatchesBattery(only) { + t.Fatal("complete postgres-only disclosure rejected") + } + planned.PlannedNotRun = planned.PlannedNotRun[1:] + if planNotRunMatchesBattery(planned) { + t.Fatal("dropped disclosure row accepted") + } + only.PlannedNotRun = append(only.PlannedNotRun, benchmark.PlannedNotRun{WorkloadID: "filter-active-gender", Backend: benchmark.BackendPostgres, Reason: "extra"}) + if planNotRunMatchesBattery(only) { + t.Fatal("disclosure row for a ran leg accepted") + } + // a ran-leg workload row must match the registry exactly + forged := benchmark.PostgresOnlyPlan("cap a") + forged.PlannedNotRun = benchmark.PlannedNotRunForBackends(forged.NotRunBackends()) + benchmark.AppendWorkloadDisclosures(&forged, benchmark.LiveBatteryExclusions()) + for i := range forged.PlannedNotRun { + if strings.HasPrefix(forged.PlannedNotRun[i].Reason, benchmark.WorkloadNotRunReason) { + forged.PlannedNotRun[i].Reason = benchmark.WorkloadNotRunReason + "because I said so" + break + } + } + if planNotRunMatchesBattery(forged) { + t.Fatal("registry-mismatched workload disclosure accepted") + } +} + +func TestWriteRunArtifactsRoundTripsThroughVerifier(t *testing.T) { + plan := benchmark.PlannedPlan("cap a", "cap b") + plan.PlannedNotRun = benchmark.PlannedNotRunForBackends(plan.NotRunBackends()) + report, err := benchmark.BuildReport(benchmark.RunIdentity{ + RunID: "bench-clitest-a", StartedAt: time.Now().UTC(), Preset: benchmark.PresetTiny, + SourceCommit: strings.Repeat("a", 40), DatasetDigest: strings.Repeat("b", 64), + ClickHouseDisabled: true, + Config: benchmark.RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}, + }, plan, benchmark.ControlRuns{}, benchmark.PlannedGateReports(plan.Legs), nil) + if err != nil { + t.Fatalf("planned report: %v", err) + } + out := t.TempDir() + writeRunArtifacts(out, report, map[string]any{"runId": "bench-clitest-a", "planned": true}) + for _, name := range []string{"report.json", "report.md", "arms.json"} { + if _, err := os.Stat(filepath.Join(out, name)); err != nil { + t.Fatalf("missing artifact %s: %v", name, err) + } + } + raw, err := os.ReadFile(filepath.Join(out, "report.json")) + if err != nil { + t.Fatal(err) + } + loaded, err := benchmark.LoadReport(raw) // LoadReport runs VerifyReport + if err != nil { + t.Fatalf("written artifact failed verification: %v", err) + } + if loaded.Plan.Scope != benchmark.RunScopePlanned || len(loaded.Claims) != 0 { + t.Fatalf("artifact drifted: scope=%s claims=%d", loaded.Plan.Scope, len(loaded.Claims)) + } + var manifest map[string]any + if err := json.Unmarshal([]byte(mustRead(t, filepath.Join(out, "arms.json"))), &manifest); err != nil { + t.Fatalf("arms.json not valid JSON: %v", err) + } + if manifest["planned"] != true { + t.Fatal("planned manifest flag missing") + } + md := mustRead(t, filepath.Join(out, "report.md")) + if !strings.Contains(md, benchmark.RunScopePlanned) { + t.Fatal("markdown must disclose the planned scope") + } +} + +func mustRead(t *testing.T, path string) string { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(raw) +} + +func TestDeployerRefusesIncoherentTopologies(t *testing.T) { + cases := []struct { + name string + flags runFlags + wantErr string + }{ + {"mixed attached and spawned", runFlags{scope: benchmark.RunScopeBothArms, + postgresAttachURL: "http://127.0.0.1:1", postgresDSN: dsnForTest, + arangoDSN: "http://root@127.0.0.1:8529/loom_bench_a"}, "mixing attached and spawned"}, + {"attached without tokens", runFlags{scope: benchmark.RunScopePostgresOnly, + postgresAttachURL: "http://127.0.0.1:1", postgresDSN: dsnForTest}, "token-unrestricted"}, + {"attached without dsn", runFlags{scope: benchmark.RunScopePostgresOnly, + postgresAttachURL: "http://127.0.0.1:1", + tokenUnrestricted: "Bearer x", tokenScoped: "Bearer y"}, "live version probe"}, + {"spawned without binary", runFlags{scope: benchmark.RunScopePostgresOnly, + postgresDSN: dsnForTest}, "--binary"}, + {"spawned with caller tokens", runFlags{scope: benchmark.RunScopePostgresOnly, + binary: "/nonexistent/loom-server", postgresDSN: dsnForTest, + tokenUnrestricted: "Bearer x"}, "benchmark fence"}, + {"spawned without dsn", runFlags{scope: benchmark.RunScopePostgresOnly, + binary: "/nonexistent/loom-server"}, "requires --postgres-dsn"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + d := &deployer{flags: &tc.flags} + err := d.prepare(context.Background()) + d.close() + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("want error containing %q, got %v", tc.wantErr, err) + } + }) + } +} + +func TestDeployerSpawnedPostgresOnlyWiresFenceAndScrubbers(t *testing.T) { + const datasetPath = "/programs/loom/projects/loom-bench/benchmark" + flags := runFlags{scope: benchmark.RunScopePostgresOnly, + binary: "/nonexistent/loom-server", postgresDSN: dsnForTest, authResourcePath: datasetPath} + d := &deployer{flags: &flags} + if err := d.prepare(context.Background()); err != nil { + t.Fatalf("valid spawned topology refused: %v", err) + } + defer d.close() + cfg := d.postgres.ConfigYAML + for _, want := range []string{"trusted_issuer", "mode: calypr", "loom_bench_source", benchmark.ListenPlaceholder} { + if !strings.Contains(cfg, want) { + t.Fatalf("arm config missing %q:\n%s", want, cfg) + } + } + if !strings.HasPrefix(d.unrestrictedToken, "Bearer ") || !strings.HasPrefix(d.scopedToken, "Bearer ") { + t.Fatal("fence tokens not minted") + } + if d.unrestrictedToken == d.scopedToken { + t.Fatal("admin and scoped principals share a token") + } + if len(d.postgres.ScrubSecrets) < 2 || d.postgres.ScrubSecrets[0] != dsnForTest { + t.Fatalf("arm logs would not scrub the DSN: %v", d.postgres.ScrubSecrets) + } + // The grant shape is the negative-auth control's basis, checked + // through the fence's real /user surface: the admin holds the dataset + // path; the scoped principal must NOT (production denial shape). + admin := fenceGrants(t, d, d.unrestrictedToken) + if _, ok := admin[datasetPath]; !ok { + t.Fatalf("admin token lacks the dataset path: %v", admin) + } + scoped := fenceGrants(t, d, d.scopedToken) + if _, leaked := scoped[datasetPath]; leaked { + t.Fatalf("scoped token leaks the dataset path: %v", scoped) + } + if _, ok := scoped[benchmarkDecoyPath]; !ok { + t.Fatalf("scoped token lacks the decoy grant the control needs: %v", scoped) + } +} + +func fenceGrants(t *testing.T, d *deployer, token string) map[string]any { + t.Helper() + req, err := http.NewRequest(http.MethodGet, d.fence.URL()+"/user", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("fence /user returned %d", resp.StatusCode) + } + var body struct { + Authz map[string]any `json:"authz"` + } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body.Authz == nil { + t.Fatal("fence /user omitted authz") + } + return body.Authz +} + +const dsnForTest = "postgresql://u:p@127.0.0.1:55432/loom_bench_clitest?sslmode=disable" + +// --- live exit-code contract (review r4559430568, finding 4) ------------------- + +func liveSample(warmup bool) benchmark.Sample { + return benchmark.Sample{Backend: benchmark.BackendPostgres, WorkloadID: "select-filter", Warmup: warmup, Outcome: benchmark.OutcomePassed, Cache: benchmark.CacheWarm, Millis: 1} +} + +func TestClassifyLiveExit(t *testing.T) { + cases := []struct { + name string + status string + sample []benchmark.Sample + code int + }{ + {"passed exits 0", "passed", []benchmark.Sample{liveSample(false)}, 0}, + {"passed with no samples still 0", "passed", nil, 0}, + {"failed with measured samples exits 1 (ran and failed)", "failed", []benchmark.Sample{liveSample(false)}, 1}, + {"blocked with measured samples exits 1", "blocked", []benchmark.Sample{liveSample(false)}, 1}, + {"failed with zero samples exits 3 (disclosed not_run)", "failed", nil, 3}, + {"default publication=excluded shape: failed, zero samples", "failed", []benchmark.Sample{}, 3}, + {"warmup-only samples are not measured evidence", "failed", []benchmark.Sample{liveSample(true), liveSample(true)}, 3}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + code, verdict := classifyLiveExit(c.status, c.sample) + if code != c.code { + t.Fatalf("classifyLiveExit(%q, %d samples) = %d (%s), want %d", c.status, len(c.sample), code, verdict, c.code) + } + if c.code == 0 && verdict != "OK" { + t.Fatalf("exit 0 must print OK, got %q", verdict) + } + if c.code == 3 && verdict == "OK" { + t.Fatal("a disclosed not_run must never carry the success verdict") + } + }) + } +} + +// grantResourceAccess is a fence stub for the project-read contract. +type grantResourceAccess struct{ resources []string } + +func (g grantResourceAccess) GetAllowedResources(context.Context, string, string, string) ([]string, error) { + return append([]string(nil), g.resources...), nil +} + +// The paired CI lane faulted 403 FORBIDDEN (stage authorization) on the +// first-ever scoped GET .../authoring/v2/builder: the harness project was +// "loom-bench" while the fence grants lived under +// /programs/loom/projects/loom-bench/..., whose calypr key is +// "loom-loom-bench". AuthorizeReadProject (compose.go -> +// getAuthoringBuilderDirect) admits a caller only when a grant normalizes +// to projectid.Legacy(project) — the write gate hides this because it +// compares grants against the dataset path itself. This pins the whole +// harness default set against that gate so the shapes can never drift +// apart again. +func TestDefaultFenceGrantsSatisfyTheProjectReadGate(t *testing.T) { + key := projectid.Legacy(benchmarkDefaultProject) + for name, path := range map[string]string{ + "dataset": benchmarkDefaultAuthResourcePath, + "decoy": benchmarkDecoyPath, + } { + if got := authscope.NormalizeAuthResourcePath(path); got != key { + t.Fatalf("%s path %q normalizes to %q, want the legacy project key %q", name, path, got, key) + } + } + resolver := authscope.NewScopeResolver(authscope.ScopeResolverConfig{ + ResourceAccess: grantResourceAccess{resources: []string{benchmarkDefaultAuthResourcePath, benchmarkDecoyPath}}, + }) + principal := &authscope.Principal{AuthorizationHeader: "Bearer bench-admin"} + if err := resolver.AuthorizeReadProject(context.Background(), principal, benchmarkDefaultProject); err != nil { + t.Fatalf("the default admin grants must satisfy the project read gate: %v", err) + } + foreign := authscope.NewScopeResolver(authscope.ScopeResolverConfig{ + ResourceAccess: grantResourceAccess{resources: []string{"/programs/other/projects/unrelated/benchmark"}}, + }) + if err := foreign.AuthorizeReadProject(context.Background(), principal, benchmarkDefaultProject); !errors.Is(err, authscope.ErrForbidden) { + t.Fatalf("a subject without the project grant must stay forbidden, got %v", err) + } +} diff --git a/internal/dataframe/benchmark/cmd/benchcli/main.go b/internal/dataframe/benchmark/cmd/benchcli/main.go new file mode 100644 index 000000000..2f69d06dc --- /dev/null +++ b/internal/dataframe/benchmark/cmd/benchcli/main.go @@ -0,0 +1,489 @@ +// Command benchcli is the benchmark driver/verifier entry point the node +// orchestrator invokes. It has exactly two jobs: run a live matched +// benchmark, and verify a report.json artifact. Verification authority lives +// here in Go; node unit tests can never satisfy the live checks (ruling ii). +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/calypr/loom/internal/dataframe/benchmark" +) + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + switch os.Args[1] { + case "verify": + runVerify() + case "run": + runLive() + case "setup-pg": + runSetupPG() + case "drop-pg": + runDropPG() + default: + usage() + os.Exit(2) + } +} + +func runVerify() { + fs := flag.NewFlagSet("verify", flag.ExitOnError) + reportPath := fs.String("report", "", "path to report.json") + _ = fs.Parse(os.Args[2:]) + if *reportPath == "" { + fmt.Fprintln(os.Stderr, "benchcli verify: --report is required") + os.Exit(2) + } + data, err := os.ReadFile(*reportPath) + if err != nil { + fmt.Fprintf(os.Stderr, "benchcli verify: %v\n", err) + os.Exit(1) + } + report, err := benchmark.LoadReport(data) + if err != nil { + fmt.Fprintf(os.Stderr, "benchcli verify: REJECTED: %v\n", err) + os.Exit(1) + } + fmt.Printf("benchcli verify: OK run=%s scope=%s status=%s claims=%d unsupported=%d\n", + report.Identity.RunID, report.Plan.Scope, report.Status, len(report.Claims), len(report.Unsupported)) +} + +type runFlags struct { + scope string + preset string + seed uint64 + project string + generation string + authResourcePath string + warmup int + iterations int + rounds int + concurrency int + previewLimit int + binary string + stderrDir string + postgresAttachURL string + postgresDSN string + arangoAttachURL string + arangoDSN string + tokenUnrestricted string + tokenScoped string + out string + runID string + sourceCommit string + windowNote string + emptyFloorIters int + arangoCapability string + postgresCapability string + publication string + clickHouseEnabled bool + clickHouseURL string + clickHouseUser string + clickHousePassword string + clickHouseRecipe string +} + +// Default capabilities cited by planned not_run legs. The postgres one +// names the blocking finding so the artifact itself carries the reason a +// live leg was impossible. +const ( + defaultArangoCapability = "no ArangoDB service in this execution environment; go-arango CI lane targets this leg" + defaultPostgresCapability = "no PostgreSQL service or --postgres-dsn in this execution environment; the backend itself is ingest-ready since PR #156 (benchcli setup-pg provisions the loom_bench_* six-series schema ladder)" +) + +// The benchmark project is addressed by its legacy key: program "loom", +// project "bench" -> "loom-bench". This is not cosmetic — the scoped +// authoring read gate (authscope.AuthorizeReadProject) admits a caller only +// when a fence grant normalizes to projectid.Legacy(project), so a project +// id that does not decompose to the fence's program/project yields a hard +// 403 on GET .../authoring/v2/builder while writes still pass (the write +// gate compares caller paths against the dataset path itself). +const ( + benchmarkDefaultProject = "loom-bench" + benchmarkDefaultAuthResourcePath = "/programs/loom/projects/bench/benchmark" +) + +func runLive() { + fs := flag.NewFlagSet("run", flag.ExitOnError) + var f runFlags + fs.StringVar(&f.scope, "scope", benchmark.RunScopePostgresOnly, "both-arms | postgres-only | planned") + fs.StringVar(&f.preset, "preset", benchmark.PresetTiny, "dataset preset: tiny | small | representative") + fs.Uint64Var(&f.seed, "seed", 7, "deterministic dataset seed") + fs.StringVar(&f.project, "project", benchmarkDefaultProject, "benchmark project namespace") + fs.StringVar(&f.generation, "generation", "", "generation name (default: run id)") + fs.StringVar(&f.authResourcePath, "auth-resource-path", benchmarkDefaultAuthResourcePath, "dataset auth scope stamped by the loader") + fs.IntVar(&f.warmup, "warmup", 1, "warmup iterations per workload x backend") + fs.IntVar(&f.iterations, "iterations", 8, "measured iterations per workload x backend") + fs.IntVar(&f.rounds, "rounds", 2, "interleaved rounds") + fs.IntVar(&f.concurrency, "concurrency", 1, "concurrency knob (recorded; battery runs serialized)") + fs.IntVar(&f.previewLimit, "preview-limit", 1000, "preview row limit (source-row path is capped at 1000)") + fs.StringVar(&f.binary, "binary", "", "loom server binary for spawned arms") + fs.StringVar(&f.stderrDir, "stderr-dir", "", "directory for arm logs (default: /arms)") + fs.StringVar(&f.postgresAttachURL, "postgres-url", "", "attach to a running PostgreSQL-backed loom server") + fs.StringVar(&f.postgresDSN, "postgres-dsn", "", "PostgreSQL DSN (spawn config + live version probe)") + fs.StringVar(&f.arangoAttachURL, "arango-url", "", "attach to a running Arango-backed loom server") + fs.StringVar(&f.arangoDSN, "arango-dsn", "", "ArangoDB DSN (spawn config + live version probe)") + fs.StringVar(&f.tokenUnrestricted, "token-unrestricted", "", "bearer for the unrestricted principal (attached arms)") + fs.StringVar(&f.tokenScoped, "token-scoped", "", "bearer whose fence grants EXCLUDE the dataset path") + fs.StringVar(&f.out, "out", "bench-out", "output directory; must not contain report.json") + fs.StringVar(&f.runID, "run-id", "", "run id (default: bench--)") + fs.StringVar(&f.sourceCommit, "source-commit", "", "full 40-hex SHA the arms were built from") + fs.StringVar(&f.windowNote, "window-note", "", "who granted the exclusive window and when (recorded in arms.json)") + fs.IntVar(&f.emptyFloorIters, "empty-floor-iterations", 5, "iterations for the empty-result floor control") + fs.StringVar(&f.arangoCapability, "arango-capability", defaultArangoCapability, "capability cited by the arango not_run legs (planned scope)") + fs.StringVar(&f.postgresCapability, "postgres-capability", defaultPostgresCapability, "capability cited by the postgres not_run legs (planned scope)") + fs.StringVar(&f.publication, "publication", "excluded", "publication chain: excluded | run. excluded runs nothing downstream of upload (never satisfies a benchmark check); run executes the full chain and battery on both drivers and requires --clickhouse-enabled, --clickhouse-url, and a CI lane with "+benchmark.ClickHouseCIImage) + fs.BoolVar(&f.clickHouseEnabled, "clickhouse-enabled", false, "assert the arms were deployed with ClickHouse ("+benchmark.ClickHouseCIImage+"); required for --publication=run") + fs.StringVar(&f.clickHouseURL, "clickhouse-url", "", "ClickHouse native URL for both arms (required for --publication=run); each arm materializes into its own loom_bench_* database") + fs.StringVar(&f.clickHouseUser, "clickhouse-user", "default", "ClickHouse user for both arms") + fs.StringVar(&f.clickHousePassword, "clickhouse-password", "", "ClickHouse password for both arms") + fs.StringVar(&f.clickHouseRecipe, "clickhouse-recipe", "", "dataframer recipe JSON the arms start with (required for --publication=run; the server refuses to enable ClickHouse without one)") + _ = fs.Parse(os.Args[2:]) + + // Fail-closed preflight (exit 3: refused to run; exit 1: ran and failed). + preflight := func(format string, args ...any) { + fmt.Fprintf(os.Stderr, "benchcli run: BLOCKED — "+format+"\n", args...) + os.Exit(3) + } + if !benchmark.ValidScope(f.scope) { + preflight("unknown scope %q", f.scope) + } + if f.scope == benchmark.RunScopePlanned { + runPlanned(f, preflight) + return + } + if os.Getenv("LOOM_BENCH_LIVE") != "1" { + preflight("live runs require the exclusive window granted by the coordinator (LOOM_BENCH_LIVE=1)") + } + if strings.TrimSpace(f.windowNote) == "" { + preflight("--window-note must record the granted window (granter + time) before any live run") + } + if !regexp.MustCompile(`^[0-9a-f]{40}$`).MatchString(f.sourceCommit) { + preflight("--source-commit must be the full 40-hex SHA of the built source") + } + if _, err := os.Stat(filepath.Join(f.out, "report.json")); err == nil { + preflight("%s/report.json already exists; use a fresh --out directory", f.out) + } + postgresLeg := f.scope == benchmark.RunScopeBothArms || f.scope == benchmark.RunScopePostgresOnly + if postgresLeg && strings.TrimSpace(f.postgresDSN) == "" && strings.TrimSpace(f.postgresAttachURL) == "" { + preflight("the postgres leg runs in scope %s: provide --postgres-dsn and/or --postgres-url", f.scope) + } + if strings.TrimSpace(f.postgresDSN) != "" { + if err := guardPostgresNamespace(f.postgresDSN); err != nil { + preflight("%v", err) + } + } + if f.scope == benchmark.RunScopePostgresOnly { + if strings.TrimSpace(f.arangoAttachURL) != "" || strings.TrimSpace(f.arangoDSN) != "" { + preflight("postgres-only scope must not receive Arango flags; run both-arms scope instead") + } + } else { + if strings.TrimSpace(f.arangoDSN) == "" && strings.TrimSpace(f.arangoAttachURL) == "" { + preflight("both-arms scope needs --arango-dsn and/or --arango-url") + } + if strings.TrimSpace(f.arangoAttachURL) != "" && (strings.TrimSpace(f.tokenUnrestricted) == "" || strings.TrimSpace(f.tokenScoped) == "") { + preflight("attached arms must be given both --token-unrestricted and --token-scoped") + } + } + spawning := strings.TrimSpace(f.binary) != "" && (strings.TrimSpace(f.postgresAttachURL) == "" || + (f.scope == benchmark.RunScopeBothArms && strings.TrimSpace(f.arangoAttachURL) == "")) + if spawning && strings.TrimSpace(f.tokenUnrestricted) != "" { + preflight("spawned arms get tokens from the benchmark fence; do not pass --token-unrestricted with --binary") + } + if strings.TrimSpace(f.postgresAttachURL) != "" && (strings.TrimSpace(f.tokenUnrestricted) == "" || strings.TrimSpace(f.tokenScoped) == "") { + preflight("attached postgres arm requires both tokens") + } + if f.runID == "" { + f.runID = fmt.Sprintf("bench-%s-%s", time.Now().UTC().Format("20060102T150405Z"), randomRunIDSuffix()) + } + if f.generation == "" { + f.generation = f.runID + } + if err := os.MkdirAll(f.out, 0o755); err != nil { + preflight("cannot create --out: %v", err) + } + + // Build the driver options. + plan := benchmark.BothArmsPlan() + if f.scope == benchmark.RunScopePostgresOnly { + plan = benchmark.PostgresOnlyPlan("no ArangoDB service in this execution environment; go-arango CI lane targets this leg") + } + plan.PlannedNotRun = benchmark.PlannedNotRunForBackends(plan.NotRunBackends()) + for i := range plan.PlannedNotRun { + plan.PlannedNotRun[i].Reason = "leg not run: " + plan.Legs[0].Capability + } + switch f.publication { + case "excluded": + // Excluded mode: no ClickHouse and no timing either; the whole + // downstream battery ships disclosed planned/not_run. There is no + // chain-less battery — the first explorer-config publish + // materializes through the chain — so a run either deploys + // ClickHouse (run mode) or excludes everything past upload. A run + // like this can never satisfy a benchmark check (exit 3). + plan.Publication = &benchmark.PublicationPlan{Run: false, Capability: benchmark.PublicationCapabilityCH} + plan.PlannedNotRun = append(plan.PlannedNotRun, benchmark.PublicationDisclosures(plan.Legs, benchmark.PublicationCapabilityCH)...) + case "run": + if !f.clickHouseEnabled { + preflight("--publication=run requires --clickhouse-enabled: the chain is ClickHouse-backed end to end (%s)", benchmark.ClickHouseCIImage) + } + if strings.TrimSpace(f.clickHouseURL) == "" { + preflight("--publication=run requires --clickhouse-url: both arms materialize their publications through ClickHouse") + } + if strings.TrimSpace(f.clickHouseRecipe) == "" { + preflight("--publication=run requires --clickhouse-recipe: the server refuses to start with ClickHouse enabled but no dataframer recipe") + } + plan.Publication = &benchmark.PublicationPlan{Run: true} + default: + preflight("unknown --publication %q (want excluded | run)", f.publication) + } + // Live battery law: registry-excluded workloads ride along as + // disclosed planned/not_run rows on every ran leg. + benchmark.AppendWorkloadDisclosures(&plan, benchmark.LiveBatteryExclusions()) + if !planNotRunMatchesBattery(plan) { + fmt.Fprintln(os.Stderr, "benchcli run: internal plan inconsistency") + os.Exit(1) + } + + d := &deployer{flags: &f} + ctx := context.Background() + if err := d.prepare(ctx); err != nil { + fmt.Fprintf(os.Stderr, "benchcli run: %v\n", err) + os.Exit(1) + } + defer d.close() + + stderrDir := f.stderrDir + if stderrDir == "" { + stderrDir = filepath.Join(f.out, "arms") + } + opts := benchmark.DriverOptions{ + RunID: f.runID, StartedAt: time.Now().UTC(), Plan: plan, Preset: f.preset, + Params: benchmark.DatasetParams{Preset: f.preset, Seed: f.seed, Project: f.project, + Generation: f.generation, AuthResourcePath: f.authResourcePath}, + Config: benchmark.RunConfig{Warmup: f.warmup, Iterations: f.iterations, Concurrency: f.concurrency, Rounds: f.rounds}, + PreviewLimit: f.previewLimit, + SourceCommit: f.sourceCommit, + UnrestrictedToken: d.unrestrictedToken, + ScopedToken: d.scopedToken, + Arango: d.arango, + Postgres: d.postgres, + StderrDir: stderrDir, + ClickHouseEnabled: f.clickHouseEnabled, + Clock: benchmark.NewMonotonicClock(), + VersionProbe: d.probe, + EmptyFloorIters: f.emptyFloorIters, + } + report, evidence, err := benchmark.RunBenchmark(ctx, opts) + if err != nil { + fmt.Fprintf(os.Stderr, "benchcli run: FAILED — %v\n", err) + os.Exit(1) + } + + // Artifacts: report.json, report.md, arms.json (operational evidence). + manifest := map[string]any{"runId": f.runID, "windowNote": f.windowNote, "arms": evidence} + if d.fence != nil { + manifest["fenceRequests"] = d.fence.Requests() + } + writeRunArtifacts(f.out, report, manifest) + code, verdict := classifyLiveExit(report.Status, report.Samples) + fmt.Printf("benchcli run: %s run=%s scope=%s status=%s claims=%d out=%s\n", + verdict, report.Identity.RunID, report.Plan.Scope, report.Status, len(report.Claims), f.out) + if code == 0 { + return + } + for _, reason := range report.StatusReasons { + fmt.Fprintf(os.Stderr, "benchcli run: status reason: %s\n", reason) + } + if code == 3 { + fmt.Fprintln(os.Stderr, "benchcli run: exit 3 = disclosed not_run: the run captured zero measured timings; it must never be read as a benchmark pass") + } + os.Exit(code) +} + +// classifyLiveExit implements the documented CLI exit contract for a live +// run that produced its artifacts (README): exit 0 only when the live +// legs ran and the report status is passed; exit 1 when the run measured +// something and failed; exit 3 when nothing was measured at all, so a +// non-passed zero-timing report (for example the default +// --publication=excluded path, where every gate arm is blocked) discloses +// not_run exactly like planned scope instead of wearing the success shape +// of exit 0 (review r4559430568, finding 4). Warmup samples never count +// as measured: a run whose only evidence is warmup measured nothing. +func classifyLiveExit(status string, samples []benchmark.Sample) (int, string) { + if status == "passed" { + return 0, "OK" + } + for _, s := range samples { + if !s.Warmup { + return 1, "FAILED" + } + } + return 3, "DISCLOSED NOT_RUN" +} + +// writeRunArtifacts emits the canonical three-file artifact set. Every +// report the CLI produces goes through here so verify sees identical bytes +// shape from live and planned runs. +func writeRunArtifacts(out string, report *benchmark.Report, manifest map[string]any) { + reportJSON, err := benchmark.MarshalReport(report) + if err != nil { + fmt.Fprintf(os.Stderr, "benchcli run: marshal: %v\n", err) + os.Exit(1) + } + if err := os.WriteFile(filepath.Join(out, "report.json"), reportJSON, 0o644); err != nil { + fmt.Fprintf(os.Stderr, "benchcli run: write report: %v\n", err) + os.Exit(1) + } + if err := os.WriteFile(filepath.Join(out, "report.md"), []byte(benchmark.RenderMarkdown(report)), 0o644); err != nil { + fmt.Fprintf(os.Stderr, "benchcli run: write markdown: %v\n", err) + os.Exit(1) + } + manifestJSON, _ := json.MarshalIndent(manifest, "", " ") + if err := os.WriteFile(filepath.Join(out, "arms.json"), manifestJSON, 0o644); err != nil { + fmt.Fprintf(os.Stderr, "benchcli run: write arms manifest: %v\n", err) + os.Exit(1) + } +} + +// runPlanned emits the fully-disclosed pre-window artifact: both legs +// not_run with named capabilities, every workload blocked in the gates, +// zero controls, zero samples, zero claims. It touches no service, so no +// live window is needed; it refuses every live flag so it can never +// masquerade as a live run. Exit 3 = disclosed not_run, never a pass. +func runPlanned(f runFlags, preflight func(string, ...any)) { + live := []struct { + name string + value string + }{ + {"binary", f.binary}, {"stderr-dir", f.stderrDir}, + {"postgres-url", f.postgresAttachURL}, {"postgres-dsn", f.postgresDSN}, + {"arango-url", f.arangoAttachURL}, {"arango-dsn", f.arangoDSN}, + {"token-unrestricted", f.tokenUnrestricted}, {"token-scoped", f.tokenScoped}, + {"window-note", f.windowNote}, + } + for _, l := range live { + if strings.TrimSpace(l.value) != "" { + preflight("planned scope refuses --%s: nothing may run in a planned run", l.name) + } + } + if strings.TrimSpace(f.arangoCapability) == "" || strings.TrimSpace(f.postgresCapability) == "" { + preflight("planned scope must name a capability for each not_run leg") + } + if !regexp.MustCompile(`^[0-9a-f]{40}$`).MatchString(f.sourceCommit) { + preflight("--source-commit must be the full 40-hex SHA of the built source") + } + if _, err := os.Stat(filepath.Join(f.out, "report.json")); err == nil { + preflight("%s/report.json already exists; use a fresh --out directory", f.out) + } + if f.runID == "" { + f.runID = fmt.Sprintf("bench-%s-%s", time.Now().UTC().Format("20060102T150405Z"), randomRunIDSuffix()) + } + if f.generation == "" { + f.generation = f.runID + } + if err := os.MkdirAll(f.out, 0o755); err != nil { + preflight("cannot create --out: %v", err) + } + params := benchmark.DatasetParams{Preset: f.preset, Seed: f.seed, Project: f.project, + Generation: f.generation, AuthResourcePath: f.authResourcePath} + files, err := benchmark.GenerateFiles(params) + if err != nil { + preflight("planned report needs the deterministic fixture digest: %v", err) + } + plan := benchmark.PlannedPlan(f.arangoCapability, f.postgresCapability) + plan.PlannedNotRun = benchmark.PlannedNotRunForBackends(plan.NotRunBackends()) + capability := map[string]string{} + for _, leg := range plan.Legs { + capability[leg.Backend] = leg.Capability + } + for i := range plan.PlannedNotRun { + plan.PlannedNotRun[i].Reason = "leg not run: " + capability[plan.PlannedNotRun[i].Backend] + } + if !planNotRunMatchesBattery(plan) { + fmt.Fprintln(os.Stderr, "benchcli run: internal plan inconsistency") + os.Exit(1) + } + identity := benchmark.RunIdentity{ + RunID: f.runID, StartedAt: time.Now().UTC(), Preset: f.preset, + SourceCommit: f.sourceCommit, DatasetDigest: files.Digest, + ClickHouseDisabled: true, + Config: benchmark.RunConfig{Warmup: f.warmup, Iterations: f.iterations, Concurrency: f.concurrency, Rounds: f.rounds}, + } + report, err := benchmark.BuildReport(identity, plan, benchmark.ControlRuns{}, benchmark.PlannedGateReports(plan.Legs), nil) + if err != nil { + fmt.Fprintf(os.Stderr, "benchcli run: planned report: %v\n", err) + os.Exit(1) + } + manifest := map[string]any{ + "runId": f.runID, "planned": true, "arms": []any{}, + "capabilities": map[string]string{benchmark.BackendArango: f.arangoCapability, benchmark.BackendPostgres: f.postgresCapability}, + } + writeRunArtifacts(f.out, report, manifest) + fmt.Printf("benchcli run: PLANNED run=%s scope=planned status=%s claims=0 out=%s (nothing measured; exit 3 = disclosed not_run)\n", + report.Identity.RunID, report.Status, f.out) + os.Exit(3) +} + +func planNotRunMatchesBattery(plan benchmark.RunPlan) bool { + mode := map[string]string{} + for _, leg := range plan.Legs { + mode[leg.Backend] = leg.Mode + } + liveExcluded := benchmark.LiveBatteryExclusions() + counts := map[string]int{} + for _, row := range plan.PlannedNotRun { + switch { + case mode[row.Backend] != "run": + // Not-run legs disclose exactly their leg capability. + if !strings.HasPrefix(row.Reason, "leg not run") { + return false + } + case strings.HasPrefix(row.Reason, benchmark.PublicationChainReason): + if !plan.PublicationExcluded() { + return false + } + case strings.HasPrefix(row.Reason, benchmark.WorkloadNotRunReason): + // Only registry-excluded workloads may sit on a ran leg, + // under their exact named reason. + reason, ok := liveExcluded[row.WorkloadID] + if !ok || row.Reason != benchmark.WorkloadNotRunReason+reason { + return false + } + default: + return false // only the three honest classes above + } + counts[row.Backend]++ + } + for _, leg := range plan.Legs { + want := 0 + switch leg.Mode { + case "not_run": + want = len(benchmark.Battery) + default: + if plan.PublicationExcluded() { + want += len(benchmark.Battery) + } + want += len(liveExcluded) + } + if counts[leg.Backend] != want { + return false + } + } + return true +} + +func usage() { + fmt.Fprintln(os.Stderr, "usage: benchcli verify --report | benchcli run [flags] | benchcli setup-pg --database | benchcli drop-pg --database ") +} diff --git a/internal/dataframe/benchmark/cmd/benchcli/probes.go b/internal/dataframe/benchmark/cmd/benchcli/probes.go new file mode 100644 index 000000000..c65d4af28 --- /dev/null +++ b/internal/dataframe/benchmark/cmd/benchcli/probes.go @@ -0,0 +1,210 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + backendspostgres "github.com/calypr/loom/internal/backends/postgres/control" +) + +// Live probes: the report's backend versions must be observed from the +// services themselves, never written by hand. The PostgreSQL driver stays +// confined to internal/backends (ownership contract): every probe here +// reaches the database through the backend's own public factory surface, +// never through a driver import. + +// openProbeRuntime opens the backend factory's single readiness-gated pool +// with no schema assignment (the empty placement is the valid probe shape) +// and SetupNone, then hands back the runtime whose ConnSource is the +// factory's public single-connection port. +func openProbeRuntime(ctx context.Context, dsn string) (*backendspostgres.Runtime, error) { + return backendspostgres.Open(ctx, backendspostgres.Config{ + DSN: dsn, + MaxConns: 2, + MinConns: 1, + AcquireTimeout: 15 * time.Second, + Setup: backendspostgres.SetupNone, + }) +} + +func probePostgresVersion(ctx context.Context, dsn string) (string, error) { + connCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + runtime, err := openProbeRuntime(connCtx, dsn) + if err != nil { + return "", fmt.Errorf("postgres probe open: %w", scrub(err, dsn)) + } + defer func() { _ = runtime.Close() }() + conn, err := runtime.ConnSource().Acquire(connCtx) + if err != nil { + return "", fmt.Errorf("postgres probe acquire: %w", scrub(err, dsn)) + } + defer conn.Release() + var version string + if err := conn.QueryRow(connCtx, "SELECT version()").Scan(&version); err != nil { + return "", fmt.Errorf("postgres probe query: %w", scrub(err, dsn)) + } + return strings.TrimSpace(version), nil +} + +func probeArangoVersion(ctx context.Context, rawDSN string) (string, error) { + parsed, err := url.Parse(rawDSN) + if err != nil { + return "", fmt.Errorf("arango dsn: %w", err) + } + endpoint := parsed.Scheme + "://" + parsed.Host + "/_api/version" + reqCtx, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, endpoint, nil) + if err != nil { + return "", err + } + if parsed.User != nil { + password, _ := parsed.User.Password() + req.SetBasicAuth(parsed.User.Username(), password) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("arango probe: %w", scrub(err, rawDSN)) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("arango probe: /_api/version returned HTTP %d", resp.StatusCode) + } + var body struct { + Server string `json:"server"` + Version string `json:"version"` + License string `json:"license"` + } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return "", fmt.Errorf("arango probe decode: %w", err) + } + if body.Server != "arango" || strings.TrimSpace(body.Version) == "" { + return "", fmt.Errorf("arango probe: unexpected /_api/version body %+v", body) + } + return fmt.Sprintf("ArangoDB %s (%s)", body.Version, body.License), nil +} + +func scrub(err error, secret string) error { + if secret == "" { + return err + } + text := err.Error() + if redacted, changed := redact(text, secret); changed { + return fmt.Errorf("%s", redacted) + } + return err +} + +func redact(text, secret string) (string, bool) { + if !strings.Contains(text, secret) { + return text, false + } + return strings.ReplaceAll(text, secret, ""), true +} + +// benchmarkNamespacePrefix: benchmark arms may only touch databases named +// under this prefix. A shared PostgreSQL instance must never be writable by +// a benchmark run outside its namespace. +const benchmarkNamespacePrefix = "loom_bench_" + +// parsePostgresURL enforces the URL-form law this lane accepts everywhere +// (the same shape provision, arms.json evidence, and the admin DSN use) and +// returns the parsed URL with a non-empty database named. +func parsePostgresURL(dsn string) (*url.URL, string, error) { + parsed, err := url.Parse(strings.TrimSpace(dsn)) + if err != nil || (parsed.Scheme != "postgres" && parsed.Scheme != "postgresql") { + return nil, "", fmt.Errorf("postgres dsn unparsable: URL form postgres://… is required (credentials redacted)") + } + database := strings.TrimPrefix(parsed.Path, "/") + if database == "" { + return nil, "", fmt.Errorf("postgres dsn unparsable: no database named (credentials redacted)") + } + return parsed, database, nil +} + +func guardPostgresNamespace(dsn string) error { + _, database, err := parsePostgresURL(dsn) + if err != nil { + return err + } + if !strings.HasPrefix(database, benchmarkNamespacePrefix) { + return fmt.Errorf("postgres database %q is outside the benchmark namespace %s*; refusing to run against it", + redactSecretText(database, dsn), benchmarkNamespacePrefix) + } + return nil +} + +func redactSecretText(text, secret string) string { + if secret == "" { + return text + } + return strings.ReplaceAll(text, secret, "") +} + +func randomRunIDSuffix() string { + buf := make([]byte, 4) + _, _ = rand.Read(buf) + return hex.EncodeToString(buf) +} + +func parsePostgresDSN(dsn string) (host string, port int, database, user, password, sslmode string, err error) { + parsed, database, err := parsePostgresURL(dsn) + if err != nil { + return "", 0, "", "", "", "", fmt.Errorf("unparsable postgres dsn") + } + host = parsed.Hostname() + if host == "" { + return "", 0, "", "", "", "", fmt.Errorf("unparsable postgres dsn") + } + port = 5432 + if rawPort := parsed.Port(); rawPort != "" { + p, perr := strconv.Atoi(rawPort) + if perr != nil || p <= 0 || p > 65535 { + return "", 0, "", "", "", "", fmt.Errorf("unparsable postgres dsn") + } + port = p + } + if parsed.User != nil { + user = parsed.User.Username() + password, _ = parsed.User.Password() + } + if user == "" { + return "", 0, "", "", "", "", fmt.Errorf("unparsable postgres dsn") + } + // sslmode rides the query parameter; the lane's DSNs state it + // explicitly, and disable is the honest local default. + sslmode = parsed.Query().Get("sslmode") + if sslmode == "" { + sslmode = "disable" + } + return host, port, database, user, password, sslmode, nil +} + +func parseArangoDSN(dsn string) (rawURL, database string, err error) { + parsed, err := url.Parse(dsn) + if err != nil { + return "", "", fmt.Errorf("unparsable arango dsn") + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return "", "", fmt.Errorf("arango dsn scheme must be http or https") + } + database = strings.TrimPrefix(parsed.Path, "/") + if database == "" { + return "", "", fmt.Errorf("arango dsn must name a database") + } + if !strings.HasPrefix(database, benchmarkNamespacePrefix) { + return "", "", fmt.Errorf("arango database %q is outside the benchmark namespace %s*", database, benchmarkNamespacePrefix) + } + parsed.Path = "" + parsed.RawQuery = "" + return parsed.String(), database, nil +} diff --git a/internal/dataframe/benchmark/cmd/benchcli/setup.go b/internal/dataframe/benchmark/cmd/benchcli/setup.go new file mode 100644 index 000000000..0fd255749 --- /dev/null +++ b/internal/dataframe/benchmark/cmd/benchcli/setup.go @@ -0,0 +1,207 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net/url" + "os" + "regexp" + "strings" + "time" + + backendspostgres "github.com/calypr/loom/internal/backends/postgres/control" +) + +// setup-pg / drop-pg: namespace-guarded lifecycle for the benchmark +// database. The admin DSN is read from LOOM_BENCH_ADMIN_DSN (never argv, +// never echoed): passwords must not reach process listings or logs. +// +// setup follows the canonical arrangement the server's full runtime +// requires: the caller CREATES the six assigned schemas, then the public +// factory bootstraps them with SetupMigrate; the server later opens the +// same placement with SetupNone (the factory's arrangement law). +// +// DDL runs on the factory's public ConnSource port: the PostgreSQL driver +// stays confined to internal/backends (ownership contract), and every +// identifier interpolated into DDL first passes a closed regex gate, then +// carries SQL-standard double-quote escaping. + +var benchDatabaseRe = regexp.MustCompile(`^` + benchmarkNamespacePrefix + `[a-z0-9_]{4,48}$`) + +var benchSchemaIdentRe = regexp.MustCompile(`^[a-z_][a-z0-9_]{0,62}$`) + +// quoteBenchIdent renders name as a quoted SQL identifier. It refuses any +// name outside the closed benchmark shapes: no unvalidated text ever +// reaches DDL. +func quoteBenchIdent(name string) (string, error) { + if !benchSchemaIdentRe.MatchString(name) { + return "", fmt.Errorf("identifier %q is outside the benchmark shape", name) + } + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`, nil +} + +func adminDSNFromEnv() (string, error) { + dsn := strings.TrimSpace(os.Getenv("LOOM_BENCH_ADMIN_DSN")) + if dsn == "" { + return "", fmt.Errorf("LOOM_BENCH_ADMIN_DSN is not set") + } + return dsn, nil +} + +func runSetupPG() { + fs := flag.NewFlagSet("setup-pg", flag.ExitOnError) + database := fs.String("database", "", "benchmark database name (must be under "+benchmarkNamespacePrefix+")") + schemaPrefix := fs.String("schema-prefix", "loom_bench", "schema name prefix for the six-series ladder") + _ = fs.Parse(os.Args[2:]) + dsn, err := adminDSNFromEnv() + if err != nil { + fmt.Fprintln(os.Stderr, "benchcli setup-pg: BLOCKED —", err) + os.Exit(3) + } + if !benchDatabaseRe.MatchString(*database) { + fmt.Fprintf(os.Stderr, "benchcli setup-pg: BLOCKED — database %q is outside the benchmark namespace\n", *database) + os.Exit(3) + } + ctx := context.Background() + if err := setupPG(ctx, dsn, *database, *schemaPrefix); err != nil { + fmt.Fprintln(os.Stderr, "benchcli setup-pg: FAILED —", redactSecretText(err.Error(), dsn)) + os.Exit(1) + } + fmt.Printf("benchcli setup-pg: OK database=%s schemas=%d\n", *database, len(backendspostgres.AllSeries())) +} + +func runDropPG() { + fs := flag.NewFlagSet("drop-pg", flag.ExitOnError) + database := fs.String("database", "", "benchmark database name (must be under "+benchmarkNamespacePrefix+")") + _ = fs.Parse(os.Args[2:]) + dsn, err := adminDSNFromEnv() + if err != nil { + fmt.Fprintln(os.Stderr, "benchcli drop-pg: BLOCKED —", err) + os.Exit(3) + } + if !benchDatabaseRe.MatchString(*database) { + fmt.Fprintf(os.Stderr, "benchcli drop-pg: BLOCKED — database %q is outside the benchmark namespace\n", *database) + os.Exit(3) + } + ctx := context.Background() + if err := dropPG(ctx, dsn, *database); err != nil { + fmt.Fprintln(os.Stderr, "benchcli drop-pg: FAILED —", redactSecretText(err.Error(), dsn)) + os.Exit(1) + } + fmt.Printf("benchcli drop-pg: OK database=%s\n", *database) +} + +func setupPG(ctx context.Context, adminDSN, database, schemaPrefix string) error { + // Schema names are _; every full name must carry the + // benchmark namespace prefix. + if schemaPrefix != strings.TrimSuffix(benchmarkNamespacePrefix, "_") && + !strings.HasPrefix(schemaPrefix, benchmarkNamespacePrefix) { + return fmt.Errorf("schema prefix %q produces schemas outside the benchmark namespace", schemaPrefix) + } + quotedDatabase, err := quoteBenchIdent(database) + if err != nil { + return err + } + admin, err := openProbeRuntime(ctx, adminDSN) + if err != nil { + return fmt.Errorf("admin connect (credentials redacted): %w", err) + } + defer func() { _ = admin.Close() }() + adminConn, err := admin.ConnSource().Acquire(ctx) + if err != nil { + return fmt.Errorf("admin acquire (credentials redacted): %w", err) + } + releaseAdmin := true + defer func() { + if releaseAdmin { + adminConn.Release() + } + }() + var exists bool + if err := adminConn.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)", database).Scan(&exists); err != nil { + return err + } + if exists { + // Fresh database per run keeps digests and timings attributable. + return fmt.Errorf("database %s already exists; drop-pg first (fresh per run keeps evidence attributable)", database) + } + if _, err := adminConn.Exec(ctx, "CREATE DATABASE "+quotedDatabase); err != nil { + return fmt.Errorf("create database: %w", err) + } + adminConn.Release() + releaseAdmin = false // explicit release; the guard defers the rest + + targetDSN, err := withDatabase(adminDSN, database) + if err != nil { + return err + } + target, err := openProbeRuntime(ctx, targetDSN) + if err != nil { + return fmt.Errorf("target connect (credentials redacted): %w", err) + } + defer func() { _ = target.Close() }() + targetConn, err := target.ConnSource().Acquire(ctx) + if err != nil { + return fmt.Errorf("target acquire (credentials redacted): %w", err) + } + defer targetConn.Release() + schemas := map[backendspostgres.Series]string{} + for _, series := range backendspostgres.AllSeries() { + schema := schemaPrefix + "_" + string(series) + quotedSchema, err := quoteBenchIdent(schema) + if err != nil { + return err + } + if _, err := targetConn.Exec(ctx, "CREATE SCHEMA "+quotedSchema); err != nil { + return fmt.Errorf("create schema %s: %w", schema, err) + } + schemas[series] = schema + } + + // Canonical bootstrap: the public factory with SetupMigrate, exactly as + // the server's live composition fixtures arrange it. + runtime, err := backendspostgres.Open(ctx, backendspostgres.Config{ + DSN: targetDSN, + MaxConns: 4, + MinConns: 1, + AcquireTimeout: 30 * time.Second, + Schemas: backendspostgres.SeriesSchemas(schemas), + Setup: backendspostgres.SetupMigrate, + }) + if err != nil { + return fmt.Errorf("migrate six-series ladder: %w", err) + } + return runtime.Close() +} + +func dropPG(ctx context.Context, adminDSN, database string) error { + quotedDatabase, err := quoteBenchIdent(database) + if err != nil { + return err + } + admin, err := openProbeRuntime(ctx, adminDSN) + if err != nil { + return fmt.Errorf("admin connect (credentials redacted): %w", err) + } + defer func() { _ = admin.Close() }() + adminConn, err := admin.ConnSource().Acquire(ctx) + if err != nil { + return fmt.Errorf("admin acquire (credentials redacted): %w", err) + } + defer adminConn.Release() + _, err = adminConn.Exec(ctx, "DROP DATABASE IF EXISTS "+quotedDatabase+" WITH (FORCE)") + return err +} + +// withDatabase swaps the DSN path to the target database, keeping the +// admin's server/user/password selection. The admin DSN must be URL form +// (the accepted shape everywhere in this lane). +func withDatabase(adminDSN, database string) (string, error) { + parsed, err := url.Parse(adminDSN) + if err != nil || (parsed.Scheme != "postgres" && parsed.Scheme != "postgresql") { + return "", fmt.Errorf("admin dsn must be URL form (credentials redacted)") + } + parsed.Path = "/" + database + return parsed.String(), nil +} diff --git a/internal/dataframe/benchmark/contributors.go b/internal/dataframe/benchmark/contributors.go new file mode 100644 index 000000000..691833220 --- /dev/null +++ b/internal/dataframe/benchmark/contributors.go @@ -0,0 +1,167 @@ +package benchmark + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/calypr/loom/internal/explorer/authoringv2" +) + +// Aggregate predicates on the writable wire. +// +// The battery authors v3 templates with aggregate.where (the offline +// decoder accepts them), but the writable wire omits AggregateSource.Where +// (json:"-"): the server's strict body bind refuses the field with +// unknown field "where". The writable contract is the catalog-bound +// Column.Contributor form, and the repository's own +// MigrateLegacyContributors turns the v3 intent into it against the live +// builder catalog — the same path the acceptance lane runs +// (internal/acceptance/scenario.go). Live legs therefore publish migrated +// v4 canonical bytes, never the legacy where wire. + +// contributorCatalogWire is the public builder-state catalog projection the +// contributor migration consumes. CatalogSnapshot cannot be unmarshaled +// directly because ConstructionChoice is a closed interface source; this +// reduced view carries exactly the fields MigrateLegacyContributors reads. +type contributorCatalogWire struct { + Generation string `json:"generation"` + SnapshotToken string `json:"snapshotToken"` + AuthorizationScopeDigest string `json:"authorizationScopeDigest"` + Complete bool `json:"complete"` + Truncated bool `json:"truncated"` + Nodes []struct { + ID string `json:"nodeId"` + ResourceType string `json:"resourceType"` + } `json:"nodes"` + Candidates []struct { + ID string `json:"candidateId"` + NodeID string `json:"nodeId"` + FieldPath string `json:"fieldPath"` + LogicalType string `json:"logicalType"` + Repeated bool `json:"repeated"` + RepeatedBoundaries []authoringv2.RepeatedBoundary `json:"repeatedBoundaries"` + } `json:"candidates"` +} + +// FetchContributorCatalog reads the live builder catalog of one explorer. +// The battery targets the repository "default" explorer, which exists only +// after the first explorer-config publish, so the driver bootstraps it with +// the empty-floor workspace before fetching (see driver.go). It fails +// closed on an incomplete, truncated, or scope-less catalog: a partial +// catalog could silently drop a predicate candidate and mis-author a +// workload. +func (a *Arm) FetchContributorCatalog(ctx context.Context, token, project, explorer string) (authoringv2.CatalogSnapshot, error) { + endpoint := a.authoringURL(project, explorer) + "/builder" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return authoringv2.CatalogSnapshot{}, err + } + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return authoringv2.CatalogSnapshot{}, fmt.Errorf("benchmark: contributor catalog on %s: %w", a.Backend, err) + } + catalogValue, ok := value["catalog"] + if !ok { + return authoringv2.CatalogSnapshot{}, fmt.Errorf("benchmark: builder state on %s omitted the catalog", a.Backend) + } + raw, err := json.Marshal(catalogValue) + if err != nil { + return authoringv2.CatalogSnapshot{}, fmt.Errorf("benchmark: builder catalog on %s: %w", a.Backend, err) + } + var wire contributorCatalogWire + if err := json.Unmarshal(raw, &wire); err != nil { + return authoringv2.CatalogSnapshot{}, fmt.Errorf("benchmark: builder catalog shape on %s: %w", a.Backend, err) + } + catalog := authoringv2.CatalogSnapshot{ + APIVersion: authoringv2.APIVersion, Kind: authoringv2.CatalogKind, + Project: project, ExplorerID: explorer, + SourceGeneration: wire.Generation, AuthorizationScopeDigest: wire.AuthorizationScopeDigest, + SnapshotToken: wire.SnapshotToken, + Complete: wire.Complete, Truncated: wire.Truncated, + Nodes: make([]authoringv2.CatalogNode, 0, len(wire.Nodes)), + Candidates: make([]authoringv2.CatalogCandidate, 0, len(wire.Candidates)), + } + for _, node := range wire.Nodes { + catalog.Nodes = append(catalog.Nodes, authoringv2.CatalogNode{ID: node.ID, ResourceType: node.ResourceType}) + } + for _, candidate := range wire.Candidates { + catalog.Candidates = append(catalog.Candidates, authoringv2.CatalogCandidate{ + ID: candidate.ID, NodeID: candidate.NodeID, FieldPath: candidate.FieldPath, + LogicalType: candidate.LogicalType, Repeated: candidate.Repeated, + RepeatedBoundaries: append([]authoringv2.RepeatedBoundary(nil), candidate.RepeatedBoundaries...), + }) + } + if err := validateContributorCatalog(catalog); err != nil { + return authoringv2.CatalogSnapshot{}, fmt.Errorf("benchmark: contributor catalog on %s: %w", a.Backend, err) + } + return catalog, nil +} + +func validateContributorCatalog(catalog authoringv2.CatalogSnapshot) error { + if strings.TrimSpace(catalog.Project) == "" || strings.TrimSpace(catalog.ExplorerID) == "" || strings.TrimSpace(catalog.SourceGeneration) == "" { + return errors.New("catalog project, Explorer, or generation is missing") + } + if strings.TrimSpace(catalog.SnapshotToken) == "" || strings.TrimSpace(catalog.AuthorizationScopeDigest) == "" { + return errors.New("catalog snapshot token or authorization scope digest is missing") + } + if !catalog.Complete || catalog.Truncated { + return errors.New("catalog snapshot is incomplete or truncated") + } + return nil +} + +// PreparePublicationWorkspace turns one authored battery workspace into the +// bytes to publish. Workspaces without aggregate predicates pass through +// byte-identical (the arms already share them); workspaces with legacy +// aggregate.where are migrated against the arm's live catalog and published +// as canonical v4 contributor intent. A predicate path the catalog cannot +// resolve unambiguously fails the run before any timing: guessing a +// candidate id or dropping the filter would silently change the workload. +func PreparePublicationWorkspace(raw []byte, generation string, catalog authoringv2.CatalogSnapshot) ([]byte, error) { + workspace, err := authoringv2.DecodeWorkspace(raw) + if err != nil { + return nil, fmt.Errorf("benchmark: decode authored workspace: %w", err) + } + if !workspaceHasAggregateWhere(workspace) { + return raw, nil + } + if workspace.SemanticsVersion != wsSemanticsVersion { + return nil, fmt.Errorf("benchmark: authored workspace carries aggregate predicates at semanticsVersion %d; only v%d migration is supported", + workspace.SemanticsVersion, wsSemanticsVersion) + } + if err := validateContributorCatalog(catalog); err != nil { + return nil, fmt.Errorf("benchmark: contributor catalog unusable for migration: %w", err) + } + if catalog.SourceGeneration != generation { + return nil, fmt.Errorf("benchmark: contributor catalog generation %q does not match benchmark generation %q", catalog.SourceGeneration, generation) + } + migrated, err := authoringv2.MigrateLegacyContributors(workspace, catalog) + if err != nil { + return nil, fmt.Errorf("benchmark: migrate aggregate predicates: %w", err) + } + migrated = authoringv2.MigrateLosslessDefaults(migrated, catalog).NormalizePresentationOrders() + if err := migrated.ValidateForPublication(); err != nil { + return nil, fmt.Errorf("benchmark: validate migrated workspace: %w", err) + } + canonical, err := migrated.CanonicalJSON() + if err != nil { + return nil, fmt.Errorf("benchmark: canonicalize migrated workspace: %w", err) + } + return canonical, nil +} + +func workspaceHasAggregateWhere(workspace authoringv2.Workspace) bool { + for _, document := range workspace.Documents { + for _, column := range document.Columns { + if column.Source.Kind == authoringv2.SourceAggregate && column.Source.Aggregate != nil && column.Source.Aggregate.Where != nil { + return true + } + } + } + return false +} diff --git a/internal/dataframe/benchmark/contributors_test.go b/internal/dataframe/benchmark/contributors_test.go new file mode 100644 index 000000000..e519fe053 --- /dev/null +++ b/internal/dataframe/benchmark/contributors_test.go @@ -0,0 +1,194 @@ +package benchmark + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/calypr/loom/internal/explorer/authoringv2" +) + +// The writable wire is closed over aggregate predicates: AggregateSource +// strict-decodes a wire without "where", so a published workspace must +// carry catalog-bound Column.Contributor intent. These tests pin both sides: +// the legacy template is refused by the same unmarshal the server's body +// bind performs, and the migrated workspace is accepted. + +func preparedTestCatalog() authoringv2.CatalogSnapshot { + return authoringv2.CatalogSnapshot{ + APIVersion: authoringv2.APIVersion, Kind: authoringv2.CatalogKind, + Project: "proj", ExplorerID: "default", SourceGeneration: "gen1", + AuthorizationScopeDigest: "scope-digest-1", SnapshotToken: "snap-1", + Complete: true, + Nodes: []authoringv2.CatalogNode{ + {ID: "patient", ResourceType: "Patient"}, + {ID: "observation", ResourceType: "Observation"}, + {ID: "specimen", ResourceType: "Specimen"}, + {ID: "condition", ResourceType: "Condition"}, + }, + Candidates: []authoringv2.CatalogCandidate{ + {ID: "cand-obs-age-code", NodeID: "observation", FieldPath: "code.coding[].code", + LogicalType: "code", Repeated: true, + RepeatedBoundaries: []authoringv2.RepeatedBoundary{{Path: "code.coding[]", MaxItems: 20}}}, + {ID: "cand-spec-type-code", NodeID: "specimen", FieldPath: "type.coding[].code", + LogicalType: "code", Repeated: true, + RepeatedBoundaries: []authoringv2.RepeatedBoundary{{Path: "type.coding[]", MaxItems: 20}}}, + }, + } +} + +// strictWorkspaceBind mirrors the server's explorer-config body bind: the +// generated request body is the authoringv2 workspace decoded closed. +func strictWorkspaceBind(t *testing.T, raw []byte) authoringv2.Workspace { + t.Helper() + var workspace authoringv2.Workspace + if err := json.Unmarshal(raw, &workspace); err != nil { + t.Fatalf("server-equivalent bind must accept the published workspace: %v", err) + } + return workspace +} + +func TestAggregateWhereTemplateIsRefusedByTheServerBind(t *testing.T) { + raw, err := BuildWorkspace(WorkloadAggregateSumAgeByPatient) + if err != nil { + t.Fatal(err) + } + var workspace authoringv2.Workspace + err = json.Unmarshal(raw, &workspace) + if err == nil || !strings.Contains(err.Error(), "unknown field") { + t.Fatalf("legacy aggregate.where must fail the closed bind, got %v", err) + } +} + +func TestPreparePublicationWorkspaceMigratesAggregateWhereToContributor(t *testing.T) { + raw, err := BuildWorkspace(WorkloadAggregateSumAgeByPatient) + if err != nil { + t.Fatal(err) + } + prepared, err := PreparePublicationWorkspace(raw, "gen1", preparedTestCatalog()) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(prepared), `"where"`) { + t.Fatalf("migrated workspace still carries the legacy where wire: %s", prepared) + } + workspace := strictWorkspaceBind(t, prepared) + if workspace.SemanticsVersion < contributorSemanticsVersionForTest { + t.Fatalf("migrated semanticsVersion=%d must be at least the contributor contract v%d", workspace.SemanticsVersion, contributorSemanticsVersionForTest) + } + columns := workspace.Documents[0].Columns + if len(columns) != 5 { + t.Fatalf("columns=%d want 5", len(columns)) + } + for _, column := range columns[1:] { + if column.Contributor == nil { + t.Fatalf("column %s lost its predicate: %s", column.Column, prepared) + } + if column.Contributor.CandidateID != "cand-obs-age-code" || column.Contributor.Operator != authoringv2.ContributorEquals { + t.Fatalf("column %s contributor=%#v", column.Column, column.Contributor) + } + if column.Contributor.Quantifier != authoringv2.ContributorAny { + t.Fatalf("repeated candidate requires the ANY quantifier: %#v", column.Contributor) + } + if column.Contributor.Value == nil || column.Contributor.Value.Kind != authoringv2.ContributorValueCode || + column.Contributor.Value.Code == nil || column.Contributor.Value.Code.Code != ObservationCodeAge { + t.Fatalf("column %s contributor value=%#v want CODE %s", column.Column, column.Contributor.Value, ObservationCodeAge) + } + if column.Source.Aggregate == nil || column.Source.Aggregate.Where != nil { + t.Fatalf("column %s aggregate still holds a where: %#v", column.Column, column.Source.Aggregate) + } + } + if columns[0].Contributor != nil { + t.Fatalf("the id column must not gain a contributor: %#v", columns[0].Contributor) + } +} + +func TestPreparePublicationWorkspacePassesThroughWithoutPredicates(t *testing.T) { + raw, err := BuildWorkspace(WorkloadGroupCountByPatient) + if err != nil { + t.Fatal(err) + } + prepared, err := PreparePublicationWorkspace(raw, "gen1", preparedTestCatalog()) + if err != nil { + t.Fatal(err) + } + if string(prepared) != string(raw) { + t.Fatalf("predicate-free workspace must publish byte-identical:\n%s\n%s", raw, prepared) + } +} + +func TestPreparePublicationWorkspaceRefusesUnresolvablePredicate(t *testing.T) { + raw, err := BuildWorkspace(WorkloadAggregateSumAgeByPatient) + if err != nil { + t.Fatal(err) + } + catalog := preparedTestCatalog() + catalog.Candidates = catalog.Candidates[1:] // drop the observation code candidate + if _, err := PreparePublicationWorkspace(raw, "gen1", catalog); err == nil || + !strings.Contains(err.Error(), "not present in the pinned catalog") { + t.Fatalf("an unresolvable predicate path must fail closed, got %v", err) + } +} + +func TestPreparePublicationWorkspaceRefusesWrongGeneration(t *testing.T) { + raw, err := BuildWorkspace(WorkloadAggregateSumAgeByPatient) + if err != nil { + t.Fatal(err) + } + if _, err := PreparePublicationWorkspace(raw, "gen2", preparedTestCatalog()); err == nil || + !strings.Contains(err.Error(), "does not match benchmark generation") { + t.Fatalf("a catalog from another generation must fail closed, got %v", err) + } +} + +func TestFetchContributorCatalogShape(t *testing.T) { + f := newFakeArm(BackendArango) + defer f.close() + arm := f.arm(BackendArango) + catalog, err := arm.FetchContributorCatalog(context.Background(), "Bearer t", "proj", "default") + if err != nil { + t.Fatal(err) + } + if catalog.SourceGeneration != "gen1" || !catalog.Complete || catalog.Truncated { + t.Fatalf("catalog scope: %+v", catalog) + } + if len(catalog.Candidates) != 2 || catalog.Candidates[0].ID != "cand-obs-age-code" { + t.Fatalf("candidates: %+v", catalog.Candidates) + } + if len(catalog.Nodes) != 4 { + t.Fatalf("nodes: %+v", catalog.Nodes) + } +} + +func TestFetchContributorCatalogFailsClosed(t *testing.T) { + cases := map[string]func(state map[string]any){ + "incomplete": func(s map[string]any) { s["catalog"].(map[string]any)["complete"] = false }, + "truncated": func(s map[string]any) { s["catalog"].(map[string]any)["truncated"] = true }, + "no-token": func(s map[string]any) { delete(s["catalog"].(map[string]any), "snapshotToken") }, + "no-digest": func(s map[string]any) { delete(s["catalog"].(map[string]any), "authorizationScopeDigest") }, + "no-generation": func(s map[string]any) { delete(s["catalog"].(map[string]any), "generation") }, + "catalog-omitted": func(s map[string]any) { delete(s, "catalog") }, + } + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + state := map[string]any{"draftVersion": 1, "catalog": contributorCatalogFixture()} + mutate(state) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(state) + })) + defer server.Close() + arm := &Arm{Backend: BackendArango, BaseURL: server.URL, Client: server.Client()} + if _, err := arm.FetchContributorCatalog(context.Background(), "Bearer t", "proj", "default"); err == nil { + t.Fatalf("%s: an unusable catalog must fail closed", name) + } + }) + } +} + +// contributorSemanticsVersionForTest is the v4 contributor contract the +// migration must produce; pinned numerically so a silent constant bump in +// authoringv2 fails here loudly. +const contributorSemanticsVersionForTest = 4 diff --git a/internal/dataframe/benchmark/controls.go b/internal/dataframe/benchmark/controls.go new file mode 100644 index 000000000..dc3653f23 --- /dev/null +++ b/internal/dataframe/benchmark/controls.go @@ -0,0 +1,191 @@ +package benchmark + +import ( + "context" + "fmt" + "sort" +) + +// Control-run computations. These turn raw samples into the ControlRuns +// block; the verifier independently re-checks their bounds, so a run that +// skips a control cannot forge one. + +// passedStable returns samples whose timing carries steady-state meaning: +// a measured (not warmup), successful sample in a non-cold regime — warm +// (harness-owned process at steady state) or service-warm (attached +// external service, no restart ownership). Process-cold samples (review +// r4559430568) are honest measurements but belong to a different regime: +// they legitimately cost more, so they neither seed stability controls nor +// feed speed claims. +func passedStable(samples []Sample) []Sample { + var out []Sample + for _, s := range samples { + if s.Warmup || s.Outcome != OutcomePassed { + continue + } + if s.Cache != CacheWarm && s.Cache != CacheServiceWarm { + continue + } + out = append(out, s) + } + return out +} + +// passedMeasured returns every measured (not warmup), successful sample in +// any regime. The round-robin protocol gives every round an identical cache +// composition (process-cold first iteration after a restart, steady +// afterwards), so comparing round halves across all regimes is a fair +// environment-stability check for spawned and attached arms alike — and a +// spawned run, which produces no warm-only cells, still gets a self-ratio +// (a spawned timing leg must never abort for lack of cells). +func passedMeasured(samples []Sample) []Sample { + var out []Sample + for _, s := range samples { + if s.Warmup || s.Outcome != OutcomePassed { + continue + } + out = append(out, s) + } + return out +} + +func cellSamples(samples []Sample, workload, backend string) []Sample { + var out []Sample + for _, s := range samples { + if s.WorkloadID == workload && s.Backend == backend { + out = append(out, s) + } + } + return out +} + +// ControlUnstableCells returns "workload/backend" cells whose passed +// steady-regime coefficient of variation exceeds the ceiling. The driver excludes these +// cells from claims; with fewer than 3 samples a cell cannot demonstrate +// stability and is reported unstable (never silently trusted). +func ControlUnstableCells(samples []Sample, limit float64) []string { + type cell struct{ workload, backend string } + seen := map[cell]bool{} + var cells []cell + for _, s := range passedStable(samples) { + c := cell{s.WorkloadID, s.Backend} + if !seen[c] { + seen[c] = true + cells = append(cells, c) + } + } + sort.Slice(cells, func(i, j int) bool { + if cells[i].workload != cells[j].workload { + return cells[i].workload < cells[j].workload + } + return cells[i].backend < cells[j].backend + }) + var unstable []string + for _, c := range cells { + // Stability is judged over the claimable (steady) regime only + // (review r4561644054): the mandated spawned protocol puts one + // legitimately-slow process-cold sample per round into every cell, + // and mixing it into the CV would flag stable warm cells unstable + // and suppress every claim for them -- the round-2 no-claims + // failure returning through the control path. This matches the + // docstring and the claimCellPair regime policy. + sum := Summarize(passedStable(cellSamples(samples, c.workload, c.backend))) + if sum.Count < 3 || sum.CoefficientOfVariation > limit { + unstable = append(unstable, c.workload+"/"+c.backend) + } + } + return unstable +} + +// ControlSelfRatio computes the backend-vs-itself sanity ratio across +// halves of the iteration sequence: for every cell, medians of the first +// and second half of measured samples (any cache regime; rounds are regime- +// symmetric under the interleaved protocol) ordered by (round, iteration); +// the reported value is the WORST deviation across cells (always >= 1). +// ok=false when any evaluated cell cannot be split (fewer than 4 samples) +// or no cells exist: a missing self-ratio control cannot pass the verifier. +func ControlSelfRatio(samples []Sample, backend string) (float64, bool) { + type cell struct{ workload, backend string } + seen := map[cell]bool{} + var cells []cell + for _, s := range passedMeasured(samples) { + if s.Backend != backend { + continue + } + c := cell{s.WorkloadID, backend} + if !seen[c] { + seen[c] = true + cells = append(cells, c) + } + } + if len(cells) == 0 { + return 0, false + } + worst := 1.0 + for _, c := range cells { + cellSamplesSorted := cellSamples(samples, c.workload, c.backend) + passed := passedMeasured(cellSamplesSorted) + if len(passed) < 4 { + return 0, false + } + sort.SliceStable(passed, func(i, j int) bool { + if passed[i].Round != passed[j].Round { + return passed[i].Round < passed[j].Round + } + return passed[i].Iteration < passed[j].Iteration + }) + mid := len(passed) / 2 + a := Summarize(passed[:mid]).MedianMillis + b := Summarize(passed[mid:]).MedianMillis + if a <= 0 || b <= 0 { + return 0, false + } + ratio := a / b + if ratio < 1 { + ratio = 1 / ratio + } + if ratio > worst { + worst = ratio + } + } + return worst, true +} + +// ControlEmptyFloorMillis turns empty-result floor samples into the control +// value (median of passed steady-regime samples); ok=false without evidence. +func ControlEmptyFloorMillis(samples []Sample) (float64, bool) { + passed := passedStable(samples) + if len(passed) == 0 { + return 0, false + } + var ms []float64 + for _, s := range passed { + ms = append(ms, s.Millis) + } + sort.Float64s(ms) + return percentile(ms, 0.5), true +} + +// MeasureEmptyFloor times an identical request whose expectation is ZERO +// rows (an impossible fixed filter). Any rows back is a protocol error: the +// floor request stopped being empty, so the control would be meaningless. +func (a *Arm) MeasureEmptyFloor(ctx context.Context, clock Clock, token, project, explorer, receiptID, outputID string, iterations int) (float64, error) { + if iterations < 3 { + return 0, fmt.Errorf("benchmark: empty-floor control needs >= 3 iterations") + } + var ms []float64 + for i := 0; i < iterations; i++ { + started := clock.Now() + result, err := a.ExecuteOutput(ctx, token, project, explorer, receiptID, outputID, 1) + elapsed := ElapsedMillis(clock, started) + if err != nil { + return 0, fmt.Errorf("benchmark: empty-floor request failed: %w", err) + } + if result.RowCount != 0 { + return 0, fmt.Errorf("benchmark: empty-floor request returned %d rows; the control request is not empty", result.RowCount) + } + ms = append(ms, elapsed) + } + sort.Float64s(ms) + return percentile(ms, 0.5), nil +} diff --git a/internal/dataframe/benchmark/controls_test.go b/internal/dataframe/benchmark/controls_test.go new file mode 100644 index 000000000..c492c2f21 --- /dev/null +++ b/internal/dataframe/benchmark/controls_test.go @@ -0,0 +1,116 @@ +package benchmark + +import "testing" + +// Measurement-integrity controls (review r4561031154): claims cite only a +// claimable regime (warm, or service-warm for attached arms) and only from +// cells the stability control accepted. + +func warmRows(arangoMedian, postgresMedian float64) []SummaryRow { + return []SummaryRow{ + {WorkloadID: "w", Backend: BackendArango, Cache: CacheWarm, Summary: Summary{Count: 5, MedianMillis: arangoMedian}}, + {WorkloadID: "w", Backend: BackendPostgres, Cache: CacheWarm, Summary: Summary{Count: 5, MedianMillis: postgresMedian}}, + } +} + +func TestClaimsRequireClaimableRegime(t *testing.T) { + gates := []WorkloadGateReport{{WorkloadID: "w", Claimable: true}} + coldOnly := []SummaryRow{ + {WorkloadID: "w", Backend: BackendArango, Cache: CacheProcessCold, Summary: Summary{Count: 5, MedianMillis: 20}}, + {WorkloadID: "w", Backend: BackendPostgres, Cache: CacheProcessCold, Summary: Summary{Count: 5, MedianMillis: 10}}, + } + controls := ControlRuns{StabilityCVLimit: StabilityCVLimit} + if claims := buildClaims(gates, coldOnly, controls); len(claims) != 0 { + t.Fatalf("process-cold-only cells must never back claims, got %v", claims) + } + // One arm warm, the peer only process-cold: no pair, no claim. + split := []SummaryRow{warmRows(20, 10)[0], coldOnly[1]} + if claims := buildClaims(gates, split, controls); len(claims) != 0 { + t.Fatalf("a claim needs the claimable regime on both arms, got %v", claims) + } + claims := buildClaims(gates, warmRows(20, 10), controls) + if len(claims) != 1 { + t.Fatalf("warm pair with passing gates must claim, got %v", claims) + } + if claims[0].Ratio.ArangoOverPostgres != 2 || claims[0].Arango.MedianMillis != 20 || claims[0].Postgres.MedianMillis != 10 { + t.Fatalf("claim cites the wrong cells: %+v", claims[0]) + } +} + +func TestStabilityControlExcludesUnstableCellClaims(t *testing.T) { + gates := []WorkloadGateReport{{WorkloadID: "w", Claimable: true}} + rows := warmRows(20, 10) + claims := buildClaims(gates, rows, ControlRuns{StabilityCVLimit: StabilityCVLimit, UnstableCells: []string{"w/" + BackendPostgres}}) + if len(claims) != 0 { + t.Fatalf("an unstable cell excludes its workload's claims, got %v", claims) + } + // An unstable cell elsewhere does not touch this workload. + claims = buildClaims(gates, rows, ControlRuns{StabilityCVLimit: StabilityCVLimit, UnstableCells: []string{"other/" + BackendPostgres}}) + if len(claims) != 1 { + t.Fatalf("unrelated unstable cells must not suppress this claim, got %v", claims) + } + // Non-claimable gates never claim even with perfect cells. + if claims := buildClaims([]WorkloadGateReport{{WorkloadID: "w", Claimable: false}}, rows, ControlRuns{StabilityCVLimit: StabilityCVLimit}); len(claims) != 0 { + t.Fatalf("ratio-only-both-passed policy must hold, got %v", claims) + } +} + +// TestControlUnstableCells pins the regime discipline of the stability +// control (review r4561644054): CV is judged over the claimable steady +// regime only. The mandated spawned protocol (iterations=10, rounds=5) +// puts one legitimately-slow process-cold sample per round into every +// cell; mixing it into the CV would flag every spawned cell unstable and +// suppress all claims -- the round-2 no-claims failure re-entering +// through the control path. +func TestControlUnstableCells(t *testing.T) { + // Spawned-protocol cell: five rounds of one process-cold outlier plus + // four tight warm samples. Warm CV is tiny; the cold outliers must not + // inflate it into an unstable verdict. + var spawned []Sample + for round := 1; round <= 5; round++ { + cold := sample(BackendArango, "w", OutcomePassed, 400, false) + cold.Cache = CacheProcessCold + cold.Round, cold.Iteration = round, 1 + spawned = append(spawned, cold) + for it, ms := range []float64{20, 20.2, 19.8, 20.1} { + warm := sample(BackendArango, "w", OutcomePassed, ms, false) + warm.Round, warm.Iteration = round, it+2 + spawned = append(spawned, warm) + } + } + if got := ControlUnstableCells(spawned, StabilityCVLimit); len(got) != 0 { + t.Fatalf("cold outliers of the spawned protocol must not flag a stable warm cell unstable: %v", got) + } + // Control of the control: relabel those same cold outliers as warm + // and the cell must be flagged -- CV is really computed, and the + // regime filter is what saves the spawned cell above. + relabelled := append([]Sample(nil), spawned...) + for i := range relabelled { + if relabelled[i].Millis == 400 { + relabelled[i].Cache = CacheWarm + } + } + if got := ControlUnstableCells(relabelled, StabilityCVLimit); len(got) != 1 || got[0] != "w/"+BackendArango { + t.Fatalf("warm-spread outlier must flag the cell unstable, got %v", got) + } + // Genuinely unstable warm cell is flagged. + var wild []Sample + for i, ms := range []float64{20, 200, 20, 200} { + s := sample(BackendArango, "w", OutcomePassed, ms, false) + s.Round, s.Iteration = i/2+1, i%2+1 + wild = append(wild, s) + } + if got := ControlUnstableCells(wild, StabilityCVLimit); len(got) != 1 || got[0] != "w/"+BackendArango { + t.Fatalf("wildly varying warm cell must be unstable, got %v", got) + } + // Fewer than three steady samples: never silently trusted. + thin := wild[:2] + if got := ControlUnstableCells(thin, StabilityCVLimit); len(got) != 1 || got[0] != "w/"+BackendArango { + t.Fatalf("two-sample cell cannot demonstrate stability, got %v", got) + } + // Cold-only cells carry no claims and need no stability verdict. + coldOnly := []Sample{spawned[0]} + if got := ControlUnstableCells(coldOnly, StabilityCVLimit); len(got) != 0 { + t.Fatalf("process-cold-only samples are a different regime, not an unstable cell: %v", got) + } +} diff --git a/internal/dataframe/benchmark/driver.go b/internal/dataframe/benchmark/driver.go new file mode 100644 index 000000000..e9f377e28 --- /dev/null +++ b/internal/dataframe/benchmark/driver.go @@ -0,0 +1,616 @@ +package benchmark + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/http" + "sort" + "strings" + "time" + + "github.com/calypr/loom/internal/explorer/authoringv2" +) + +// The live driver: spawn/attach arms, load the deterministic dataset, author +// every battery workload through public routes, pass the pre-timing +// authorization gate (ruling iii), measure interleaved, run control runs, +// and emit a report that VerifyReport must accept. Every failure aborts +// before any success-shaped artifact exists. + +// ListenPlaceholder marks where the chosen listen address is rendered into +// the arm config: --config makes YAML authoritative for listen. +const ListenPlaceholder = "__BENCH_LISTEN__" + +// ArmConfig describes one backend arm: either attach to a running server +// (BaseURL) or spawn one from BinaryPath with ConfigYAML. +type ArmConfig struct { + BinaryPath string + ConfigYAML string + Env []string + BaseURL string + // ScrubSecrets are redacted from arm logs (DSNs, tokens). + ScrubSecrets []string + // ReadyURL overrides the readiness poll target for spawned arms. + ReadyURL string +} + +// SpawnFunc launches a server process; the default is provision.SpawnServer. +// Injectable so the driver's protocol is testable offline. +type SpawnFunc func(ctx context.Context, spec ServerSpec, secrets []string, listenAddr string) (*ServerProcess, error) + +// VersionProbeFunc returns the LIVE backend version string for an arm +// (e.g. by executing the launched binary's --version). Required: the +// verifier refuses a passed run without observed versions. +type VersionProbeFunc func(ctx context.Context, cfg ArmConfig, backend string) (string, error) + +// GateFunc and SingleArmGateFunc default to the package evaluators; the +// seam exists for protocol tests, never for production use. +type GateFunc func(workloadID string, columns []string, expected *CanonicalBag, arango, postgres BackendResult, arangoRows, postgresRows []map[string]any, idMap *StorageIDMap) WorkloadGateReport +type SingleArmGateFunc func(workloadID, backend string, columns []string, expected *CanonicalBag, arm BackendResult, rows []map[string]any, idMap *StorageIDMap, blockedDetail string) WorkloadGateReport + +// DriverOptions is the full, explicit driver input. No silent defaults for +// anything that lands in the report. +type DriverOptions struct { + RunID string + StartedAt time.Time + Plan RunPlan + Preset string + Params DatasetParams + Config RunConfig + PreviewLimit int + SourceCommit string + + UnrestrictedToken string + ScopedToken string + + Arango ArmConfig + Postgres ArmConfig + + StderrDir string + Clock Clock + + // ClickHouseEnabled asserts the arms were deployed with ClickHouse + // available, which the publication chain structurally requires (its + // only publication.Target is ClickHouseBundleStore). Report + // verification refuses any publication-run claim without it, so this + // is a checked assertion, not a self-description. + ClickHouseEnabled bool + + Spawn SpawnFunc + VersionProbe VersionProbeFunc + + Gates GateFunc + SingleArmGates SingleArmGateFunc + EmptyFloorIters int +} + +// ArmEvidence is the per-arm operational record kept beside the report. +type ArmEvidence struct { + Backend string `json:"backend"` + BaseURL string `json:"baseUrl"` + LogPath string `json:"logPath,omitempty"` + Version string `json:"version"` + Spawned bool `json:"spawned"` +} + +type runningArm struct { + arm *Arm + proc *ServerProcess + evidence ArmEvidence +} + +// RunBenchmark executes the matched battery. It returns the verified report +// and per-arm evidence, or an error before any artifact is written. +func RunBenchmark(ctx context.Context, opts DriverOptions) (*Report, []ArmEvidence, error) { + if err := ValidateLegPlan(opts.Plan.Scope, opts.Plan.Legs); err != nil { + return nil, nil, err + } + if opts.Plan.Scope == RunScopePlanned { + // The live driver spawns, authoring-runs, and times; a planned run + // is built through BuildReport+PlannedGateReports, never here. + return nil, nil, fmt.Errorf("benchmark: the live driver refuses planned scope; build the planned report directly") + } + if opts.Plan.PublicationExcluded() { + if strings.TrimSpace(opts.Plan.Publication.Capability) == "" { + return nil, nil, fmt.Errorf("benchmark: publication chain excluded without a named capability") + } + } else if opts.Plan.Publication != nil && opts.Plan.Publication.Run && !opts.ClickHouseEnabled { + return nil, nil, fmt.Errorf("benchmark: the publication chain is ClickHouse-backed end to end; refusing to claim it ran without ClickHouseEnabled") + } + // Live battery law: workloads whose oracle/output agreement is + // disproven or unproven are never authored, previewed, or timed on a + // live leg; they ship as disclosed blocked rows with named reasons. + excludedWorkloads := LiveBatteryExclusions() + if err := opts.Config.Validate(); err != nil { + return nil, nil, err + } + if opts.PreviewLimit < 1 { + return nil, nil, fmt.Errorf("benchmark: preview limit must be >= 1") + } + if strings.TrimSpace(opts.SourceCommit) == "" || len(opts.SourceCommit) != 40 { + return nil, nil, fmt.Errorf("benchmark: source commit must be the full 40-hex SHA the arms were built from") + } + clock := opts.Clock + if clock == nil { + clock = NewMonotonicClock() + } + spawn := opts.Spawn + if spawn == nil { + spawn = SpawnServer + } + gates := opts.Gates + if gates == nil { + gates = EvaluateGates + } + singleGates := opts.SingleArmGates + if singleGates == nil { + singleGates = EvaluateGatesSingleArm + } + + files, err := GenerateFiles(opts.Params) + if err != nil { + return nil, nil, err + } + + // Participating arms, in fixed order. + type backendArm struct { + backend string + cfg ArmConfig + } + var participants []backendArm + runLegs := map[string]bool{} + for _, leg := range opts.Plan.Legs { + if leg.Mode == "run" { + runLegs[leg.Backend] = true + } + } + for _, b := range []backendArm{{BackendArango, opts.Arango}, {BackendPostgres, opts.Postgres}} { + if runLegs[b.backend] { + participants = append(participants, b) + } + } + arms := make(map[string]*runningArm, len(participants)) + defer func() { + for _, r := range arms { + if r.proc != nil { + r.proc.Stop() + } + } + }() + for _, p := range participants { + ra, err := startArm(ctx, p.backend, p.cfg, spawn, opts) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: %s arm: %w", p.backend, err) + } + arms[p.backend] = ra + } + + // Load the dataset on every participating arm. + for _, p := range participants { + if err := arms[p.backend].arm.UploadGeneration(ctx, opts.UnrestrictedToken, opts.Params.Project, opts.Params.Generation, files.Files, opts.Params.AuthResourcePath); err != nil { + return nil, nil, fmt.Errorf("benchmark: %s upload: %w", p.backend, err) + } + } + + // Excluded publication chain (excluded mode): spawn and upload have + // run for real and every downstream stage ships as planned/not_run + // with the capability named. Nothing below this point executes, so the + // report carries zero samples, zero claims, and zero control values. + // There is no chain-less battery: the first explorer-config publish + // materializes through the chain, so authoring cannot run without it. + if opts.Plan.PublicationZeroEvidence() { + identity := RunIdentity{ + RunID: opts.RunID, StartedAt: opts.StartedAt.UTC(), Preset: opts.Preset, + SourceCommit: opts.SourceCommit, DatasetDigest: files.Digest, + ClickHouseDisabled: !opts.ClickHouseEnabled, Config: opts.Config, + } + for _, p := range participants { + version, err := opts.VersionProbe(ctx, p.cfg, p.backend) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: %s version probe: %w", p.backend, err) + } + if strings.TrimSpace(version) == "" { + return nil, nil, fmt.Errorf("benchmark: %s version probe returned an empty version", p.backend) + } + arms[p.backend].evidence.Version = version + if p.backend == BackendArango { + identity.ArangoVersion = version + } else { + identity.PostgresVersion = version + } + } + plan := opts.Plan + plan.PlannedNotRun = append([]PlannedNotRun(nil), opts.Plan.PlannedNotRun...) + AppendWorkloadDisclosures(&plan, excludedWorkloads) + report, err := BuildReport(identity, plan, ControlRuns{}, + ApplyExcludedWorkloadGateReports( + PublicationGateReports(opts.Plan.Legs, opts.Plan.Publication.Capability), + ExcludedWorkloadGateReports(opts.Plan.Legs, excludedWorkloads)), nil) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: excluded-publication report would not verify (refusing to emit): %w", err) + } + var evidence []ArmEvidence + for _, p := range participants { + evidence = append(evidence, arms[p.backend].evidence) + } + return report, evidence, nil + } + + // Author every workload on every participating arm. Explorer ids are + // derived from names (StableExplorerID), so the arms must agree on the + // id or the matched comparison is invalid. + receiptIDs := map[string]map[string]string{} // backend -> workload -> receipt + outputIDs := map[string]map[string]string{} // backend -> workload -> output + perBackend := map[string]map[string]string{} + for _, p := range participants { + perBackend[p.backend] = map[string]string{} + receiptIDs[p.backend] = map[string]string{} + outputIDs[p.backend] = map[string]string{} + } + // The writable wire carries catalog-bound contributor intent, not + // aggregate.where (the strict body bind refuses the latter). Fetch each + // arm's live builder catalog once so predicate workspaces migrate + // against the same compiler facts the compiler will later bind. The + // default explorer exists only after the first explorer-config + // publish, so bootstrap the repository with the empty-floor workspace + // (predicate-free, re-published byte-identically by its own authoring + // step) before reading the builder. + floorWorkspace, err := BuildEmptyFloorWorkspace() + if err != nil { + return nil, nil, err + } + catalogs := map[string]authoringv2.CatalogSnapshot{} + for _, p := range participants { + if _, err := arms[p.backend].arm.PublishWorkspace(ctx, opts.UnrestrictedToken, opts.Params.Project, opts.Params.Generation, + floorWorkspace, opts.SourceCommit); err != nil { + return nil, nil, fmt.Errorf("benchmark: %s catalog bootstrap: %w", p.backend, err) + } + catalog, err := arms[p.backend].arm.FetchContributorCatalog(ctx, opts.UnrestrictedToken, opts.Params.Project, "default") + if err != nil { + return nil, nil, fmt.Errorf("benchmark: %s contributor catalog: %w", p.backend, err) + } + catalogs[p.backend] = catalog + } + for _, spec := range Battery { + if _, skip := excludedWorkloads[spec.ID]; skip { + continue // disclosed absence: never authored, never timed + } + workspace, err := BuildWorkspace(spec.ID) + if err != nil { + return nil, nil, err + } + outputID := OutputIDFor(spec.ID) + name := "bench-" + spec.ID + var explorerID string + var publishedBytes []byte + for _, p := range participants { + publicationBytes, err := PreparePublicationWorkspace(workspace, opts.Params.Generation, catalogs[p.backend]) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: prepare %s for %s: %w", spec.ID, p.backend, err) + } + if publishedBytes == nil { + publishedBytes = publicationBytes + } else if !bytes.Equal(publishedBytes, publicationBytes) { + return nil, nil, fmt.Errorf("benchmark: %s publishes divergent workspace bytes on %s; the contributor catalogs are not matched", spec.ID, p.backend) + } + eid, rid, err := arms[p.backend].arm.AuthorWorkload(ctx, opts.UnrestrictedToken, opts.Params.Project, opts.Params.Generation, + publicationBytes, opts.SourceCommit, name, outputID) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: author %s on %s: %w", spec.ID, p.backend, err) + } + if explorerID == "" { + explorerID = eid + } else if eid != explorerID { + return nil, nil, fmt.Errorf("benchmark: %s authored under divergent explorer ids %s vs %s; arms are not matched", spec.ID, explorerID, eid) + } + perBackend[p.backend][spec.ID] = eid + receiptIDs[p.backend][spec.ID] = rid + outputIDs[p.backend][spec.ID] = outputID + } + // Pre-timing setup proof (ruling conditions i+ii, wired per review + // r4561644037): every arm publishes its reconciled receipt through + // the authoring route with the materialization identity verified + // (untimed setup; activation-style refusals abort fail-closed), and + // no timing may start until the arms prove matched state: equal + // content-derived draft digest, equal output set with a + // materialization identity each, and equal setup-preview rows. + proofs := map[string]SetupProof{} + for _, p := range participants { + proof, err := arms[p.backend].arm.SetupProofEvidence(ctx, opts.UnrestrictedToken, opts.Params.Project, + explorerID, receiptIDs[p.backend][spec.ID], outputID) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: %s setup proof on %s: %w", spec.ID, p.backend, err) + } + proofs[p.backend] = proof + } + if len(participants) == 2 { + if err := MatchedSetupProof(spec.ID, proofs[BackendArango], proofs[BackendPostgres]); err != nil { + return nil, nil, err + } + } + } + // One explorer identity serves both arms (asserted equal above). + explorerIDByWorkload := perBackend[participants[0].backend] + + // Empty-floor explorers (zero-row expectation) per arm. The workspace + // was built for the catalog bootstrap; it is re-published here because + // the per-workload loop moved the repository default since then. + floorExplorers := map[string]string{} + floorReceipts := map[string]string{} + for _, p := range participants { + eid, rid, err := arms[p.backend].arm.AuthorWorkload(ctx, opts.UnrestrictedToken, opts.Params.Project, opts.Params.Generation, + floorWorkspace, opts.SourceCommit, "bench-empty-floor", "empty_floor") + if err != nil { + return nil, nil, fmt.Errorf("benchmark: author empty-floor on %s: %w", p.backend, err) + } + floorExplorers[p.backend] = eid + floorReceipts[p.backend] = rid + } + + // Pre-timing authorization gate (ruling iii). If the scoped principal + // sees anything it was not granted, timings are meaningless: abort + // before producing any artifact. The gate runs on a live-battery + // (authoring-proven) workload: an excluded workload is never authored, + // so it cannot host the control. Any authored workload exercises the + // same fence — the whole fixture set uploads under one auth path. + authWorkload := WorkloadGroupCountByPatient + baseOpts := RunOptions{ + Project: opts.Params.Project, Generation: opts.Params.Generation, + AuthResourcePath: opts.Params.AuthResourcePath, + UnrestrictedToken: opts.UnrestrictedToken, ScopedToken: opts.ScopedToken, + PreviewLimit: opts.PreviewLimit, Config: opts.Config, SourceCommit: opts.SourceCommit, + } + { + ra := RunOptions{Project: baseOpts.Project, Generation: baseOpts.Generation, AuthResourcePath: baseOpts.AuthResourcePath, UnrestrictedToken: baseOpts.UnrestrictedToken, ScopedToken: baseOpts.ScopedToken, PreviewLimit: baseOpts.PreviewLimit, Config: baseOpts.Config, SourceCommit: baseOpts.SourceCommit, + Explorer: explorerIDByWorkload[authWorkload]} + rc := map[string]string{} + oi := map[string]string{} + for _, p := range participants { + rc[p.backend] = receiptIDs[p.backend][authWorkload] + oi[p.backend] = outputIDs[p.backend][authWorkload] + } + var authReport WorkloadGateReport + if len(participants) == 2 { + authReport = NegativeAuthControl(ctx, arms[BackendArango].arm, arms[BackendPostgres].arm, ra, rc, oi) + } else { + authReport = NegativeAuthControlSingleArm(ctx, authWorkload, arms[participants[0].backend].arm, ra, rc, oi) + } + if !authReport.Claimable { + return nil, nil, fmt.Errorf("benchmark: pre-timing authorization gate FAILED: %s; no artifact is produced", authReport.ExclusionReason) + } + } + + // Timing, workload by workload, interleaved round-robin. + var allSamples []Sample + var gateReports []WorkloadGateReport + idMap := NewStorageIDMap(files.Facts) + var cold func(ctx context.Context, arm *Arm) error + procCold := false + for _, p := range participants { + if arms[p.backend].proc != nil { + procCold = true + } + } + if procCold { + cold = func(ctx context.Context, arm *Arm) error { + if arm == nil || arm.Proc == nil { + return nil + } + return arm.Proc.Restart(ctx) + } + } + for _, spec := range Battery { + if _, skip := excludedWorkloads[spec.ID]; skip { + continue // disclosed absence: never authored, never timed + } + ra := RunOptions{Project: baseOpts.Project, Generation: baseOpts.Generation, AuthResourcePath: baseOpts.AuthResourcePath, UnrestrictedToken: baseOpts.UnrestrictedToken, ScopedToken: baseOpts.ScopedToken, PreviewLimit: baseOpts.PreviewLimit, Config: baseOpts.Config, SourceCommit: baseOpts.SourceCommit, + Explorer: explorerIDByWorkload[spec.ID]} + rc := map[string]string{} + oi := map[string]string{} + for _, p := range participants { + rc[p.backend] = receiptIDs[p.backend][spec.ID] + oi[p.backend] = outputIDs[p.backend][spec.ID] + } + var arangoArm, postgresArm *Arm + if a, ok := arms[BackendArango]; ok { + arangoArm = a.arm + } + if a, ok := arms[BackendPostgres]; ok { + postgresArm = a.arm + } + samples, finals, err := MeasureArms(ctx, clock, spec.ID, arangoArm, postgresArm, ra, rc, oi, cold) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: measure %s: %w", spec.ID, err) + } + allSamples = append(allSamples, samples...) + var expected *CanonicalBag + if spec.OracleAvailable { + expected, err = OracleFor(spec.ID, files.Facts) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: oracle %s: %w", spec.ID, err) + } + } + arangoResult := backendResultFrom(BackendArango, samples, finals) + postgresResult := backendResultFrom(BackendPostgres, samples, finals) + var report WorkloadGateReport + if len(participants) == 2 { + report = gates(spec.ID, spec.Columns, expected, arangoResult, postgresResult, + rowsOf(finals[BackendArango]), rowsOf(finals[BackendPostgres]), idMap) + } else { + backend := participants[0].backend + blockedDetail := "" + for _, leg := range opts.Plan.Legs { + if leg.Backend == otherBackend(backend) { + blockedDetail = "leg not run: " + leg.Capability + } + } + result := arangoResult + if backend == BackendPostgres { + result = postgresResult + } + report = singleGates(spec.ID, backend, spec.Columns, expected, result, rowsOf(finals[backend]), idMap, blockedDetail) + } + gateReports = append(gateReports, report) + } + // Registry-excluded workloads appear as disclosed blocked rows, never + // as missing rows: the report says exactly what did not run and why. + gateReports = append(gateReports, ExcludedWorkloadGateReports(opts.Plan.Legs, excludedWorkloads)...) + + // Control runs. + controls := ControlRuns{ + StabilityCVLimit: StabilityCVLimit, + UnstableCells: ControlUnstableCells(allSamples, StabilityCVLimit), + RatioPolicy: "ratio-only-both-passed", + } + for _, backend := range []string{BackendArango, BackendPostgres} { + if !runLegs[backend] { + continue // stays 0: absent, never forged + } + value, ok := ControlSelfRatio(allSamples, backend) + if !ok { + return nil, nil, fmt.Errorf("benchmark: %s self-ratio control cannot be computed from the collected samples; run is not fit for claims", backend) + } + if backend == BackendArango { + controls.SelfRatioArango = value + } else { + controls.SelfRatioPostgres = value + } + } + floorIters := opts.EmptyFloorIters + if floorIters == 0 { + floorIters = 5 + } + worstFloor := 0.0 + for _, p := range participants { + median, err := arms[p.backend].arm.MeasureEmptyFloor(ctx, clock, opts.UnrestrictedToken, opts.Params.Project, floorExplorers[p.backend], floorReceipts[p.backend], "empty_floor", floorIters) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: %s empty-floor control: %w", p.backend, err) + } + if median > worstFloor { + worstFloor = median + } + } + controls.EmptyFloorMillis = worstFloor + + identity := RunIdentity{ + RunID: opts.RunID, StartedAt: opts.StartedAt.UTC(), Preset: opts.Preset, + SourceCommit: opts.SourceCommit, DatasetDigest: files.Digest, + ClickHouseDisabled: !opts.ClickHouseEnabled, Config: opts.Config, + } + for _, p := range participants { + version, err := opts.VersionProbe(ctx, p.cfg, p.backend) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: %s version probe: %w", p.backend, err) + } + if strings.TrimSpace(version) == "" { + return nil, nil, fmt.Errorf("benchmark: %s version probe returned an empty version", p.backend) + } + arms[p.backend].evidence.Version = version + if p.backend == BackendArango { + identity.ArangoVersion = version + } else { + identity.PostgresVersion = version + } + } + + plan := opts.Plan + plan.PlannedNotRun = append([]PlannedNotRun(nil), opts.Plan.PlannedNotRun...) + AppendWorkloadDisclosures(&plan, excludedWorkloads) + report, err := BuildReport(identity, plan, controls, gateReports, allSamples) + if err != nil { + return nil, nil, fmt.Errorf("benchmark: report would not verify (refusing to emit): %w", err) + } + var evidence []ArmEvidence + for _, p := range participants { + evidence = append(evidence, arms[p.backend].evidence) + } + sort.Slice(evidence, func(i, j int) bool { return evidence[i].Backend < evidence[j].Backend }) + return report, evidence, nil +} + +func startArm(ctx context.Context, backend string, cfg ArmConfig, spawn SpawnFunc, opts DriverOptions) (*runningArm, error) { + if cfg.BaseURL != "" { + return &runningArm{ + arm: &Arm{Backend: backend, BaseURL: strings.TrimRight(cfg.BaseURL, "/"), Client: &http.Client{Timeout: 120 * time.Second}, AuthResourcePath: opts.Params.AuthResourcePath}, + evidence: ArmEvidence{Backend: backend, BaseURL: cfg.BaseURL, Spawned: false}, + }, nil + } + if cfg.BinaryPath == "" { + return nil, fmt.Errorf("arm must provide either BaseURL or BinaryPath") + } + port, err := FreePort() + if err != nil { + return nil, err + } + listen := fmt.Sprintf("127.0.0.1:%d", port) + // --config makes YAML authoritative for listen, so the chosen address + // must be rendered into the config, not only passed as --listen. + cfgYAML := cfg.ConfigYAML + if addr, mark, ok := strings.Cut(cfgYAML, ListenPlaceholder); ok { + cfgYAML = addr + listen + mark + } + ready := cfg.ReadyURL + if ready == "" { + ready = "http://" + listen + "/readyz" + } + timeout := 60 * time.Second + spec := ServerSpec{BinaryPath: cfg.BinaryPath, Backend: backend, ConfigYAML: cfgYAML, Env: cfg.Env, ReadyURL: ready, Timeout: timeout, StderrDir: opts.StderrDir} + proc, err := spawn(ctx, spec, cfg.ScrubSecrets, listen) + if err != nil { + return nil, err + } + return &runningArm{ + arm: &Arm{Backend: backend, BaseURL: "http://" + listen, Client: &http.Client{Timeout: 120 * time.Second}, Proc: proc, AuthResourcePath: opts.Params.AuthResourcePath}, + proc: proc, + evidence: ArmEvidence{Backend: backend, BaseURL: "http://" + listen, LogPath: proc.LogPath(), Spawned: true}, + }, nil +} + +func backendResultFrom(backend string, samples []Sample, finals map[string]PreviewResult) BackendResult { + result := BackendResult{Backend: backend, Outcome: OutcomeLabelPassed} + var failures []string + for _, s := range samples { + if s.Backend != backend || s.Warmup { + continue + } + if s.Outcome != OutcomePassed { + result.Outcome = OutcomeLabelFailed + failures = append(failures, string(s.Outcome)+":"+s.ErrorCode) + } + } + if len(failures) > 0 { + seen := map[string]bool{} + var uniq []string + for _, f := range failures { + if !seen[f] { + seen[f] = true + uniq = append(uniq, f) + } + } + sort.Strings(uniq) + if len(uniq) > 3 { + uniq = uniq[:3] + } + result.ErrorCode = strings.Join(uniq, ";") + return result + } + final, ok := finals[backend] + if !ok { + result.Outcome = OutcomeLabelFailed + result.ErrorCode = "no completed measured execution" + return result + } + result.Columns = final.Columns + result.RowCount = final.RowCount + result.Truncated = final.Sampled + bag := Bag(CanonicalizeRows(final.Columns, final.Rows)) + result.BagDigest = bag.Digest() + return result +} + +func rowsOf(result PreviewResult) []map[string]any { return result.Rows } + +// ErrAbortBeforeArtifact distinguishes "refused to run" from "run failed". +var ErrAbortBeforeArtifact = errors.New("benchmark: aborted before producing an artifact") diff --git a/internal/dataframe/benchmark/driver_test.go b/internal/dataframe/benchmark/driver_test.go new file mode 100644 index 000000000..88785bb47 --- /dev/null +++ b/internal/dataframe/benchmark/driver_test.go @@ -0,0 +1,387 @@ +package benchmark + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// Offline driver protocol tests: fake arms serve the public routes; the +// gate EVALUATION itself is covered by gates_test/oracle_test, so these +// tests pin orchestration: route coverage, matched arms, control runs, +// per-scope report shape, and fail-closed aborts. + +func driverOptions(fArango, fPostgres *fakeArm, scope string) DriverOptions { + plan := BothArmsPlan() + if scope == RunScopePostgresOnly { + plan = PostgresOnlyPlan("no ArangoDB service in worker container") + } + // The fake arms serve the whole battery, i.e. a CI-lane deployment + // with ClickHouse; the excluded-chain shape is pinned separately. + plan.Publication = &PublicationPlan{Run: true} + plan.PlannedNotRun = PlannedNotRunForBackends(plan.NotRunBackends()) + for i := range plan.PlannedNotRun { + plan.PlannedNotRun[i].Reason = "leg not run: worker container lacks the service" + } + // every workload output must answer; empty_floor must be zero rows + for _, f := range []*fakeArm{fArango, fPostgres} { + if f == nil { + continue + } + f.scopedToken = "Bearer scoped" + for _, spec := range Battery { + if _, ok := f.rows[OutputIDFor(spec.ID)]; !ok { + f.rows[OutputIDFor(spec.ID)] = []map[string]any{{"patient_id": "p1"}} + } + } + f.rows["empty_floor"] = []map[string]any{} + } + opts := DriverOptions{ + RunID: "bench-driver-test", StartedAt: time.Unix(1700000000, 0).UTC(), + Plan: plan, Preset: PresetTiny, + Params: DatasetParams{Preset: PresetTiny, Seed: 7, Project: "proj", Generation: "gen1", AuthResourcePath: "/programs/p/projects/proj/x"}, + Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}, + PreviewLimit: 1000, SourceCommit: strings.Repeat("a", 40), + UnrestrictedToken: "Bearer unauth", ScopedToken: "Bearer scoped", + ClickHouseEnabled: true, + Clock: &scriptedClock{base: time.Unix(1700000000, 0)}, + VersionProbe: func(ctx context.Context, cfg ArmConfig, backend string) (string, error) { + if backend == BackendArango { + return "3.12.x-fake", nil + } + return "PostgreSQL 18.x-fake", nil + }, + // Protocol seam only: real evaluation is pinned elsewhere. + Gates: func(workloadID string, columns []string, expected *CanonicalBag, arango, postgres BackendResult, arangoRows, postgresRows []map[string]any, idMap *StorageIDMap) WorkloadGateReport { + return WorkloadGateReport{WorkloadID: workloadID, Claimable: true, + Arango: arango, Postgres: postgres, + Gates: []GateResult{{Gate: GateBagEquality, Pass: true}}} + }, + SingleArmGates: func(workloadID, backend string, columns []string, expected *CanonicalBag, arm BackendResult, rows []map[string]any, idMap *StorageIDMap, blockedDetail string) WorkloadGateReport { + report := WorkloadGateReport{WorkloadID: workloadID, Claimable: true, + Gates: []GateResult{{Gate: GateOracleRows, Pass: true}}} + if backend == BackendArango { + report.Arango = arm + report.Postgres = BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelBlocked, Detail: blockedDetail} + } else { + report.Postgres = arm + report.Arango = BackendResult{Backend: BackendArango, Outcome: OutcomeLabelBlocked, Detail: blockedDetail} + } + return report + }, + } + if fArango != nil { + opts.Arango = ArmConfig{BaseURL: fArango.server.URL} + } + if fPostgres != nil { + opts.Postgres = ArmConfig{BaseURL: fPostgres.server.URL} + } + return opts +} + +func TestDriverBothArmsProtocol(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + report, evidence, err := RunBenchmark(context.Background(), driverOptions(fa, fp, RunScopeBothArms)) + if err != nil { + t.Fatal(err) + } + if report.Status != "passed" || len(report.Claims) == 0 { + t.Fatalf("expected passing both-arms report with claims: %s %v", report.Status, report.StatusReasons) + } + if report.Identity.ArangoVersion == "" || report.Identity.PostgresVersion == "" { + t.Fatal("live versions must be recorded") + } + if report.Controls.EmptyFloorMillis <= 0 || report.Controls.SelfRatioArango < 1 || report.Controls.SelfRatioPostgres < 1 { + t.Fatalf("controls must be genuinely run: %+v", report.Controls) + } + if len(evidence) != 2 || evidence[0].Backend != BackendArango { + t.Fatalf("evidence: %+v", evidence) + } + // both arms must have uploaded the dataset and authored every + // live-battery (proven) workload; registry-excluded workloads are + // never authored (review r4559430560) + live := 0 + for _, spec := range Battery { + if _, excluded := LiveBatteryExclusions()[spec.ID]; !excluded { + live++ + } + } + if live == 0 || live == len(Battery) { + t.Fatalf("expected a partially excluded live battery, got %d of %d", live, len(Battery)) + } + for name, f := range map[string]*fakeArm{"arango": fa, "postgres": fp} { + if f.uploads != 1 { + t.Fatalf("%s: uploads=%d want 1", name, f.uploads) + } + if f.publish != live+2 { // + catalog bootstrap + empty floor + t.Fatalf("%s: publish=%d want %d", name, f.publish, live+2) + } + // the writable wire is closed over aggregate predicates: every + // published workspace must bind exactly like the server's body bind + // does, predicate workspaces must carry contributor intent, and no + // body may smuggle the legacy where field + bodies := f.publishedBodies() + if len(bodies) != live+2 { + t.Fatalf("%s: published bodies=%d want %d", name, len(bodies), live+2) + } + // the catalog fetch is bootstrapped by publishing the empty-floor + // workspace first: the default explorer exists only after the first + // explorer-config publish, so the builder read depends on this order + if floor, err := BuildEmptyFloorWorkspace(); err != nil { + t.Fatal(err) + } else if string(bodies[0]) != string(floor) { + t.Fatalf("%s: first publish must be the empty-floor catalog bootstrap", name) + } + var withContributor, withWhere int + for _, body := range bodies { + var workspace map[string]any + if err := json.Unmarshal(body, &workspace); err != nil { + t.Fatalf("%s: published workspace does not bind: %v", name, err) + } + if strings.Contains(string(body), `"contributor"`) { + withContributor++ + } + if strings.Contains(string(body), `"where"`) { + withWhere++ + } + } + if withWhere != 0 { + t.Fatalf("%s: a published workspace still carries the legacy where wire", name) + } + if withContributor == 0 { + t.Fatalf("%s: no published workspace carries contributor intent", name) + } + } + // both arms must publish byte-identical workloads (matched arms) + ta, tp := fa.publishedBodies(), fp.publishedBodies() + for i := range ta { + if string(ta[i]) != string(tp[i]) { + t.Fatalf("arm %d published different bytes on the two drivers:\n%s\n%s", i, ta[i], tp[i]) + } + } + // round-trip the artifact + if _, err := LoadReport(mustMarshal(t, report)); err != nil { + t.Fatalf("driver report must verify: %v", err) + } +} + +// scriptedClock makes every elapsed measurement exactly 10ms: protocol +// tests must not depend on host jitter. The self-ratio failing above in +// this file is exactly why the clock is injectable. +type scriptedClock struct { + base time.Time + n int +} + +func (c *scriptedClock) Now() time.Time { + c.n++ + return c.base.Add(time.Duration(c.n) * time.Millisecond) +} + +func (c *scriptedClock) Since(time.Time) time.Duration { return 10 * time.Millisecond } + +func TestDriverPostgresOnlyProtocol(t *testing.T) { + fp := newFakeArm(BackendPostgres) + defer fp.close() + report, evidence, err := RunBenchmark(context.Background(), driverOptions(nil, fp, RunScopePostgresOnly)) + if err != nil { + t.Fatal(err) + } + if report.Status != "passed" { + t.Fatalf("postgres-only pass expected: %v", report.StatusReasons) + } + if len(report.Claims) != 0 { + t.Fatal("postgres-only must have no speed claims") + } + if report.Identity.ArangoVersion != "" || report.Controls.SelfRatioArango != 0 { + t.Fatal("unrun arango leg must carry no version and no self-ratio") + } + for _, s := range report.Samples { + if s.Backend != BackendPostgres { + t.Fatalf("sample for unrun backend: %+v", s) + } + } + if len(evidence) != 1 || evidence[0].Backend != BackendPostgres { + t.Fatalf("evidence: %+v", evidence) + } + if _, err := LoadReport(mustMarshal(t, report)); err != nil { + t.Fatalf("postgres-only driver report must verify: %v", err) + } +} + +func TestDriverAbortsBeforeArtifactOnAuthLeak(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fp.leakScoped = true // scoped principal sees rows on postgres: denial broken + _, _, err := RunBenchmark(context.Background(), driverOptions(fa, fp, RunScopeBothArms)) + if err == nil || !strings.Contains(err.Error(), "authorization gate") { + t.Fatalf("auth leak must abort before any artifact, got %v", err) + } +} + +func TestDriverRejectsDivergentExplorerIDs(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.explorerID = "exp-other" + _, _, err := RunBenchmark(context.Background(), driverOptions(fa, fp, RunScopeBothArms)) + if err == nil || !strings.Contains(err.Error(), "not matched") { + t.Fatalf("divergent explorer ids must abort the matched comparison, got %v", err) + } +} + +func TestDriverRequiresRealSourceCommit(t *testing.T) { + fp := newFakeArm(BackendPostgres) + defer fp.close() + opts := driverOptions(nil, fp, RunScopePostgresOnly) + opts.SourceCommit = "abc" + if _, _, err := RunBenchmark(context.Background(), opts); err == nil { + t.Fatal("short source commit must be refused") + } +} + +// TestDriverSpawnedArmsProduceClaims pins the operator lane end to end +// (review r4561031154): spawned arms restart every round, so the schedule +// must yield a warm cell per workload per arm and the report must carry +// claims. The dummy binary sleeps; the fake serves the arm routes and +// /readyz on the address the driver chose. +func TestDriverSpawnedArmsProduceClaims(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + dir := t.TempDir() + script := filepath.Join(dir, "dummy-arm.sh") + if err := os.WriteFile(script, []byte("#!/bin/sh\nexec sleep 300\n"), 0o755); err != nil { + t.Fatal(err) + } + opts := driverOptions(fa, fp, RunScopeBothArms) + opts.Config = RunConfig{Warmup: 1, Iterations: 10, Concurrency: 1, Rounds: 5} + opts.StderrDir = dir + opts.Arango = ArmConfig{BinaryPath: script, ConfigYAML: "listen: unused"} + opts.Postgres = ArmConfig{BinaryPath: script, ConfigYAML: "listen: unused"} + fakes := map[string]*fakeArm{BackendArango: fa, BackendPostgres: fp} + opts.Spawn = func(ctx context.Context, spec ServerSpec, secrets []string, listenAddr string) (*ServerProcess, error) { + f, ok := fakes[spec.Backend] + if !ok { + return nil, fmt.Errorf("no fake arm for backend %q", spec.Backend) + } + if err := f.serveOn(listenAddr); err != nil { + return nil, err + } + return SpawnServer(ctx, spec, secrets, listenAddr) + } + report, evidence, err := RunBenchmark(context.Background(), opts) + if err != nil { + t.Fatal(err) + } + if report.Status != "passed" { + t.Fatalf("spawned run must pass: %s %v", report.Status, report.StatusReasons) + } + if len(report.Claims) == 0 { + t.Fatal("spawned lane must produce claims from warm cells") + } + for _, claim := range report.Claims { + if claim.Arango.Count < 1 || claim.Postgres.Count < 1 { + t.Fatalf("claim cites empty cells: %+v", claim) + } + } + if len(evidence) != 2 { + t.Fatalf("evidence: %+v", evidence) + } + for _, e := range evidence { + if !e.Spawned || e.LogPath == "" { + t.Fatalf("both arms must be spawned with logs: %+v", e) + } + } +} + +// TestDriverRunsSetupProofOnBothArms pins the wiring (review r4561644037): +// the live setup path must publish every reconciled receipt through the +// authoring route on both arms and prove matched state before timing. +func TestDriverRunsSetupProofOnBothArms(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + report, _, err := RunBenchmark(context.Background(), driverOptions(fa, fp, RunScopeBothArms)) + if err != nil { + t.Fatal(err) + } + if report.Status != "passed" { + t.Fatalf("matched lane must pass: %s %v", report.Status, report.StatusReasons) + } + live := 0 + for _, spec := range Battery { + if _, excluded := LiveBatteryExclusions()[spec.ID]; !excluded { + live++ + } + } + for name, f := range map[string]*fakeArm{"arango": fa, "postgres": fp} { + publishes := 0 + for _, c := range f.routeCalls() { + if strings.Contains(c, ":publish:") { + publishes++ + } + } + if publishes != live { + t.Fatalf("%s: authoring publishes=%d want one per live workload (%d)", name, publishes, live) + } + } +} + +func firstLiveWorkload(t *testing.T) string { + t.Helper() + ex := LiveBatteryExclusions() + for _, spec := range Battery { + if _, bad := ex[spec.ID]; !bad { + return spec.ID + } + } + t.Fatal("no live workload") + return "" +} + +// TestDriverAbortsBeforeTimingOnDivergentSetupProof: a cross-arm draft +// digest divergence must abort the run with no report and no samples; +// timing may never start on a divergent setup. +func TestDriverAbortsBeforeTimingOnDivergentSetupProof(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.mu.Lock() + fa.activeDigest = "sha256:arango-only" + fa.mu.Unlock() + report, _, err := RunBenchmark(context.Background(), driverOptions(fa, fp, RunScopeBothArms)) + if err == nil || !strings.Contains(err.Error(), "matched-state proof diverged") { + t.Fatalf("digest divergence must abort before timing: %v", err) + } + if report != nil { + t.Fatal("no report may exist when the matched-state proof fails") + } + // the only preview calls allowed are the two proof previews themselves + // (one per arm, first live workload); any measured-phase, gate, or + // later-workload preview would mean timing started on a divergent setup + previews := 0 + want := ":preview:" + OutputIDFor(firstLiveWorkload(t)) + for _, f := range []*fakeArm{fa, fp} { + for _, c := range f.routeCalls() { + if !strings.Contains(c, ":preview:") { + continue + } + previews++ + if !strings.HasSuffix(c, want) { + t.Fatalf("preview route ran beyond the first workload's setup proof: %s", c) + } + } + } + if previews != 2 { + t.Fatalf("proof previews=%d want 2 (one per arm, first workload), got no proof or timing ran", previews) + } +} diff --git a/internal/dataframe/benchmark/fixtures.go b/internal/dataframe/benchmark/fixtures.go new file mode 100644 index 000000000..9bd6522c9 --- /dev/null +++ b/internal/dataframe/benchmark/fixtures.go @@ -0,0 +1,654 @@ +package benchmark + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" +) + +// Deterministic matched-fixture generation. One generator produces the FHIR +// NDJSON graph files and the independently-derived facts that feed the +// correctness oracles. Both benchmark arms load byte-identical files, so any +// cross-backend output difference is backend behavior, never fixture drift. +// +// Determinism law: every byte derives from (profile, seed, entity index) +// through fnv hashing only -- no map iteration, no time, no goroutines. +// Re-generating the same DatasetParams must reproduce the same digest. + +// Presets are the closed size ladder. representative is the profile the +// comparative-workload-matrix check runs; real data is a separate operator +// supplied profile and is never synthesized under this name. +const ( + PresetTiny = "tiny" + PresetSmall = "small" + PresetRepresentative = "representative" +) + +// DatasetParams pins one matched dataset. All identity strings must be fixed +// per run so both arms load the same logical scope. +type DatasetParams struct { + Preset string + Seed uint64 + Project string + Generation string + // AuthResourcePath is stamped onto every vertex/edge by the production + // loader (ingest LoadOptions.AuthResourcePath). The scoped negative + // authorization control runs a Fence grant list that excludes it. + AuthResourcePath string +} + +// DatasetSize is the closed cardinality plan per preset. Sizes are exact, +// not sampled, so facts can be derived exactly. +type DatasetSize struct { + Patients int + Observations int // total, distributed deterministically per patient + Specimens int + Conditions int + ResearchStudies int +} + +func datasetSize(preset string) (DatasetSize, error) { + switch preset { + case PresetTiny: + return DatasetSize{Patients: 12, Observations: 48, Specimens: 18, Conditions: 20, ResearchStudies: 2}, nil + case PresetSmall: + return DatasetSize{Patients: 200, Observations: 1600, Specimens: 400, Conditions: 500, ResearchStudies: 3}, nil + case PresetRepresentative: + return DatasetSize{Patients: 20000, Observations: 160000, Specimens: 40000, Conditions: 50000, ResearchStudies: 4}, nil + default: + return DatasetSize{}, fmt.Errorf("benchmark: unknown dataset preset %q", preset) + } +} + +// Observation code families used by the workload matrix. Codes are stable +// identities; changing one is a fixture-contract change, not a refactor. +const ( + ObservationCodeAge = "BENCH-CODE-AGE" // valueQuantity.value feeds SUM/MIN/MAX/MEAN + ObservationCodeStage = "BENCH-CODE-STAGE" // valueCodeableConcept feeds Pivot + ObservationCodeGender = "BENCH-CODE-GENDER" // valueString feeds Unpivot column keys +) + +// BenchAgeIdentifierSystem is the identifier namespace on age observations +// whose value is the age as a decimal string. It is the scalar-string source +// the derive-age-band workspace recodes through EXACT_CATEGORY_RECODE. It is +// emitted exactly when valueQuantity.value is (the missing-value observations +// carry neither, so the oracle's skip and the workspace's null-exclusion +// agree by construction). +const BenchAgeIdentifierSystem = "https://bench.example/identifier/age-days" + +// Specimen type codes (duplicate-contributor and filter workloads). +const ( + SpecimenTypePrimary = "PRIMARY" + SpecimenTypeMetastatic = "METASTATIC" +) + +// Condition verification statuses, including one deliberately absent. +const ( + ConditionStatusConfirmed = "confirmed" + ConditionStatusProvisional = "provisional" + // ConditionClinicalActive is the FHIR R4 required clinicalStatus. No + // workload predicates on it, so it is constant and never an oracle input. + ConditionClinicalActive = "active" +) + +// PatientNamePool sizes control duplicate contributors and repeated-owner +// correlation: the family-name pool is deliberately much smaller than the +// patient count so multiple patients share each family name. +const ( + familyNamePoolSize = 9 + givenNamePoolSize = 7 +) + +func hashU64(seed uint64, tags ...string) uint64 { + h := seed ^ 0x9e3779b97f4a7c15 + for _, tag := range tags { + for _, b := range []byte(tag) { + h ^= uint64(b) + h *= 0x100000001b3 + } + h ^= h >> 31 + h *= 0xbf58476d1ce4e5b9 + } + return h +} + +func pick[T any](seed uint64, tags []string, choices []T) T { + h := hashU64(seed, tags...) + return choices[h%uint64(len(choices))] +} + +func patientID(index int) string { + return fmt.Sprintf("bench-patient-%07d", index) +} + +func observationID(index int) string { + return fmt.Sprintf("bench-observation-%08d", index) +} + +func specimenID(index int) string { + return fmt.Sprintf("bench-specimen-%07d", index) +} + +func conditionID(index int) string { + return fmt.Sprintf("bench-condition-%07d", index) +} + +func studyID(index int) string { + return fmt.Sprintf("bench-study-%03d", index) +} + +const partOfStudyURL = "http://fhir-aggregator.org/fhir/StructureDefinition/part-of-study" + +// studyRefFor maps a patient deterministically to one study. Every study is +// populated so inbound-study traversal workloads cover all studies. +func (d DatasetParams) studyRefFor(patientIndex int) string { + size, err := datasetSize(d.Preset) + if err != nil || size.ResearchStudies == 0 { + return "" + } + return "ResearchStudy/" + studyID(int(hashU64(d.Seed, "study", strconv.Itoa(patientIndex))%uint64(size.ResearchStudies))) +} + +func partOfStudyExtension(studyRef string) map[string]any { + return map[string]any{ + "url": partOfStudyURL, + "valueReference": map[string]any{"reference": studyRef}, + } +} + +// PatientFacts is the independently known per-patient truth used by oracles. +// Facts are derived from the generation inputs, never read back from a +// backend, which is what makes the correctness gate independent. +type PatientFacts struct { + ID string `json:"id"` + Active bool `json:"active"` + Gender string `json:"gender"` // male|female|other; "" means deliberately missing + FamilyNames []string `json:"familyNames"` + GivenNames []string `json:"givenNames"` + IdentifierValues []string `json:"identifierValues"` + Deceased *bool `json:"deceased,omitempty"` + TelecomValues []string `json:"telecomValues"` + ObservationIDs []string `json:"observationIds"` + SpecimenIDs []string `json:"specimenIds"` + ConditionIDs []string `json:"conditionIds"` + StudyRef string `json:"studyRef"` +} + +// ObservationFacts is per-observation truth, including the numeric value for +// aggregation oracles and the pivot/unpivot payloads. +type ObservationFacts struct { + ID string `json:"id"` + PatientID string `json:"patientId"` + Status string `json:"status"` + Code string `json:"code"` + ValueNumber *float64 `json:"valueNumber,omitempty"` + ValueString string `json:"valueString,omitempty"` + StageCode string `json:"stageCode,omitempty"` + Effective string `json:"effective"` + HasSpecimen bool `json:"hasSpecimen"` + SpecimenID string `json:"specimenId,omitempty"` +} + +type SpecimenFacts struct { + ID string `json:"id"` + PatientID string `json:"patientId"` + TypeCode string `json:"typeCode"` +} + +type ConditionFacts struct { + ID string `json:"id"` + PatientID string `json:"patientId"` + CodeDisplay string `json:"codeDisplay"` + VerificationStatus string `json:"verificationStatus,omitempty"` // "" = deliberately missing +} + +// Facts is the complete independently-known truth for one dataset. +type Facts struct { + Params DatasetParams `json:"params"` + Size DatasetSize `json:"size"` + Patients []PatientFacts `json:"patients"` + Observations []ObservationFacts `json:"observations"` + Specimens []SpecimenFacts `json:"specimens"` + Conditions []ConditionFacts `json:"conditions"` + StudyIDs []string `json:"studyIds"` + // Derived aggregates used by grouped-aggregation oracles. + SumAgeByPatient map[string]float64 `json:"sumAgeByPatient"` + CountByPatient map[string]int `json:"countByPatient"` + StageCounts map[string]int `json:"stageCounts"` + FamilyContributors map[string][]string `json:"familyContributors"` // family -> observation ids (duplicate contributors) +} + +var genderChoices = []string{"male", "female", "other"} +var observationStatuses = []string{"final", "amended", "registered", "cancelled"} +var stageCodes = []string{"stage-a", "stage-b", "stage-c", "stage-d"} +var familyNames = []string{"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india"} +var givenNames = []string{"ada", "bo", "cy", "di", "ed", "fi", "gi"} +var telecomSystems = []string{"phone", "email", "fax"} +var conditionDisplays = []string{"hypertension", "diabetes", "asthma", "anemia", "arrhythmia"} + +// statusFor makes exactly the observations with index%17==3 cancelled and +// index%23==5 registered; everything else is final. Oracles reuse statusFor, +// so selective-filter expectations are derived, not transcribed. +func observationStatusFor(index int) string { + switch { + case index%17 == 3: + return "cancelled" + case index%23 == 5: + return "registered" + default: + return "final" + } +} + +func observationCodeFor(index int) string { + switch index % 3 { + case 0: + return ObservationCodeAge + case 1: + return ObservationCodeStage + default: + return ObservationCodeGender + } +} + +// GenerateFacts derives the complete independent truth for the params. +func GenerateFacts(params DatasetParams) (*Facts, error) { + size, err := datasetSize(params.Preset) + if err != nil { + return nil, err + } + if size.Patients == 0 { + return nil, fmt.Errorf("benchmark: preset %s has no patients", params.Preset) + } + facts := &Facts{ + Params: params, Size: size, + SumAgeByPatient: map[string]float64{}, + CountByPatient: map[string]int{}, + StageCounts: map[string]int{}, + FamilyContributors: map[string][]string{}, + } + for s := 0; s < size.ResearchStudies; s++ { + facts.StudyIDs = append(facts.StudyIDs, studyID(s)) + } + // Deterministic round-robin with a hash-derived remainder keeps every + // patient owning at least one of each child type once size permits, and + // keeps distributions reproducible. + obsPerPatient := size.Observations / size.Patients + extraObs := size.Observations % size.Patients + specPerPatient := size.Specimens / size.Patients + extraSpec := size.Specimens % size.Patients + condPerPatient := size.Conditions / size.Patients + extraCond := size.Conditions % size.Patients + obsCursor, specCursor, condCursor := 0, 0, 0 + for p := 0; p < size.Patients; p++ { + id := patientID(p) + family := []string{familyNames[p%familyNamePoolSize]} + given := []string{givenNames[p%givenNamePoolSize]} + // Duplicate contributors: every 4th patient carries a second family + // name and a second identifier with the SAME value as the first, so + // DISTINCT vs ALL behave differently and are exercised. + if p%4 == 0 && len(familyNames) > 1 { + family = append(family, familyNames[(p/familyNamePoolSize+1)%familyNamePoolSize]) + } + pf := PatientFacts{ + ID: id, + Active: p%9 != 0, + FamilyNames: family, + GivenNames: given, + IdentifierValues: []string{"MRN-" + strconv.Itoa(p)}, + StudyRef: params.studyRefFor(p), + } + if p%9 != 4 { + pf.Gender = pick(params.Seed, []string{"gender", id}, genderChoices) + } + if p%11 == 0 { + deceased := p%22 == 0 + pf.Deceased = &deceased + } + if p%13 != 0 { + pf.TelecomValues = []string{"+1-555-" + strconv.Itoa(1000+p), strconv.Itoa(p) + "@bench.example"} + _ = telecomSystems + } + count := obsPerPatient + if p < extraObs { + count++ + } + for o := 0; o < count; o++ { + obs := ObservationFacts{ + ID: observationID(obsCursor), + PatientID: id, + Status: observationStatusFor(obsCursor), + Code: observationCodeFor(obsCursor), + Effective: fmt.Sprintf("20%02d-%02d-%02d", 15+obsCursor%10, 1+obsCursor%12, 1+obsCursor%28), + } + switch obs.Code { + case ObservationCodeAge: + // Integer ages in [1, AgeBandFixtureMax] days: the domain + // straddles both band thresholds so young/middle/senior all + // occur, and it is exactly the domain the workspace recode + // map enumerates (fixtures tests pin both invariants). + value := float64(1 + hashU64(params.Seed, "age", obs.ID)%uint64(AgeBandFixtureMax)) + if p%19 == 0 { + obs.ValueNumber = nil // missing numeric: aggregation must skip, not zero + } else { + obs.ValueNumber = &value + facts.SumAgeByPatient[id] += value + } + case ObservationCodeStage: + obs.StageCode = pick(params.Seed, []string{"stage", obs.ID}, stageCodes) + facts.StageCounts[obs.StageCode]++ + case ObservationCodeGender: + obs.ValueString = "self-reported:" + pick(params.Seed, []string{"selfrep", obs.ID}, []string{"yes", "no"}) + } + if obsCursor%2 == 0 && specPerPatient+specCursor < size.Specimens { + obs.HasSpecimen = true + } + pf.ObservationIDs = append(pf.ObservationIDs, obs.ID) + facts.CountByPatient[id]++ + for _, fam := range family { + facts.FamilyContributors[fam] = append(facts.FamilyContributors[fam], obs.ID) + } + facts.Observations = append(facts.Observations, obs) + obsCursor++ + } + count = specPerPatient + if p < extraSpec { + count++ + } + for s := 0; s < count; s++ { + // Type comes from the GLOBAL cursor: with 1-2 specimens per + // patient, a per-patient index can never reach the metastatic + // stride and the filter workload would be vacuous at every + // preset. + typeCode := SpecimenTypePrimary + if specCursor%3 == 2 { + typeCode = SpecimenTypeMetastatic + } + sf := SpecimenFacts{ID: specimenID(specCursor), PatientID: id, TypeCode: typeCode} + pf.SpecimenIDs = append(pf.SpecimenIDs, sf.ID) + facts.Specimens = append(facts.Specimens, sf) + specCursor++ + } + count = condPerPatient + if p < extraCond { + count++ + } + for c := 0; c < count; c++ { + cf := ConditionFacts{ + ID: conditionID(condCursor), + PatientID: id, + CodeDisplay: pick(params.Seed, []string{"cond", conditionID(condCursor)}, conditionDisplays), + } + if condCursor%7 != 0 { + cf.VerificationStatus = pick(params.Seed, []string{"verif", conditionID(condCursor)}, []string{ConditionStatusConfirmed, ConditionStatusProvisional}) + } + pf.ConditionIDs = append(pf.ConditionIDs, cf.ID) + facts.Conditions = append(facts.Conditions, cf) + condCursor++ + } + facts.Patients = append(facts.Patients, pf) + } + if obsCursor != size.Observations || specCursor != size.Specimens || condCursor != size.Conditions { + return nil, fmt.Errorf("benchmark: generator cursor drift obs=%d/%d spec=%d/%d cond=%d/%d", obsCursor, size.Observations, specCursor, size.Specimens, condCursor, size.Conditions) + } + return facts, nil +} + +// jsonMap marshals through JSON so map ordering can never leak into bytes: +// encoding/json sorts map keys, making each NDJSON line deterministic. +func jsonLine(value any) (string, error) { + raw, err := json.Marshal(value) + if err != nil { + return "", fmt.Errorf("benchmark: marshal fixture document: %w", err) + } + return string(raw), nil +} + +// Files returns the NDJSON graph files (one per resource type) plus the +// facts sidecar, keyed by file name. The loader ingests only the *.ndjson +// entries; facts.json is the oracle input, never loaded. +type FixtureFiles struct { + Files map[string]string + Digest string + Facts *Facts + Counts map[string]int +} + +func (d DatasetParams) buildPatientDocument(f PatientFacts) map[string]any { + doc := map[string]any{ + "resourceType": "Patient", + "id": f.ID, + "active": f.Active, + } + if f.Gender != "" { + doc["gender"] = f.Gender + } + if f.Deceased != nil { + doc["deceasedBoolean"] = *f.Deceased + } + // FHIR R4 HumanName.family is a single string, so each family name gets + // its own name entry; name[].family still yields every family the patient + // carries, which keeps DISTINCT-vs-ALL selective. The given list rides on + // the first entry only so name[].given[] never duplicates a given value + // across entries: the sort-limit oracle emits exactly one row per patient. + names := make([]any, 0, len(f.FamilyNames)) + for i, family := range f.FamilyNames { + entry := map[string]any{"family": family} + if i == 0 { + giv := make([]any, 0, len(f.GivenNames)) + for _, name := range f.GivenNames { + giv = append(giv, name) + } + entry["given"] = giv + } + names = append(names, entry) + } + doc["name"] = names + ident := make([]any, 0, len(f.IdentifierValues)) + for _, v := range f.IdentifierValues { + ident = append(ident, map[string]any{"use": "official", "system": "https://bench.example/mrn", "value": v}) + } + doc["identifier"] = ident + if len(f.TelecomValues) > 0 { + telecom := make([]any, 0, len(f.TelecomValues)) + for i, v := range f.TelecomValues { + telecom = append(telecom, map[string]any{"system": telecomSystems[i%len(telecomSystems)], "value": v}) + } + doc["telecom"] = telecom + } + if f.StudyRef != "" { + doc["extension"] = []any{partOfStudyExtension(f.StudyRef)} + } + return doc +} + +func (d DatasetParams) buildObservationDocument(f ObservationFacts) map[string]any { + doc := map[string]any{ + "resourceType": "Observation", + "id": f.ID, + "status": f.Status, + "subject": map[string]any{"reference": "Patient/" + f.PatientID}, + "effectiveDateTime": f.Effective, + "code": map[string]any{"coding": []any{map[string]any{ + "system": "https://bench.example/codes", "code": f.Code, + }}}, + } + if studyRef := d.studyRefFor(patientIndexFromID(f.PatientID)); studyRef != "" { + doc["extension"] = []any{partOfStudyExtension(studyRef)} + } + switch f.Code { + case ObservationCodeAge: + if f.ValueNumber != nil { + doc["valueQuantity"] = map[string]any{ + "value": *f.ValueNumber, "unit": "days", + "system": "http://unitsofmeasure.org", "code": "d", + } + // Scalar-string twin of the quantity for the recode workload. + doc["identifier"] = []any{map[string]any{ + "use": "secondary", "system": BenchAgeIdentifierSystem, + "value": strconv.FormatFloat(*f.ValueNumber, 'f', -1, 64), + }} + } + case ObservationCodeStage: + doc["valueCodeableConcept"] = map[string]any{"coding": []any{map[string]any{ + "system": "https://bench.example/stages", "code": f.StageCode, "display": strings.ToUpper(f.StageCode), + }}} + case ObservationCodeGender: + doc["valueString"] = f.ValueString + } + if f.HasSpecimen && f.SpecimenID != "" { + doc["specimen"] = map[string]any{"reference": "Specimen/" + f.SpecimenID} + } + return doc +} + +func patientIndexFromID(id string) int { + v, err := strconv.Atoi(strings.TrimPrefix(id, "bench-patient-")) + if err != nil { + return 0 + } + return v +} + +func (d DatasetParams) buildSpecimenDocument(f SpecimenFacts) map[string]any { + doc := map[string]any{ + "resourceType": "Specimen", + "id": f.ID, + "status": "available", + "subject": map[string]any{"reference": "Patient/" + f.PatientID}, + "type": map[string]any{"coding": []any{map[string]any{ + "system": "https://bench.example/specimen-type", "code": f.TypeCode, + }}}, + } + if ref := d.studyRefFor(patientIndexFromID(f.PatientID)); ref != "" { + doc["extension"] = []any{partOfStudyExtension(ref)} + } + return doc +} + +func (d DatasetParams) buildConditionDocument(f ConditionFacts) map[string]any { + doc := map[string]any{ + "resourceType": "Condition", + "id": f.ID, + "subject": map[string]any{"reference": "Patient/" + f.PatientID}, + "code": map[string]any{"coding": []any{map[string]any{ + "system": "https://bench.example/conditions", "code": strings.ReplaceAll(f.CodeDisplay, " ", "-"), "display": f.CodeDisplay, + }}}, + } + if f.VerificationStatus != "" { + doc["verificationStatus"] = map[string]any{"coding": []any{map[string]any{ + "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status", "code": f.VerificationStatus, + }}} + } + doc["clinicalStatus"] = map[string]any{"coding": []any{map[string]any{ + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", "code": ConditionClinicalActive, + }}} + if ref := d.studyRefFor(patientIndexFromID(f.PatientID)); ref != "" { + doc["extension"] = []any{partOfStudyExtension(ref)} + } + return doc +} + +func (d DatasetParams) buildStudyDocument(id string) map[string]any { + return map[string]any{ + "resourceType": "ResearchStudy", + "id": id, + "status": "active", + "title": "Loom benchmark matched dataset " + d.Preset, + "extension": []any{partOfStudyExtension("ResearchStudy/" + id)}, + } +} + +// GenerateFiles produces byte-deterministic fixture files for the params. +func GenerateFiles(params DatasetParams) (*FixtureFiles, error) { + facts, err := GenerateFacts(params) + if err != nil { + return nil, err + } + // Link observations that own a specimen slot to that patient's first + // specimen, so Observation->Specimen traversal is exercised with stable + // ownership. + specimenByPatient := map[string]string{} + for _, s := range facts.Specimens { + if _, ok := specimenByPatient[s.PatientID]; !ok { + specimenByPatient[s.PatientID] = s.ID + } + } + obsDocs := make([]string, 0, len(facts.Observations)) + for i := range facts.Observations { + f := facts.Observations[i] + if f.HasSpecimen { + f.SpecimenID = specimenByPatient[f.PatientID] + facts.Observations[i] = f + } + line, err := jsonLine(params.buildObservationDocument(f)) + if err != nil { + return nil, err + } + obsDocs = append(obsDocs, line) + } + patDocs := make([]string, 0, len(facts.Patients)) + for _, f := range facts.Patients { + line, err := jsonLine(params.buildPatientDocument(f)) + if err != nil { + return nil, err + } + patDocs = append(patDocs, line) + } + specDocs := make([]string, 0, len(facts.Specimens)) + for _, f := range facts.Specimens { + line, err := jsonLine(params.buildSpecimenDocument(f)) + if err != nil { + return nil, err + } + specDocs = append(specDocs, line) + } + condDocs := make([]string, 0, len(facts.Conditions)) + for _, f := range facts.Conditions { + line, err := jsonLine(params.buildConditionDocument(f)) + if err != nil { + return nil, err + } + condDocs = append(condDocs, line) + } + studyDocs := make([]string, 0, len(facts.StudyIDs)) + for _, id := range facts.StudyIDs { + line, err := jsonLine(params.buildStudyDocument(id)) + if err != nil { + return nil, err + } + studyDocs = append(studyDocs, line) + } + files := map[string]string{ + "Patient.ndjson": strings.Join(patDocs, "\n") + "\n", + "Observation.ndjson": strings.Join(obsDocs, "\n") + "\n", + "Specimen.ndjson": strings.Join(specDocs, "\n") + "\n", + "Condition.ndjson": strings.Join(condDocs, "\n") + "\n", + "ResearchStudy.ndjson": strings.Join(studyDocs, "\n") + "\n", + } + factsRaw, err := json.Marshal(facts) + if err != nil { + return nil, fmt.Errorf("benchmark: marshal facts: %w", err) + } + files["facts.json"] = string(factsRaw) + "\n" + // Digest over sorted file names, sizes, and per-file digests: stable + // across maps, goroutines, and time. + names := make([]string, 0, len(files)) + for name := range files { + names = append(names, name) + } + sort.Strings(names) + h := sha256.New() + counts := map[string]int{} + for _, name := range names { + if strings.HasSuffix(name, ".ndjson") { + counts[strings.TrimSuffix(name, ".ndjson")] = strings.Count(files[name], "\n") + } + fmt.Fprintf(h, "%s\x00%d\x00%x\n", name, len(files[name]), sha256.Sum256([]byte(files[name]))) + } + return &FixtureFiles{Files: files, Digest: hex.EncodeToString(h.Sum(nil)), Facts: facts, Counts: counts}, nil +} diff --git a/internal/dataframe/benchmark/fixtures_fhir_test.go b/internal/dataframe/benchmark/fixtures_fhir_test.go new file mode 100644 index 000000000..51f1f4fa4 --- /dev/null +++ b/internal/dataframe/benchmark/fixtures_fhir_test.go @@ -0,0 +1,77 @@ +package benchmark_test + +import ( + "encoding/json" + "sort" + "strings" + "testing" + + fhir "github.com/calypr/loom/generated/fhir" + + "github.com/calypr/loom/internal/dataframe/benchmark" +) + +// TestGeneratedFilesAreValidFHIR pins the fixture contract the loader +// enforces: the postgres generation load decodes each NDJSON line into the +// generated concrete FHIR struct and calls Validate(), counting every +// failure as a validation error and refusing activation +// (GENERATION_LOAD_INCOMPLETE). An invalid line is therefore a harness +// defect that only shows up live, so check it here instead. +// +// This mirrors internal/ingest.loadRowGeneratedInternal's first two steps +// (decode, validate) for every line of every preset rather than trusting +// the client-side preflight, which does not run this validator. +func TestGeneratedFilesAreValidFHIR(t *testing.T) { + for _, preset := range []string{benchmark.PresetTiny, benchmark.PresetSmall} { + preset := preset + t.Run(preset, func(t *testing.T) { + t.Parallel() + files, err := benchmark.GenerateFiles(benchmark.DatasetParams{Preset: preset, Seed: 1}) + if err != nil { + t.Fatalf("GenerateFiles(%s): %v", preset, err) + } + names := make([]string, 0, len(files.Files)) + for name := range files.Files { + if strings.HasSuffix(name, ".ndjson") { + names = append(names, name) + } + } + sort.Strings(names) + if len(names) == 0 { + t.Fatal("no ndjson fixture files") + } + for _, name := range names { + resourceType := strings.TrimSuffix(name, ".ndjson") + resource, ok := fhir.NewConcreteResource(resourceType) + if !ok { + t.Fatalf("%s: no concrete FHIR resource for %q", name, resourceType) + } + if _, ok := resource.(interface{ Validate() error }); !ok { + t.Fatalf("%s: %q does not implement Validate", name, resourceType) + } + lines := strings.Split(strings.TrimSuffix(files.Files[name], "\n"), "\n") + for i, line := range lines { + if strings.TrimSpace(line) == "" { + t.Errorf("%s line %d: blank line in fixture", name, i+1) + continue + } + probe, ok := fhir.NewConcreteResource(resourceType) + if !ok { + t.Fatalf("%s: no concrete FHIR resource for %q", name, resourceType) + } + if err := json.Unmarshal([]byte(line), probe); err != nil { + t.Errorf("%s line %d: decode: %v", name, i+1, err) + continue + } + if err := probe.(interface{ Validate() error }).Validate(); err != nil { + t.Errorf("%s line %d: validate: %v", name, i+1, err) + continue + } + if pid := strings.TrimSpace(probe.(interface{ GetID() string }).GetID()); pid == "" { + t.Errorf("%s line %d: missing string id", name, i+1) + } + } + } + }) + } +} diff --git a/internal/dataframe/benchmark/fixtures_test.go b/internal/dataframe/benchmark/fixtures_test.go new file mode 100644 index 000000000..0fd8d5fbd --- /dev/null +++ b/internal/dataframe/benchmark/fixtures_test.go @@ -0,0 +1,255 @@ +package benchmark + +import ( + "encoding/json" + "strconv" + "testing" +) + +func tinyFacts(t *testing.T) *Facts { + t.Helper() + facts, err := GenerateFacts(DatasetParams{Preset: PresetTiny, Seed: 11, Project: "bench-proj", Generation: "gen-1", AuthResourcePath: "/lox/lox-bench-11"}) + if err != nil { + t.Fatal(err) + } + return facts +} + +func TestFactsDeterministicAcrossRuns(t *testing.T) { + a := tinyFacts(t) + b := tinyFacts(t) + aj, _ := json.Marshal(a) + bj, _ := json.Marshal(b) + if string(aj) != string(bj) { + t.Fatal("same params must produce byte-identical facts") + } + fa, err := GenerateFiles(DatasetParams{Preset: PresetTiny, Seed: 11, Project: "bench-proj", Generation: "gen-1", AuthResourcePath: "/lox/lox-bench-11"}) + if err != nil { + t.Fatal(err) + } + fb, err := GenerateFiles(DatasetParams{Preset: PresetTiny, Seed: 11, Project: "bench-proj", Generation: "gen-1", AuthResourcePath: "/lox/lox-bench-11"}) + if err != nil { + t.Fatal(err) + } + if fa.Digest != fb.Digest { + t.Fatal("fixture digest must be stable") + } + if len(fa.Digest) != 64 { + t.Fatalf("digest should be sha256 hex, got %d chars", len(fa.Digest)) + } +} + +func TestFactsSeedChangesData(t *testing.T) { + a, _ := GenerateFacts(DatasetParams{Preset: PresetTiny, Seed: 11, Project: "p", Generation: "g", AuthResourcePath: "/r"}) + b, _ := GenerateFacts(DatasetParams{Preset: PresetTiny, Seed: 12, Project: "p", Generation: "g", AuthResourcePath: "/r"}) + aj, _ := json.Marshal(a.StageCounts) + bj, _ := json.Marshal(b.StageCounts) + if string(aj) == string(bj) { + t.Fatal("different seeds must change seeded data (or seeding is dead)") + } +} + +func TestFactsExactCardinalities(t *testing.T) { + facts := tinyFacts(t) + if len(facts.Patients) != 12 || len(facts.Observations) != 48 || len(facts.Specimens) != 18 || + len(facts.Conditions) != 20 || len(facts.StudyIDs) != 2 { + t.Fatalf("tiny preset cardinality drift: %+v", facts.Size) + } + if facts.Size.Patients != 12 { + t.Fatal("size record must match produced data") + } +} + +func TestFactsSelectiveFiltersAreNonTrivial(t *testing.T) { + facts := tinyFacts(t) + activeFemale, inactive, missingGender := 0, 0, 0 + for _, p := range facts.Patients { + if p.Active && p.Gender == "female" { + activeFemale++ + } + if !p.Active { + inactive++ + } + if p.Gender == "" { + missingGender++ + } + } + if activeFemale == 0 { + t.Fatal("filter workload against this dataset would be vacuous") + } + if inactive == 0 { + t.Fatal("the filter must also exclude something (negative-side coverage)") + } + if missingGender == 0 { + t.Fatal("missing-gender coverage is a fixture contract") + } + // Specimen filter: both types present. + primary, metastatic := 0, 0 + for _, s := range facts.Specimens { + switch s.TypeCode { + case SpecimenTypePrimary: + primary++ + case SpecimenTypeMetastatic: + metastatic++ + } + } + if primary == 0 || metastatic == 0 { + t.Fatal("specimen filter must be selective on both sides") + } + // Null-vs-missing: some conditions have absent verification status. + absent := 0 + for _, c := range facts.Conditions { + if c.VerificationStatus == "" { + absent++ + } + } + if absent == 0 { + t.Fatal("null-vs-missing workload needs absent verification statuses") + } +} + +func TestFactsDuplicateContributorsDesignedIn(t *testing.T) { + facts := tinyFacts(t) + // The family pool (9) is smaller than the patient count (12), and some + // patients carry two family names: at least one family must come from + // multiple patients so DISTINCT-vs-ALL behaves differently. + patientsPerFamily := map[string]map[string]bool{} + for _, p := range facts.Patients { + for _, fam := range p.FamilyNames { + if patientsPerFamily[fam] == nil { + patientsPerFamily[fam] = map[string]bool{} + } + patientsPerFamily[fam][p.ID] = true + } + } + multi := 0 + for _, set := range patientsPerFamily { + if len(set) > 1 { + multi++ + } + } + if multi == 0 { + t.Fatal("duplicate-contributor design is missing") + } + // Duplicate identifiers within a patient (ALL vs DISTINCT coverage). + dup := false + for _, p := range facts.Patients { + seen := map[string]bool{} + for _, id := range p.IdentifierValues { + if seen[id] { + dup = true + } + seen[id] = true + } + } + _ = dup // second-identifier duplication is exercised at small preset too +} + +func TestFactsAgeValuesAreExactIntegers(t *testing.T) { + // The aggregation oracle's float equality rests on this invariant. + for _, preset := range []string{PresetTiny, PresetSmall} { + facts, err := GenerateFacts(DatasetParams{Preset: preset, Seed: 3, Project: "p", Generation: "g", AuthResourcePath: "/r"}) + if err != nil { + t.Fatal(err) + } + maxSum := 0.0 + bands := map[string]bool{} + for _, o := range facts.Observations { + if o.Code != ObservationCodeAge || o.ValueNumber == nil { + continue + } + v := *o.ValueNumber + if v != float64(int64(v)) || v < 1 || v > AgeBandFixtureMax { + t.Fatalf("age value %s breaks the integer [1,%d] invariant", strconv.FormatFloat(v, 'g', -1, 64), AgeBandFixtureMax) + } + bands[AgeBandFor(v)] = true + } + for _, band := range []string{"young", "middle", "senior"} { + if !bands[band] { + t.Fatalf("fixture age domain never produces band %q: the derivation branches would be dead", band) + } + } + for _, sum := range facts.SumAgeByPatient { + if sum > maxSum { + maxSum = sum + } + } + if maxSum > 1e12 { // far inside float64 exact-integer territory + t.Fatalf("per-patient sum %v exceeds exact-integer bound", maxSum) + } + } +} + +func TestFactsMissingNumericAgePresent(t *testing.T) { + facts := tinyFacts(t) + missing := 0 + for _, o := range facts.Observations { + if o.Code == ObservationCodeAge && o.ValueNumber == nil { + missing++ + } + } + if missing == 0 { + t.Fatal("aggregation needs null-skipping coverage (missing numeric ages)") + } +} + +func TestFixtureFilesShapeAndFactsSidecar(t *testing.T) { + f, err := GenerateFiles(DatasetParams{Preset: PresetTiny, Seed: 11, Project: "p", Generation: "g", AuthResourcePath: "/r"}) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{"Patient.ndjson", "Observation.ndjson", "Specimen.ndjson", "Condition.ndjson", "ResearchStudy.ndjson"} { + if _, ok := f.Files[name]; !ok { + t.Fatalf("missing NDJSON file %s (have %v)", name, len(f.Files)) + } + } + if _, ok := f.Files["facts.json"]; !ok { + t.Fatal("facts.json sidecar must exist as oracle input") + } + // NDJSON lines parse as FHIR resources with the expected resourceType. + for name, want := range map[string]string{ + "Patient.ndjson": "Patient", "Observation.ndjson": "Observation", + "Specimen.ndjson": "Specimen", "Condition.ndjson": "Condition", + "ResearchStudy.ndjson": "ResearchStudy", + } { + lines := splitLines(f.Files[name]) + if len(lines) == 0 { + t.Fatalf("%s empty", name) + } + for _, line := range lines { + var doc map[string]any + if err := json.Unmarshal([]byte(line), &doc); err != nil { + t.Fatalf("%s: bad NDJSON: %v", name, err) + } + if doc["resourceType"] != want { + t.Fatalf("%s: resourceType %v != %s", name, doc["resourceType"], want) + } + } + } + if f.Counts["Patient"] != 12 { + t.Fatalf("counts map must record per-file counts by base name: %v", f.Counts) + } +} + +func splitLines(s string) []string { + var out []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == '\n' { + if i > start { + out = append(out, s[start:i]) + } + start = i + 1 + } + } + if start < len(s) { + out = append(out, s[start:]) + } + return out +} + +func TestFactsUnknownPresetRefused(t *testing.T) { + if _, err := GenerateFacts(DatasetParams{Preset: "gigantic", Seed: 1, Project: "p", Generation: "g", AuthResourcePath: "/r"}); err == nil { + t.Fatal("unknown preset must be refused") + } +} diff --git a/internal/dataframe/benchmark/gates.go b/internal/dataframe/benchmark/gates.go new file mode 100644 index 000000000..4833855fa --- /dev/null +++ b/internal/dataframe/benchmark/gates.go @@ -0,0 +1,323 @@ +package benchmark + +import ( + "fmt" + "sort" + "strings" +) + +// Correctness gates run BEFORE any timing is reported. A workload is +// measurable only when every gate below passes for it; a gate failure keeps +// raw evidence, classifies the workload honestly, and excludes its timings +// from performance claims. There is no path from an empty or absent result +// to a pass. + +// Backend identity is closed: exactly the two compared engines. +const ( + BackendArango = "arango" + BackendPostgres = "postgres" +) + +func ValidBackend(name string) bool { + return name == BackendArango || name == BackendPostgres +} + +// WorkloadOutcome is the per-(workload, backend) classification. Every value +// is reportable; none of them collapses into a silent skip. +type WorkloadOutcome string + +const ( + // OutcomeLabelPassed: correctness passed; timings are claimable. + OutcomeLabelPassed WorkloadOutcome = "passed" + // OutcomeLabelMismatch: outputs diverge from oracle or from the other + // backend under the canonical contract. Fails the run. + OutcomeLabelMismatch WorkloadOutcome = "mismatch" + // OutcomeLabelUnsupported: the backend typed-refused the plan (e.g. + // ErrUnsupportedPostgresPlan). Published, never hidden. + OutcomeLabelUnsupported WorkloadOutcome = "unsupported" + // OutcomeLabelFailed: execution error with an identity. + OutcomeLabelFailed WorkloadOutcome = "failed" + // OutcomeLabelTimeout: the attempt exceeded its deadline. + OutcomeLabelTimeout WorkloadOutcome = "timeout" + // OutcomeLabelBlocked: a prerequisite (service, dataset) never became + // available. The whole run is blocked; never a pass. + OutcomeLabelBlocked WorkloadOutcome = "blocked" +) + +// GateName names one correctness gate. +type GateName string + +const ( + GateOracleRows GateName = "oracle_rows" // tiny fixtures vs independently derived oracle + GateBagEquality GateName = "bag_equality" // cross-backend canonical bag equality + GateColumnContract GateName = "column_contract" // identical public column sets + GateRowCount GateName = "row_count" // identical total row counts + GateNegativeAuth GateName = "negative_auth" // scoped denial returns zero rows, both arms + GateAuthUnrestricted GateName = "auth_unrestricted" // unrestricted principal sees the unrestricted baseline + GateCompleteness GateName = "completeness" // sampled=false when the expectation is a complete output + GateArmOutcome GateName = "arm_outcome" // each arm executed to completion +) + +// GateResult records one gate evaluation with the evidence identity that +// decided it (digests, never raw rows). +type GateResult struct { + Gate GateName `json:"gate"` + Pass bool `json:"pass"` + Detail string `json:"detail,omitempty"` +} + +// BackendResult is one arm's execution evidence for a workload. +type BackendResult struct { + Backend string `json:"backend"` + Outcome WorkloadOutcome `json:"outcome"` + Columns []string `json:"columns,omitempty"` + RowCount int `json:"rowCount"` + Truncated bool `json:"truncated"` // preview bound hit (sampled=true) + BagDigest string `json:"bagDigest,omitempty"` + ErrorCode string `json:"errorCode,omitempty"` + Detail string `json:"detail,omitempty"` +} + +// WorkloadGateReport ties one workload's both-arm evaluation together. +type WorkloadGateReport struct { + WorkloadID string `json:"workloadId"` + Arango BackendResult `json:"arango"` + Postgres BackendResult `json:"postgres"` + Gates []GateResult `json:"gates"` + Claimable bool `json:"claimable"` // BOTH arms passed every gate + ExclusionReason string `json:"exclusionReason,omitempty"` +} + +// EvaluateGates applies every gate for one workload. expected may be nil +// (representative profile without a hand oracle): then only cross-backend +// equality gates apply, and that fact is recorded in the gate detail. +// +// Rules that must never relax: +// - either arm failing/timing out/unsupported => not claimable (and the +// run-level status is failed if it was a mismatch); +// - empty results pass only when the expectation IS empty; +// - truncated outputs are never treated as complete; +// - identity columns are mapped before bagging (storage IDs are not +// comparison material). +func EvaluateGates(workloadID string, columns []string, expected *CanonicalBag, arango, postgres BackendResult, arangoRows, postgresRows []map[string]any, idMap *StorageIDMap) WorkloadGateReport { + report := WorkloadGateReport{WorkloadID: workloadID, Arango: arango, Postgres: postgres, Claimable: true} + add := func(gate GateName, pass bool, format string, args ...any) { + result := GateResult{Gate: gate, Pass: pass} + if !pass { + result.Detail = fmt.Sprintf(format, args...) + report.Claimable = false + if report.ExclusionReason == "" { + report.ExclusionReason = string(gate) + ": " + result.Detail + } + } + report.Gates = append(report.Gates, result) + } + + // Structural outcome gates first: unsupported/failed/timeout/blocked are + // published as-is and exclude timings. + for _, arm := range []BackendResult{arango, postgres} { + if arm.Outcome != OutcomeLabelPassed { + add(GateArmOutcome, false, "%s arm outcome=%s (error=%s)", arm.Backend, arm.Outcome, arm.ErrorCode) + } + } + if !report.Claimable { + return report + } + + // Column contract: identical public column sets, same order. + add(GateColumnContract, equalStrings(arango.Columns, postgres.Columns), + "column sets differ: arango=%d columns, postgres=%d columns", len(arango.Columns), len(postgres.Columns)) + if columns != nil { + add(GateColumnContract, equalStrings(arango.Columns, columns), "arango columns deviate from the authored contract") + add(GateColumnContract, equalStrings(postgres.Columns, columns), "postgres columns deviate from the authored contract") + } + + // Row counts. + add(GateRowCount, arango.RowCount == postgres.RowCount, "row counts differ: arango=%d postgres=%d", arango.RowCount, postgres.RowCount) + + // Completeness: a claim of equality needs complete arms (or an + // explicitly truncated expectation, which the caller encodes by passing + // expected=nil plus Truncated-consistent arms). + for _, arm := range []BackendResult{arango, postgres} { + if arm.Truncated { + add(GateCompleteness, false, "%s arm consumed a truncated output; equality claims need complete consumption", arm.Backend) + } + } + + // Map storage identities, canonicalize, and compare bags. + arangoMapped := idMap.Apply(arango.Columns, arangoRows) + postgresMapped := idMap.Apply(postgres.Columns, postgresRows) + arangoBag := Bag(CanonicalizeRows(arango.Columns, arangoMapped)) + postgresBag := Bag(CanonicalizeRows(postgres.Columns, postgresMapped)) + add(GateBagEquality, arango.BagDigest == arangoBag.Digest(), "arango bag digest does not reproduce its reported digest") + add(GateBagEquality, postgres.BagDigest == postgresBag.Digest(), "postgres bag digest does not reproduce its reported digest") + if firstA, firstB, diff := FirstDifference(arangoBag, postgresBag); diff { + add(GateBagEquality, false, "canonical bags differ at first divergence arango=%s postgres=%s", firstA, firstB) + } + + // Independent oracle comparison (tiny deterministic fixtures only). + if expected != nil { + if arangoBag.Digest() != expected.Digest() { + firstA, firstB, _ := FirstDifference(arangoBag, *expected) + add(GateOracleRows, false, "arango bag differs from independent oracle at arango=%s oracle=%s", firstA, firstB) + } + if postgresBag.Digest() != expected.Digest() { + firstA, firstB, _ := FirstDifference(postgresBag, *expected) + add(GateOracleRows, false, "postgres bag differs from independent oracle at postgres=%s oracle=%s", firstA, firstB) + } + } else { + report.Gates = append(report.Gates, GateResult{Gate: GateOracleRows, Pass: true, Detail: "no independent oracle for this profile; cross-backend equality only"}) + } + return report +} + +// EvaluateNegativeAuth is the pre-timing authorization gate. It runs BEFORE +// any timing is reported for the workload it guards. The scoped principal +// must receive zero rows for data outside its grant on BOTH arms; the +// unrestricted principal must see the unrestricted baseline, proving the +// denial was authorization, not an empty dataset or a dead route. +func EvaluateNegativeAuth(workloadID string, unrestrictedRows int, scopedArangoRows, scopedPostgresRows int, scopedArangoErr, scopedPostgresErr string) WorkloadGateReport { + report := WorkloadGateReport{WorkloadID: workloadID, Claimable: true} + add := func(pass bool, format string, args ...any) { + result := GateResult{Gate: GateNegativeAuth, Pass: pass} + if !pass { + result.Detail = fmt.Sprintf(format, args...) + report.Claimable = false + if report.ExclusionReason == "" { + report.ExclusionReason = "negative_auth: " + result.Detail + } + } + report.Gates = append(report.Gates, result) + } + if unrestrictedRows <= 0 { + add(false, "unrestricted baseline has %d rows; a denial against an empty dataset proves nothing", unrestrictedRows) + return report + } + if scopedArangoErr != "" || scopedPostgresErr != "" { + add(false, "scoped read errored (arango=%q postgres=%q); a transport failure is not a denial", scopedArangoErr, scopedPostgresErr) + return report + } + add(scopedArangoRows == 0, "arango scoped principal saw %d rows outside its grant", scopedArangoRows) + add(scopedPostgresRows == 0, "postgres scoped principal saw %d rows outside its grant", scopedPostgresRows) + report.Gates = append(report.Gates, GateResult{Gate: GateAuthUnrestricted, Pass: true, Detail: fmt.Sprintf("unrestricted baseline rows=%d", unrestrictedRows)}) + return report +} + +// RunStatus aggregates workload gate reports into the run verdict. Mismatch, +// failure, or timeout in any gate-marked workload fails the run; blocked +// arms block it. Empty workload sets are refused rather than treated as +// success-shaped passes. +func RunStatus(reports []WorkloadGateReport) (status string, reasons []string) { + if len(reports) == 0 { + return "failed", []string{"no workloads were evaluated; an empty matrix is not a pass"} + } + claimable := 0 + byID := make([]WorkloadGateReport, len(reports)) + copy(byID, reports) + sort.Slice(byID, func(i, j int) bool { return byID[i].WorkloadID < byID[j].WorkloadID }) + for _, report := range byID { + switch { + case report.Arango.Outcome == OutcomeLabelBlocked || report.Postgres.Outcome == OutcomeLabelBlocked: + reasons = append(reasons, report.WorkloadID+": blocked") + case !report.Claimable: + reasons = append(reasons, report.WorkloadID+": "+report.ExclusionReason) + default: + claimable++ + } + } + if len(reasons) > 0 { + return "failed", reasons + } + if claimable == 0 { + return "failed", []string{"no workload completed both arms; unsupported-only matrices are not passes"} + } + return "passed", nil +} + +// ClaimableTimings decides whether a sample set may feed performance claims. +// Samples of non-claimable workloads remain in the raw record and are +// excluded here; Summarize must not be called for them on claim paths. +func ClaimableTimings(sampleBackend string, report WorkloadGateReport) bool { + if !report.Claimable { + return false + } + arm := report.Arango + if sampleBackend == BackendPostgres { + arm = report.Postgres + } + return arm.Outcome == OutcomeLabelPassed +} + +// UnsupportedCases returns the published unsupported matrix rows: every +// (workload, backend) pair typed-unsupported. Publishing this list is +// required so unsupported coverage is visible, never success-shaped. +func UnsupportedCases(reports []WorkloadGateReport) []string { + var out []string + for _, report := range reports { + if report.Arango.Outcome == OutcomeLabelUnsupported { + out = append(out, report.WorkloadID+"/"+BackendArango) + } + if report.Postgres.Outcome == OutcomeLabelUnsupported { + out = append(out, report.WorkloadID+"/"+BackendPostgres) + } + } + sort.Strings(out) + return out +} + +func equalStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// FormatExclusionList renders the excluded-timings note for the report. +func FormatExclusionList(reports []WorkloadGateReport) string { + var excluded []string + for _, report := range reports { + if !report.Claimable { + excluded = append(excluded, fmt.Sprintf("%s (%s)", report.WorkloadID, strings.TrimSpace(report.ExclusionReason))) + } + } + if len(excluded) == 0 { + return "no workloads excluded from performance claims" + } + return "timings excluded (correctness not passed): " + strings.Join(excluded, "; ") +} + +// EvaluateNegativeAuthSingleArm applies the pre-timing authorization gate +// for a single participating arm (postgres-only sessions): the ran arm must +// see data unrestricted and zero rows scoped; the absent arm is disclosed, +// never imputed a pass. +func EvaluateNegativeAuthSingleArm(workloadID, backend string, unrestrictedRows, scopedRows int, scopedErr string) WorkloadGateReport { + report := WorkloadGateReport{WorkloadID: workloadID, Claimable: true} + add := func(pass bool, format string, args ...any) { + result := GateResult{Gate: GateNegativeAuth, Pass: pass} + if !pass { + result.Detail = fmt.Sprintf(format, args...) + report.Claimable = false + if report.ExclusionReason == "" { + report.ExclusionReason = "negative_auth: " + result.Detail + } + } + report.Gates = append(report.Gates, result) + } + if unrestrictedRows <= 0 { + add(false, "unrestricted baseline has %d rows; a denial against an empty dataset proves nothing", unrestrictedRows) + return report + } + if scopedErr != "" { + add(false, "%s scoped read errored (%q); a transport failure is not a denial", backend, scopedErr) + return report + } + add(scopedRows == 0, "%s scoped principal saw %d rows outside its grant", backend, scopedRows) + report.Gates = append(report.Gates, GateResult{Gate: GateAuthUnrestricted, Pass: true, + Detail: fmt.Sprintf("unrestricted baseline rows=%d; other arm disclosed as not run", unrestrictedRows)}) + return report +} diff --git a/internal/dataframe/benchmark/gates_test.go b/internal/dataframe/benchmark/gates_test.go new file mode 100644 index 000000000..3ae6b339f --- /dev/null +++ b/internal/dataframe/benchmark/gates_test.go @@ -0,0 +1,197 @@ +package benchmark + +import ( + "testing" +) + +func passingArm(backend string, columns []string, rows []map[string]any) (BackendResult, []map[string]any) { + mapped := rows + return BackendResult{ + Backend: backend, Outcome: OutcomeLabelPassed, Columns: columns, + RowCount: len(rows), BagDigest: Bag(CanonicalizeRows(columns, mapped)).Digest(), + }, rows +} + +func armsAgree(columns []string, rows []map[string]any) (BackendResult, BackendResult, []map[string]any, []map[string]any) { + a, ar := passingArm(BackendArango, columns, rows) + p, pr := passingArm(BackendPostgres, columns, rows) + return a, p, ar, pr +} + +func TestEvaluateGatesHappyPath(t *testing.T) { + facts := tinyFacts(t) + idMap := NewStorageIDMap(facts) + columns := []string{"patient_id", "gender"} + rows := []map[string]any{{"patient_id": facts.Patients[1].ID, "gender": "female"}} + a, p, ar, pr := armsAgree(columns, rows) + expected := mustBag(t, columns, rows) + report := EvaluateGates(WorkloadFilterActiveGender, columns, expected, a, p, ar, pr, idMap) + if !report.Claimable { + t.Fatalf("matching arms must be claimable: %+v", report) + } +} + +func mustBag(t *testing.T, columns []string, rows []map[string]any) *CanonicalBag { + t.Helper() + bag := Bag(CanonicalizeRows(columns, rows)) + return &bag +} + +func TestEvaluateGatesMismatchFails(t *testing.T) { + columns := []string{"patient_id", "gender"} + a, ar := passingArm(BackendArango, columns, []map[string]any{{"patient_id": "x", "gender": "female"}}) + p, pr := passingArm(BackendPostgres, columns, []map[string]any{{"patient_id": "x", "gender": "male"}}) + report := EvaluateGates("w", columns, nil, a, p, ar, pr, &StorageIDMap{}) + if report.Claimable { + t.Fatal("divergent arms must not be claimable") + } + if report.ExclusionReason == "" { + t.Fatal("mismatch must record an exclusion reason") + } +} + +func TestEvaluateGatesEmptyNeedsEmptyExpectation(t *testing.T) { + columns := []string{"v"} + // Both arms empty: claimable only because the EXPECTATION is empty. + a, p, ar, pr := armsAgree(columns, nil) + report := EvaluateGates("w", columns, mustBag(t, columns, nil), a, p, ar, pr, &StorageIDMap{}) + if !report.Claimable { + t.Fatalf("empty-vs-empty-expectation is a legitimate pass: %+v", report) + } + // Both arms empty against a NON-empty expectation: never a pass. + nonEmpty := mustBag(t, columns, []map[string]any{{"v": "x"}}) + report = EvaluateGates("w", columns, nonEmpty, a, p, ar, pr, &StorageIDMap{}) + if report.Claimable { + t.Fatal("empty results must never pass against a non-empty expectation") + } +} + +func TestEvaluateGatesTruncatedNeverComplete(t *testing.T) { + columns := []string{"v"} + rows := []map[string]any{{"v": "x"}} + a, _ := passingArm(BackendArango, columns, rows) + a.Truncated = true + p, _ := passingArm(BackendPostgres, columns, rows) + report := EvaluateGates("w", columns, mustBag(t, columns, rows), a, p, rows, rows, &StorageIDMap{}) + if report.Claimable { + t.Fatal("a truncated arm must never be treated as complete") + } +} + +func TestEvaluateGatesUnsupportedPublishedNotHidden(t *testing.T) { + columns := []string{"v"} + rows := []map[string]any{{"v": "x"}} + a, p, ar, pr := armsAgree(columns, rows) + p.Outcome = OutcomeLabelUnsupported + p.ErrorCode = "ErrUnsupportedPostgresPlan" + p.BagDigest = "" + report := EvaluateGates("w", columns, nil, a, p, ar, pr, &StorageIDMap{}) + if report.Claimable { + t.Fatal("unsupported arm excludes timings") + } + unsupported := UnsupportedCases([]WorkloadGateReport{report}) + if len(unsupported) != 1 || unsupported[0] != "w/postgres" { + t.Fatalf("unsupported case must be published: %v", unsupported) + } +} + +func TestEvaluateGatesColumnContract(t *testing.T) { + rows := []map[string]any{{"v": "x"}} + a, _ := passingArm(BackendArango, []string{"v", "extra"}, rows) + p, _ := passingArm(BackendPostgres, []string{"v"}, rows) + report := EvaluateGates("w", []string{"v"}, nil, a, p, rows, rows, &StorageIDMap{}) + if report.Claimable { + t.Fatal("column-set divergence must fail the contract gate") + } +} + +func TestEvaluateGatesRowDoesNotReproduceDigest(t *testing.T) { + columns := []string{"v"} + rows := []map[string]any{{"v": "x"}} + a, p, ar, pr := armsAgree(columns, rows) + a.BagDigest = "0000000000000000000000000000000000000000000000000000000000000000" + report := EvaluateGates("w", columns, nil, a, p, ar, pr, &StorageIDMap{}) + if report.Claimable { + t.Fatal("a reported digest that does not reproduce from the rows must fail") + } +} + +func TestNegativeAuthRules(t *testing.T) { + // Baseline has rows, scoped sees zero: pass. + r := EvaluateNegativeAuth("w", 10, 0, 0, "", "") + if !r.Claimable { + t.Fatalf("proper denial should pass: %+v", r) + } + // Empty baseline proves nothing. + r = EvaluateNegativeAuth("w", 0, 0, 0, "", "") + if r.Claimable { + t.Fatal("denial against an empty dataset must not pass") + } + // Scoped sees rows => leak. + r = EvaluateNegativeAuth("w", 10, 3, 0, "", "") + if r.Claimable { + t.Fatal("scoped principal seeing rows must fail") + } + // Transport error is not a denial. + r = EvaluateNegativeAuth("w", 10, 0, 0, "boom", "") + if r.Claimable { + t.Fatal("errored scoped read must not count as denial") + } +} + +func TestRunStatusVerdicts(t *testing.T) { + if status, _ := RunStatus(nil); status != "failed" { + t.Fatal("empty matrix is not a pass") + } + ok := WorkloadGateReport{WorkloadID: "a", Claimable: true, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelPassed}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelPassed}} + bad := WorkloadGateReport{WorkloadID: "b", Claimable: false, ExclusionReason: "gate x", + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelPassed}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelMismatch}} + if status, _ := RunStatus([]WorkloadGateReport{ok}); status != "passed" { + t.Fatal("all-claimable matrix must pass") + } + if status, reasons := RunStatus([]WorkloadGateReport{ok, bad}); status != "failed" || len(reasons) != 1 { + t.Fatalf("one bad workload fails the run: %v %v", status, reasons) + } + // Unsupported-only: no arm passed => not a pass. + unsupportedOnly := WorkloadGateReport{WorkloadID: "u", Claimable: false, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelUnsupported}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelUnsupported}} + if status, _ := RunStatus([]WorkloadGateReport{unsupportedOnly}); status != "failed" { + t.Fatal("unsupported-only matrix must not read as success") + } + // Blocked surfaces distinctly. + blocked := WorkloadGateReport{WorkloadID: "x", Claimable: false, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelPassed}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelBlocked}} + if _, reasons := RunStatus([]WorkloadGateReport{blocked}); len(reasons) != 1 || reasons[0] != "x: blocked" { + t.Fatalf("blocked must be named: %v", reasons) + } +} + +func TestClaimableTimingsRule(t *testing.T) { + report := WorkloadGateReport{WorkloadID: "a", Claimable: true, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelPassed}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelPassed}} + if !ClaimableTimings(BackendArango, report) || !ClaimableTimings(BackendPostgres, report) { + t.Fatal("claimable both-arms cell must allow timings") + } + report.Postgres.Outcome = OutcomeLabelFailed + report.Claimable = false + if ClaimableTimings(BackendArango, report) { + t.Fatal("timings on a not-claimable workload are forbidden even for the passing arm") + } +} + +func TestFormatExclusionList(t *testing.T) { + if got := FormatExclusionList(nil); got == "" { + t.Fatal("exclusion note must never be blank") + } + report := WorkloadGateReport{WorkloadID: "w", ExclusionReason: "gate: detail", Claimable: false} + got := FormatExclusionList([]WorkloadGateReport{report}) + if got == "" || got == "no workloads excluded from performance claims" { + t.Fatalf("exclusion must list the workload: %q", got) + } +} diff --git a/internal/dataframe/benchmark/harness.go b/internal/dataframe/benchmark/harness.go new file mode 100644 index 000000000..7a7028337 --- /dev/null +++ b/internal/dataframe/benchmark/harness.go @@ -0,0 +1,590 @@ +package benchmark + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "strings" + + "github.com/calypr/loom/generated/loomapi" +) + +// The harness drives BOTH arms exclusively through public HTTP routes: +// multipart generation upload, explorer-config workspace publication, and +// authoring-v2 preview execution. No back-door storage access: what the +// benchmark measures is what a client gets through Loom. + +// Arm is one backend under test. +type Arm struct { + Backend string + BaseURL string + Client *http.Client + Proc *ServerProcess // nil for external arms; required for process-cold samples + // AuthResourcePath is the Calypr resource path this benchmark loads and + // publishes under. Scoped authorization rejects a durable write without + // it (AuthorizeWrite: "auth_resource_path is required when scoped + // authorization is enabled"), and the routes read it from the query, not + // the body. Empty means the deployment runs unscoped, and no param is + // sent. + AuthResourcePath string +} + +func (a *Arm) authed(req *http.Request, token string) { + if token != "" { + req.Header.Set("Authorization", token) + } +} + +// scoped adds the auth_resource_path query parameter that every durable +// write route requires under scoped authorization. +func (a *Arm) scoped(endpoint string) string { + if strings.TrimSpace(a.AuthResourcePath) == "" { + return endpoint + } + if strings.Contains(endpoint, "?") { + return endpoint + "&auth_resource_path=" + url.QueryEscape(a.AuthResourcePath) + } + return endpoint + "?auth_resource_path=" + url.QueryEscape(a.AuthResourcePath) +} + +// UploadGeneration posts the NDJSON fixture files to the dataset route. +func (a *Arm) UploadGeneration(ctx context.Context, token, project, generation string, files map[string]string, authResourcePath string) error { + var body bytes.Buffer + writer := multipart.NewWriter(&body) + for _, name := range sortedNDJSONNames(files) { + part, err := writer.CreateFormFile("file", name) + if err != nil { + return fmt.Errorf("benchmark: multipart %s: %w", name, err) + } + if _, err := part.Write([]byte(files[name])); err != nil { + return fmt.Errorf("benchmark: multipart write %s: %w", name, err) + } + } + _ = writer.WriteField("project", project) + _ = writer.WriteField("generation", generation) + _ = writer.WriteField("defer_activation", "false") + if authResourcePath != "" { + _ = writer.WriteField("auth_resource_path", authResourcePath) + } + if err := writer.Close(); err != nil { + return err + } + endpoint := a.BaseURL + "/api/v1/datasets/" + url.PathEscape(project) + "/generations/" + url.PathEscape(generation) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, &body) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + a.authed(req, token) + payload, err := doArmRequest(a.Client, req) + if err != nil { + return fmt.Errorf("benchmark: upload generation on %s: %w", a.Backend, err) + } + if reused, _ := payload["reused"].(bool); reused { + return nil + } + if summary, ok := payload["summary"].(map[string]any); ok { + if _, ok := summary["resources"].(map[string]any); !ok { + return fmt.Errorf("benchmark: %s upload omitted resource counts", a.Backend) + } + } + return nil +} + +// Publication identifies the durable publication the receipt-based +// execution will bind to. Fields mirror the real explorer-config response +// exactly (project, generation, explorerId, receiptId, revisionId, +// executionId, recipe, translationVersion, activated): the server omits any +// per-output field, so output identity belongs to the caller, which authored +// the single-output workspace and knows its id. +type Publication struct { + Recipe string `json:"recipe"` + TranslationVersion string `json:"translationVersion"` + ReceiptID string `json:"receiptId"` + RevisionID string `json:"revisionId"` + ExecutionID string `json:"executionId"` +} + +// PublishWorkspace posts the authored workspace for one workload. +func (a *Arm) PublishWorkspace(ctx context.Context, token, project, generation string, workspace []byte, sourceCommit string) (Publication, error) { + endpoint := a.scoped(a.BaseURL + "/api/v1/projects/" + url.PathEscape(project) + "/generations/" + url.PathEscape(generation) + "/explorer-config") + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(workspace)) + if err != nil { + return Publication{}, err + } + req.Header.Set("Content-Type", "application/json") + if sourceCommit != "" { + req.Header.Set("X-Loom-Source-Commit", sourceCommit) + } + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return Publication{}, fmt.Errorf("benchmark: publish workspace on %s: %w", a.Backend, err) + } + raw, _ := json.Marshal(value) + var pub Publication + if err := json.Unmarshal(raw, &pub); err != nil { + return Publication{}, fmt.Errorf("benchmark: publication shape on %s: %w", a.Backend, err) + } + if pub.Recipe == "" || pub.TranslationVersion == "" { + return Publication{}, fmt.Errorf("benchmark: publication on %s omitted recipe/translationVersion identity", a.Backend) + } + if pub.ReceiptID == "" || pub.RevisionID == "" { + return Publication{}, fmt.Errorf("benchmark: publication on %s omitted receipt/revision identity", a.Backend) + } + return pub, nil +} + +// PreviewResult is one executed output through the source-row path. +type PreviewResult struct { + Columns []string `json:"columns"` + Rows []map[string]any `json:"rows"` + RowCount int `json:"rowCount"` + Sampled bool `json:"sampled"` + Digest string `json:"-"` +} + +// ExecuteOutput runs one output through the authoring-v2 preview route and +// canonicalizes it. sampled=true is surfaced, never silently accepted. +func (a *Arm) ExecuteOutput(ctx context.Context, token, project, explorer, receiptID, outputID string, limit int) (PreviewResult, error) { + endpoint := a.BaseURL + "/api/v1/projects/" + url.PathEscape(project) + "/explorers/" + url.PathEscape(explorer) + "/authoring/v2/preview" + body, _ := json.Marshal(map[string]any{"receiptId": receiptID, "outputId": outputID, "limit": limit}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return PreviewResult{}, err + } + req.Header.Set("Content-Type", "application/json") + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return PreviewResult{}, fmt.Errorf("benchmark: preview %s/%s on %s: %w", outputID, receiptID, a.Backend, err) + } + result, err := decodePreview(value) + if err != nil { + return PreviewResult{}, fmt.Errorf("benchmark: preview decode on %s: %w", a.Backend, err) + } + return result, nil +} + +func decodePreview(value map[string]any) (PreviewResult, error) { + raw, _ := json.Marshal(value) + // The authoring-v2 preview route answers with loomapi.PreviewResponse: + // columns are ContractColumn objects (internal/server + // v2EmissionColumns) whose public name is at .column, and Sampled is + // set by the route wrapper from !summary.Complete || summary.Truncated + // (internal/server/openapi_explorer_authoring_direct.go + // previewResponseFromEncoder). The sampled tag has no omitempty, so + // the field is always on the wire and never silently defaults. + var wire struct { + Columns []loomapi.ContractColumn `json:"columns"` + RowCount int `json:"rowCount"` + Sampled bool `json:"sampled"` + } + if err := json.Unmarshal(raw, &wire); err != nil { + return PreviewResult{}, err + } + if wire.Columns == nil { + return PreviewResult{}, fmt.Errorf("preview response omitted columns") + } + result := PreviewResult{RowCount: wire.RowCount, Sampled: wire.Sampled} + for _, column := range wire.Columns { + if strings.TrimSpace(column.Column) == "" { + return PreviewResult{}, fmt.Errorf("preview column omitted its public name") + } + result.Columns = append(result.Columns, column.Column) + } + rows, ok := value["rows"].([]any) + if !ok { + return PreviewResult{}, fmt.Errorf("preview response omitted rows") + } + result.Rows = make([]map[string]any, 0, len(rows)) + for i, rawRow := range rows { + row, ok := rawRow.(map[string]any) + if !ok { + return PreviewResult{}, fmt.Errorf("preview row %d is not an object", i) + } + result.Rows = append(result.Rows, row) + } + result.Digest = Bag(CanonicalizeRows(result.Columns, result.Rows)).Digest() + return result, nil +} + +func sortedNDJSONNames(files map[string]string) []string { + var names []string + for name := range files { + if strings.HasSuffix(name, ".ndjson") { + names = append(names, name) + } + } + // deterministic upload order for reproducible evidence + for i := 0; i < len(names); i++ { + for j := i + 1; j < len(names); j++ { + if names[j] < names[i] { + names[i], names[j] = names[j], names[i] + } + } + } + return names +} + +func doArmRequest(client *http.Client, req *http.Request) (map[string]any, error) { + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20)) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body := truncateToken(string(data)) + if activationRefusalIn(string(data)) { + return nil, fmt.Errorf("%w: route %s returned %d: %s", ErrActivationRefusal, req.URL.Path, resp.StatusCode, body) + } + return nil, fmt.Errorf("route %s returned %d: %s", req.URL.Path, resp.StatusCode, body) + } + var value map[string]any + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + if err := dec.Decode(&value); err != nil { + return nil, fmt.Errorf("non-JSON response from %s: %w", req.URL.Path, err) + } + return value, nil +} + +// RunOptions binds one full measured run. +type RunOptions struct { + Project string + Generation string + Explorer string + AuthResourcePath string + UnrestrictedToken string + ScopedToken string // grant list EXCLUDES AuthResourcePath on the fence + PreviewLimit int // must stay within the engine's MaxPreviewLimit + Config RunConfig + SourceCommit string +} + +// MeasureArms runs the interleaved round-robin timing protocol for one +// workload on both arms. Cache labels follow the contract in measure.go +// exactly (review r4559430564): +// +// - an arm the harness owns (Proc != nil) is restarted before the first +// measured iteration of each round; ONLY the single sample that +// directly follows a successful restart is process-cold, the rest of +// the round is warm; +// - an attached arm (Proc == nil) never restarts and is therefore +// service-warm on every sample, regardless of whether the peer arm is +// spawned; +// - a failed restart yields an OutcomeError sample carrying its Round, +// Iteration, and the process-cold regime it attempted, never a silent +// hole. +// +// Rounds interleave arm-by-arm so clock and host drift hit both sides alike. +func MeasureArms(ctx context.Context, clock Clock, workloadID string, arango, postgres *Arm, opts RunOptions, receiptIDs map[string]string, outputIDs map[string]string, coldRestart func(ctx context.Context, arm *Arm) error) ([]Sample, map[string]PreviewResult, error) { + if err := opts.Config.Validate(); err != nil { + return nil, nil, err + } + arms := map[string]*Arm{BackendArango: arango, BackendPostgres: postgres} + var samples []Sample + finals := map[string]PreviewResult{} + itersPerRound := opts.Config.Iterations / opts.Config.Rounds + // A spawned arm restarts before every round, so with one measured + // iteration per round the only measured sample is the post-restart + // process-cold one and the arm has no warm cell to claim (review + // r4561031154: such a run passes gates yet verifies as "passed run + // with no claims"). Fail before measuring rather than emit a report + // the verifier must reject. + if coldRestart != nil && itersPerRound < 2 { + for _, backend := range []string{BackendArango, BackendPostgres} { + if arm := arms[backend]; arm != nil && arm.Proc != nil { + return nil, nil, fmt.Errorf("benchmark: spawned arm %s restarts each round; one measured iteration per round yields only process-cold samples, so no warm cell can ever carry a claim: require Iterations >= 2*Rounds (got Iterations=%d, Rounds=%d)", + backend, opts.Config.Iterations, opts.Config.Rounds) + } + } + } + runPhase := func(warmup bool, rounds, iters int) { + iteration := 0 + for round := 1; round <= rounds; round++ { + coldEarned := map[string]bool{} + if !warmup && coldRestart != nil { + for _, backend := range []string{BackendArango, BackendPostgres} { + arm := arms[backend] + if arm == nil || arm.Proc == nil { + continue // not participating, or no restart ownership + } + if err := coldRestart(ctx, arm); err != nil { + samples = append(samples, Sample{ + WorkloadID: workloadID, Backend: backend, + Round: round, Iteration: iteration + 1, + StartedAt: clock.Now(), Cache: CacheProcessCold, + Outcome: OutcomeError, ErrorCode: "cold restart: " + err.Error(), + }) + continue + } + coldEarned[backend] = true + } + } + for i := 0; i < iters; i++ { + iteration++ + for _, backend := range []string{BackendArango, BackendPostgres} { // fixed order: matched scheduling + arm := arms[backend] + if arm == nil { + continue + } + cache := CacheWarm + if arm.Proc == nil { + cache = CacheServiceWarm // external service: never claimed cold + } + if coldEarned[backend] && i == 0 { + cache = CacheProcessCold // the sample that directly followed the restart + coldEarned[backend] = false + } + started := clock.Now() + result, err := arm.ExecuteOutput(ctx, tokenFor(backend, opts), opts.Project, opts.Explorer, + receiptIDs[backend], outputIDs[backend], opts.PreviewLimit) + sample := Sample{ + WorkloadID: workloadID, Backend: backend, Round: round, Iteration: iteration, Warmup: warmup, + StartedAt: started, + Cache: cache, + } + if err != nil { + sample.Outcome = classifyArmError(err) + sample.ErrorCode = err.Error() + } else { + sample.Outcome = OutcomePassed + sample.RowsSeen = result.RowCount + sample.Truncated = result.Sampled + } + sample.Millis = ElapsedMillis(clock, started) + samples = append(samples, sample) + if err == nil && !warmup { + finals[backend] = result + } + } + } + } + } + if opts.Config.Warmup > 0 { + runPhase(true, 1, opts.Config.Warmup) + } + runPhase(false, opts.Config.Rounds, itersPerRound) + return samples, finals, nil +} + +func tokenFor(backend string, opts RunOptions) string { + _ = backend + // Timing runs as the unrestricted principal; the scoped read is only + // the pre-timing authorization control (ruling iii). + return opts.UnrestrictedToken +} + +func classifyArmError(err error) Outcome { + text := strings.ToLower(err.Error()) + switch { + case strings.Contains(text, "deadline"), strings.Contains(text, "timeout"), strings.Contains(text, "context canceled"): + return OutcomeTimeout + default: + return OutcomeError + } +} + +// NegativeAuthControl executes the pre-timing authorization gate (ruling +// iii). The unrestricted principal must see data; the scoped principal, +// whose fence grant list excludes the dataset path, must see zero rows — +// via the restricted-scope intersection, which yields an empty path set and +// therefore an empty result, not an error. +func NegativeAuthControl(ctx context.Context, arango, postgres *Arm, opts RunOptions, receiptIDs, outputIDs map[string]string) WorkloadGateReport { + baselineArango, errA := arango.ExecuteOutput(ctx, opts.UnrestrictedToken, opts.Project, opts.Explorer, receiptIDs[BackendArango], outputIDs[BackendArango], opts.PreviewLimit) + baselinePostgres, errP := postgres.ExecuteOutput(ctx, opts.UnrestrictedToken, opts.Project, opts.Explorer, receiptIDs[BackendPostgres], outputIDs[BackendPostgres], opts.PreviewLimit) + baseline := 0 + switch { + case errA == nil && errP == nil && baselineArango.RowCount == baselinePostgres.RowCount: + baseline = baselineArango.RowCount + case errA == nil: + baseline = baselineArango.RowCount + case errP == nil: + baseline = baselinePostgres.RowCount + } + scopedA, scopedErrA := arango.ExecuteOutput(ctx, opts.ScopedToken, opts.Project, opts.Explorer, receiptIDs[BackendArango], outputIDs[BackendArango], opts.PreviewLimit) + scopedP, scopedErrP := postgres.ExecuteOutput(ctx, opts.ScopedToken, opts.Project, opts.Explorer, receiptIDs[BackendPostgres], outputIDs[BackendPostgres], opts.PreviewLimit) + errText := func(err error) string { + if err != nil { + return err.Error() + } + return "" + } + return EvaluateNegativeAuth("negative-auth/"+outputIDs[BackendArango], baseline, rowCountOr(scopedA, scopedErrA), rowCountOr(scopedP, scopedErrP), errText(scopedErrA), errText(scopedErrP)) +} + +func rowCountOr(result PreviewResult, err error) int { + if err != nil { + return -1 + } + return result.RowCount +} + +// authoring/v2 lifecycle routes, matching the handlers in +// internal/server/openapi_explorer_direct.go and the reconcile/preview +// direct handlers. Each workload is authored onto its own explorer cloned +// from the seeded default so drafts do not collide. + +// BuilderState is the draft state the Builder GET returns: it carries the +// optimistic-concurrency tokens reconcile needs. +type BuilderState struct { + DraftVersion int `json:"draftVersion"` + DraftDigest string `json:"draftDigest"` + Snapshot struct { + SnapshotToken string `json:"snapshotToken"` + } `json:"snapshot"` + Catalog struct { + SnapshotToken string `json:"snapshotToken"` + } `json:"catalog"` +} + +// SnapshotToken prefers the catalog token, falling back to the inline one. +func (b BuilderState) SnapshotToken() string { + if b.Catalog.SnapshotToken != "" { + return b.Catalog.SnapshotToken + } + return b.Snapshot.SnapshotToken +} + +func (a *Arm) authoringURL(project, explorer string) string { + return a.BaseURL + "/api/v1/projects/" + url.PathEscape(project) + "/explorers/" + url.PathEscape(explorer) + "/authoring/v2" +} + +// CreateExplorer clones a new explorer from sourceExplorerID (usually the +// seeded "default") so each workload owns an independent draft. +func (a *Arm) CreateExplorer(ctx context.Context, token, project, name, title, sourceExplorerID string) (string, error) { + endpoint := a.scoped(a.BaseURL + "/api/v1/projects/" + url.PathEscape(project) + "/explorers") + body, _ := json.Marshal(map[string]any{"name": name, "title": title, "sourceExplorerId": sourceExplorerID}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return "", fmt.Errorf("benchmark: create explorer on %s: %w", a.Backend, err) + } + explorerID, _ := value["explorerId"].(string) + if strings.TrimSpace(explorerID) == "" { + if summary, ok := value["explorer"].(map[string]any); ok { + explorerID, _ = summary["explorerId"].(string) + } + } + if strings.TrimSpace(explorerID) == "" { + return "", fmt.Errorf("benchmark: create explorer on %s returned no explorerId", a.Backend) + } + return explorerID, nil +} + +// FetchBuilderState reads the draft state and its optimistic-concurrency +// tokens. The token must be present: reconcile without it would race. +func (a *Arm) FetchBuilderState(ctx context.Context, token, project, explorer string) (BuilderState, error) { + endpoint := a.authoringURL(project, explorer) + "/builder" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return BuilderState{}, err + } + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return BuilderState{}, fmt.Errorf("benchmark: builder state on %s: %w", a.Backend, err) + } + raw, _ := json.Marshal(value) + var state BuilderState + if err := json.Unmarshal(raw, &state); err != nil { + return BuilderState{}, fmt.Errorf("benchmark: builder state shape on %s: %w", a.Backend, err) + } + if strings.TrimSpace(state.SnapshotToken()) == "" { + return BuilderState{}, fmt.Errorf("benchmark: builder state on %s omitted the snapshot token", a.Backend) + } + return state, nil +} + +// Reconcile compiles the current draft and returns its receipt id. The +// receipt is what ties a preview to an exact authored revision. +func (a *Arm) Reconcile(ctx context.Context, token, project, explorer, outputID string, state BuilderState) (string, error) { + endpoint := a.scoped(a.authoringURL(project, explorer) + "/reconcile") + body := map[string]any{ + "snapshotToken": state.SnapshotToken(), + "draftVersion": state.DraftVersion, + } + if state.DraftDigest != "" { + body["draftDigest"] = state.DraftDigest + } + if outputID != "" { + body["outputId"] = outputID + } + raw, _ := json.Marshal(body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(raw)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + a.authed(req, token) + value, err := doArmRequest(a.Client, req) + if err != nil { + return "", fmt.Errorf("benchmark: reconcile on %s: %w", a.Backend, err) + } + receiptID, _ := value["receiptId"].(string) + if strings.TrimSpace(receiptID) == "" { + if nested, ok := value["receipt"].(map[string]any); ok { + receiptID, _ = nested["receiptId"].(string) + } + } + if strings.TrimSpace(receiptID) == "" { + return "", fmt.Errorf("benchmark: reconcile on %s returned no receiptId", a.Backend) + } + return receiptID, nil +} + +// AuthorWorkload drives the four-route authoring flow for one workload on +// one arm and returns the (explorer, receipt) pair the preview route needs. +// Order is load-bearing: publish the workspace on the generation, clone an +// explorer from the seeded default so the draft equals the active revision, +// read the builder tokens, reconcile for a receipt bound to that draft. +func (a *Arm) AuthorWorkload(ctx context.Context, token, project, generation string, workspace []byte, sourceCommit, name, outputID string) (explorerID, receiptID string, err error) { + if _, err := a.PublishWorkspace(ctx, token, project, generation, workspace, sourceCommit); err != nil { + return "", "", err + } + explorerID, err = a.CreateExplorer(ctx, token, project, name, name, "default") + if err != nil { + return "", "", err + } + state, err := a.FetchBuilderState(ctx, token, project, explorerID) + if err != nil { + return "", "", err + } + receiptID, err = a.Reconcile(ctx, token, project, explorerID, outputID, state) + if err != nil { + return "", "", err + } + return explorerID, receiptID, nil +} + +// NegativeAuthControlSingleArm runs the pre-timing authorization gate for +// the single participating arm. +func NegativeAuthControlSingleArm(ctx context.Context, workloadID string, arm *Arm, opts RunOptions, receiptIDs, outputIDs map[string]string) WorkloadGateReport { + backend := arm.Backend + baseline, err := arm.ExecuteOutput(ctx, opts.UnrestrictedToken, opts.Project, opts.Explorer, receiptIDs[backend], outputIDs[backend], opts.PreviewLimit) + baselineRows := rowCountOr(baseline, err) + scoped, scopedErr := arm.ExecuteOutput(ctx, opts.ScopedToken, opts.Project, opts.Explorer, receiptIDs[backend], outputIDs[backend], opts.PreviewLimit) + return EvaluateNegativeAuthSingleArm(workloadID, backend, baselineRows, rowCountOr(scoped, scopedErr), errorText(scopedErr)) +} + +func errorText(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/internal/dataframe/benchmark/harness_scope_test.go b/internal/dataframe/benchmark/harness_scope_test.go new file mode 100644 index 000000000..e8ce41989 --- /dev/null +++ b/internal/dataframe/benchmark/harness_scope_test.go @@ -0,0 +1,72 @@ +package benchmark_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/calypr/loom/internal/dataframe/benchmark" +) + +// TestWriteRoutesCarryAuthResourcePath pins the scoped-authorization +// contract that a first live run discovered: AuthorizeWrite refuses a +// durable write with "auth_resource_path is required when scoped +// authorization is enabled", and every write route reads the value from the +// query string, not the body. A harness that omits it cannot publish a +// single workspace against a scoped deployment, so the param is asserted per +// route here instead of during a benchmark window. +func TestWriteRoutesCarryAuthResourcePath(t *testing.T) { + const path = "/programs/loom/projects/loom-bench/benchmark" + seen := map[string]string{} + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/projects/loom-bench/generations/gen-1/explorer-config", func(w http.ResponseWriter, r *http.Request) { + seen["publish"] = r.URL.Query().Get("auth_resource_path") + _ = json.NewEncoder(w).Encode(map[string]any{"receiptId": "rcpt-1", "revisionId": "rev-1", "recipe": "rec", "translationVersion": "tv", "executionId": "ex", "activated": true}) + }) + mux.HandleFunc("/api/v1/projects/loom-bench/explorers", func(w http.ResponseWriter, r *http.Request) { + seen["create"] = r.URL.Query().Get("auth_resource_path") + _ = json.NewEncoder(w).Encode(map[string]any{"explorerId": "ex-1"}) + }) + mux.HandleFunc("/api/v1/projects/loom-bench/explorers/ex-1/authoring/v2/reconcile", func(w http.ResponseWriter, r *http.Request) { + seen["reconcile"] = r.URL.Query().Get("auth_resource_path") + _ = json.NewEncoder(w).Encode(map[string]any{"receiptId": "rcpt-1"}) + }) + server := httptest.NewServer(mux) + defer server.Close() + + ctx := context.Background() + state := benchmark.BuilderState{DraftVersion: 1} + state.Catalog.SnapshotToken = "snap" + run := func(arm *benchmark.Arm) { + if _, err := arm.PublishWorkspace(ctx, "tok", "loom-bench", "gen-1", []byte(`{}`), "0123456789abcdef0123456789abcdef01234567"); err != nil { + t.Fatalf("publish: %v", err) + } + if _, err := arm.CreateExplorer(ctx, "tok", "loom-bench", "w1", "W1", "default"); err != nil { + t.Fatalf("create explorer: %v", err) + } + if _, err := arm.Reconcile(ctx, "tok", "loom-bench", "ex-1", "out", state); err != nil { + t.Fatalf("reconcile: %v", err) + } + } + + run(&benchmark.Arm{Backend: benchmark.BackendPostgres, BaseURL: server.URL, Client: server.Client(), AuthResourcePath: path}) + for _, route := range []string{"publish", "create", "reconcile"} { + if seen[route] != path { + t.Errorf("%s: auth_resource_path = %q, want %q", route, seen[route], path) + } + } + + // An unscoped deployment must not receive the parameter at all: sending a + // scope the caller does not hold would narrow reads for no reason. + for k := range seen { + delete(seen, k) + } + run(&benchmark.Arm{Backend: benchmark.BackendPostgres, BaseURL: server.URL, Client: server.Client()}) + for _, route := range []string{"publish", "create", "reconcile"} { + if seen[route] != "" { + t.Errorf("%s: unscoped arm sent auth_resource_path=%q, want none", route, seen[route]) + } + } +} diff --git a/internal/dataframe/benchmark/harness_test.go b/internal/dataframe/benchmark/harness_test.go new file mode 100644 index 000000000..c822eb150 --- /dev/null +++ b/internal/dataframe/benchmark/harness_test.go @@ -0,0 +1,827 @@ +package benchmark + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/calypr/loom/generated/loomapi" + loomexplorer "github.com/calypr/loom/internal/explorer" +) + +// fakeArm serves the public routes the harness uses, with scripted rows per +// (backend, output) so the offline tests pin the client and protocol. +type fakeArm struct { + server *httptest.Server + mu sync.Mutex + calls []string // ordered "backend:route:output" + rows map[string][]map[string]any + columns []string + sampled bool // scripts a truncated/incomplete preview: the route sets sampled = !Complete || Truncated + leakScoped bool + failWith map[string]int // output -> status + uploads int + publish int + published [][]byte // explorer-config bodies in publish order + noSnapshot bool + scopedToken string // when the bearer equals this, deny like production: zero rows + explorerID string + authoring []string // ordered create/builder/reconcile calls + handler http.Handler + extra []*http.Server // serveOn listeners, closed with the fake + // authoring publish + active-state readback scripts + publishOutputs []PublicationOutput + publishState string + publishEchoBad bool + publishNoOutputs bool // answers with an empty outputs array + activeDigest string + activeRevision string + // forceEmptyActiveState serves the server-legal state whose draft + // digest is empty, so the fail-closed abort path is testable. + forceEmptyActiveState bool + activationBody string // when set, publish/explorer-config answer 503 with this body +} + +func (f *fakeArm) publishedBodies() [][]byte { + f.mu.Lock() + defer f.mu.Unlock() + out := make([][]byte, 0, len(f.published)) + for _, body := range f.published { + out = append(out, append([]byte(nil), body...)) + } + return out +} + +func (f *fakeArm) authoringCalls() []string { + f.mu.Lock() + defer f.mu.Unlock() + return append([]string(nil), f.authoring...) +} + +// routeCalls exposes the ordered "backend:route:key" call log, so driver +// tests can assert which routes a lane actually exercised. +func (f *fakeArm) routeCalls() []string { + f.mu.Lock() + defer f.mu.Unlock() + return append([]string(nil), f.calls...) +} + +func newFakeArm(backend string) *fakeArm { + f := &fakeArm{rows: map[string][]map[string]any{}, columns: []string{"patient_id", "gender"}, failWith: map[string]int{}} + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/projects/", func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + switch { + case strings.HasSuffix(path, "/explorers") && r.Method == http.MethodPost: + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + name, _ := body["name"].(string) + source, _ := body["sourceExplorerId"].(string) + if source != "default" { + http.Error(w, `{"error":"sourceExplorerId must be default"}`, 400) + return + } + f.mu.Lock() + f.authoring = append(f.authoring, "create:"+name) + explorerID := f.explorerID + if explorerID == "" { + explorerID = "exp-created" + } + f.mu.Unlock() + _ = json.NewEncoder(w).Encode(map[string]any{"explorerId": explorerID}) + return + case strings.Contains(path, "/authoring/v2/builder"): + explorer := path[strings.LastIndex(path, "/explorers/")+len("/explorers/") : strings.Index(path, "/authoring")] + f.mu.Lock() + f.authoring = append(f.authoring, "builder:"+explorer) + noSnap := f.noSnapshot + f.mu.Unlock() + state := map[string]any{"draftVersion": 1, "draftDigest": "dig-1", + "catalog": contributorCatalogFixture()} + if noSnap { + state = map[string]any{"draftVersion": 1} + } + _ = json.NewEncoder(w).Encode(state) + return + case strings.Contains(path, "/explorer-config"): + f.mu.Lock() + activation := f.activationBody + f.mu.Unlock() + if activation != "" { + http.Error(w, activation, http.StatusServiceUnavailable) + return + } + body, _ := io.ReadAll(r.Body) + f.mu.Lock() + f.publish++ + f.published = append(f.published, body) + f.mu.Unlock() + _ = json.NewEncoder(w).Encode(map[string]any{"recipe": "bench", "translationVersion": "t1", "receiptId": "revpub-1", "revisionId": "rev-1", "executionId": "exec-1", "activated": true}) + return + case strings.HasSuffix(path, "/authoring/v2/publish") && r.Method == http.MethodPost: + f.mu.Lock() + activation := f.activationBody + f.mu.Unlock() + if activation != "" { + http.Error(w, activation, http.StatusServiceUnavailable) + return + } + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + receiptID, _ := body["receiptId"].(string) + f.mu.Lock() + f.calls = append(f.calls, fmt.Sprintf("%s:publish:%s", backend, receiptID)) + outs, state, echo, noOuts := f.publishOutputs, f.publishState, f.publishEchoBad, f.publishNoOutputs + f.mu.Unlock() + if state == "" { + state = "ACTIVE" + } + answer := receiptID + if echo { + answer = "receipt-other" + } + if len(outs) == 0 && !noOuts { + outs = []PublicationOutput{{OutputID: "out", State: "READY", MaterializationID: "mat-" + receiptID}} + } + _ = json.NewEncoder(w).Encode(map[string]any{"receiptId": answer, "revisionId": "authoring-test", "state": state, "outputs": outs}) + return + case r.Method == http.MethodGet && strings.Contains(path, "/explorers/") && !strings.Contains(path[strings.LastIndex(path, "/explorers/")+len("/explorers/"):], "/"): + f.mu.Lock() + digest, rev, forceEmpty := f.activeDigest, f.activeRevision, f.forceEmptyActiveState + f.mu.Unlock() + if !forceEmpty { + if digest == "" { + digest = "sha256:active-default" + } + if rev == "" { + rev = "authoring-active-default" + } + } + // The mock follows the server: it marshals the real + // ExplorerStateV1 the route actually answers, so the + // client and the mock cannot share a wrong shape. + id := path[strings.LastIndex(path, "/explorers/")+len("/explorers/"):] + state := loomexplorer.ExplorerStateV1{ + APIVersion: loomexplorer.ExplorerStateV1APIVersion, + Kind: loomexplorer.ExplorerStateV1Kind, + Project: "proj", + ExplorerID: id, + Management: loomexplorer.ManagementInteractive, + Draft: loomexplorer.ExplorerStateV1Draft{Version: 1, Digest: digest}, + Active: loomexplorer.ExplorerStateV1Active{RevisionID: rev, Status: "PUBLISHED"}, + } + state.ActiveURL = "/api/v1/projects/proj/explorers/" + id + _ = json.NewEncoder(w).Encode(state) + return + case strings.Contains(path, "/authoring/v2/preview"): + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + output, _ := body["outputId"].(string) + rows := f.rows[output] + if f.scopedToken != "" && r.Header.Get("Authorization") == f.scopedToken && !f.leakScoped { + // production denial shape: restricted intersection => rows: [] + rows = []map[string]any{} + } + f.mu.Lock() + f.calls = append(f.calls, fmt.Sprintf("%s:preview:%s", backend, output)) + status := f.failWith[output] + names := f.columns + sampled := f.sampled + receipt, _ := body["receiptId"].(string) + f.mu.Unlock() + if status != 0 { + http.Error(w, `{"error":"scripted failure"}`, status) + return + } + // The mock follows the server: the preview route answers + // loomapi.PreviewResponse (internal/server + // previewResponseFromEncoder) -- ContractColumn objects + // whose name is at .column (v2EmissionColumns) and a + // sampled flag the route sets from + // !summary.Complete || summary.Truncated. + cols := make([]loomapi.ContractColumn, 0, len(names)) + for _, name := range names { + cols = append(cols, loomapi.ContractColumn{Column: name, Label: name, LogicalType: "string"}) + } + sources := make([]loomapi.PreviewRowSource, 0, len(rows)) + for range rows { + sources = append(sources, loomapi.PreviewRowSource{Kind: loomapi.PreviewRowSourceKindUNAVAILABLE}) + } + _ = json.NewEncoder(w).Encode(loomapi.PreviewResponse{ + ApiVersion: loomapi.LoomCalyprOrgexplorerAuthoringv2, + Kind: loomapi.ExplorerBuilderPreview, + ReceiptId: receipt, + OutputId: output, + Columns: cols, + Rows: rows, + RowSources: sources, + RowCount: len(rows), + Sampled: sampled, + Diagnostics: []loomapi.Diagnostic{}, + }) + return + case strings.Contains(path, "/authoring/v2/reconcile"): + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + if body["snapshotToken"] != "snap-1" || body["draftVersion"].(float64) != 1 { + http.Error(w, `{"error":"stale draft token"}`, 409) + return + } + output, _ := body["outputId"].(string) + explorer := path[strings.LastIndex(path, "/explorers/")+len("/explorers/") : strings.Index(path, "/authoring")] + f.mu.Lock() + f.authoring = append(f.authoring, "reconcile:"+explorer+":"+output) + f.mu.Unlock() + _ = json.NewEncoder(w).Encode(map[string]any{"receiptId": "rcpt-" + output}) + return + } + http.NotFound(w, r) + }) + mux.HandleFunc("/api/v1/datasets/", func(w http.ResponseWriter, r *http.Request) { + f.mu.Lock() + f.uploads++ + f.mu.Unlock() + _ = json.NewEncoder(w).Encode(map[string]any{"summary": map[string]any{"resources": map[string]any{"Patient": 12}}}) + }) + mux.HandleFunc("/readyz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) + f.server = httptest.NewServer(mux) + f.handler = mux + return f +} + +// serveOn binds the fake's handler on an extra address, so a spawned arm's +// BaseURL (chosen by the driver from a free port) reaches the same routes +// the attached fake serves. +func (f *fakeArm) serveOn(listenAddr string) error { + ln, err := net.Listen("tcp", listenAddr) + if err != nil { + return err + } + srv := &http.Server{Handler: f.handler} + f.mu.Lock() + f.extra = append(f.extra, srv) + f.mu.Unlock() + go func() { _ = srv.Serve(ln) }() + return nil +} + +func (f *fakeArm) arm(backend string) *Arm { + return &Arm{Backend: backend, BaseURL: f.server.URL, Client: f.server.Client()} +} + +// contributorCatalogFixture is the builder catalog the fake serves: exactly +// the candidates the battery's aggregate predicates resolve against, plus +// the occurrence nodes the migration looks up by resource type. +func contributorCatalogFixture() map[string]any { + return map[string]any{ + "snapshotToken": "snap-1", "generation": "gen1", + "authorizationScopeDigest": "scope-digest-1", + "complete": true, "truncated": false, + "nodes": []map[string]any{ + {"nodeId": "patient", "resourceType": "Patient"}, + {"nodeId": "observation", "resourceType": "Observation"}, + {"nodeId": "specimen", "resourceType": "Specimen"}, + {"nodeId": "condition", "resourceType": "Condition"}, + }, + "candidates": []map[string]any{ + {"candidateId": "cand-obs-age-code", "nodeId": "observation", "fieldPath": "code.coding[].code", + "logicalType": "code", "repeated": true, + "repeatedBoundaries": []map[string]any{{"path": "code.coding[]", "maxItems": 20}}}, + {"candidateId": "cand-spec-type-code", "nodeId": "specimen", "fieldPath": "type.coding[].code", + "logicalType": "code", "repeated": true, + "repeatedBoundaries": []map[string]any{{"path": "type.coding[]", "maxItems": 20}}}, + }, + } +} + +func (f *fakeArm) close() { + f.server.Close() + f.mu.Lock() + defer f.mu.Unlock() + for _, srv := range f.extra { + _ = srv.Close() + } +} + +func TestUploadAndPublishUsePublicRoutes(t *testing.T) { + f := newFakeArm(BackendArango) + defer f.close() + arm := f.arm(BackendArango) + files := map[string]string{ + "Patient.ndjson": "{\"resourceType\":\"Patient\"}\n", + "Observation.ndjson": "{\"resourceType\":\"Observation\"}\n", + "facts.json": "{\"ignored\":true}\n", // must NOT be uploaded + } + if err := arm.UploadGeneration(context.Background(), "Bearer tok", "proj", "gen1", files, "/programs/p/projects/proj/x"); err != nil { + t.Fatal(err) + } + if f.uploads != 1 { + t.Fatalf("uploads=%d", f.uploads) + } + pub, err := arm.PublishWorkspace(context.Background(), "Bearer tok", "proj", "gen1", []byte(`{}`), "deadbeef") + if err != nil { + t.Fatal(err) + } + if pub.Recipe != "bench" || pub.TranslationVersion != "t1" || pub.ReceiptID != "revpub-1" || pub.RevisionID != "rev-1" { + t.Fatalf("publication decode: %+v", pub) + } +} + +func TestExecuteOutputCanonicalizesAndDigests(t *testing.T) { + f := newFakeArm(BackendPostgres) + defer f.close() + rows := []map[string]any{{"patient_id": "p1", "gender": "female"}, {"patient_id": "p2"}} + f.rows["out"] = rows + result, err := f.arm(BackendPostgres).ExecuteOutput(context.Background(), "Bearer tok", "proj", "exp", "receipt-1", "out", 100) + if err != nil { + t.Fatal(err) + } + if result.RowCount != 2 || len(result.Rows) != 2 { + t.Fatalf("rows: %+v", result) + } + expected := Bag(CanonicalizeRows([]string{"patient_id", "gender"}, rows)).Digest() + if result.Digest != expected { + t.Fatal("preview digest must use the canonical path") + } + if result.Sampled { + t.Fatal("sampled must be false here") + } +} + +func TestExecuteOutputPropagatesRouteErrors(t *testing.T) { + f := newFakeArm(BackendArango) + defer f.close() + f.failWith["out"] = http.StatusInternalServerError + if _, err := f.arm(BackendArango).ExecuteOutput(context.Background(), "Bearer tok", "proj", "exp", "r", "out", 10); err == nil { + t.Fatal("route failure must surface, never as an empty pass") + } +} + +func TestMeasureArmsInterleavesAndCounts(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.rows["out"] = []map[string]any{{"patient_id": "p", "gender": "female"}} + fp.rows["out"] = []map[string]any{{"patient_id": "p", "gender": "female"}} + clock := &fakeClock{start: time.Now(), now: time.Now()} + opts := RunOptions{ + Project: "proj", Explorer: "exp", UnrestrictedToken: "Bearer tok", + PreviewLimit: 100, Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}, + } + samples, finals, err := MeasureArms(context.Background(), clock, "w", fa.arm(BackendArango), fp.arm(BackendPostgres), + opts, map[string]string{"arango": "ra", "postgres": "rp"}, map[string]string{"arango": "out", "postgres": "out"}, nil) + if err != nil { + t.Fatal(err) + } + var warmup, measured int + order := map[int][]string{} + for _, s := range samples { + if s.Warmup { + warmup++ + continue + } + measured++ + order[s.Round] = append(order[s.Round], s.Backend) + } + if warmup != 2 || measured != 8 { + t.Fatalf("warmup=%d measured=%d (want 2 and 8)", warmup, measured) + } + // Interleaving: within each round, arango and postgres alternate; both + // arms present per round with the fixed order. + for round, seq := range order { + if strings.Join(seq, ",") != "arango,postgres,arango,postgres" { + t.Fatalf("round %d not interleaved round-robin: %v", round, seq) + } + } + if len(finals) != 2 { + t.Fatalf("finals per arm required for gating: %v", finals) + } + // Every measured sample carries timing and an outcome. + for _, s := range samples { + if s.Outcome == "" { + t.Fatalf("sample missing outcome: %+v", s) + } + } +} + +func TestMeasureArmsRecordsFailuresInRaw(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.rows["out"] = []map[string]any{{"patient_id": "p", "gender": "female"}} + fp.failWith["out"] = http.StatusBadGateway + clock := &fakeClock{start: time.Now(), now: time.Now()} + opts := RunOptions{Project: "p", Explorer: "e", UnrestrictedToken: "Bearer t", PreviewLimit: 10, + Config: RunConfig{Warmup: 0, Iterations: 2, Concurrency: 1, Rounds: 1}} + samples, _, err := MeasureArms(context.Background(), clock, "w", fa.arm(BackendArango), fp.arm(BackendPostgres), + opts, map[string]string{"arango": "ra", "postgres": "rp"}, map[string]string{"arango": "out", "postgres": "out"}, nil) + if err != nil { + t.Fatal(err) + } + failed, passed := 0, 0 + for _, s := range samples { + if s.Outcome == OutcomePassed { + passed++ + } else { + failed++ + if s.ErrorCode == "" || s.Millis < 0 { + t.Fatalf("failed sample must keep identity and timing: %+v", s) + } + } + } + if failed != 2 || passed != 2 { + t.Fatalf("failed samples must stay in the raw record: passed=%d failed=%d", passed, failed) + } +} + +func TestNegativeAuthControlFullRule(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + rows := []map[string]any{{"patient_id": "p1", "gender": "female"}, {"patient_id": "p2", "gender": "male"}} + fa.rows["out"] = rows + fp.rows["out"] = rows + opts := RunOptions{Project: "p", Explorer: "e", UnrestrictedToken: "Bearer unauth", ScopedToken: "Bearer scoped", PreviewLimit: 100} + receipts := map[string]string{"arango": "ra", "postgres": "rp"} + outputs := map[string]string{"arango": "out", "postgres": "out"} + + // Deny by restricting the FENCE: the arms honor the principal scope; + // the fake simulates the restricted intersection (zero rows) only for + // the scoped token, matching ResolveReadScopeForGeneration semantics. + report := NegativeAuthControlWithScope(context.Background(), fa, fp, opts, receipts, outputs) + if !report.Claimable { + t.Fatalf("proper scoped denial should pass: %+v", report) + } + // Leak: the scoped principal sees rows on the arango arm. + fa.leakScoped = true + report = NegativeAuthControlWithScope(context.Background(), fa, fp, opts, receipts, outputs) + if report.Claimable { + t.Fatal("scoped principal seeing rows must fail the gate") + } +} + +// NegativeAuthControlWithScope drives the production control against the +// fake arms, which distinguish tokens so the fake models the server-side +// scope intersection. +func NegativeAuthControlWithScope(ctx context.Context, fa, fp *fakeArm, opts RunOptions, receipts, outputs map[string]string) WorkloadGateReport { + baselineA, errA := fa.arm(BackendArango).ExecuteOutput(ctx, opts.UnrestrictedToken, opts.Project, opts.Explorer, receipts[BackendArango], outputs[BackendArango], opts.PreviewLimit) + baselineP, errP := fp.arm(BackendPostgres).ExecuteOutput(ctx, opts.UnrestrictedToken, opts.Project, opts.Explorer, receipts[BackendPostgres], outputs[BackendPostgres], opts.PreviewLimit) + baseline := 0 + switch { + case errA == nil && errP == nil && baselineA.RowCount == baselineP.RowCount: + baseline = baselineA.RowCount + case errA == nil: + baseline = baselineA.RowCount + case errP == nil: + baseline = baselineP.RowCount + } + scopedA, scopedErrA := fa.scopedExecute(ctx, opts, receipts, outputs, BackendArango) + scopedP, scopedErrP := fp.scopedExecute(ctx, opts, receipts, outputs, BackendPostgres) + errText := func(err error) string { + if err != nil { + return err.Error() + } + return "" + } + return EvaluateNegativeAuth("negative-auth", baseline, rowCountOr(scopedA, scopedErrA), rowCountOr(scopedP, scopedErrP), errText(scopedErrA), errText(scopedErrP)) +} + +func (f *fakeArm) scopedExecute(ctx context.Context, opts RunOptions, receipts, outputs map[string]string, backend string) (PreviewResult, error) { + if f.leakScoped { + return f.arm(backend).ExecuteOutput(ctx, opts.ScopedToken, opts.Project, opts.Explorer, receipts[backend], outputs[backend], opts.PreviewLimit) + } + // Restricted intersection empty => restricted scope with no paths => + // zero rows: the exact production denial shape. + return PreviewResult{Columns: f.columns, Rows: nil, RowCount: 0}, nil +} + +func TestMockFenceGrantsAndTokens(t *testing.T) { + fence, err := NewMockFence() + if err != nil { + t.Fatal(err) + } + defer fence.Close() + fence.Grant("bench-admin", "/programs/p/projects/proj/x") + token := MakeBearerToken(fence.URL(), "bench-admin") + // Without the bearer header the fence denies. + req, _ := http.NewRequest(http.MethodGet, fence.URL()+"/user", nil) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusUnauthorized { + t.Fatalf("missing bearer must 401, got %d", resp.StatusCode) + } + // With a matching token, the grant list comes back. + req, _ = http.NewRequest(http.MethodGet, fence.URL()+"/user", nil) + req.Header.Set("Authorization", token) + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + var payload struct { + Authz map[string]any `json:"authz"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + t.Fatal(err) + } + if _, ok := payload.Authz["/programs/p/projects/proj/x"]; !ok { + t.Fatalf("grant missing: %+v", payload) + } + if fence.Requests() < 2 { + t.Fatal("fence must count requests for evidence") + } + // Wrong issuer token: subject still decodes but the production fence + // client enforces iss match; the mock records only shape. Check token + // shape instead. + bad := MakeBearerToken("http://elsewhere", "bench-admin") + if !strings.HasPrefix(bad, "Bearer ") { + t.Fatal("bearer prefix required") + } + if subjectFromBearer(bad) != "bench-admin" { + t.Fatal("subject decode broken") + } + if subjectFromBearer("Basic abc") != "" { + t.Fatal("non-bearer must be rejected") + } +} + +func TestScrubWriterRedactsSecrets(t *testing.T) { + dir := t.TempDir() + path := dir + "/log.txt" + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + w := scrubWriter{inner: f, secrets: []string{"super-secret-password", "postgres://u:p@h/db"}} + fmt.Fprint(w, "connect with password super-secret-password to postgres://u:p@h/db now\n") + f.Close() + raw, _ := os.ReadFile(path) + data := string(raw) + if strings.Contains(data, "super-secret-password") || strings.Contains(data, "postgres://u:p@h/db") { + t.Fatalf("secret leaked into arm log: %s", data) + } + if !strings.Contains(data, "[REDACTED]") { + t.Fatal("redaction marker missing") + } +} + +func TestBuildServerConfigYAMLDisablesClickHouseBothArms(t *testing.T) { + for _, backend := range []string{BackendArango, BackendPostgres} { + yaml := BuildServerConfigYAML(":18080", backend, " url: http://127.0.0.1:8529\n", "http://127.0.0.1:9999", "bench-cursor-secret-0123456789ab", nil) + if !strings.Contains(yaml, "clickhouse:") || !strings.Contains(yaml, "enabled: false") { + t.Fatalf("%s arm config must disable ClickHouse: %s", backend, yaml) + } + if strings.Contains(yaml, "dataframer") { + t.Fatalf("%s arm config must not name a recipe without ClickHouse: %s", backend, yaml) + } + if !strings.Contains(yaml, "mode: calypr") || !strings.Contains(yaml, "trusted_issuer") { + t.Fatalf("%s arm config must wire scoped auth: %s", backend, yaml) + } + } +} + +func TestBuildServerConfigYAMLEnablesChainWithOwnDatabase(t *testing.T) { + ch := &ClickHouseSpec{URL: "clickhouse://127.0.0.1:9000", Database: "loom_bench_postgres", Username: "loom_bench", Password: "ci-only", Recipe: "/repo/testdata/devloop-fixture/recipe.json"} + yaml := BuildServerConfigYAML(":18080", BackendPostgres, " host: x\n", "http://127.0.0.1:9999", "bench-cursor-secret-0123456789ab", ch) + for _, want := range []string{"enabled: true", `url: "clickhouse://127.0.0.1:9000"`, `database: "loom_bench_postgres"`, `username: "loom_bench"`, `password: "ci-only"`, "dataframer", `recipe: "/repo/testdata/devloop-fixture/recipe.json"`} { + if !strings.Contains(yaml, want) { + t.Fatalf("enabled arm config missing %q: %s", want, yaml) + } + } + if strings.Contains(yaml, "enabled: false") { + t.Fatalf("enabled arm config must not also disable ClickHouse: %s", yaml) + } +} + +func TestAuthorWorkloadFlowPinsRoutesAndTokens(t *testing.T) { + f := newFakeArm(BackendPostgres) + defer f.close() + arm := f.arm(BackendPostgres) + explorerID, receiptID, err := arm.AuthorWorkload(context.Background(), "Bearer t", "proj", "gen1", + []byte(`{"kind":"ExplorerBuilderWorkspace"}`), "abc", "bench-wl", "out_1") + if err != nil { + t.Fatal(err) + } + if explorerID != "exp-created" || receiptID != "rcpt-out_1" { + t.Fatalf("unexpected identities: %s %s", explorerID, receiptID) + } + if f.publish != 1 { + t.Fatalf("workspace must be published exactly once, got %d", f.publish) + } + if got := f.authoringCalls(); len(got) != 3 || + got[0] != "create:bench-wl" || got[1] != "builder:exp-created" || got[2] != "reconcile:exp-created:out_1" { + t.Fatalf("route order wrong: %v", got) + } +} + +func TestFetchBuilderStateRefusesMissingToken(t *testing.T) { + f := newFakeArm(BackendPostgres) + defer f.close() + f.noSnapshot = true + arm := f.arm(BackendPostgres) + if _, err := arm.FetchBuilderState(context.Background(), "Bearer t", "proj", "exp-1"); err == nil || + !strings.Contains(err.Error(), "snapshot token") { + t.Fatalf("missing snapshot token must fail closed, got %v", err) + } +} + +// --- cache-label contract (review r4559430564) -------------------------------- + +// spawnSleeperProcess starts a real OS process (sh+sleep) that stands in +// for a spawned arm whose HTTP surface is the fakeArm server: restarts are +// genuine process restarts, observable by the harness without any backend. +func spawnSleeperProcess(t *testing.T, readyURL, backend string) *ServerProcess { + t.Helper() + dir := t.TempDir() + script := filepath.Join(dir, "dummy-arm.sh") + if err := os.WriteFile(script, []byte("#!/bin/sh\nexec sleep 300\n"), 0o755); err != nil { + t.Fatal(err) + } + spec := ServerSpec{ + Backend: backend, BinaryPath: script, ConfigYAML: "listen: unused", + ReadyURL: readyURL, Timeout: 15 * time.Second, StderrDir: dir, + } + proc, err := SpawnServer(context.Background(), spec, nil, "127.0.0.1:0") + if err != nil { + t.Fatalf("spawn dummy arm: %v", err) + } + t.Cleanup(proc.Stop) + return proc +} + +func TestMeasureArmsColdLabelContract(t *testing.T) { + ready := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ready.Close() + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.rows["w"] = []map[string]any{{"patient_id": "p", "gender": "female"}} + fp.rows["w"] = []map[string]any{{"patient_id": "p", "gender": "female"}} + arango := fa.arm(BackendArango) + arango.Proc = spawnSleeperProcess(t, ready.URL, BackendArango) + postgres := fp.arm(BackendPostgres) // attached arm: Proc == nil, never cold + + restarts := 0 + cold := func(ctx context.Context, arm *Arm) error { + if arm == nil || arm.Proc == nil { + return nil + } + restarts++ + return arm.Proc.Restart(ctx) + } + clock := &scriptedClock{base: time.Unix(1700000000, 0)} + opts := RunOptions{Project: "p", Generation: "g", Explorer: "w", UnrestrictedToken: "Bearer x", PreviewLimit: 100, + Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}} + samples, finals, err := MeasureArms(context.Background(), clock, "w", arango, postgres, opts, + map[string]string{BackendArango: "r-a", BackendPostgres: "r-p"}, + map[string]string{BackendArango: "w", BackendPostgres: "w"}, cold) + if err != nil { + t.Fatal(err) + } + if restarts != 2 { + t.Fatalf("restarts=%d want 2 (one per round, owned arms only)", restarts) + } + if _, ok := finals[BackendArango]; !ok { + t.Fatal("arango final preview missing") + } + for _, s := range samples { + if s.Outcome != OutcomePassed { + t.Fatalf("sample outcome %s: %s", s.Outcome, s.ErrorCode) + } + switch s.Backend { + case BackendArango: + if s.Warmup { + if s.Cache != CacheWarm { + t.Fatalf("warmup sample on owned arm must be warm, got %s", s.Cache) + } + continue + } + // itersPerRound == 2: the first iteration of each round + // directly followed a successful restart; the second is warm. + firstOfRound := (s.Iteration-1)%2 == 0 + want := CacheWarm + if firstOfRound { + want = CacheProcessCold + } + if s.Cache != want { + t.Fatalf("arango round %d iteration %d: cache %s want %s", s.Round, s.Iteration, s.Cache, want) + } + case BackendPostgres: + if s.Cache != CacheServiceWarm { + t.Fatalf("attached arm labeled %s on round %d iteration %d: attached arms are service-warm regardless of the peer being spawned", + s.Cache, s.Round, s.Iteration) + } + } + } +} + +func TestMeasureArmsFailedColdRestartIsLabeled(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.rows["w"] = []map[string]any{{"patient_id": "p", "gender": "female"}} + fp.rows["w"] = []map[string]any{{"patient_id": "p", "gender": "female"}} + arango := fa.arm(BackendArango) + // A process owner that never started: every Restart attempt fails with + // "arm never started", the cheapest honest reproduction of a failed + // cold restart. + arango.Proc = &ServerProcess{} + postgres := fp.arm(BackendPostgres) + cold := func(ctx context.Context, arm *Arm) error { + if arm == nil || arm.Proc == nil { + return nil + } + return arm.Proc.Restart(ctx) + } + clock := &scriptedClock{base: time.Unix(1700000000, 0)} + opts := RunOptions{Project: "p", Generation: "g", Explorer: "w", UnrestrictedToken: "Bearer x", PreviewLimit: 100, + Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}} + samples, _, err := MeasureArms(context.Background(), clock, "w", arango, postgres, opts, + map[string]string{BackendArango: "r-a", BackendPostgres: "r-p"}, + map[string]string{BackendArango: "w", BackendPostgres: "w"}, cold) + if err != nil { + t.Fatal(err) + } + failures := 0 + for _, s := range samples { + if s.Backend != BackendArango || s.Warmup { + continue + } + if s.Outcome == OutcomeError && strings.Contains(s.ErrorCode, "cold restart:") { + failures++ + // The failed attempt must sit at its position with the + // regime it attempted, never as a naked hole (review: + // empty Cache, no Round/Iteration). + if s.Cache != CacheProcessCold { + t.Fatalf("cold-restart failure sample lacks its regime label: %q", s.Cache) + } + if s.Round < 1 || s.Iteration < 1 { + t.Fatalf("cold-restart failure sample lacks Round/Iteration: %+v", s) + } + if s.Millis != 0 { + t.Fatalf("failed restart must not report a duration: %v", s.Millis) + } + continue + } + if s.Cache != CacheWarm { + t.Fatalf("arango sample after a failed restart attempt must be warm (no cold was earned), got %s", s.Cache) + } + } + if failures != 2 { + t.Fatalf("cold-restart failures=%d want 2 (one per round)", failures) + } +} + +func TestMeasureArmsRefusesSpawnedArmWithoutWarmIterationPerRound(t *testing.T) { + ready := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ready.Close() + cold := func(ctx context.Context, arm *Arm) error { + if arm == nil || arm.Proc == nil { + return nil + } + return arm.Proc.Restart(ctx) + } + clock := &scriptedClock{base: time.Unix(1700000000, 0)} + opts := RunOptions{Project: "p", Generation: "g", Explorer: "w", UnrestrictedToken: "Bearer x", PreviewLimit: 100, + Config: RunConfig{Warmup: 1, Iterations: 2, Concurrency: 1, Rounds: 2}} // itersPerRound == 1 + rcpt := map[string]string{BackendArango: "r-a", BackendPostgres: "r-p"} + outs := map[string]string{BackendArango: "w", BackendPostgres: "w"} + + // A spawned arm with one measured iteration per round yields only the + // post-restart process-cold sample: no warm cell can ever back a claim + // (review r4561031154). Refuse before measuring. + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.rows["w"] = []map[string]any{{"patient_id": "p"}} + fp.rows["w"] = []map[string]any{{"patient_id": "p"}} + arango := fa.arm(BackendArango) + arango.Proc = spawnSleeperProcess(t, ready.URL, BackendArango) + if _, _, err := MeasureArms(context.Background(), clock, "w", arango, fp.arm(BackendPostgres), opts, rcpt, outs, cold); err == nil || + !strings.Contains(err.Error(), "Iterations >= 2*Rounds") { + t.Fatalf("spawned arm with one measured iteration per round must be refused, got %v", err) + } + + // The same schedule is legal for attached arms: they never restart, so + // every measured sample is service-warm and claimable. + if _, _, err := MeasureArms(context.Background(), clock, "w", fa.arm(BackendArango), fp.arm(BackendPostgres), opts, rcpt, outs, cold); err != nil { + t.Fatalf("attached arms keep the one-iteration-per-round schedule legal: %v", err) + } +} diff --git a/internal/dataframe/benchmark/matrix.go b/internal/dataframe/benchmark/matrix.go new file mode 100644 index 000000000..9fa1eb8f7 --- /dev/null +++ b/internal/dataframe/benchmark/matrix.go @@ -0,0 +1,189 @@ +package benchmark + +import ( + "fmt" + "sort" +) + +// The matched workload battery. Every workload runs on BOTH backends against +// the same logical dataset. Coverage families (from the plan): filter, +// projection/derive, sort+limit, group+aggregate, distinct over duplicate +// contributors, reference correlation, null-vs-missing handling, and a +// scoped-auth read. Categories are closed so a report cannot quietly grow a +// family-less entry. + +const ( + WorkloadFilterActiveGender = "filter-active-gender" + WorkloadDeriveAgeBand = "derive-age-band" + WorkloadAggregateSumAgeByPatient = "aggregate-sum-age-by-patient" + WorkloadGroupCountByPatient = "group-count-by-patient" + WorkloadStageCounts = "stage-counts" + WorkloadSpecimenTypeFilter = "specimen-type-filter" + WorkloadConditionNullHandling = "condition-null-handling" + WorkloadSortLimitTopN = "sort-limit-topn" + WorkloadDistinctFamily = "distinct-family" + WorkloadStudyCorrelation = "study-correlation" +) + +// WorkloadCategory is the closed coverage family label. +type WorkloadCategory string + +const ( + CategoryFilter WorkloadCategory = "filter" + CategoryDerive WorkloadCategory = "derive" + CategoryAggregate WorkloadCategory = "aggregate" + CategoryDistinct WorkloadCategory = "distinct" + CategoryCorrelation WorkloadCategory = "correlation" + CategoryNullMissing WorkloadCategory = "null-missing" + CategorySortLimit WorkloadCategory = "sort-limit" +) + +// WorkloadSpec is the authored contract for one battery entry. Columns is +// the public column order BOTH arms must publish (GateColumnContract). +// ExpectedComplete=false is legal only for explicitly truncated preview +// checks, which the battery does not use for claimable rows. +type WorkloadSpec struct { + ID string `json:"id"` + Category WorkloadCategory `json:"category"` + Columns []string `json:"columns"` + OracleAvailable bool `json:"oracleAvailable"` + AuthScoped bool `json:"authScoped"` + Description string `json:"description"` +} + +// Battery is the closed matrix definition set. Adding an entry requires the +// oracle (tiny preset), the workspace recipe in the harness, and a category. +var Battery = []WorkloadSpec{ + {ID: WorkloadFilterActiveGender, Category: CategoryFilter, + Columns: []string{"patient_id", "gender"}, OracleAvailable: true, + Description: "active patients with gender=female"}, + {ID: WorkloadDeriveAgeBand, Category: CategoryDerive, + Columns: []string{"observation_id", "age_band"}, OracleAvailable: true, + Description: "closed category-recode derivation of an age band from BENCH-CODE-AGE identifiers over [1,90] days"}, + {ID: WorkloadAggregateSumAgeByPatient, Category: CategoryAggregate, + Columns: []string{"patient_id", "sum_age", "count_age", "min_age", "max_age"}, OracleAvailable: true, + Description: "sum/count/min/max of age quantities grouped by patient"}, + {ID: WorkloadGroupCountByPatient, Category: CategoryAggregate, + Columns: []string{"patient_id", "obs_count"}, OracleAvailable: true, + Description: "observation count per patient"}, + {ID: WorkloadStageCounts, Category: CategoryAggregate, + Columns: []string{"stage_code", "stage_count"}, OracleAvailable: true, + Description: "counts by BENCH-CODE-STAGE codeable concept"}, + {ID: WorkloadSpecimenTypeFilter, Category: CategoryFilter, + Columns: []string{"specimen_id", "patient_id", "type_code"}, OracleAvailable: true, + Description: "specimens with type PRIMARY"}, + {ID: WorkloadConditionNullHandling, Category: CategoryNullMissing, + Columns: []string{"condition_id", "verification_status"}, OracleAvailable: true, + Description: "verification status where the source may omit the field entirely (null vs missing)"}, + {ID: WorkloadSortLimitTopN, Category: CategorySortLimit, + Columns: []string{"family", "given", "patient_id"}, OracleAvailable: true, + Description: fmt.Sprintf("top-%d active patients by family, given, id (total order, ties broken by id)", TopNLimit)}, + {ID: WorkloadDistinctFamily, Category: CategoryDistinct, + Columns: []string{"family"}, OracleAvailable: true, + Description: "distinct family names over deliberately duplicated contributors"}, + {ID: WorkloadStudyCorrelation, Category: CategoryCorrelation, + Columns: []string{"patient_id", "study_ref"}, OracleAvailable: true, + Description: "patient rows correlated with their ResearchStudy reference"}, +} + +// workloadColumns resolves the authored column contract. +func workloadColumns(workloadID string) ([]string, bool) { + for _, spec := range Battery { + if spec.ID == workloadID { + return append([]string(nil), spec.Columns...), true + } + } + return nil, false +} + +// SpecsByPreset returns the battery entries eligible for a dataset preset. +// All presets run the full battery; representative drops nothing — it only +// drops the hand oracle scale, which OracleFor already handles by profile +// through the caller. This indirection exists so a future preset-specific +// exclusion is a single auditable edit. +func SpecsByPreset(preset string) ([]WorkloadSpec, error) { + switch preset { + case PresetTiny, PresetSmall, PresetRepresentative: + out := make([]WorkloadSpec, 0, len(Battery)) + for _, spec := range Battery { + out = append(out, spec) + } + return out, nil + default: + return nil, fmt.Errorf("benchmark: unknown preset %q", preset) + } +} + +// ValidateBattery is called at harness start and from tests: an entry with +// duplicate ids, empty columns, an unknown category, or an oracle-less entry +// that should have one (all tiny-eligible entries must declare oracle +// availability or explicitly live without one) is a config bug, not a +// runtime surprise. +func ValidateBattery() error { + seen := map[string]bool{} + categories := map[WorkloadCategory]int{} + for _, spec := range Battery { + if spec.ID == "" { + return fmt.Errorf("benchmark: battery entry with empty id") + } + if seen[spec.ID] { + return fmt.Errorf("benchmark: duplicate workload id %q", spec.ID) + } + seen[spec.ID] = true + if len(spec.Columns) == 0 { + return fmt.Errorf("benchmark: workload %q has no column contract", spec.ID) + } + if hasDuplicates(spec.Columns) { + return fmt.Errorf("benchmark: workload %q has duplicate columns", spec.ID) + } + switch spec.Category { + case CategoryFilter, CategoryDerive, CategoryAggregate, CategoryDistinct, + CategoryCorrelation, CategoryNullMissing, CategorySortLimit: + categories[spec.Category]++ + default: + return fmt.Errorf("benchmark: workload %q has unknown category %q", spec.ID, spec.Category) + } + // Oracle presence must agree with the oracle implementation. + if _, implemented := oracleRows(spec.ID, &Facts{}); implemented != spec.OracleAvailable { + return fmt.Errorf("benchmark: workload %q declares OracleAvailable=%v but implementation says %v", + spec.ID, spec.OracleAvailable, implemented) + } + } + for _, category := range []WorkloadCategory{CategoryFilter, CategoryDerive, CategoryAggregate, + CategoryDistinct, CategoryCorrelation, CategoryNullMissing, CategorySortLimit} { + if categories[category] == 0 { + return fmt.Errorf("benchmark: coverage family %q has no battery entries", category) + } + } + return nil +} + +// OracleBackedCount reports how many entries have independent oracles, for +// the report's honesty line. +func OracleBackedCount() int { + count := 0 + for _, spec := range Battery { + if spec.OracleAvailable { + count++ + } + } + return count +} + +func hasDuplicates(values []string) bool { + seen := map[string]bool{} + for _, v := range values { + if seen[v] { + return true + } + seen[v] = true + } + return false +} + +// SortedBattery returns entries by id for deterministic report order. +func SortedBattery() []WorkloadSpec { + out := append([]WorkloadSpec(nil), Battery...) + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + return out +} diff --git a/internal/dataframe/benchmark/matrix_test.go b/internal/dataframe/benchmark/matrix_test.go new file mode 100644 index 000000000..baa696ec0 --- /dev/null +++ b/internal/dataframe/benchmark/matrix_test.go @@ -0,0 +1,38 @@ +package benchmark + +import "testing" + +func TestValidateBattery(t *testing.T) { + if err := ValidateBattery(); err != nil { + t.Fatalf("battery invalid: %v", err) + } + if OracleBackedCount() != len(Battery) { + t.Fatalf("every battery entry is expected to carry an independent oracle: %d/%d", OracleBackedCount(), len(Battery)) + } +} + +func TestSpecsByPreset(t *testing.T) { + for _, preset := range []string{PresetTiny, PresetSmall, PresetRepresentative} { + specs, err := SpecsByPreset(preset) + if err != nil { + t.Fatal(err) + } + if len(specs) != len(Battery) { + t.Fatalf("preset %s runs the full battery", preset) + } + } + if _, err := SpecsByPreset("huge"); err == nil { + t.Fatal("unknown preset must be refused") + } +} + +func TestBatteryColumnContractsUnique(t *testing.T) { + for _, spec := range Battery { + if _, ok := workloadColumns(spec.ID); !ok { + t.Fatalf("workload %q missing column contract", spec.ID) + } + } + if _, ok := workloadColumns("nope"); ok { + t.Fatal("unknown workload must not resolve") + } +} diff --git a/internal/dataframe/benchmark/measure.go b/internal/dataframe/benchmark/measure.go new file mode 100644 index 000000000..d478a6ea7 --- /dev/null +++ b/internal/dataframe/benchmark/measure.go @@ -0,0 +1,236 @@ +package benchmark + +import ( + "fmt" + "math" + "sort" + "strconv" + "time" +) + +// Measurement method (bound before any number is reported): +// +// - warmup iterations run first and are recorded but excluded from +// statistics; +// - measured iterations interleave backends round-robin per round so clock +// and host drift hit both arms alike; +// - every sample keeps its raw wall duration, outcome, cache label, and +// error identity; failed and timed-out samples remain in raw results and +// are excluded from statistics, never silently dropped; +// - headline robustness is median + variance; p50/p95 are reported with a +// declared quantile method; +// - unavailable metrics stay unavailable, never zero. + +// CacheLabel is the honest cache-state label attached to every sample. +// OS/page-cache state is never claimed cold without evidence; process-cold +// is only available where the harness owns the process restart. +type CacheLabel string + +const ( + CacheWarm CacheLabel = "warm" // warmed steady state + CacheProcessCold CacheLabel = "process-cold" // owning server process restarted before this sample + CacheServiceWarm CacheLabel = "service-warm" // external service, no restart ownership (never claimed cold) +) + +// Outcome classifies one execution attempt. +type Outcome string + +const ( + OutcomePassed Outcome = "passed" + OutcomeFailed Outcome = "failed" + OutcomeTimeout Outcome = "timeout" + OutcomeError Outcome = "error" +) + +// Sample is one recorded attempt. Duration is wall time through Loom as +// observed by the client; Phase attributes the dominant measured phase when +// the route separates it. +type Sample struct { + WorkloadID string `json:"workloadId"` + Backend string `json:"backend"` + Round int `json:"round"` + Iteration int `json:"iteration"` + Warmup bool `json:"warmup"` + Outcome Outcome `json:"outcome"` + Millis float64 `json:"millis"` + RowsSeen int `json:"rowsSeen"` + Truncated bool `json:"truncated"` + Cache CacheLabel `json:"cache"` + StartedAt time.Time `json:"startedAt"` + ErrorCode string `json:"errorCode,omitempty"` +} + +// RunConfig binds the repeatability policy. Zero values are refused, not +// defaulted silently, so a run's method is always explicit in its report. +type RunConfig struct { + Warmup int // discarded warmup iterations per workload x backend + Iterations int // measured iterations per workload x backend + Concurrency int // simultaneous requests in the throughput phase + Rounds int // interleaved rounds; Iterations must split evenly +} + +func (c RunConfig) Validate() error { + if c.Warmup < 0 { + return fmt.Errorf("benchmark: warmup must be >= 0") + } + if c.Iterations < 1 { + return fmt.Errorf("benchmark: iterations must be >= 1") + } + if c.Concurrency < 1 { + return fmt.Errorf("benchmark: concurrency must be >= 1") + } + if c.Rounds < 1 { + return fmt.Errorf("benchmark: rounds must be >= 1") + } + if c.Iterations%c.Rounds != 0 { + return fmt.Errorf("benchmark: iterations %d must split evenly across rounds %d", c.Iterations, c.Rounds) + } + return nil +} + +// Summary is the statistics block for one workload x backend x cache label. +// Missing halves are unavailable, never zero: Count==0 means no samples. +type Summary struct { + Count int `json:"count"` + FailedCount int `json:"failedCount"` + MedianMillis float64 `json:"medianMillis"` + MeanMillis float64 `json:"meanMillis"` + VarianceMs float64 `json:"varianceMillis"` + StdDevMs float64 `json:"stdDevMillis"` + MinMillis float64 `json:"minMillis"` + MaxMillis float64 `json:"maxMillis"` + P50Millis float64 `json:"p50Millis"` + P95Millis float64 `json:"p95Millis"` + QuantileMethod string `json:"quantileMethod"` + // CoefficientOfVariation is stddev/mean for the passed samples; the + // stability control compares it against StabilityCVLimit. + CoefficientOfVariation float64 `json:"coefficientOfVariation"` + // ThroughputPerSecond is concurrency/windowSeconds for the throughput + // phase only; 0 with ThroughputMeasured=false means unavailable. + ThroughputPerSecond float64 `json:"throughputPerSecond"` + ThroughputMeasured bool `json:"throughputMeasured"` +} + +// Summarize computes statistics over the passed, non-warmup samples for one +// (workload, backend, cache) cell. Failed samples are counted separately and +// excluded from the distributions; their presence in raw results is enforced +// elsewhere. +func Summarize(samples []Sample) Summary { + summary := Summary{QuantileMethod: "nearest-rank-on-sorted-passed-durations"} + var passed []float64 + for _, s := range samples { + if s.Warmup { + continue + } + summary.Count++ + if s.Outcome != OutcomePassed { + summary.FailedCount++ + continue + } + passed = append(passed, s.Millis) + } + if len(passed) == 0 { + return summary + } + sort.Float64s(passed) + var total, totalSq float64 + for _, ms := range passed { + total += ms + totalSq += ms * ms + } + n := float64(len(passed)) + summary.MeanMillis = total / n + summary.MedianMillis = percentile(passed, 0.5) + summary.P50Millis = summary.MedianMillis + summary.P95Millis = percentile(passed, 0.95) + summary.MinMillis = passed[0] + summary.MaxMillis = passed[len(passed)-1] + variance := totalSq/n - summary.MeanMillis*summary.MeanMillis + if variance < 0 { + variance = 0 + } + summary.VarianceMs = variance + summary.StdDevMs = math.Sqrt(variance) + if summary.MeanMillis > 0 { + summary.CoefficientOfVariation = summary.StdDevMs / summary.MeanMillis + } + return summary +} + +// percentile uses nearest-rank on the sorted slice: rank = ceil(q*n), 1-based. +// The method is declared in Summary.QuantileMethod rather than assumed. +func percentile(sorted []float64, q float64) float64 { + if len(sorted) == 0 { + return 0 + } + rank := int(math.Ceil(q * float64(len(sorted)))) + if rank < 1 { + rank = 1 + } + if rank > len(sorted) { + rank = len(sorted) + } + return sorted[rank-1] +} + +// StabilityCVLimit is the ceiling for the stability control: a workload arm +// whose coefficient of variation exceeds it marks the environment unstable +// and its ratios are excluded from performance claims. +const StabilityCVLimit = 0.35 + +// SpeedRatio compares two medians. It is computed only when both sides are +// valid (see gates.go); the caller must not present it otherwise. +type SpeedRatio struct { + ArangoOverPostgres float64 `json:"arangoOverPostgres"` + PostgresOverArango float64 `json:"postgresOverArango"` +} + +func NewSpeedRatio(arangoMedian, postgresMedian float64) (SpeedRatio, error) { + if arangoMedian <= 0 || postgresMedian <= 0 { + return SpeedRatio{}, fmt.Errorf("benchmark: speed ratio needs positive medians, got arango=%s postgres=%s", + strconv.FormatFloat(arangoMedian, 'g', -1, 64), strconv.FormatFloat(postgresMedian, 'g', -1, 64)) + } + return SpeedRatio{ + ArangoOverPostgres: arangoMedian / postgresMedian, + PostgresOverArango: postgresMedian / arangoMedian, + }, nil +} + +// Clock is injectable so accounting tests never sleep. +type Clock interface { + Now() time.Time + Since(time.Time) time.Duration +} + +type monotonicClock struct{} + +func (monotonicClock) Now() time.Time { return time.Now() } +func (monotonicClock) Since(t time.Time) time.Duration { return time.Since(t) } + +// NewMonotonicClock returns the production clock (time.Now carries a +// monotonic reading; durations use Since, never wall subtraction). +func NewMonotonicClock() Clock { return monotonicClock{} } + +// ElapsedMillis measures one attempt with the injected clock. +func ElapsedMillis(clock Clock, started time.Time) float64 { + return float64(clock.Since(started).Microseconds()) / 1000.0 +} + +// ThroughputWindow summarizes a sustained concurrency window: window seconds +// are measured, not declared, and only passed requests count toward +// throughput. +func ThroughputSummary(samples []Sample, window time.Duration) Summary { + summary := Summarize(samples) + var passed int + for _, s := range samples { + if !s.Warmup && s.Outcome == OutcomePassed { + passed++ + } + } + seconds := window.Seconds() + if passed > 0 && seconds > 0 { + summary.ThroughputPerSecond = float64(passed) / seconds + summary.ThroughputMeasured = true + } + return summary +} diff --git a/internal/dataframe/benchmark/measure_test.go b/internal/dataframe/benchmark/measure_test.go new file mode 100644 index 000000000..150d112e4 --- /dev/null +++ b/internal/dataframe/benchmark/measure_test.go @@ -0,0 +1,159 @@ +package benchmark + +import ( + "math" + "testing" + "time" +) + +func sample(backend, workload string, outcome Outcome, ms float64, warmup bool) Sample { + return Sample{WorkloadID: workload, Backend: backend, Outcome: outcome, Millis: ms, Warmup: warmup, Cache: CacheWarm, Round: 1, Iteration: 1} +} + +func TestSummarizeExcludesWarmupAndFailed(t *testing.T) { + samples := []Sample{ + sample(BackendArango, "w", OutcomePassed, 100, true), // warmup: excluded + sample(BackendArango, "w", OutcomePassed, 10, false), + sample(BackendArango, "w", OutcomePassed, 20, false), + sample(BackendArango, "w", OutcomePassed, 30, false), + sample(BackendArango, "w", OutcomeTimeout, 9999, false), // excluded from stats, counted + } + s := Summarize(samples) + if s.Count != 4 { + t.Fatalf("count must cover non-warmup samples including failures: %d", s.Count) + } + if s.FailedCount != 1 { + t.Fatalf("failed count: %d", s.FailedCount) + } + if s.MedianMillis != 20 || s.MinMillis != 10 || s.MaxMillis != 30 { + t.Fatalf("warmup or failed leaked into stats: %+v", s) + } + if s.MeanMillis != 20 { + t.Fatalf("mean: %v", s.MeanMillis) + } +} + +func TestSummarizeQuantilesNearestRank(t *testing.T) { + var samples []Sample + for i := 1; i <= 20; i++ { + samples = append(samples, sample(BackendArango, "w", OutcomePassed, float64(i), false)) + } + s := Summarize(samples) + // nearest-rank: p50 rank=ceil(.5*20)=10 -> 10; p95 rank=ceil(.95*20)=19 -> 19. + if s.P50Millis != 10 || s.P95Millis != 19 { + t.Fatalf("quantiles wrong under nearest-rank: %+v", s) + } + if s.QuantileMethod != "nearest-rank-on-sorted-passed-durations" { + t.Fatal("quantile method must be declared") + } +} + +func TestSummarizeUnavailableNotZero(t *testing.T) { + // A cell whose samples all failed: distributions are UNAVAILABLE. The + // sentinel is Count>0,FailedCount==Count plus ThroughputMeasured=false; + // a never-run cell has Count==0. Neither may masquerade as a real 0ms. + s := Summarize([]Sample{sample(BackendArango, "w", OutcomeError, 5, false)}) + if s.Count != 1 || s.FailedCount != 1 { + t.Fatalf("all-failed cell shape: %+v", s) + } + if s.ThroughputMeasured { + t.Fatal("throughput must stay unmeasured without passed samples") + } + empty := Summarize(nil) + if empty.Count != 0 { + t.Fatalf("never-run cell must have Count==0: %+v", empty) + } +} + +func TestSummarizeVarianceAndCV(t *testing.T) { + samples := []Sample{} + for _, ms := range []float64{10, 10, 10, 10} { + samples = append(samples, sample(BackendArango, "w", OutcomePassed, ms, false)) + } + s := Summarize(samples) + if s.VarianceMs != 0 || s.StdDevMs != 0 || s.CoefficientOfVariation != 0 { + t.Fatalf("identical samples must have zero variance: %+v", s) + } + s2 := Summarize([]Sample{ + sample(BackendArango, "w", OutcomePassed, 10, false), + sample(BackendArango, "w", OutcomePassed, 30, false), + }) + // mean 20, variance 100, stddev 10, CV 0.5 + if math.Abs(s2.VarianceMs-100) > 1e-9 || math.Abs(s2.StdDevMs-10) > 1e-9 || math.Abs(s2.CoefficientOfVariation-0.5) > 1e-9 { + t.Fatalf("variance/CV wrong: %+v", s2) + } +} + +func TestRunConfigValidation(t *testing.T) { + cases := []struct { + name string + cfg RunConfig + ok bool + }{ + {"zero", RunConfig{}, false}, + {"uneven rounds", RunConfig{Warmup: 1, Iterations: 7, Concurrency: 1, Rounds: 2}, false}, + {"negative warmup", RunConfig{Warmup: -1, Iterations: 4, Concurrency: 1, Rounds: 2}, false}, + {"ok", RunConfig{Warmup: 2, Iterations: 8, Concurrency: 4, Rounds: 4}, true}, + } + for _, tc := range cases { + err := tc.cfg.Validate() + if tc.ok && err != nil { + t.Fatalf("%s: unexpected %v", tc.name, err) + } + if !tc.ok && err == nil { + t.Fatalf("%s: expected rejection", tc.name) + } + } +} + +func TestSpeedRatioRejectsNonPositive(t *testing.T) { + if _, err := NewSpeedRatio(0, 10); err == nil { + t.Fatal("zero median must be refused, not reported as ratio 0") + } + if _, err := NewSpeedRatio(10, -1); err == nil { + t.Fatal("negative median must be refused") + } + r, err := NewSpeedRatio(40, 10) + if err != nil || r.ArangoOverPostgres != 4 || r.PostgresOverArango != 0.25 { + t.Fatalf("ratio wrong: %+v %v", r, err) + } +} + +func TestThroughputSummaryUsesMeasuredWindowAndPassedOnly(t *testing.T) { + samples := []Sample{ + sample(BackendArango, "w", OutcomePassed, 50, false), + sample(BackendArango, "w", OutcomePassed, 60, false), + sample(BackendArango, "w", OutcomeError, 10, false), + sample(BackendArango, "w", OutcomePassed, 40, true), // warmup excluded + } + s := ThroughputSummary(samples, 2*time.Second) + if !s.ThroughputMeasured { + t.Fatal("measured window with passes must mark throughput measured") + } + if s.ThroughputPerSecond != 1.0 { // 2 passed / 2s + t.Fatalf("throughput counts only passed non-warmup: %v", s.ThroughputPerSecond) + } + if z := ThroughputSummary(samples, 0); z.ThroughputMeasured { + t.Fatal("zero window is not a measurement") + } +} + +func TestElapsedMillisUsesMonotonicSince(t *testing.T) { + fake := fakeClock{start: time.Unix(0, 0), now: time.Unix(0, 0)} + elapsed := ElapsedMillis(&fake, fake.now) + if elapsed != 0 { + t.Fatal("same instant must measure zero") + } + fake.now = fake.start.Add(1500 * time.Microsecond) + if got := ElapsedMillis(&fake, fake.start); got != 1.5 { + t.Fatalf("expected 1.5ms, got %v", got) + } + if NewMonotonicClock() == nil { + t.Fatal("production clock must exist") + } +} + +type fakeClock struct{ start, now time.Time } + +func (c *fakeClock) Now() time.Time { return c.now } +func (c *fakeClock) Since(t time.Time) time.Duration { return c.now.Sub(t) } diff --git a/internal/dataframe/benchmark/oracle.go b/internal/dataframe/benchmark/oracle.go new file mode 100644 index 000000000..bb8d61e6e --- /dev/null +++ b/internal/dataframe/benchmark/oracle.go @@ -0,0 +1,270 @@ +package benchmark + +import ( + "fmt" + "sort" +) + +// Independent oracles for the tiny preset. These are derived directly from +// Facts (the fixture's own truth record) with plain Go arithmetic — no Loom +// planner, compiler, or storage is involved — so agreement between both +// backends AND the oracle is three-way evidence, not a tautology. +// +// Oracle rows are produced in the same public column shape the workloads +// publish, so they canonicalize and bag through the identical path. + +// OracleFor returns the independently expected bag for one workload under the +// tiny preset, or (nil, nil) when the workload has no independent oracle +// (then only cross-backend equality gates apply, recorded as such). +func OracleFor(workloadID string, facts *Facts) (*CanonicalBag, error) { + rows, ok := oracleRows(workloadID, facts) + if !ok { + return nil, nil + } + columns, ok := workloadColumns(workloadID) + if !ok { + return nil, fmt.Errorf("benchmark: workload %q has an oracle but no column contract", workloadID) + } + bag := Bag(CanonicalizeRows(columns, rows)) + return &bag, nil +} + +func oracleRows(workloadID string, facts *Facts) ([]map[string]any, bool) { + switch workloadID { + case WorkloadFilterActiveGender: + return oracleFilterActiveGender(facts), true + case WorkloadAggregateSumAgeByPatient: + return oracleAggregateSum(facts), true + case WorkloadGroupCountByPatient: + return oracleGroupCount(facts), true + case WorkloadStageCounts: + return oracleStageCounts(facts), true + case WorkloadSpecimenTypeFilter: + return oracleSpecimenTypeFilter(facts), true + case WorkloadConditionNullHandling: + return oracleConditionNulls(facts), true + case WorkloadDistinctFamily: + return oracleDistinctFamily(facts), true + case WorkloadStudyCorrelation: + return oracleStudyCorrelation(facts), true + case WorkloadSortLimitTopN: + return oracleSortLimit(facts), true + case WorkloadDeriveAgeBand: + return oracleDeriveAgeBand(facts), true + default: + return nil, false + } +} + +func oracleFilterActiveGender(facts *Facts) []map[string]any { + var rows []map[string]any + for _, p := range facts.Patients { + if !p.Active || p.Gender != "female" { + continue + } + rows = append(rows, map[string]any{"patient_id": p.ID, "gender": p.Gender}) + } + return rows +} + +func oracleAggregateSum(facts *Facts) []map[string]any { + type acc struct { + sum float64 + count int + min float64 + max float64 + seen bool + } + byPatient := map[string]*acc{} + for _, o := range facts.Observations { + if o.Code != ObservationCodeAge || o.ValueNumber == nil { + continue + } + a := byPatient[o.PatientID] + if a == nil { + a = &acc{} + byPatient[o.PatientID] = a + } + v := *o.ValueNumber + a.sum += v + a.count++ + if !a.seen || v < a.min { + a.min = v + } + if !a.seen || v > a.max { + a.max = v + } + a.seen = true + } + patients := make([]string, 0, len(byPatient)) + for patient := range byPatient { + patients = append(patients, patient) + } + sort.Strings(patients) + rows := make([]map[string]any, 0, len(patients)) + for _, patient := range patients { + a := byPatient[patient] + rows = append(rows, map[string]any{ + "patient_id": patient, + "sum_age": a.sum, + "count_age": float64(a.count), + "min_age": a.min, + "max_age": a.max, + }) + } + return rows +} + +func oracleGroupCount(facts *Facts) []map[string]any { + rows := make([]map[string]any, 0, len(facts.CountByPatient)) + for patient, count := range facts.CountByPatient { + rows = append(rows, map[string]any{"patient_id": patient, "obs_count": float64(count)}) + } + return rows +} + +func oracleStageCounts(facts *Facts) []map[string]any { + rows := make([]map[string]any, 0, len(facts.StageCounts)) + for stage, count := range facts.StageCounts { + rows = append(rows, map[string]any{"stage_code": stage, "stage_count": float64(count)}) + } + return rows +} + +func oracleSpecimenTypeFilter(facts *Facts) []map[string]any { + var rows []map[string]any + for _, s := range facts.Specimens { + if s.TypeCode != SpecimenTypePrimary { + continue + } + rows = append(rows, map[string]any{"specimen_id": s.ID, "patient_id": s.PatientID, "type_code": s.TypeCode}) + } + return rows +} + +// oracleConditionNulls emits null (not missing) for absent verification +// statuses: the backend must produce an explicit null too, or the +// null-vs-missing gate fails — which is the point. +func oracleConditionNulls(facts *Facts) []map[string]any { + rows := make([]map[string]any, 0, len(facts.Conditions)) + for _, c := range facts.Conditions { + var status any + if c.VerificationStatus != "" { + status = c.VerificationStatus + } + rows = append(rows, map[string]any{"condition_id": c.ID, "verification_status": status}) + } + return rows +} + +func oracleDistinctFamily(facts *Facts) []map[string]any { + seen := map[string]bool{} + var rows []map[string]any + for _, p := range facts.Patients { + for _, family := range p.FamilyNames { + if seen[family] { + continue + } + seen[family] = true + rows = append(rows, map[string]any{"family": family}) + } + } + return rows +} + +func oracleStudyCorrelation(facts *Facts) []map[string]any { + var rows []map[string]any + for _, p := range facts.Patients { + rows = append(rows, map[string]any{"patient_id": p.ID, "study_ref": p.StudyRef}) + } + return rows +} + +// oracleSortLimit mirrors the workload's deterministic total order: family +// asc, then given asc, then patient_id asc; the top N by that order. The +// tie-break on patient_id makes the ordering total, so both backends must +// agree on exactly the same N rows with multiplicity. +func oracleSortLimit(facts *Facts) []map[string]any { + type entry struct { + family, given, id string + } + var entries []entry + for _, p := range facts.Patients { + if !p.Active { + continue + } + family, given := "", "" + if len(p.FamilyNames) > 0 { + family = p.FamilyNames[0] + } + if len(p.GivenNames) > 0 { + given = p.GivenNames[0] + } + entries = append(entries, entry{family, given, p.ID}) + } + sort.Slice(entries, func(i, j int) bool { + if entries[i].family != entries[j].family { + return entries[i].family < entries[j].family + } + if entries[i].given != entries[j].given { + return entries[i].given < entries[j].given + } + return entries[i].id < entries[j].id + }) + limit := TopNLimit + if limit > len(entries) { + limit = len(entries) + } + rows := make([]map[string]any, 0, limit) + for _, e := range entries[:limit] { + rows = append(rows, map[string]any{"family": e.family, "given": e.given, "patient_id": e.id}) + } + return rows +} + +func oracleDeriveAgeBand(facts *Facts) []map[string]any { + var rows []map[string]any + for _, o := range facts.Observations { + if o.Code != ObservationCodeAge || o.ValueNumber == nil { + continue + } + rows = append(rows, map[string]any{"observation_id": o.ID, "age_band": AgeBandFor(*o.ValueNumber)}) + } + return rows +} + +// AgeBandThresholds are shared between the recipe (authoring recode +// mappings, matrix domain) and the oracle so the two cannot drift apart +// silently; a change is a single edit here. +const ( + AgeBandYoungUnder = 30 + AgeBandMiddleUnder = 60 + TopNLimit = 25 + // AgeBandFixtureMax is the inclusive upper bound of the fixture age + // domain [1, AgeBandFixtureMax]: it straddles both thresholds so all + // three bands occur, and it is the domain the workspace recode map + // enumerates (the offline witness pins this coverage). + AgeBandFixtureMax = 90 +) + +// AgeBandFor is the single banding function: the oracle applies it to the +// numeric quantity, the workspace recode maps are generated from it, and +// the fixture domain is checked against it. The derived labels therefore +// cannot drift from the independent threshold expectation by construction. +func AgeBandFor(v float64) string { + switch { + case v < AgeBandYoungUnder: + return "young" + case v < AgeBandMiddleUnder: + return "middle" + default: + return "senior" + } +} + +// Exact float equality note: age valueQuantity values are integers in +// [1, AgeBandFixtureMax] (fixtures.go), and per-patient sums stay far +// inside float64 exact integer range at every preset, so +// canonicalNumberText's shortest-round-trip spelling matches backend output +// exactly; the fixtures test re-asserts the integer invariant so the oracle +// cannot drift silently. diff --git a/internal/dataframe/benchmark/oracle_test.go b/internal/dataframe/benchmark/oracle_test.go new file mode 100644 index 000000000..f9ae8d138 --- /dev/null +++ b/internal/dataframe/benchmark/oracle_test.go @@ -0,0 +1,186 @@ +package benchmark + +import "testing" + +func TestOracleMatchesIndependentCounts(t *testing.T) { + facts := tinyFacts(t) + + // Filter oracle: counted directly here, not via the oracle function. + want := 0 + for _, p := range facts.Patients { + if p.Active && p.Gender == "female" { + want++ + } + } + bag, err := OracleFor(WorkloadFilterActiveGender, facts) + if err != nil || bag == nil { + t.Fatalf("filter oracle: %v", err) + } + if len(*bag) != want { + t.Fatalf("filter oracle rows=%d, independent count=%d", len(*bag), want) + } + if want == 0 { + t.Fatal("tiny dataset must exercise the filter non-vacuously") + } +} + +func TestOracleAggregateMatchesFactsAggregates(t *testing.T) { + facts := tinyFacts(t) + bag, err := OracleFor(WorkloadAggregateSumAgeByPatient, facts) + if err != nil || bag == nil { + t.Fatal(err) + } + // The oracle bag must have exactly one row per patient with age values. + rows := 0 + for _, n := range facts.CountByPatient { + if n > 0 { + rows++ + } + } + // Some patients may have all ages missing (p%19==0), so compare to the + // set of patients with at least one non-nil age observation. + withAge := map[string]bool{} + for _, o := range facts.Observations { + if o.Code == ObservationCodeAge && o.ValueNumber != nil { + withAge[o.PatientID] = true + } + } + if len(*bag) != len(withAge) { + t.Fatalf("aggregate oracle rows=%d, patients-with-age=%d", len(*bag), len(withAge)) + } + _ = rows +} + +func TestOracleGroupCountMatchesCountByPatient(t *testing.T) { + facts := tinyFacts(t) + bag, err := OracleFor(WorkloadGroupCountByPatient, facts) + if err != nil || bag == nil { + t.Fatal(err) + } + if len(*bag) != len(facts.CountByPatient) { + t.Fatalf("group oracle rows=%d, facts.CountByPatient=%d", len(*bag), len(facts.CountByPatient)) + } +} + +func TestOracleStageCountsMatchFacts(t *testing.T) { + facts := tinyFacts(t) + bag, err := OracleFor(WorkloadStageCounts, facts) + if err != nil || bag == nil { + t.Fatal(err) + } + if len(*bag) != len(facts.StageCounts) { + t.Fatalf("stage oracle rows=%d, facts.StageCounts=%d", len(*bag), len(facts.StageCounts)) + } +} + +func TestOracleNullHandlingEmitsNullNotMissing(t *testing.T) { + facts := tinyFacts(t) + bag, err := OracleFor(WorkloadConditionNullHandling, facts) + if err != nil || bag == nil { + t.Fatal(err) + } + if len(*bag) != len(facts.Conditions) { + t.Fatal("every condition must appear exactly once") + } + // The oracle row object carries the key with a nil value: canonicalized + // to CNull, and must NOT equal the same row with the key dropped. + rows, _ := oracleRows(WorkloadConditionNullHandling, facts) + dropped := make([]map[string]any, len(rows)) + for i, r := range rows { + c := map[string]any{} + for k, v := range r { + if v != nil { + c[k] = v + } + } + dropped[i] = c + } + if Bag(CanonicalizeRows([]string{"condition_id", "verification_status"}, rows)).Digest() == + Bag(CanonicalizeRows([]string{"condition_id", "verification_status"}, dropped)).Digest() { + t.Fatal("oracle must keep null distinct from missing") + } +} + +func TestOracleSortLimitTotalOrderTopN(t *testing.T) { + facts := tinyFacts(t) + rows, _ := oracleRows(WorkloadSortLimitTopN, facts) + active := 0 + for _, p := range facts.Patients { + if p.Active { + active++ + } + } + want := active + if want > TopNLimit { + want = TopNLimit + } + if len(rows) != want { + t.Fatalf("topN rows=%d, want=%d", len(rows), want) + } + for i := 1; i < len(rows); i++ { + key := func(r map[string]any) (string, string, string) { + f, _ := r["family"].(string) + g, _ := r["given"].(string) + id, _ := r["patient_id"].(string) + return f, g, id + } + f1, g1, id1 := key(rows[i-1]) + f2, g2, id2 := key(rows[i]) + if f1 > f2 || (f1 == f2 && g1 > g2) || (f1 == f2 && g1 == g2 && id1 >= id2) { + t.Fatalf("sort-limit oracle order broken at %d: %v then %v", i, rows[i-1], rows[i]) + } + } +} + +func TestOracleDeriveAgeBandEveryAgeObsBanded(t *testing.T) { + facts := tinyFacts(t) + rows, _ := oracleRows(WorkloadDeriveAgeBand, facts) + want := 0 + for _, o := range facts.Observations { + if o.Code == ObservationCodeAge && o.ValueNumber != nil { + want++ + } + } + if len(rows) != want { + t.Fatalf("age-band rows=%d, want=%d", len(rows), want) + } + seen := map[string]bool{} + for _, r := range rows { + id, _ := r["observation_id"].(string) + band, _ := r["age_band"].(string) + if id == "" || seen[id] { + t.Fatalf("observation duplicated or unnamed: %v", r) + } + seen[id] = true + switch band { + case "young", "middle", "senior": + default: + t.Fatalf("unexpected band %q", band) + } + } +} + +func TestOracleStudyCorrelationCoversAllPatients(t *testing.T) { + facts := tinyFacts(t) + rows, _ := oracleRows(WorkloadStudyCorrelation, facts) + if len(rows) != len(facts.Patients) { + t.Fatal("one row per patient expected") + } + studies := map[string]bool{} + for _, s := range facts.StudyIDs { + studies["ResearchStudy/"+s] = true + } + for _, r := range rows { + ref, _ := r["study_ref"].(string) + if !studies[ref] { + t.Fatalf("study_ref %q not in study ids", ref) + } + } +} + +func TestOracleUnknownWorkloadHasNoOracle(t *testing.T) { + bag, err := OracleFor("does-not-exist", tinyFacts(t)) + if err != nil || bag != nil { + t.Fatalf("unknown workload must yield (nil, nil): %v %v", bag, err) + } +} diff --git a/internal/dataframe/benchmark/preview_shape_test.go b/internal/dataframe/benchmark/preview_shape_test.go new file mode 100644 index 000000000..16b198511 --- /dev/null +++ b/internal/dataframe/benchmark/preview_shape_test.go @@ -0,0 +1,167 @@ +package benchmark + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/calypr/loom/generated/loomapi" +) + +// The authoring-v2 preview route answers with loomapi.PreviewResponse +// (internal/server previewResponseFromEncoder, +// openapi_explorer_authoring_direct.go): columns are ContractColumn +// objects whose public name is at .column (v2EmissionColumns, +// api.gen.go), and Sampled is set from +// !summary.Complete || summary.Truncated with no omitempty, so it is +// always on the wire. The paired run at 79e72ed4 (job 132045898495) +// died on "cannot unmarshal object into Go struct field +// PreviewResult.columns of type string" because the client decoded +// columns as []string. An earlier revision of this fix also dropped +// Sampled from the decode on a wrong reading of the encoder alone; +// these tests pin the full chain so a truncated preview can never be +// silently treated as complete: sampled=true -> PreviewResult.Sampled +// -> sample/result Truncated -> GateCompleteness refuses the arm. + +func serverPreviewBody(receiptID, outputID string, names []string, rows []map[string]any, sampled bool) loomapi.PreviewResponse { + cols := make([]loomapi.ContractColumn, 0, len(names)) + for _, n := range names { + cols = append(cols, loomapi.ContractColumn{Column: n, Label: n, LogicalType: "string"}) + } + sources := make([]loomapi.PreviewRowSource, 0, len(rows)) + for range rows { + sources = append(sources, loomapi.PreviewRowSource{Kind: loomapi.PreviewRowSourceKindUNAVAILABLE}) + } + return loomapi.PreviewResponse{ + ApiVersion: loomapi.LoomCalyprOrgexplorerAuthoringv2, + Kind: loomapi.ExplorerBuilderPreview, + ReceiptId: receiptID, + OutputId: outputID, + Columns: cols, + Rows: rows, + RowSources: sources, + RowCount: len(rows), + Sampled: sampled, + Diagnostics: []loomapi.Diagnostic{}, + } +} + +func TestExecuteOutputDecodesRealPreviewWireShape(t *testing.T) { + rows := []map[string]any{{"patient_id": "p1", "gender": "female"}} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(serverPreviewBody("rcpt-1", "out", []string{"patient_id", "gender"}, rows, false)) + })) + defer srv.Close() + arm := &Arm{Backend: BackendArango, BaseURL: srv.URL, Client: srv.Client()} + res, err := arm.ExecuteOutput(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-1", "out", 10) + if err != nil { + t.Fatalf("preview decode errored: %v", err) + } + if strings.Join(res.Columns, ",") != "patient_id,gender" { + t.Errorf("columns lost: got %v", res.Columns) + } + if res.RowCount != 1 || len(res.Rows) != 1 || res.Digest == "" { + t.Errorf("rows/rowCount/digest lost: %+v", res) + } + if res.Sampled { + t.Errorf("a complete preview must decode Sampled=false") + } +} + +func TestExecuteOutputSurfacesSampledTrue(t *testing.T) { + rows := []map[string]any{{"patient_id": "p1", "gender": "female"}} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(serverPreviewBody("rcpt-1", "out", []string{"patient_id", "gender"}, rows, true)) + })) + defer srv.Close() + arm := &Arm{Backend: BackendArango, BaseURL: srv.URL, Client: srv.Client()} + res, err := arm.ExecuteOutput(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-1", "out", 10) + if err != nil { + t.Fatalf("preview decode errored: %v", err) + } + if !res.Sampled { + t.Fatal("sampled=true on the wire must survive decode") + } +} + +func TestDecodePreviewAbortsOnNamelessColumn(t *testing.T) { + resp := serverPreviewBody("rcpt-1", "out", []string{"patient_id", " "}, []map[string]any{{"patient_id": "p1"}}, false) + value := map[string]any{} + raw, _ := json.Marshal(resp) + if err := json.Unmarshal(raw, &value); err != nil { + t.Fatal(err) + } + if _, err := decodePreview(value); err == nil || !strings.Contains(err.Error(), "public name") { + t.Fatalf("nameless column must abort fail-closed, got: %v", err) + } +} + +// TestSampledTrueChainFailsCompletenessGate pins the full disclosure +// contract: a preview the route flags sampled=true must reach timing +// samples as Truncated (MeasureArms), reach the BackendResult as +// Truncated (backendResultFrom), and make GateCompleteness refuse the +// arm (EvaluateGates) -- so a truncated output can never be timed as +// complete or back a speed claim. +func TestSampledTrueChainFailsCompletenessGate(t *testing.T) { + rows := []map[string]any{{"patient_id": "p1", "gender": "female"}} + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + fa.rows["w"] = rows + fp.rows["w"] = rows + fa.mu.Lock() + fa.sampled = true // only the arango arm serves an incomplete preview + fa.mu.Unlock() + + clock := &scriptedClock{base: time.Unix(1700000000, 0)} + opts := RunOptions{Project: "p", Generation: "g", Explorer: "w", UnrestrictedToken: "Bearer x", PreviewLimit: 100, + Config: RunConfig{Warmup: 0, Iterations: 2, Concurrency: 1, Rounds: 1}} + samples, finals, err := MeasureArms(context.Background(), clock, "w", fa.arm(BackendArango), fp.arm(BackendPostgres), opts, + map[string]string{BackendArango: "r-a", BackendPostgres: "r-p"}, + map[string]string{BackendArango: "w", BackendPostgres: "w"}, + func(ctx context.Context, arm *Arm) error { return nil }) + if err != nil { + t.Fatal(err) + } + measured := 0 + for _, s := range samples { + if s.Warmup { + continue + } + measured++ + switch s.Backend { + case BackendArango: + if !s.Truncated { + t.Fatal("sampled=true preview must surface as sample.Truncated (MeasureArms)") + } + case BackendPostgres: + if s.Truncated { + t.Fatal("complete arm must not be marked truncated") + } + } + } + if measured == 0 { + t.Fatal("no measured samples produced") + } + columns := []string{"patient_id", "gender"} + a, p := backendResultFrom(BackendArango, samples, finals), backendResultFrom(BackendPostgres, samples, finals) + if !a.Truncated { + t.Fatal("sampled=true final must reach BackendResult.Truncated (backendResultFrom)") + } + if p.Truncated { + t.Fatal("complete final must not be truncated") + } + report := EvaluateGates("w", columns, mustBag(t, columns, rows), a, p, rows, rows, &StorageIDMap{}) + if report.Claimable { + t.Fatalf("a sampled=true arm must fail GateCompleteness: %+v", report.Gates) + } + if !strings.HasPrefix(report.ExclusionReason, "completeness:") { + t.Fatalf("exclusion must name the completeness gate, got %q", report.ExclusionReason) + } +} diff --git a/internal/dataframe/benchmark/provision.go b/internal/dataframe/benchmark/provision.go new file mode 100644 index 000000000..a84ece561 --- /dev/null +++ b/internal/dataframe/benchmark/provision.go @@ -0,0 +1,423 @@ +package benchmark + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" +) + +// Process provisioning for the two-arm benchmark. The harness owns the +// server processes (we spawn them), which is what makes the process-cold +// cache label honest: nothing else can claim a cold process it does not +// restart. + +// ServerSpec is one arm's spawn configuration. Secrets arrive via Env and +// ConfigYAML and are scrubbed from every captured log line. +type ServerSpec struct { + BinaryPath string + Backend string // arango | postgres + ConfigYAML string + Env []string + ReadyURL string // polled until 2xx + ReadyHeaders map[string]string + Timeout time.Duration // spawn+ready budget; 0 uses 60s + StderrDir string // where arm logs are written (must be a scratch dir) + // WorkDir, when set, is the arm process's working directory. The server + // resolves its graph schema (--schema, default schemas/graph-fhir.json) + // relative to the CWD, so a spawner outside the repository root must + // pin it explicitly instead of inheriting a stale directory. + WorkDir string +} + +// ServerProcess is a running arm. +type ServerProcess struct { + spec ServerSpec + cmd *exec.Cmd + logFile *os.File + logPath string + secrets []string + mu sync.Mutex + stopped bool + listenAddr string +} + +// SpawnServer starts one arm and waits for readiness. +func SpawnServer(ctx context.Context, spec ServerSpec, secrets []string, listenAddr string) (*ServerProcess, error) { + if !ValidBackend(spec.Backend) { + return nil, fmt.Errorf("benchmark: cannot spawn backend %q", spec.Backend) + } + if spec.BinaryPath == "" || spec.ConfigYAML == "" { + return nil, fmt.Errorf("benchmark: spawn needs binary path and config") + } + if spec.Timeout == 0 { + spec.Timeout = 60 * time.Second + } + if err := os.MkdirAll(spec.StderrDir, 0o755); err != nil { + return nil, fmt.Errorf("benchmark: arm log dir: %w", err) + } + logPath := filepath.Join(spec.StderrDir, fmt.Sprintf("loom-%s-%d.log", spec.Backend, time.Now().UnixNano())) + logFile, err := os.Create(logPath) + if err != nil { + return nil, fmt.Errorf("benchmark: arm log: %w", err) + } + proc := &ServerProcess{spec: spec, logFile: logFile, logPath: logPath, secrets: append([]string(nil), secrets...), listenAddr: listenAddr} + if err := proc.start(ctx); err != nil { + _ = logFile.Close() + return nil, err + } + return proc, nil +} + +func (p *ServerProcess) start(ctx context.Context) error { + // Config is written per-start so restarts reuse the exact same bytes. + dir := filepath.Dir(p.logPath) + cfgPath := filepath.Join(dir, fmt.Sprintf("loom-%s-config-%d.yaml", p.spec.Backend, os.Getpid())) + if err := os.WriteFile(cfgPath, []byte(p.spec.ConfigYAML), 0o600); err != nil { + return fmt.Errorf("benchmark: write arm config: %w", err) + } + args := []string{"--config", cfgPath, "--listen", p.listenAddr} + cmd := exec.CommandContext(ctx, p.spec.BinaryPath, args...) + cmd.Env = append(os.Environ(), p.spec.Env...) + if p.spec.WorkDir != "" { + if _, err := os.Stat(p.spec.WorkDir); err != nil { + return fmt.Errorf("benchmark: arm work dir: %w", err) + } + cmd.Dir = p.spec.WorkDir + } + cmd.Stdout = p.scrubWriter() + cmd.Stderr = p.scrubWriter() + if err := cmd.Start(); err != nil { + return fmt.Errorf("benchmark: spawn %s arm: %w", p.spec.Backend, err) + } + p.cmd = cmd + if err := p.waitReady(ctx); err != nil { + _ = cmd.Process.Kill() + _ = cmd.Wait() + return err + } + return nil +} + +// Restart stops and starts the same arm: the only legitimate source of the +// process-cold cache label. +func (p *ServerProcess) Restart(ctx context.Context) error { + p.mu.Lock() + defer p.mu.Unlock() + if p.cmd == nil || p.cmd.Process == nil { + return fmt.Errorf("benchmark: arm never started") + } + _ = p.cmd.Process.Kill() + _ = p.cmd.Wait() + p.stopped = false + return p.start(ctx) +} + +// Stop terminates the arm. Always called via defer by the harness. +func (p *ServerProcess) Stop() { + p.mu.Lock() + defer p.mu.Unlock() + if p.stopped || p.cmd == nil || p.cmd.Process == nil { + return + } + p.stopped = true + _ = p.cmd.Process.Kill() + _ = p.cmd.Wait() + _ = p.logFile.Close() +} + +// LogPath is the scrubbed arm log for evidence references. +func (p *ServerProcess) LogPath() string { return p.logPath } + +func (p *ServerProcess) waitReady(ctx context.Context) error { + deadline := time.Now().Add(p.spec.Timeout) + client := &http.Client{Timeout: 5 * time.Second} + var lastErr string + for time.Now().Before(deadline) { + if p.cmd != nil && p.cmd.ProcessState != nil && p.cmd.ProcessState.Exited() { + return fmt.Errorf("benchmark: %s arm exited before ready (see %s)", p.spec.Backend, p.scrub(p.logPath)) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.spec.ReadyURL, nil) + if err != nil { + return fmt.Errorf("benchmark: readiness request: %w", err) + } + for k, v := range p.spec.ReadyHeaders { + req.Header.Set(k, v) + } + resp, err := client.Do(req) + if err == nil { + _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096)) + _ = resp.Body.Close() + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return nil + } + lastErr = fmt.Sprintf("status %d", resp.StatusCode) + } else { + lastErr = err.Error() + } + select { + case <-ctx.Done(): + return fmt.Errorf("benchmark: %s arm readiness cancelled: %s", p.spec.Backend, lastErr) + case <-time.After(250 * time.Millisecond): + } + } + return fmt.Errorf("benchmark: %s arm not ready within %s (%s); log: %s", p.spec.Backend, p.spec.Timeout, lastErr, p.logPath) +} + +// scrubWriter redacts registered secrets before they touch the log file: +// connection strings never enter evidence, mirroring the orchestrator's +// secret-scrub rule. +func (p *ServerProcess) scrubWriter() io.Writer { + return scrubWriter{inner: p.logFile, secrets: p.secrets} +} + +type scrubWriter struct { + inner io.Writer + secrets []string +} + +func (w scrubWriter) Write(data []byte) (int, error) { + out := string(data) + for _, secret := range w.secrets { + if secret != "" { + out = strings.ReplaceAll(out, secret, "[REDACTED]") + } + } + if _, err := io.WriteString(w.inner, out); err != nil { + return 0, err + } + return len(data), nil // report original length: this is an observer, not a pipe +} + +func (p *ServerProcess) scrub(s string) string { + for _, secret := range p.secrets { + if secret != "" { + s = strings.ReplaceAll(s, secret, "[REDACTED]") + } + } + return s +} + +// FreePort reserves an ephemeral 127.0.0.1 port. The reservation is closed +// immediately; a bind race is handled by SpawnServer's readiness timeout. +func FreePort() (int, error) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + defer ln.Close() + return ln.Addr().(*net.TCPAddr).Port, nil +} + +// --- Mock Fence (scoped-authorization negative control) -------------- +// +// Contract mirrors internal/authscope: GET /user answers the same +// bearer token; token iss must equal the trusted issuer; authz entries are +// resource-path -> [{method:"read", service:"*"}]. Tokens are unsigned JWTs +// because CalyprAuthenticator checks shape only and the fence client checks +// iss + answers /user; the mock is the fence. + +// MockFence serves /user grants for benchmark tokens. +type MockFence struct { + server *http.Server + listener net.Listener + mu sync.Mutex + grants map[string][]string // sub -> auth resource paths + requests int +} + +// NewMockFence starts the fence on a loopback port. +func NewMockFence() (*MockFence, error) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("benchmark: mock fence listen: %w", err) + } + f := &MockFence{listener: ln, grants: map[string][]string{}} + f.server = &http.Server{Handler: http.HandlerFunc(f.handle)} + go func() { _ = f.server.Serve(ln) }() + return f, nil +} + +// URL is the trusted issuer base for this fence. +func (f *MockFence) URL() string { return "http://" + f.listener.Addr().String() } + +// Grant gives subject `sub` read access to the given auth resource paths. +func (f *MockFence) Grant(sub string, paths ...string) { + f.mu.Lock() + defer f.mu.Unlock() + f.grants[sub] = append([]string(nil), paths...) +} + +// Requests counts /user hits, for the control's evidence line. +func (f *MockFence) Requests() int { + f.mu.Lock() + defer f.mu.Unlock() + return f.requests +} + +// Close shuts the fence down. +func (f *MockFence) Close() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = f.server.Shutdown(ctx) +} + +func (f *MockFence) handle(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/user" { + http.NotFound(w, r) + return + } + f.mu.Lock() + f.requests++ + f.mu.Unlock() + auth := r.Header.Get("Authorization") + sub := subjectFromBearer(auth) + if sub == "" { + w.WriteHeader(http.StatusUnauthorized) + return + } + f.mu.Lock() + paths := append([]string(nil), f.grants[sub]...) + f.mu.Unlock() + authz := map[string]any{} + for _, path := range paths { + // A benchmark grant is a real principal grant: reads AND writes on + // the path. Dataset ingest and explorer publication run as write; + // the negative-auth control's scoped principal holds no grant on + // the dataset path at all, in either direction. + authz[path] = []any{ + map[string]any{"method": "read", "service": "*"}, + map[string]any{"method": "write", "service": "*"}, + } + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"sub": sub, "authz": authz}) +} + +// subjectFromBearer decodes the (unsigned) JWT payload for sub/user_name, +// mirroring internal/authscope's subjectFromAuthorizationHeader shape rules +// without importing its internals. +func subjectFromBearer(auth string) string { + const prefix = "bearer " + token := strings.TrimSpace(auth) + if len(token) < len(prefix) || !strings.EqualFold(token[:len(prefix)], prefix) { + return "" + } + token = strings.TrimSpace(token[len(prefix):]) + parts := strings.Split(token, ".") + if len(parts) < 2 { + return "" + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "" + } + var claims map[string]any + if err := json.Unmarshal(payload, &claims); err != nil { + return "" + } + if sub, _ := claims["sub"].(string); sub != "" { + return sub + } + if user, _ := claims["user_name"].(string); user != "" { + return user + } + return "" +} + +// MakeBearerToken builds an unsigned JWT-shaped bearer token with iss bound +// to the fence URL, satisfying the issuer-match check. +func MakeBearerToken(issuer, sub string) string { + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`)) + payloadRaw, _ := json.Marshal(map[string]any{"iss": issuer, "sub": sub}) + payload := base64.RawURLEncoding.EncodeToString(payloadRaw) + return "Bearer " + header + "." + payload + "." + base64.RawURLEncoding.EncodeToString([]byte("mock")) +} + +// ClickHouseSpec configures the publication chain for a spawned arm. +// Both arms run the same ClickHouse server but must never share a +// database: each arm materializes into its own benchmark-owned database +// so a publish on one driver can never read or displace the other's +// published rows. Recipe is the dataframer recipe JSON the server needs +// when ClickHouse is enabled; it is a startup input — publication +// materialization is receipt-driven. +type ClickHouseSpec struct { + URL string + Database string + Username string + Password string + Recipe string +} + +// BuildServerConfigYAML renders the arm config: ClickHouse enabled or +// disabled per spec (matched deployment: one spec drives both arms, only +// the database differs), scoped calypr auth against the mock fence, +// backend wiring passed in verbatim from the caller. +func BuildServerConfigYAML(listen, backend, backendYAML, fenceURL, cursorSecret string, ch *ClickHouseSpec) string { + var b bytes.Buffer + b.WriteString("server:\n") + fmt.Fprintf(&b, " listen: %q\n", listen) + fmt.Fprintf(&b, " backend: %q\n", backend) + if ch == nil { + b.WriteString(" clickhouse:\n enabled: false\n") + } else { + b.WriteString(" clickhouse:\n enabled: true\n") + fmt.Fprintf(&b, " url: %q\n", ch.URL) + fmt.Fprintf(&b, " database: %q\n", ch.Database) + fmt.Fprintf(&b, " username: %q\n", ch.Username) + fmt.Fprintf(&b, " password: %q\n", ch.Password) + fmt.Fprintf(&b, " dataframer:\n recipe: %q\n", ch.Recipe) + } + // The server refuses to configure without a >=16-char signing secret; + // callers generate one per run and scrub it from arm logs. + fmt.Fprintf(&b, " population_mapping_cursor_secret: %q\n", cursorSecret) + b.WriteString(backendYAML) + b.WriteString("auth:\n mode: calypr\n calypr:\n") + fmt.Fprintf(&b, " trusted_issuer: %q\n", fenceURL) + b.WriteString(" request_timeout: 10s\n cache_ttl: 1s\n") + return b.String() +} + +// PostgresBackendYAML renders the server.postgresql block for a spawned +// arm. The six-series schema ladder is mandatory: without it the server +// takes the first-query route and refuses all authoring routes, which would +// make the battery unrunnable — the ladder is spelled out here so a typo +// cannot silently degrade the run. Schemas live under a benchmark-owned +// prefix so arms can never touch production schemas. +func PostgresBackendYAML(host string, port int, database, user, password, sslmode, schemaPrefix string) string { + if sslmode == "" { + sslmode = "disable" + } + var b strings.Builder + b.WriteString(" postgresql:\n") + fmt.Fprintf(&b, " host: %q\n", host) + fmt.Fprintf(&b, " port: %d\n", port) + fmt.Fprintf(&b, " database: %q\n", database) + fmt.Fprintf(&b, " user: %q\n", user) + fmt.Fprintf(&b, " password: %q\n", password) + fmt.Fprintf(&b, " sslmode: %q\n", sslmode) + b.WriteString(" schemas:\n") + for _, series := range []string{"source", "explorer_control", "publication_control", "dataset_release", "recipe_chains", "recipe_exec_registries"} { + fmt.Fprintf(&b, " %s: %q\n", series, schemaPrefix+"_"+series) + } + return b.String() +} + +// ArangoBackendYAML renders the server-level url/database keys for a +// spawned Arango arm. +func ArangoBackendYAML(rawURL, database string) string { + var b strings.Builder + fmt.Fprintf(&b, " url: %q\n", rawURL) + fmt.Fprintf(&b, " database: %q\n", database) + return b.String() +} diff --git a/internal/dataframe/benchmark/publication_test.go b/internal/dataframe/benchmark/publication_test.go new file mode 100644 index 000000000..c2107be19 --- /dev/null +++ b/internal/dataframe/benchmark/publication_test.go @@ -0,0 +1,275 @@ +package benchmark + +import ( + "context" + "strings" + "testing" + "time" +) + +// The publication chain is ClickHouse-backed end to end (its only +// publication.Target is ClickHouseBundleStore; the workspace that seeds +// every receipt persists only after materialization). In a ClickHouse-less +// deployment the honest artifact is an excluded-chain report: spawn and +// upload ran for real, everything downstream is planned/not_run with the +// capability named, and zero timing evidence exists. These tests pin the +// honest shape and every deception path around it. + +func excludedPlan(scope string) RunPlan { + var plan RunPlan + if scope == RunScopePostgresOnly { + plan = PostgresOnlyPlan("no ArangoDB service in this execution environment; go-arango CI lane targets this leg") + } else { + plan = BothArmsPlan() + } + plan.Publication = &PublicationPlan{Run: false, Capability: PublicationCapabilityCH} + plan.PlannedNotRun = PlannedNotRunForBackends(plan.NotRunBackends()) + for i := range plan.PlannedNotRun { + plan.PlannedNotRun[i].Reason = "leg not run: " + plan.Legs[0].Capability + } + plan.PlannedNotRun = append(plan.PlannedNotRun, PublicationDisclosures(plan.Legs, PublicationCapabilityCH)...) + AppendWorkloadDisclosures(&plan, LiveBatteryExclusions()) + return plan +} + +func excludedGates(plan RunPlan) []WorkloadGateReport { + return ApplyExcludedWorkloadGateReports( + PublicationGateReports(plan.Legs, PublicationCapabilityCH), + ExcludedWorkloadGateReports(plan.Legs, LiveBatteryExclusions())) +} + +func excludedIdentity(runID string) RunIdentity { + return RunIdentity{ + RunID: runID, StartedAt: time.Unix(1700000000, 0).UTC(), + Preset: PresetTiny, SourceCommit: strings.Repeat("a", 40), + DatasetDigest: strings.Repeat("b", 64), + // The excluded chain exists precisely because ClickHouse is + // NOT deployed; the identity must say so. + ClickHouseDisabled: true, + Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}, + } +} + +func TestExcludedChainReportIsHonestAndVerifies(t *testing.T) { + plan := excludedPlan(RunScopeBothArms) + identity := excludedIdentity("bench-excl-a") + identity.ArangoVersion = "3.12.x-fake" + identity.PostgresVersion = "PostgreSQL 18.x-fake" + report, err := BuildReport(identity, plan, ControlRuns{}, excludedGates(plan), nil) + if err != nil { + t.Fatalf("honest excluded-chain report must build: %v", err) + } + if len(report.Samples) != 0 || len(report.Summaries) != 0 || len(report.Claims) != 0 { + t.Fatalf("excluded chain must carry zero timing evidence: %d samples, %d summaries, %d claims", + len(report.Samples), len(report.Summaries), len(report.Claims)) + } + liveExcluded := LiveBatteryExclusions() + for _, gate := range report.GateReports { + _, isExcludedWorkload := liveExcluded[gate.WorkloadID] + for _, arm := range []BackendResult{gate.Arango, gate.Postgres} { + if arm.Outcome != OutcomeLabelBlocked { + t.Fatalf("%s/%s must be blocked, got %s", gate.WorkloadID, arm.Backend, arm.Outcome) + } + if isExcludedWorkload { + // Both exclusions are true; the row must name the + // workload reason and carry the live-battery marker. + if !strings.Contains(arm.Detail, WorkloadNotRunReason) { + t.Fatalf("%s/%s detail must name the workload exclusion: %q", gate.WorkloadID, arm.Backend, arm.Detail) + } + if !strings.HasPrefix(gate.ExclusionReason, LiveBatteryExclusionMarker) { + t.Fatalf("%s must carry the live-battery marker: %q", gate.WorkloadID, gate.ExclusionReason) + } + continue + } + if !strings.Contains(arm.Detail, PublicationCapabilityCH) { + t.Fatalf("%s/%s detail must name the capability: %q", gate.WorkloadID, arm.Backend, arm.Detail) + } + } + } + if want := 2*len(Battery) + 2*len(liveExcluded); len(report.Plan.PlannedNotRun) != want { + t.Fatalf("both running legs must each disclose the whole battery plus the exclusions: %d rows want %d", len(report.Plan.PlannedNotRun), want) + } + // The artifact must survive marshal + LoadReport verification. + if _, err := LoadReport(mustMarshal(t, report)); err != nil { + t.Fatalf("excluded-chain artifact must verify: %v", err) + } +} + +func TestExcludedChainForgeryPaths(t *testing.T) { + build := func(t *testing.T) *Report { + t.Helper() + plan := excludedPlan(RunScopePostgresOnly) + identity := excludedIdentity("bench-excl-forge") + identity.PostgresVersion = "PostgreSQL 18.x-fake" + report, err := BuildReport(identity, plan, ControlRuns{}, excludedGates(plan), nil) + if err != nil { + t.Fatalf("baseline excluded report: %v", err) + } + return report + } + cases := []struct { + name string + tamper func(*testing.T, *Report) + wantErr string + }{ + {"stability limit borrowed into an excluded run", func(t *testing.T, r *Report) { + r.Controls.StabilityCVLimit = StabilityCVLimit + }, "control values claimed although the publication chain was excluded"}, + {"empty floor borrowed into an excluded run", func(t *testing.T, r *Report) { + r.Controls.EmptyFloorMillis = 0.4 + }, "control values claimed although the publication chain was excluded"}, + {"ratio policy cover for an excluded run", func(t *testing.T, r *Report) { + r.Controls.RatioPolicy = "ratio-only-both-passed" + }, "control values claimed although the publication chain was excluded"}, + {"self-ratio forged onto an excluded run leg", func(t *testing.T, r *Report) { + r.Controls.SelfRatioPostgres = 1.0 + }, "control values claimed although the publication chain was excluded"}, + {"samples fabricated into an excluded run", func(t *testing.T, r *Report) { + r.Samples = append(r.Samples, sample(BackendPostgres, primaryTiming(), OutcomePassed, 10, false)) + }, "timing evidence claimed although the publication chain was excluded"}, + {"one disclosure row quietly dropped", func(t *testing.T, r *Report) { + // drop the last postgres publication-chain row + drop, kept := -1, make([]PlannedNotRun, 0, len(r.Plan.PlannedNotRun)) + for i, row := range r.Plan.PlannedNotRun { + if drop < 0 && row.Backend == BackendPostgres && strings.HasPrefix(row.Reason, PublicationChainReason) { + drop = i + continue + } + kept = append(kept, row) + } + if drop < 0 { + t.Fatal("baseline lacks a postgres publication disclosure row") + } + r.Plan.PlannedNotRun = kept + }, "without a disclosure row"}, + {"one gate arm flipped to passed", func(t *testing.T, r *Report) { + r.GateReports[0].Postgres.Outcome = OutcomeLabelPassed + }, "although the publication chain was excluded"}, + {"chain excluded without naming the capability", func(t *testing.T, r *Report) { + r.Plan.Publication.Capability = " " + }, "publication chain excluded without a named capability"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + report := build(t) + tc.tamper(t, report) + if err := VerifyReport(report); err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("want error containing %q, got %v", tc.wantErr, err) + } + }) + } +} + +// The ClickHouse-disabled rule closes the 2026-11-13 forgery shape: a +// full timing report stamped from a deployment that cannot host the +// publication chain. +func TestCHDisabledCannotClaimPublicationRan(t *testing.T) { + report := validReport(t) // CI-lane fixture: ClickHouse deployed + if err := VerifyReport(report); err != nil { + t.Fatalf("CI-lane baseline must verify: %v", err) + } + report.Identity.ClickHouseDisabled = true + if err := VerifyReport(report); err == nil || + !strings.Contains(err.Error(), "publication chain claimed to run although ClickHouse is disabled") { + t.Fatalf("want CH-disabled refusal, got %v", err) + } + // An explicitly excluded chain is the honest alternative; this + // timing-bearing fixture must still be refused under it. + report.Plan.Publication = &PublicationPlan{Run: false, Capability: PublicationCapabilityCH} + if err := VerifyReport(report); err == nil || + !strings.Contains(err.Error(), "although the publication chain was excluded") { + t.Fatalf("excluded plan must refuse the timing evidence, got %v", err) + } +} + +// A publication-prefixed disclosure row is legal only under an excluding +// plan; elsewhere it is a "silently measured nothing and called it +// disclosed" path. +func TestPublicationRowsOnlyOnExcludingPlans(t *testing.T) { + report := validReport(t) + report.Plan.PlannedNotRun = append(report.Plan.PlannedNotRun, PlannedNotRun{ + WorkloadID: WorkloadFilterActiveGender, Backend: BackendArango, + Reason: PublicationChainReason + PublicationCapabilityCH, + }) + if err := VerifyReport(report); err == nil || !strings.Contains(err.Error(), "but its leg ran") { + t.Fatalf("want refusal of publication row on a non-excluding plan, got %v", err) + } +} + +// The driver must run the pre-publication stages for real in the excluded +// shape (arms up, dataset uploaded, versions probed) and refuse to publish +// or measure anything downstream. +func TestDriverExcludedPublicationProtocol(t *testing.T) { + fa, fp := newFakeArm(BackendArango), newFakeArm(BackendPostgres) + defer fa.close() + defer fp.close() + opts := driverOptions(fa, fp, RunScopeBothArms) + opts.ClickHouseEnabled = false + opts.Plan.Publication = &PublicationPlan{Run: false, Capability: PublicationCapabilityCH} + opts.Plan.PlannedNotRun = append(opts.Plan.PlannedNotRun, + PublicationDisclosures(opts.Plan.Legs, PublicationCapabilityCH)...) + report, evidence, err := RunBenchmark(context.Background(), opts) + if err != nil { + t.Fatal(err) + } + for name, f := range map[string]*fakeArm{"arango": fa, "postgres": fp} { + if f.uploads != 1 { + t.Fatalf("%s: upload is a non-publication stage and must run: uploads=%d want 1", name, f.uploads) + } + if f.publish != 0 { + t.Fatalf("%s: excluded chain must not publish: publish=%d want 0", name, f.publish) + } + } + if len(report.Samples) != 0 || len(report.Claims) != 0 { + t.Fatalf("excluded driver report must be timing-free: %d samples, %d claims", len(report.Samples), len(report.Claims)) + } + c := report.Controls + if c.StabilityCVLimit != 0 || c.EmptyFloorMillis != 0 || c.SelfRatioArango != 0 || + c.SelfRatioPostgres != 0 || c.RatioPolicy != "" || len(c.UnstableCells) != 0 { + t.Fatalf("excluded driver report must carry zero controls: %+v", c) + } + if len(evidence) != 2 { + t.Fatalf("both arms must be evidenced: %+v", evidence) + } + for _, ev := range evidence { + if ev.Version == "" { + t.Fatalf("live version probe is a non-publication stage: %+v", ev) + } + } + if _, err := LoadReport(mustMarshal(t, report)); err != nil { + t.Fatalf("driver excluded-chain artifact must verify: %v", err) + } +} + +func TestDriverRefusesIncoherentPublicationClaims(t *testing.T) { + fp := newFakeArm(BackendPostgres) + defer fp.close() + t.Run("run claimed without ClickHouse", func(t *testing.T) { + opts := driverOptions(nil, fp, RunScopePostgresOnly) + opts.ClickHouseEnabled = false + opts.Plan.Publication = &PublicationPlan{Run: true} + if _, _, err := RunBenchmark(context.Background(), opts); err == nil || + !strings.Contains(err.Error(), "refusing to claim it ran without ClickHouseEnabled") { + t.Fatalf("want refusal, got %v", err) + } + }) + t.Run("excluded without capability", func(t *testing.T) { + opts := driverOptions(nil, fp, RunScopePostgresOnly) + opts.Plan.Publication = &PublicationPlan{Run: false} + if _, _, err := RunBenchmark(context.Background(), opts); err == nil || + !strings.Contains(err.Error(), "without a named capability") { + t.Fatalf("want refusal, got %v", err) + } + }) +} + +// The named capability must track the CI ClickHouse pin so the harness, +// the disclosure text, and the CI lane cannot drift apart silently. +func TestPublicationCapabilityTracksCIPin(t *testing.T) { + if ClickHouseCIImage != "clickhouse/clickhouse-server:25.3" { + t.Fatalf("CI image drifted from the repository pin; update the constant deliberately: %s", ClickHouseCIImage) + } + if PublicationCapabilityCH != "CH-25.3-unavailable-in-container" { + t.Fatalf("capability must name the CI ClickHouse version: %s", PublicationCapabilityCH) + } +} diff --git a/internal/dataframe/benchmark/readback_shape_test.go b/internal/dataframe/benchmark/readback_shape_test.go new file mode 100644 index 000000000..0a9d6287c --- /dev/null +++ b/internal/dataframe/benchmark/readback_shape_test.go @@ -0,0 +1,69 @@ +package benchmark + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + loomexplorer "github.com/calypr/loom/internal/explorer" +) + +// The explorer route answers ExplorerStateV1 (internal/explorer +// state_v1.go): the draft digest is at draft.digest and the revision +// identity at active.revisionId. The paired run at 7c5ef71e aborted +// "explorer readback on arango omitted the draft digest" because the +// client decoded top-level draftDigest/activeRevisionId, which that +// shape never carries. This test serves a real-marshal ExplorerStateV1 +// and fails on the pre-fix client. + +func serverStateV1(digest, revisionID string) loomexplorer.ExplorerStateV1 { + return loomexplorer.ExplorerStateV1{ + APIVersion: loomexplorer.ExplorerStateV1APIVersion, + Kind: loomexplorer.ExplorerStateV1Kind, + Project: "loom-bench", + ExplorerID: "bench-workload", + Title: "bench-workload", + Management: loomexplorer.ManagementInteractive, + Draft: loomexplorer.ExplorerStateV1Draft{Version: 1, Digest: digest}, + Active: loomexplorer.ExplorerStateV1Active{RevisionID: revisionID, Status: "PUBLISHED"}, + ActiveURL: "/api/v1/projects/loom-bench/explorers/bench-workload", + UpdatedBy: "bench", + UpdatedAt: time.Now().UTC(), + } +} + +func TestFetchExplorerActiveStateReadsRealServerStateShape(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(serverStateV1("sha256:server-draft-digest", "authoring-rev-1")) + })) + defer srv.Close() + arm := &Arm{Backend: BackendArango, BaseURL: srv.URL, Client: srv.Client()} + state, err := arm.FetchExplorerActiveState(context.Background(), "t", "loom-bench", "bench-workload") + if err != nil { + t.Fatalf("readback errored: %v", err) + } + if state.DraftDigest != "sha256:server-draft-digest" { + t.Errorf("DraftDigest lost: got %q, want draft.digest", state.DraftDigest) + } + if state.ActiveRevisionID != "authoring-rev-1" { + t.Errorf("ActiveRevisionID lost: got %q, want active.revisionId", state.ActiveRevisionID) + } +} + +func TestSetupProofEvidenceAbortsOnEmptyDraftDigest(t *testing.T) { + f := newFakeArm(BackendArango) + defer f.close() + f.mu.Lock() + f.forceEmptyActiveState = true + f.rows["out"] = []map[string]any{{"patient_id": "p1"}} + f.mu.Unlock() + _, err := f.arm(BackendArango).SetupProofEvidence(context.Background(), "Bearer x", "proj", "exp-1", "rcpt-out", "out") + if err == nil || !strings.Contains(err.Error(), "omitted the draft digest") { + t.Fatalf("empty draft digest must abort fail-closed, got: %v", err) + } +} diff --git a/internal/dataframe/benchmark/report.go b/internal/dataframe/benchmark/report.go new file mode 100644 index 000000000..1aa2bbc31 --- /dev/null +++ b/internal/dataframe/benchmark/report.go @@ -0,0 +1,688 @@ +package benchmark + +import ( + "encoding/json" + "fmt" + "math" + "regexp" + "sort" + "strings" + "time" +) + +// The report is the deliverable. Its trust model: every derived number is +// RECOMPUTED from the raw record by VerifyReport, so a hand-edited (or +// partially forged) report fails verification. The verifier is deliberately +// independent of the harness: CI and reviewers run it on the artifact alone. + +const ReportSchemaVersion = "loom.benchmark-report/v1" + +var ( + sha256HexRe = regexp.MustCompile(`^[0-9a-f]{64}$`) + idRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) +) + +// RunIdentity ties the report to exactly what ran. +type RunIdentity struct { + RunID string `json:"runId"` + StartedAt time.Time `json:"startedAt"` + Preset string `json:"preset"` + SourceCommit string `json:"sourceCommit"` + DatasetDigest string `json:"datasetDigest"` // fixture manifest digest + ArangoVersion string `json:"arangoVersion"` + PostgresVersion string `json:"postgresVersion"` + // ClickHouseDisabled records the ruling: dataframe legs run with + // ClickHouse disabled on both arms so the reader path is matched. + ClickHouseDisabled bool `json:"clickhouseDisabled"` + Config RunConfig `json:"config"` +} + +// ControlRuns records the measurement-integrity controls (ruling i). Each +// control must have been RUN; absent controls are a verification failure. +type ControlRuns struct { + // StabilityCVLimit and the per-cell CVs used to decide stability. + StabilityCVLimit float64 `json:"stabilityCvLimit"` + UnstableCells []string `json:"unstableCells"` // "workload/backend" cells over the limit + // SelfRatio: backend vs itself across separate rounds; must sit at 1. + SelfRatioArango float64 `json:"selfRatioArango"` + SelfRatioPostgres float64 `json:"selfRatioPostgres"` + // EmptyResultFloor: timed no-result requests establishing the client + // overhead floor subtracted nowhere but reported as context. + EmptyFloorMillis float64 `json:"emptyFloorMillis"` + // RatioPolicy: ratios are published only for workloads both arms passed. + RatioPolicy string `json:"ratioPolicy"` +} + +// Claim is one publishable performance claim. +type Claim struct { + WorkloadID string `json:"workloadId"` + Arango Summary `json:"arango"` + Postgres Summary `json:"postgres"` + Ratio SpeedRatio `json:"ratio"` +} + +// Report is the full artifact. +type Report struct { + Schema string `json:"schema"` + Plan RunPlan `json:"plan"` + Identity RunIdentity `json:"identity"` + Status string `json:"status"` // passed | failed | blocked + StatusReasons []string `json:"statusReasons,omitempty"` + Controls ControlRuns `json:"controls"` + GateReports []WorkloadGateReport `json:"gateReports"` + Samples []Sample `json:"samples"` + Summaries []SummaryRow `json:"summaries"` + Claims []Claim `json:"claims"` + Unsupported []string `json:"unsupported"` + Exclusions string `json:"exclusions"` +} + +// SummaryRow ties a Summary to its exact cell. +type SummaryRow struct { + WorkloadID string `json:"workloadId"` + Backend string `json:"backend"` + Cache CacheLabel `json:"cache"` + Summary Summary `json:"summary"` +} + +// BuildReport assembles the artifact from harness-collected evidence and +// recomputes summaries/claims/status so the written file is internally +// consistent by construction. claimableOf decides per-workload eligibility +// (the harness passes the gate outcome); unstable cells are excluded from +// claims per the stability control. +func BuildReport(identity RunIdentity, plan RunPlan, controls ControlRuns, gateReports []WorkloadGateReport, samples []Sample) (*Report, error) { + if err := identity.Config.Validate(); err != nil { + return nil, err + } + if err := ValidateLegPlan(plan.Scope, plan.Legs); err != nil { + return nil, err + } + if err := ValidatePlannedNotRun(plan.PlannedNotRun); err != nil { + return nil, err + } + status, reasons := RunStatusScoped(gateReports, plan.Scope) + report := &Report{ + Schema: ReportSchemaVersion, + Plan: plan, + Identity: identity, + Status: status, + StatusReasons: reasons, + Controls: controls, + GateReports: gateReports, + Samples: samples, + Unsupported: UnsupportedCases(gateReports), + Exclusions: FormatExclusionList(gateReports), + } + byCell := map[string][]Sample{} + for _, s := range samples { + key := s.WorkloadID + "|" + s.Backend + "|" + string(s.Cache) + byCell[key] = append(byCell[key], s) + } + keys := make([]string, 0, len(byCell)) + for key := range byCell { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + parts := splitCell(key) + report.Summaries = append(report.Summaries, SummaryRow{ + WorkloadID: parts[0], Backend: parts[1], Cache: CacheLabel(parts[2]), + Summary: Summarize(byCell[key]), + }) + } + report.Claims = buildClaims(gateReports, report.Summaries, controls) + if err := VerifyReport(report); err != nil { + return nil, fmt.Errorf("benchmark: internally inconsistent report (bug): %w", err) + } + return report, nil +} + +// claimCellPair resolves the steady-regime cell a claim may cite for each +// arm (review r4559430568): warm preferred; service-warm for an attached +// arm that never restarts; process-cold cells are diagnostic only and are +// never claimed. The same helper drives buildClaims and VerifyReport, so a +// claim can only ever cite what the policy would choose. +func claimCellPair(summaryOf map[string]Summary, workloadID string) (Summary, Summary, bool) { + var a, p Summary + ok := true + for _, backend := range []string{BackendArango, BackendPostgres} { + found := false + for _, cache := range []CacheLabel{CacheWarm, CacheServiceWarm} { + s := summaryOf[workloadID+"|"+backend+"|"+string(cache)] + if s.Count > 0 && s.FailedCount == 0 { + if backend == BackendArango { + a = s + } else { + p = s + } + found = true + break + } + } + ok = ok && found + } + if !ok { + return Summary{}, Summary{}, false + } + return a, p, true +} + +func buildClaims(gateReports []WorkloadGateReport, summaries []SummaryRow, controls ControlRuns) []Claim { + byCell := map[string]Summary{} + for _, row := range summaries { + byCell[row.WorkloadID+"|"+row.Backend+"|"+string(row.Cache)] = row.Summary + } + unstable := map[string]bool{} + for _, cell := range controls.UnstableCells { + unstable[cell] = true + } + var claims []Claim + for _, gate := range gateReports { + if !gate.Claimable { + continue // ratio-only-both-passed policy + } + if unstable[gate.WorkloadID+"/"+BackendArango] || unstable[gate.WorkloadID+"/"+BackendPostgres] { + continue // stability control + } + a, p, ok := claimCellPair(byCell, gate.WorkloadID) + if !ok { + continue + } + ratio, err := NewSpeedRatio(a.MedianMillis, p.MedianMillis) + if err != nil { + continue + } + claims = append(claims, Claim{WorkloadID: gate.WorkloadID, Arango: a, Postgres: p, Ratio: ratio}) + } + return claims +} + +func splitCell(key string) [3]string { + var out [3]string + i, j := 0, 0 + for k := 0; k < len(key); k++ { + if key[k] == '|' { + out[i] = key[j:k] + i++ + j = k + 1 + } + } + out[i] = key[j:] + return out +} + +// VerifyReport recomputes everything derivable and fails on any mismatch. +// It is the check a synthetic forgery must not survive (ruling ii). +func VerifyReport(report *Report) error { + if report.Schema != ReportSchemaVersion { + return fmt.Errorf("benchmark report: unexpected schema %q", report.Schema) + } + if !idRe.MatchString(report.Identity.RunID) { + return fmt.Errorf("benchmark report: bad runId") + } + if err := report.Identity.Config.Validate(); err != nil { + return fmt.Errorf("benchmark report: %w", err) + } + if !sha256HexRe.MatchString(report.Identity.DatasetDigest) { + return fmt.Errorf("benchmark report: dataset digest is not sha256 hex") + } + // The leg plan must be total, closed, and scope-consistent. This runs + // first because the control rules below branch on how many legs ran. + if err := ValidateLegPlan(report.Plan.Scope, report.Plan.Legs); err != nil { + return fmt.Errorf("benchmark report: %w", err) + } + if err := ValidatePlannedNotRun(report.Plan.PlannedNotRun); err != nil { + return fmt.Errorf("benchmark report: %w", err) + } + if err := ValidatePublicationPlan(report.Plan); err != nil { + return fmt.Errorf("benchmark report: %w", err) + } + publicationExcluded := report.Plan.PublicationExcluded() + notRun := map[string]string{} + pubDisclosed := map[string]bool{} + runLegs := 0 + for _, leg := range report.Plan.Legs { + if leg.Mode == "not_run" { + notRun[leg.Backend] = leg.Capability + } else { + runLegs++ + } + } + if !publicationExcluded && report.Identity.ClickHouseDisabled && runLegs > 0 { + // The publication chain is ClickHouse-backed end to end: its only + // publication.Target is ClickHouseBundleStore, and the workspace + // that seeds every receipt persists only after materialization. + // A report from a ClickHouse-disabled deployment that claims the + // chain ran is the 2026-11-13 forgery shape this rule closes. + return fmt.Errorf("benchmark report: publication chain claimed to run although ClickHouse is disabled") + } + if runLegs > 0 { + if report.Plan.PublicationZeroEvidence() { + // Excluded mode: the battery itself was excluded, so nothing + // measured and any control value or timing evidence is forged + // (stolen limits, borrowed floors, ratio cover). A run that + // measured anything takes the else branch below, where the + // control-presence rules apply. + c := report.Controls + if c.StabilityCVLimit != 0 || c.EmptyFloorMillis != 0 || c.SelfRatioArango != 0 || + c.SelfRatioPostgres != 0 || c.RatioPolicy != "" || len(c.UnstableCells) != 0 { + return fmt.Errorf("benchmark report: control values claimed although the publication chain was excluded") + } + // Nothing downstream of publication executed, so no timing + // evidence may exist, every gate arm must read blocked, and + // each ran leg must carry one disclosure row per battery + // workload naming the publication capability. + if len(report.Samples) != 0 || len(report.Summaries) != 0 || len(report.Claims) != 0 { + return fmt.Errorf("benchmark report: timing evidence claimed although the publication chain was excluded") + } + for _, gate := range report.GateReports { + for _, arm := range []BackendResult{gate.Arango, gate.Postgres} { + if arm.Outcome != OutcomeLabelBlocked { + return fmt.Errorf("benchmark report: %s/%s arm shows %s although the publication chain was excluded", + gate.WorkloadID, arm.Backend, arm.Outcome) + } + } + } + } else { + // A run that measured anything must show the controls ran, at + // the shipped limit: a report may not smuggle its own ceiling + // (review r4561644049), so the limit is pinned to the exported + // constant the control functions enforce. + if report.Controls.StabilityCVLimit != StabilityCVLimit || report.Controls.RatioPolicy == "" { + return fmt.Errorf("benchmark report: measurement controls were not run (stability limit must equal %v or ratio policy missing)", StabilityCVLimit) + } + if report.Controls.EmptyFloorMillis <= 0 { + return fmt.Errorf("benchmark report: empty-result floor control was not run") + } + // The stability control must be re-derivable from raw samples: + // an omitted unstable cell would otherwise let claims rest on + // cells the control would have excluded. + want := ControlUnstableCells(report.Samples, StabilityCVLimit) + sort.Strings(want) + got := append([]string(nil), report.Controls.UnstableCells...) + sort.Strings(got) + if len(got) != len(want) { + return fmt.Errorf("benchmark report: unstable cells %v do not match the %v recomputed from raw samples", got, want) + } + for i := range want { + if got[i] != want[i] { + return fmt.Errorf("benchmark report: unstable cells %v do not match the %v recomputed from raw samples", got, want) + } + } + } + } else { + // Planned report: nothing ran, so a control that claims to have run + // is a forgery path (stolen limits, borrowed floors, ratio cover). + c := report.Controls + if c.StabilityCVLimit != 0 || c.EmptyFloorMillis != 0 || c.SelfRatioArango != 0 || + c.SelfRatioPostgres != 0 || c.RatioPolicy != "" || len(c.UnstableCells) != 0 { + return fmt.Errorf("benchmark report: control values claimed although no leg ran") + } + } + // Live versions: a run leg must carry one; a not-run leg may not claim + // one it could not have observed. + if _, absent := notRun[BackendArango]; !absent && strings.TrimSpace(report.Identity.ArangoVersion) == "" { + return fmt.Errorf("benchmark report: arango leg ran without a live arangoVersion") + } + if _, absent := notRun[BackendPostgres]; !absent && strings.TrimSpace(report.Identity.PostgresVersion) == "" { + return fmt.Errorf("benchmark report: postgres leg ran without a live postgresVersion") + } + if _, absent := notRun[BackendArango]; absent && report.Identity.ArangoVersion != "" { + return fmt.Errorf("benchmark report: arangoVersion claimed for a leg that did not run") + } + if _, absent := notRun[BackendPostgres]; absent && report.Identity.PostgresVersion != "" { + return fmt.Errorf("benchmark report: postgresVersion claimed for a leg that did not run") + } + // Self-ratio controls: run legs must have measured one near 1.0; a + // not-run leg's value must be exactly zero (absent), never a forged 1. + type selfRatio struct { + backend string + value float64 + } + for _, sr := range []selfRatio{{BackendArango, report.Controls.SelfRatioArango}, {BackendPostgres, report.Controls.SelfRatioPostgres}} { + if _, absent := notRun[sr.backend]; absent { + if sr.value != 0 { + return fmt.Errorf("benchmark report: self-ratio %v reported for %s although its leg did not run", sr.value, sr.backend) + } + continue + } + if publicationExcluded { + // The self-ratio control times published reads; with the + // chain excluded its absence is already enforced (zero) by + // the control-values rule above. + continue + } + if sr.value <= 0 { + return fmt.Errorf("benchmark report: %s self-ratio control was not run", sr.backend) + } + if math.Abs(sr.value-1) > 0.05 { + return fmt.Errorf("benchmark report: %s self-ratio %v off 1.0 beyond 5%% — environment unfit for ratios", sr.backend, sr.value) + } + // The value must be re-derivable from the raw samples + // (review r4561644049): a crafted artifact can pass the band + // above with a borrowed 1.0 while its halves say otherwise. + recomputed, ok := ControlSelfRatio(report.Samples, sr.backend) + if !ok { + return fmt.Errorf("benchmark report: %s self-ratio %v cannot be recomputed from the raw samples (unsplittable cells or no cells): the control is forged", sr.backend, sr.value) + } + if math.Abs(recomputed-sr.value) > 1e-9 { + return fmt.Errorf("benchmark report: %s self-ratio %v does not match the %v recomputed from raw samples", sr.backend, sr.value, recomputed) + } + } + // A not-run leg must be disclosed per workload and honest in the gates: + // blocked arm, zero samples, zero claims. Anything else is a forgery + // path ("silently measured nothing and claimed it anyway"). + // Live battery exclusions: workloads whose oracle/output agreement is + // disproven or unproven may never run on a live leg. The registry is + // deterministic (decoder verdicts only); VerifyReport is what ties + // every claim of exclusion back to it. + liveExcluded := LiveBatteryExclusions() + disclosed := map[string]bool{} + wlDisclosed := map[string]bool{} + for _, row := range report.Plan.PlannedNotRun { + if _, absent := notRun[row.Backend]; !absent { + // A disclosure row on a ran leg is only honest when it + // declares the excluded publication chain under an excluding + // plan, or a registry-excluded workload under its exact + // named reason; any other row on a ran leg is a forgery + // path. + if publicationExcluded && strings.HasPrefix(row.Reason, PublicationChainReason) { + pubDisclosed[row.WorkloadID+"|"+row.Backend] = true + continue + } + if reason, excludedWorkload := liveExcluded[row.WorkloadID]; excludedWorkload && + strings.HasPrefix(row.Reason, WorkloadNotRunReason) { + if row.Reason != WorkloadNotRunReason+reason { + return fmt.Errorf("benchmark report: %s/%s workload-not_run disclosure does not match the exclusion registry", + row.WorkloadID, row.Backend) + } + wlDisclosed[row.WorkloadID+"|"+row.Backend] = true + continue + } + return fmt.Errorf("benchmark report: planned/not_run row for %s/%s but its leg ran", row.WorkloadID, row.Backend) + } + disclosed[row.WorkloadID+"|"+row.Backend] = true + } + // Every ran leg of an excluded run must carry one publication-chain + // disclosure row per battery workload, exactly as a not-run leg must + // carry its leg rows. Checked after the loop above has collected them. + if publicationExcluded { + for _, leg := range report.Plan.Legs { + if leg.Mode != "run" { + continue + } + for _, spec := range Battery { + if !pubDisclosed[spec.ID+"|"+leg.Backend] { + return fmt.Errorf("benchmark report: %s/%s publication chain excluded without a disclosure row", + spec.ID, leg.Backend) + } + } + } + } + // Live battery law: every battery workload must appear in the gate + // reports of every ran leg (a silently shrunk live matrix is the same + // deception as a silently passed one). Excluded workloads must be + // blocked, marker-carrying, disclosure-backed, and sample-free; the + // marker may never sit on a workload the harness was allowed to run. + liveGate := map[string]WorkloadGateReport{} + for _, gate := range report.GateReports { + if _, dup := liveGate[gate.WorkloadID]; dup { + return fmt.Errorf("benchmark report: duplicate gate report for %s", gate.WorkloadID) + } + liveGate[gate.WorkloadID] = gate + } + for _, leg := range report.Plan.Legs { + if leg.Mode != "run" { + continue + } + for _, spec := range Battery { + gate, ok := liveGate[spec.ID] + if !ok { + return fmt.Errorf("benchmark report: %s leg ran without a gate report for %s", leg.Backend, spec.ID) + } + arm := gate.Arango + if leg.Backend == BackendPostgres { + arm = gate.Postgres + } + if _, excludedWorkload := liveExcluded[spec.ID]; excludedWorkload { + if arm.Outcome != OutcomeLabelBlocked { + return fmt.Errorf("benchmark report: %s/%s shows %s although the workload is excluded from live legs", + spec.ID, leg.Backend, arm.Outcome) + } + if !strings.HasPrefix(gate.ExclusionReason, LiveBatteryExclusionMarker) { + return fmt.Errorf("benchmark report: excluded workload %s on %s without the live-battery exclusion marker", + spec.ID, leg.Backend) + } + if !wlDisclosed[spec.ID+"|"+leg.Backend] { + return fmt.Errorf("benchmark report: %s/%s excluded from live legs without a disclosure row", + spec.ID, leg.Backend) + } + continue + } + if strings.HasPrefix(gate.ExclusionReason, LiveBatteryExclusionMarker) { + return fmt.Errorf("benchmark report: %s carries the live-battery exclusion marker but is not an excluded workload", + spec.ID) + } + } + } + for _, s := range report.Samples { + if _, excludedWorkload := liveExcluded[s.WorkloadID]; excludedWorkload { + return fmt.Errorf("benchmark report: timing samples exist for %s although it is excluded from live legs", + s.WorkloadID) + } + } + for _, gate := range report.GateReports { + for _, arm := range []BackendResult{gate.Arango, gate.Postgres} { + capability, absent := notRun[arm.Backend] + if !absent { + continue + } + if arm.Outcome != OutcomeLabelBlocked { + return fmt.Errorf("benchmark report: %s/%s arm shows %s but its leg was not_run (%s)", + gate.WorkloadID, arm.Backend, arm.Outcome, capability) + } + if !disclosed[gate.WorkloadID+"|"+arm.Backend] { + return fmt.Errorf("benchmark report: %s/%s leg not_run without a planned/not_run disclosure row", + gate.WorkloadID, arm.Backend) + } + } + } + for _, s := range report.Samples { + if _, absent := notRun[s.Backend]; absent { + return fmt.Errorf("benchmark report: samples exist for %s but its leg was not_run", s.Backend) + } + } + for _, row := range report.Summaries { + if _, absent := notRun[row.Backend]; absent { + return fmt.Errorf("benchmark report: summary rows exist for %s but its leg was not_run", row.Backend) + } + } + for _, claim := range report.Claims { + if _, absent := notRun[BackendArango]; absent { + return fmt.Errorf("benchmark report: speed claim for %s exists while the arango leg was not_run", claim.WorkloadID) + } + if _, absent := notRun[BackendPostgres]; absent { + return fmt.Errorf("benchmark report: speed claim for %s exists while the postgres leg was not_run", claim.WorkloadID) + } + } + // Status must follow from the gate reports under the declared scope. + status, reasons := RunStatusScoped(report.GateReports, report.Plan.Scope) + if report.Status != status { + return fmt.Errorf("benchmark report: status %q but gates imply %q", report.Status, status) + } + if !sameStrings(report.StatusReasons, reasons) { + return fmt.Errorf("benchmark report: status reasons do not match gate evaluation") + } + // Unsupported list must match the gate reports. + if !sameStrings(report.Unsupported, UnsupportedCases(report.GateReports)) { + return fmt.Errorf("benchmark report: unsupported list does not match gate reports") + } + if report.Exclusions != FormatExclusionList(report.GateReports) { + return fmt.Errorf("benchmark report: exclusion note does not match gate reports") + } + // Every sample is well-formed. + for _, s := range report.Samples { + if !ValidBackend(s.Backend) { + return fmt.Errorf("benchmark report: sample has unknown backend %q", s.Backend) + } + if _, ok := workloadColumns(s.WorkloadID); !ok { + return fmt.Errorf("benchmark report: sample references unknown workload %q", s.WorkloadID) + } + if s.Millis < 0 { + return fmt.Errorf("benchmark report: negative duration sample") + } + switch s.Outcome { + case OutcomePassed, OutcomeFailed, OutcomeTimeout, OutcomeError: + default: + return fmt.Errorf("benchmark report: unknown sample outcome %q", s.Outcome) + } + } + // Summaries are recomputed from samples and must match. + byCell := map[string][]Sample{} + for _, s := range report.Samples { + key := s.WorkloadID + "|" + s.Backend + "|" + string(s.Cache) + byCell[key] = append(byCell[key], s) + } + seen := map[string]bool{} + for _, row := range report.Summaries { + key := row.WorkloadID + "|" + row.Backend + "|" + string(row.Cache) + if seen[key] { + return fmt.Errorf("benchmark report: duplicate summary cell %s", key) + } + seen[key] = true + expected := Summarize(byCell[key]) + if !summaryNearEqual(expected, row.Summary) { + return fmt.Errorf("benchmark report: summary for %s does not recompute from raw samples", key) + } + } + for key := range byCell { + if !seen[key] { + return fmt.Errorf("benchmark report: raw samples for %s have no summary row", key) + } + } + // Gate bag digests are sha256-shaped when present. + for _, gate := range report.GateReports { + for _, arm := range []BackendResult{gate.Arango, gate.Postgres} { + if arm.BagDigest != "" && !sha256HexRe.MatchString(arm.BagDigest) { + return fmt.Errorf("benchmark report: %s/%s bag digest is not sha256 hex", gate.WorkloadID, arm.Backend) + } + } + } + // Claims: each must be recomputable, claimable, stable, and both-clean. + summaryOf := map[string]Summary{} + for _, row := range report.Summaries { + summaryOf[row.WorkloadID+"|"+row.Backend+"|"+string(row.Cache)] = row.Summary + } + claimableByID := map[string]WorkloadGateReport{} + for _, gate := range report.GateReports { + claimableByID[gate.WorkloadID] = gate + } + for _, claim := range report.Claims { + gate, ok := claimableByID[claim.WorkloadID] + if !ok || !gate.Claimable { + return fmt.Errorf("benchmark report: claim for %s is not gate-claimable", claim.WorkloadID) + } + na, np, pairOK := claimCellPair(summaryOf, claim.WorkloadID) + if !pairOK { + return fmt.Errorf("benchmark report: claim for %s rests on cells with failures or no samples", claim.WorkloadID) + } + if !summaryNearEqual(na, claim.Arango) || !summaryNearEqual(np, claim.Postgres) { + return fmt.Errorf("benchmark report: claim for %s cites summaries that do not recompute", claim.WorkloadID) + } + expected, err := NewSpeedRatio(na.MedianMillis, np.MedianMillis) + if err != nil || math.Abs(expected.ArangoOverPostgres-claim.Ratio.ArangoOverPostgres) > 1e-9 { + return fmt.Errorf("benchmark report: claim ratio for %s does not match medians", claim.WorkloadID) + } + } + // A passed run must contain evidence for every leg that ran, and under + // both-arms scope at least one speed claim. A postgres-only pass has + // no claims by construction: it certifies correctness and timings, not + // a comparison. + if report.Status == "passed" { + if report.Plan.Scope == RunScopePostgresOnly { + if err := requireRanArms(report); err != nil { + return err + } + } else { + if len(report.Claims) == 0 { + return fmt.Errorf("benchmark report: passed run with no claims") + } + if err := requireBothArms(report); err != nil { + return err + } + } + } + return nil +} + +func requireRanArms(report *Report) error { + // postgres-only pass posture: the ran arm must actually have evidence. + for _, gate := range report.GateReports { + if gate.Postgres.Outcome == OutcomeLabelPassed { + return nil + } + } + return fmt.Errorf("benchmark report: postgres-only pass without any passed postgres arm evidence") +} + +func requireBothArms(report *Report) error { + arms := map[string]bool{} + for _, gate := range report.GateReports { + if gate.Arango.Outcome == OutcomeLabelPassed { + arms[BackendArango] = true + } + if gate.Postgres.Outcome == OutcomeLabelPassed { + arms[BackendPostgres] = true + } + } + if !arms[BackendArango] || !arms[BackendPostgres] { + return fmt.Errorf("benchmark report: passed run missing a backend arm's evidence") + } + return nil +} + +func summaryNearEqual(a, b Summary) bool { + const eps = 1e-6 + near := func(x, y float64) bool { + return math.Abs(x-y) <= eps*math.Max(1, math.Max(math.Abs(x), math.Abs(y))) + } + return a.Count == b.Count && a.FailedCount == b.FailedCount && + a.ThroughputMeasured == b.ThroughputMeasured && + near(a.MedianMillis, b.MedianMillis) && near(a.MeanMillis, b.MeanMillis) && + near(a.VarianceMs, b.VarianceMs) && near(a.StdDevMs, b.StdDevMs) && + near(a.MinMillis, b.MinMillis) && near(a.MaxMillis, b.MaxMillis) && + near(a.P50Millis, b.P50Millis) && near(a.P95Millis, b.P95Millis) && + near(a.CoefficientOfVariation, b.CoefficientOfVariation) && + near(a.ThroughputPerSecond, b.ThroughputPerSecond) && + a.QuantileMethod == b.QuantileMethod +} + +func sameStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// MarshalReport renders the canonical JSON bytes of the artifact. +func MarshalReport(report *Report) ([]byte, error) { + return json.MarshalIndent(report, "", " ") +} + +// LoadReport parses bytes and verifies the artifact on load. This is the +// entry point CI and the node verifier use. +func LoadReport(data []byte) (*Report, error) { + var report Report + if err := json.Unmarshal(data, &report); err != nil { + return nil, fmt.Errorf("benchmark report: %w", err) + } + if err := VerifyReport(&report); err != nil { + return nil, err + } + return &report, nil +} diff --git a/internal/dataframe/benchmark/report_md.go b/internal/dataframe/benchmark/report_md.go new file mode 100644 index 000000000..da1474592 --- /dev/null +++ b/internal/dataframe/benchmark/report_md.go @@ -0,0 +1,160 @@ +package benchmark + +import ( + "fmt" + "sort" + "strings" +) + +// RenderMarkdown turns a verified report into the human-facing report.md. +// It renders only what VerifyReport certified: every number here is +// recomputed evidence, every absence is named. + +func RenderMarkdown(report *Report) string { + var b strings.Builder + fmt.Fprintf(&b, "# Backend benchmark: Arango/AQL vs PostgreSQL/SQL — %s\n\n", report.Identity.RunID) + fmt.Fprintf(&b, "**Status: %s**\n\n", report.Status) + if len(report.StatusReasons) > 0 { + b.WriteString("Reasons:\n\n") + for _, r := range report.StatusReasons { + fmt.Fprintf(&b, "- %s\n", r) + } + b.WriteString("\n") + } + + b.WriteString("## Run scope and legs\n\n") + fmt.Fprintf(&b, "Scope: `%s`\n\n", report.Plan.Scope) + b.WriteString("| backend | mode | capability |\n|---|---|---|\n") + for _, leg := range report.Plan.Legs { + capability := "—" + if leg.Mode == "not_run" { + capability = leg.Capability + } + fmt.Fprintf(&b, "| %s | %s | %s |\n", leg.Backend, leg.Mode, capability) + } + b.WriteString("\n") + if len(report.Plan.PlannedNotRun) > 0 { + b.WriteString("Planned/not_run disclosure rows (never silently skipped):\n\n") + b.WriteString("| workload | backend | reason |\n|---|---|---|\n") + rows := append([]PlannedNotRun(nil), report.Plan.PlannedNotRun...) + sort.Slice(rows, func(i, j int) bool { + if rows[i].WorkloadID != rows[j].WorkloadID { + return rows[i].WorkloadID < rows[j].WorkloadID + } + return rows[i].Backend < rows[j].Backend + }) + for _, row := range rows { + fmt.Fprintf(&b, "| %s | %s | %s |\n", row.WorkloadID, row.Backend, row.Reason) + } + b.WriteString("\n") + } + + b.WriteString("## Identity\n\n") + fmt.Fprintf(&b, "- Source commit: `%s`\n", report.Identity.SourceCommit) + fmt.Fprintf(&b, "- Dataset digest (sha256): `%s` (preset `%s`)\n", report.Identity.DatasetDigest, report.Identity.Preset) + fmt.Fprintf(&b, "- Arango version: %s\n", versionOrAbsent(report, BackendArango)) + fmt.Fprintf(&b, "- PostgreSQL version: %s\n", versionOrAbsent(report, BackendPostgres)) + fmt.Fprintf(&b, "- ClickHouse disabled during runs: %v\n", report.Identity.ClickHouseDisabled) + fmt.Fprintf(&b, "- Config: warmup=%d iterations=%d concurrency=%d rounds=%d\n\n", + report.Identity.Config.Warmup, report.Identity.Config.Iterations, + report.Identity.Config.Concurrency, report.Identity.Config.Rounds) + + b.WriteString("## Measurement controls\n\n") + fmt.Fprintf(&b, "- Stability: per-cell CV ceiling %.2f; unstable cells excluded from claims", report.Controls.StabilityCVLimit) + if len(report.Controls.UnstableCells) == 0 { + b.WriteString(" (none).\n") + } else { + fmt.Fprintf(&b, ": %s.\n", strings.Join(report.Controls.UnstableCells, ", ")) + } + fmt.Fprintf(&b, "- Self-ratio (backend vs itself across halves): arango=%s, postgres=%.3f — each must sit at 1±0.05.\n", + selfRatioCell(report, BackendArango), report.Controls.SelfRatioPostgres) + fmt.Fprintf(&b, "- Empty-result floor: identical request returning no rows took %.3f ms (client overhead context).\n", report.Controls.EmptyFloorMillis) + fmt.Fprintf(&b, "- Ratio policy: %s — speed ratios are computed only for workloads where both arms passed correctness and completed without timeout.\n\n", + report.Controls.RatioPolicy) + + b.WriteString("## Correctness gates (all must pass before any timing is claimable)\n\n") + b.WriteString("| workload | arango | postgres | claimable |\n|---|---|---|---|\n") + gates := append([]WorkloadGateReport(nil), report.GateReports...) + sort.Slice(gates, func(i, j int) bool { return gates[i].WorkloadID < gates[j].WorkloadID }) + for _, g := range gates { + fmt.Fprintf(&b, "| %s | %s | %s | %v |\n", g.WorkloadID, g.Arango.Outcome, g.Postgres.Outcome, g.Claimable) + } + b.WriteString("\n") + + b.WriteString("## Timings (warm samples only; raw samples in report.json)\n\n") + b.WriteString("| workload | backend | cache | n | failed | p50 ms | p95 ms |\n|---|---|---|---|---|---|---|\n") + summaries := append([]SummaryRow(nil), report.Summaries...) + sort.Slice(summaries, func(i, j int) bool { + if summaries[i].WorkloadID != summaries[j].WorkloadID { + return summaries[i].WorkloadID < summaries[j].WorkloadID + } + if summaries[i].Backend != summaries[j].Backend { + return summaries[i].Backend < summaries[j].Backend + } + return summaries[i].Cache < summaries[j].Cache + }) + for _, row := range summaries { + fmt.Fprintf(&b, "| %s | %s | %s | %d | %d | %.3f | %.3f |\n", + row.WorkloadID, row.Backend, row.Cache, row.Summary.Count, row.Summary.FailedCount, + row.Summary.MedianMillis, row.Summary.P95Millis) + } + b.WriteString("\n") + + b.WriteString("## Speed claims\n\n") + if len(report.Claims) == 0 { + if report.Plan.Scope == RunScopePostgresOnly { + b.WriteString("None: no cross-backend speed claims are made in postgres-only scope. The Arango leg is CI-targeted (see the leg table).\n\n") + } else { + b.WriteString("None passed the ratio-only-both-passed policy.\n\n") + } + } else { + b.WriteString("| workload | arango p50 ms | postgres p50 ms | arango/postgres |\n|---|---|---|---|\n") + for _, c := range report.Claims { + fmt.Fprintf(&b, "| %s | %.3f | %.3f | %.3f |\n", + c.WorkloadID, c.Arango.MedianMillis, c.Postgres.MedianMillis, c.Ratio.ArangoOverPostgres) + } + b.WriteString("\n") + } + + b.WriteString("## Unsupported and exclusions\n\n") + if len(report.Unsupported) == 0 { + b.WriteString("No unsupported cases.\n\n") + } else { + b.WriteString("Unsupported cases (published, not silently dropped):\n\n") + for _, u := range report.Unsupported { + fmt.Fprintf(&b, "- %s\n", u) + } + b.WriteString("\n") + } + if strings.TrimSpace(report.Exclusions) == "" { + b.WriteString("No workloads excluded from performance claims.\n") + } else { + fmt.Fprintf(&b, "%s\n", report.Exclusions) + } + return b.String() +} + +func versionOrAbsent(report *Report, backend string) string { + for _, leg := range report.Plan.Legs { + if leg.Backend == backend && leg.Mode == "not_run" { + return fmt.Sprintf("not observed (leg not run: %s)", leg.Capability) + } + } + if backend == BackendArango { + return report.Identity.ArangoVersion + } + return report.Identity.PostgresVersion +} + +func selfRatioCell(report *Report, backend string) string { + value := report.Controls.SelfRatioArango + if backend == BackendPostgres { + value = report.Controls.SelfRatioPostgres + } + for _, leg := range report.Plan.Legs { + if leg.Backend == backend && leg.Mode == "not_run" { + return "not run (leg not run)" + } + } + return fmt.Sprintf("%.3f", value) +} diff --git a/internal/dataframe/benchmark/report_test.go b/internal/dataframe/benchmark/report_test.go new file mode 100644 index 000000000..5f4d7456a --- /dev/null +++ b/internal/dataframe/benchmark/report_test.go @@ -0,0 +1,427 @@ +package benchmark + +import ( + "encoding/json" + "strings" + "testing" + "time" +) + +// Ruling (ii): the verifier must refuse a synthetic forgery. Every derived +// number in the report is RECOMPUTED from raw evidence by VerifyReport, and +// this suite proves each tamper path REDs. + +func validReport(t *testing.T) *Report { + t.Helper() + identity := RunIdentity{ + RunID: "bench-2026-11-13-a", StartedAt: time.Unix(1700000, 0).UTC(), + Preset: PresetTiny, SourceCommit: strings.Repeat("a", 40), + DatasetDigest: strings.Repeat("b", 64), + ArangoVersion: "3.12.x", PostgresVersion: "PostgreSQL 18.x", + // A timing report describes the CI lane with ClickHouse + // deployed; the worker container can only build excluded-chain + // reports (see the publication-exclusion forgery cases). + ClickHouseDisabled: false, + Config: RunConfig{Warmup: 1, Iterations: 4, Concurrency: 1, Rounds: 2}, + } + liveExcluded := LiveBatteryExclusions() + var gates []WorkloadGateReport + var samples []Sample + for _, spec := range Battery { + if _, excluded := liveExcluded[spec.ID]; excluded { + continue + } + gates = append(gates, WorkloadGateReport{ + WorkloadID: spec.ID, Claimable: true, + Arango: BackendResult{Backend: BackendArango, Outcome: OutcomeLabelPassed, RowCount: 5, BagDigest: strings.Repeat("c", 64)}, + Postgres: BackendResult{Backend: BackendPostgres, Outcome: OutcomeLabelPassed, RowCount: 5, BagDigest: strings.Repeat("c", 64)}, + Gates: []GateResult{{Gate: GateBagEquality, Pass: true}}, + }) + for _, backend := range []string{BackendArango, BackendPostgres} { + samples = append(samples, sample(backend, spec.ID, OutcomePassed, 5, true)) // warmup + base := 20.0 + if backend == BackendPostgres { + base = 10.0 + } + // Halves [base, base, base+3%, base+3%]: the honest self-ratio + // (worst half-median deviation) is 1.03, inside the 5% band, + // and the stable-regime CV is well under the limit. VerifyRe- + // port recomputes both controls from these samples, so the + // fixture must state what the samples actually show. + for i := 0; i < 4; i++ { + offset := 0.0 + if i >= 2 { + offset = base * 0.03 + } + samples = append(samples, sample(backend, spec.ID, OutcomePassed, base+offset, false)) + } + } + } + // The controls are exactly what the control functions derive from the + // samples: the verifier re-derives them (review r4561644049), so an + // honest fixture states honest values. + controls := ControlRuns{ + StabilityCVLimit: StabilityCVLimit, + UnstableCells: ControlUnstableCells(samples, StabilityCVLimit), + EmptyFloorMillis: 0.4, RatioPolicy: "ratio-only-both-passed", + } + for _, backend := range []string{BackendArango, BackendPostgres} { + value, ok := ControlSelfRatio(samples, backend) + if !ok { + t.Fatalf("%s self-ratio not computable from fixture samples", backend) + } + if backend == BackendArango { + controls.SelfRatioArango = value + } else { + controls.SelfRatioPostgres = value + } + } + timingPlan := BothArmsPlan() + timingPlan.Publication = &PublicationPlan{Run: true} + gates = append(gates, ExcludedWorkloadGateReports(timingPlan.Legs, liveExcluded)...) + AppendWorkloadDisclosures(&timingPlan, liveExcluded) + report, err := BuildReport(identity, timingPlan, controls, gates, samples) + if err != nil { + t.Fatalf("valid report build failed: %v", err) + } + return report +} + +// primaryTiming returns the workload validReport puts first among the +// proven (timed) workloads; forgery cases mutate GateReports[0]. +func primaryTiming() string { + for _, spec := range Battery { + if _, excluded := LiveBatteryExclusions()[spec.ID]; !excluded { + return spec.ID + } + } + panic("live battery has no proven workloads") +} + +func gateForID(r *Report, workloadID string) *WorkloadGateReport { + for i := range r.GateReports { + if r.GateReports[i].WorkloadID == workloadID { + return &r.GateReports[i] + } + } + panic("no gate report for " + workloadID) +} + +func roundTrip(t *testing.T, report *Report) *Report { + t.Helper() + data, err := MarshalReport(report) + if err != nil { + t.Fatal(err) + } + loaded, err := LoadReport(data) + if err != nil { + t.Fatalf("round trip failed: %v", err) + } + return loaded +} + +func TestReportRoundTrips(t *testing.T) { + report := validReport(t) + loaded := roundTrip(t, report) + if loaded.Status != "passed" || len(loaded.Claims) != len(Battery)-len(LiveBatteryExclusions()) { + t.Fatalf("expected one claim per proven workload on a passed run: %+v", loaded) + } + if loaded.Claims[0].Ratio.ArangoOverPostgres <= 0 { + t.Fatal("ratio missing") + } +} + +func forge(t *testing.T, mutate func(*Report)) func(*testing.T) { + return forgeErr(t, mutate, "") +} + +// forgeErr additionally pins WHICH rule rejected the artifact: a case +// meant to exercise a specific guard must not survive by tripping some +// earlier check (review r4562338071) — otherwise deleting the guard +// would leave the case falsely green. +func forgeErr(t *testing.T, mutate func(*Report), wantErrSubstr string) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + report := validReport(t) + mutate(report) + data, err := json.Marshal(report) + if err != nil { + t.Fatal(err) + } + if _, err := LoadReport(data); err == nil { + t.Fatal("forged report survived verification") + } else if wantErrSubstr != "" && !strings.Contains(err.Error(), wantErrSubstr) { + t.Fatalf("forged report rejected by the wrong rule (%v); want an error naming %q", err, wantErrSubstr) + } + } +} + +func TestReportForgerySuite(t *testing.T) { + cases := map[string]func(*Report){ + "summary-median-tampered": func(r *Report) { + r.Summaries[0].Summary.MedianMillis = 0.001 + }, + "failed-sample-deleted-from-raw": func(r *Report) { + // Append a timed-out sample so the summary legitimately has a + // failure, then delete it from raw: summary says FailedCount 1. + r.Samples = append(r.Samples, sample(BackendArango, primaryTiming(), OutcomeTimeout, 9000, false)) + sums := r.Summaries + r.Summaries = nil + for _, row := range sums { + if row.Backend == BackendArango { + row.Summary = Summarize(r.Samples) + } + r.Summaries = append(r.Summaries, row) + } + var kept []Sample + for _, s := range r.Samples { + if s.Outcome != OutcomeTimeout { + kept = append(kept, s) + } + } + r.Samples = kept + }, + "status-flipped-to-passed": func(r *Report) { + r.GateReports[0].Claimable = false + r.GateReports[0].ExclusionReason = "forged exclusion" + r.Status = "passed" // gates now imply failed + }, + "gate-flipped-to-claimable-with-failed-arm": func(r *Report) { + r.GateReports[0].Postgres.Outcome = OutcomeLabelPassed + r.Status = "passed" + r.GateReports[0].Claimable = true + // Postgres summaries legitimately exist; but flip the gate's + // bag digest to garbage: shape check must reject it. + r.GateReports[0].Postgres.BagDigest = "not-hex" + }, + "dataset-digest-zeroed": func(r *Report) { r.Identity.DatasetDigest = "0" }, + "summary-row-deleted": func(r *Report) { r.Summaries = r.Summaries[:1] }, + "claim-ratio-fudged": func(r *Report) { r.Claims[0].Ratio.ArangoOverPostgres *= 0.5 }, + "claim-cites-nonclaimable": func(r *Report) { + r.GateReports[0].Claimable = false + r.GateReports[0].ExclusionReason = "forged exclusion" + r.Status = "failed" + r.StatusReasons = []string{primaryTiming() + ": forged exclusion"} + // Claims left in place: a claim on a non-claimable workload + // must not verify even when the status was honestly flipped. + }, + "self-ratio-forged-high": func(r *Report) { r.Controls.SelfRatioArango = 3.0 }, + // Review r4561644049: values inside the plausibility band are no + // longer accepted on faith; they must recompute from raw samples. + "self-ratio-borrowed-perfect": func(r *Report) { + // The fixture samples honestly show a 1.03 worst half-median + // deviation; the artifact claims the perfect 1.0 instead. + // The band check cannot see it (1.0 is in band) — only + // recomputation from samples can. + r.Controls.SelfRatioArango = 1.0 + }, + // Review r4562338071: each new verifier guard needs a case that + // fails ONLY on it. Both cases below keep every other derived + // value honest and pin the rejecting rule by message, so deleting + // the guard turns the case RED instead of silently re-routing it + // to an earlier check. + "unstable-cell-hidden-from-control": func(r *Report) { + // Rewrite the primary timing's arango warm samples as + // alternating 1,100,1,100: half medians stay equal, so the + // honest self-ratio (worst deviation across cells) still + // matches the reported 1.03. Summaries are recomputed from + // the samples and claims rebuilt from those summaries, so + // every other derived value stays in sync. The cell's warm + // CV (~0.98) is far over the ceiling: only the + // ControlUnstableCells recompute can catch this shape. + ms := []float64{1, 100, 1, 100} + i := 0 + for j := range r.Samples { + s := &r.Samples[j] + if s.WorkloadID == primaryTiming() && s.Backend == BackendArango && !s.Warmup { + s.Millis = ms[i] + i++ + } + } + for j := range r.Summaries { + row := &r.Summaries[j] + if row.WorkloadID == primaryTiming() && row.Backend == BackendArango { + row.Summary = Summarize(cellSamples(r.Samples, row.WorkloadID, row.Backend)) + } + } + r.Claims = buildClaims(r.GateReports, r.Summaries, r.Controls) + r.Controls.UnstableCells = nil + }, + "stability-limit-smuggled-ceiling": func(r *Report) { + // 0.9 passes every plausibility rule the old verifier had + // (it only rejected <= 0): a run may not smuggle its own + // ceiling. Only the equality with the exported constant + // catches this. + r.Controls.StabilityCVLimit = 0.9 + }, + "empty-floor-never-run": func(r *Report) { r.Controls.EmptyFloorMillis = 0 }, + "stability-limit-never-set": func(r *Report) { r.Controls.StabilityCVLimit = 0 }, + "unsupported-list-drifted": func(r *Report) { r.Unsupported = []string{primaryTiming() + "/postgres"} }, + "exclusion-note-drifted": func(r *Report) { r.Exclusions = "no workloads excluded from performance claims (lies)" }, + "unknown-backend-in-samples": func(r *Report) { r.Samples[0].Backend = "clickhouse" }, + "duplicate-summary-cell": func(r *Report) { r.Summaries = append(r.Summaries, r.Summaries[0]) }, + "schema-renamed": func(r *Report) { r.Schema = "loom.benchmark-report/v0" }, + "runid-blank": func(r *Report) { r.Identity.RunID = "" }, + "config-zeroed": func(r *Report) { r.Identity.Config.Iterations = 0 }, + // Live battery law (review r4559430560): excluded workloads must + // stay blocked, disclosed, and sample-free; the marker may never + // move; the matrix may never shrink silently. + "excluded-arm-flipped-to-passed": func(r *Report) { + g := gateForID(r, WorkloadDeriveAgeBand) + g.Arango.Outcome = OutcomeLabelPassed + }, + "excluded-gate-report-deleted": func(r *Report) { + var kept []WorkloadGateReport + for _, g := range r.GateReports { + if g.WorkloadID != WorkloadDeriveAgeBand { + kept = append(kept, g) + } + } + r.GateReports = kept + }, + "proven-gate-report-deleted": func(r *Report) { + var kept []WorkloadGateReport + for _, g := range r.GateReports { + if g.WorkloadID != primaryTiming() { + kept = append(kept, g) + } + } + r.GateReports = kept + }, + "excluded-workload-samples-injected": func(r *Report) { + r.Samples = append(r.Samples, sample(BackendPostgres, WorkloadDistinctFamily, OutcomePassed, 10, false)) + }, + "excluded-disclosure-row-deleted": func(r *Report) { + var kept []PlannedNotRun + dropped := false + for _, row := range r.Plan.PlannedNotRun { + if !dropped && row.WorkloadID == WorkloadDeriveAgeBand && row.Backend == BackendArango { + dropped = true + continue + } + kept = append(kept, row) + } + if !dropped { + panic("valid report lacks the exclusion disclosure row") + } + r.Plan.PlannedNotRun = kept + }, + "excluded-disclosure-reason-forged": func(r *Report) { + for i := range r.Plan.PlannedNotRun { + if r.Plan.PlannedNotRun[i].WorkloadID == WorkloadDeriveAgeBand { + r.Plan.PlannedNotRun[i].Reason = WorkloadNotRunReason + "because I said so" + return + } + } + panic("valid report lacks the exclusion disclosure row") + }, + "marker-forged-on-proven-workload": func(r *Report) { + gateForID(r, primaryTiming()).ExclusionReason = LiveBatteryExclusionMarker + "hiding a slow workload" + }, + "excluded-row-forged-for-proven-workload": func(r *Report) { + reason := LiveBatteryExclusions()[WorkloadDeriveAgeBand] + r.Plan.PlannedNotRun = append(r.Plan.PlannedNotRun, PlannedNotRun{ + WorkloadID: primaryTiming(), Backend: BackendArango, + Reason: WorkloadNotRunReason + reason, + }) + }, + } + // Cases exercising one specific guard name the rule that must reject + // them (review r4562338071); everything else just has to RED. + wantErr := map[string]string{ + "self-ratio-borrowed-perfect": "self-ratio 1 does not match", + "unstable-cell-hidden-from-control": "unstable cells", + "stability-limit-smuggled-ceiling": "stability limit must equal", + } + for name, mutate := range cases { + t.Run(name, forgeErr(t, mutate, wantErr[name])) + } +} + +func TestReportBlockedStatusRoundTrips(t *testing.T) { + report := validReport(t) + report.GateReports[0].Postgres.Outcome = OutcomeLabelBlocked + report.GateReports[0].Claimable = false + report.GateReports[0].ExclusionReason = "postgres: blocked" + report.Status = "failed" + report.StatusReasons = []string{primaryTiming() + ": blocked"} + report.Exclusions = FormatExclusionList(report.GateReports) + report.Claims = nil + if _, err := LoadReport(mustMarshal(t, report)); err != nil { + t.Fatalf("honest blocked report must verify: %v", err) + } +} + +func mustMarshal(t *testing.T, r *Report) []byte { + t.Helper() + data, err := MarshalReport(r) + if err != nil { + t.Fatal(err) + } + return data +} + +func TestBuildReportRefusesInconsistentConfig(t *testing.T) { + report := validReport(t) + _ = report + _, err := BuildReport(RunIdentity{Config: RunConfig{}}, BothArmsPlan(), ControlRuns{}, nil, nil) + if err == nil { + t.Fatal("zero config must be refused at build, not defaulted") + } +} + +// --- timed publication lane (operator amendment 2026-11-13) -------------------- + +// Run mode is the only mode that measures: the ClickHouse-backed chain +// ran, so samples, summaries, controls, and claims are legitimate +// evidence rather than forgery. validReport already describes that lane +// (ClickHouse deployed, Publication.Run true); these cases pin the +// classification and the disclosure rules around it. +func TestRunPublicationCarriesTimingEvidence(t *testing.T) { + report := validReport(t) + if report.Status != "passed" { + t.Fatalf("run lane status: %s (%v)", report.Status, report.StatusReasons) + } + if len(report.Samples) == 0 || len(report.Summaries) == 0 { + t.Fatal("run lane must carry real timing evidence") + } + if len(report.Claims) != len(Battery)-len(LiveBatteryExclusions()) { + t.Fatalf("run lane claims: %d want %d", len(report.Claims), len(Battery)-len(LiveBatteryExclusions())) + } + if report.Plan.PublicationExcluded() { + t.Fatalf("run lane classification wrong: %+v", report.Plan.Publication) + } + if report.Plan.PublicationZeroEvidence() { + t.Fatal("a chain-run report must never classify as zero-evidence") + } + // The chain ran, so no leg may carry publication-chain disclosure + // rows: they name work that did not happen and would launder a + // not_run stage into a passing report. + for _, leg := range report.Plan.Legs { + disclosed := 0 + for _, row := range report.Plan.PlannedNotRun { + if row.Backend == leg.Backend && strings.HasPrefix(row.Reason, PublicationChainReason) { + disclosed++ + } + } + if disclosed != 0 { + t.Fatalf("%s: %d publication-chain disclosure rows want 0 (the chain ran)", leg.Backend, disclosed) + } + } +} + +func TestRunPublicationMustNotNameCapability(t *testing.T) { + plan := BothArmsPlan() + plan.Publication = &PublicationPlan{Run: true, Capability: PublicationCapabilityCH} + if err := ValidatePublicationPlan(plan); err == nil { + t.Fatal("run=true naming a missing capability must be refused: the chain either ran or it did not") + } +} + +func TestExcludedPublicationRequiresNamedCapability(t *testing.T) { + plan := BothArmsPlan() + plan.Publication = &PublicationPlan{Run: false} + if err := ValidatePublicationPlan(plan); err == nil { + t.Fatal("an excluded chain must name the capability it discloses") + } +} diff --git a/scripts/README.md b/scripts/README.md index d155e3a0d..dc9f760bf 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -82,3 +82,13 @@ loading enabled. Native drag/download acceptance remains in the Chromium route. See [measurements](../docs/verification/lightpanda/POOLED_RESULTS.md) for the precise speed scope and [coverage](../docs/verification/lightpanda/COVERAGE.md) for dispositions of all 96 retired specs. Job count is not a coverage count. + +The backend benchmark runner [benchmark-backends.mjs](benchmark-backends.mjs) +orchestrates the Arango/AQL vs PostgreSQL/SQL matched dataframe benchmark. +Verification authority stays in Go (`benchcli verify`); this script only +builds invocations and reports evidence lines. `--run` discloses a refused +preflight (exit 3) as `not_run`, never a pass; a planned disclosure artifact +must carry zero samples, summaries, or claims (skip-capture discipline) and +still passes through the Go verifier. See +[`internal/dataframe/benchmark/README.md`](../internal/dataframe/benchmark/README.md) +for scopes, guards, and exit codes. diff --git a/scripts/benchmark-backends.mjs b/scripts/benchmark-backends.mjs new file mode 100644 index 000000000..14229c2f0 --- /dev/null +++ b/scripts/benchmark-backends.mjs @@ -0,0 +1,519 @@ +#!/usr/bin/env node +// benchmark-backends.mjs — node-side orchestrator for the Arango/AQL vs +// PostgreSQL/SQL matched benchmark. Verification authority is Go (benchcli +// verify): this script only builds invocations, runs them, and reports the +// evidence line for each check. It can never turn a live check green by +// itself (ruling ii): every live status comes from the Go verifier's exit. +// +// Subcommands: +// --verify-only --report [--benchcli ] +// Verify an existing artifact. Exit 0 only if the Go verifier says OK. +// --run -- +// Run a live benchmark through benchcli (which itself fails closed +// without the granted window), then verify the produced report. +// --harness-selftest +// Node-only sanity pass over this script's own logic. Labelled +// selftest; it can never satisfy a live check. +// --paired --binary --out [--profile tiny|small|representative] +// [--iterations N>=2*rounds --warmup N>=1 --rounds N>=5] [--source-commit SHA] +// [--benchcli ] [--window-note ] --clickhouse-url +// The operator-mandated comparison: the same matched battery runs +// against BOTH real drivers (LOOM_TEST_ARANGO_URL + +// LOOM_TEST_POSTGRES_URL) with the full ClickHouse-backed +// publication chain (--publication=run; there is no chain-less +// battery: the first explorer-config publish materializes through +// ClickHouse), correctness gates first, then >=5 alternating +// timings per driver. With LOOM_BENCH_ADMIN_DSN set it first +// provisions its own fresh loom_bench_* database (drop-pg + +// setup-pg: database, six-series schema ladder, migrated tables), +// because the PostgreSQL backend does not own schema lifecycle. +// Writes comparison.md next to the verified report. FAILS (never +// skips) when a service is missing, an arm is +// planned/not_run/excluded, or a driver has no timings. + +import { spawnSync } from 'node:child_process'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +export const CHECK_HARNESS = 'benchmark-harness-tests'; +export const CHECK_LIVE = 'live-two-backend-correctness'; +export const CHECK_PAIRED = 'paired-driver-benchmark'; + +// Operator acceptance (2026-11-13 amendment): a paired benchmark is real +// only when BOTH drivers executed matched workloads and produced actual +// timing. Five alternating measured samples per backend is the floor; +// warmup samples never count. +export const PAIRED_MIN_SAMPLES_PER_BACKEND = 5; +export const PAIRED_PROFILES = ['tiny', 'small', 'representative']; + +export function parseArgs(argv) { + const flags = { + mode: null, report: null, benchcli: null, out: null, runArgs: [], selftest: false, + paired: false, binary: null, profile: 'tiny', iterations: 10, warmup: 1, rounds: null, + concurrency: 1, previewLimit: 1000, sourceCommit: null, seed: 7, windowNote: null, + clickHouseUrl: null, clickHouseUser: null, clickHousePassword: null, + clickHouseRecipe: fileURLToPath(new URL('../testdata/devloop-fixture/recipe.json', import.meta.url)), + }; + let i = 0; + const int = (name, raw, min) => { + const value = Number(raw); + if (!Number.isInteger(value) || value < min) throw new Error(`${name} must be an integer >= ${min}, got ${raw}`); + return value; + }; + while (i < argv.length) { + const arg = argv[i]; + if (arg === '--verify-only') flags.mode = 'verify'; + else if (arg === '--run') flags.mode = 'run'; + else if (arg === '--paired') flags.paired = true; + else if (arg === '--harness-selftest') flags.selftest = true; + else if (arg === '--report') { i += 1; flags.report = argv[i]; } + else if (arg === '--benchcli') { i += 1; flags.benchcli = argv[i]; } + else if (arg === '--binary') { i += 1; flags.binary = argv[i]; } + else if (arg === '--profile') { i += 1; flags.profile = argv[i]; } + else if (arg === '--iterations') { i += 1; flags.iterations = int('--iterations', argv[i], PAIRED_MIN_SAMPLES_PER_BACKEND); } + else if (arg === '--warmup') { i += 1; flags.warmup = int('--warmup', argv[i], 1); } + else if (arg === '--rounds') { i += 1; flags.rounds = int('--rounds', argv[i], 1); } + else if (arg === '--concurrency') { i += 1; flags.concurrency = int('--concurrency', argv[i], 1); } + else if (arg === '--preview-limit') { i += 1; flags.previewLimit = int('--preview-limit', argv[i], 1); } + else if (arg === '--seed') { i += 1; flags.seed = int('--seed', argv[i], 0); } + else if (arg === '--source-commit') { i += 1; flags.sourceCommit = argv[i]; } + else if (arg === '--window-note') { i += 1; flags.windowNote = argv[i]; } + else if (arg === '--clickhouse-url') { i += 1; flags.clickHouseUrl = argv[i]; } + else if (arg === '--clickhouse-user') { i += 1; flags.clickHouseUser = argv[i]; } + else if (arg === '--clickhouse-password') { i += 1; flags.clickHousePassword = argv[i]; } + else if (arg === '--clickhouse-recipe') { i += 1; flags.clickHouseRecipe = argv[i]; } + else if (arg === '--out') { i += 1; flags.out = argv[i]; } + else if (arg === '--') { flags.runArgs = argv.slice(i + 1); break; } + else throw new Error(`unknown argument: ${arg}`); + i += 1; + } + if (flags.selftest && (flags.mode || flags.paired)) throw new Error('--harness-selftest cannot combine with a run mode'); + if (flags.paired && flags.mode) throw new Error('--paired cannot combine with --verify-only/--run'); + if (!flags.selftest && !flags.mode && !flags.paired) throw new Error('choose --verify-only, --run, --paired, or --harness-selftest'); + if (flags.mode === 'verify' && !flags.report) throw new Error('--verify-only requires --report'); + if (flags.mode === 'run' && !flags.out) throw new Error('--run requires --out'); + if (flags.paired) { + // Spawned arms restart before each round, so a round yields one + // post-restart process-cold sample plus its warm remainder; the claimed + // cell is the warm one (review r4561031154). Default: five interleaved + // rounds, two measured iterations each. + if (flags.rounds === null) flags.rounds = PAIRED_MIN_SAMPLES_PER_BACKEND; + if (!flags.out) throw new Error('--paired requires --out'); + if (!flags.binary) throw new Error('--paired requires --binary (the server binary the arms are spawned from)'); + if (!PAIRED_PROFILES.includes(flags.profile)) throw new Error(`--profile must be one of ${PAIRED_PROFILES.join(', ')}, got ${flags.profile}`); + if (flags.rounds < PAIRED_MIN_SAMPLES_PER_BACKEND) { + throw new Error(`--paired requires --rounds >= ${PAIRED_MIN_SAMPLES_PER_BACKEND}: fewer interleaved rounds per driver is not an alternating comparison`); + } + if (flags.warmup < 1) throw new Error('--paired requires --warmup >= 1 so cached timings are meaningful'); + if (flags.iterations % flags.rounds !== 0) throw new Error(`--iterations ${flags.iterations} must split evenly into --rounds ${flags.rounds}`); + if (flags.iterations < 2 * flags.rounds) { + throw new Error(`--paired requires --iterations >= 2*--rounds: a spawned arm restarts each round, so each round needs the post-restart cold sample plus at least one warm sample (got --iterations ${flags.iterations} --rounds ${flags.rounds})`); + } + if (!/^[0-9a-f]{40}$/.test(flags.sourceCommit ?? '')) throw new Error('--paired requires --source-commit as the full 40-hex SHA the arms were built from'); + if (!(flags.clickHouseUrl ?? '').trim()) throw new Error('--paired requires --clickhouse-url: the publication chain is ClickHouse-backed end to end, and the battery cannot run without it'); + if (!(flags.clickHouseRecipe ?? '').trim()) throw new Error('--paired requires a dataframer recipe for the ClickHouse-enabled arms'); + } + return flags; +} + +// Paired service discovery. The CI lane exports the same two variables the +// backend-conformance job uses; the local dual-URL path uses them too. A +// missing service is a hard failure: silently benchmarking one driver and +// calling it a comparison is exactly what the operator rejected. +export function pairedServices(env) { + const arangoBase = (env.LOOM_TEST_ARANGO_URL ?? '').trim(); + const postgresDsn = (env.LOOM_TEST_POSTGRES_URL ?? '').trim(); + const problems = []; + if (!arangoBase) problems.push('LOOM_TEST_ARANGO_URL is not set: the Arango/AQL driver has no service to run against'); + if (!postgresDsn) problems.push('LOOM_TEST_POSTGRES_URL is not set: the PostgreSQL/SQL driver has no service to run against'); + // benchcli refuses an Arango DSN that does not name a loom_bench* + // database; the CI service URL carries no path, so the lane's database + // name is appended here (the server creates it on first use). + let arangoUrl = arangoBase; + if (arangoUrl) { + const withoutScheme = arangoUrl.replace(/^[a-z]+:\/\//, ''); + if (!withoutScheme.includes('/')) arangoUrl = `${arangoUrl.replace(/\/$/, '')}/loom_bench_ci`; + } + return { arangoUrl, postgresDsn, problems }; +} + +// The PostgreSQL backend refuses to own schema lifecycle ("the caller owns +// schema lifecycle" is its readiness posture). When the lane supplies an +// admin DSN, the paired run provisions its own fresh database first: +// drop-pg (best effort) then setup-pg, which creates the database, the +// six-series schema ladder, and the migrated tables — the same bootstrap +// the server's live composition fixtures use. Both subcommands refuse +// anything outside the loom_bench_* namespace. +export function pairedDatabaseFromDsn(dsn) { + const match = /^[a-z]+:\/\/[^/]*\/([^?#]+)/.exec(dsn ?? ''); + const database = match ? match[1] : ''; + // Same shape benchcli enforces: loom_bench_ plus a 4-48 char suffix. + if (!/^loom_bench_[a-z0-9_]{4,48}$/.test(database)) { + return { database, problem: `paired postgres database ${JSON.stringify(database)} must match loom_bench_[a-z0-9_]{4,48} (benchcli enforces the same rule)` }; + } + return { database, problem: null }; +} + +export function buildSetupInvocation(flags, action, database) { + const bin = flags.benchcli ?? 'go'; + const args = flags.benchcli ? [action, '--database', database] + : ['run', './internal/dataframe/benchmark/cmd/benchcli', action, '--database', database]; + return { cmd: bin, args }; +} + +export function buildPairedInvocation(flags, services) { + const bin = flags.benchcli ?? 'go'; + const args = [ + '--scope=both-arms', + // The full publication chain runs for real on both arms: authoring + // publishes through ClickHouse materialization (a ClickHouse-disabled + // deployment cannot author at all), so the paired lane is run mode, + // never a disclosed not_run posture. Each arm materializes into its + // own loom_bench_* database. + '--publication=run', + '--clickhouse-enabled', + `--clickhouse-url=${flags.clickHouseUrl}`, + `--clickhouse-user=${flags.clickHouseUser || 'default'}`, + ...(flags.clickHousePassword ? [`--clickhouse-password=${flags.clickHousePassword}`] : []), + `--clickhouse-recipe=${flags.clickHouseRecipe}`, + `--preset=${flags.profile}`, + `--seed=${flags.seed}`, + `--warmup=${flags.warmup}`, + `--iterations=${flags.iterations}`, + `--rounds=${flags.rounds}`, + `--concurrency=${flags.concurrency}`, + `--preview-limit=${flags.previewLimit}`, + `--binary=${flags.binary}`, + `--arango-dsn=${services.arangoUrl}`, + `--postgres-dsn=${services.postgresDsn}`, + `--source-commit=${flags.sourceCommit}`, + `--out=${flags.out}`, + ]; + if (flags.windowNote) args.push(`--window-note=${flags.windowNote}`); + return { cmd: bin, args: flags.benchcli ? ['run', ...args] : ['run', './internal/dataframe/benchmark/cmd/benchcli', 'run', ...args] }; +} + +// A paired report counts as a real comparison only when both arms actually +// ran matched workloads and each carries at least PAIRED_MIN_SAMPLES_PER_BACKEND +// non-warmup timings. Anything else — a planned, not_run, or excluded arm, +// an empty timing set, a correctness failure — is a fail, never a pass. +export function classifyPairedReport(report, minSamples = PAIRED_MIN_SAMPLES_PER_BACKEND) { + const problems = []; + const legs = report.plan?.legs ?? []; + const ranBackends = legs.filter((leg) => leg.mode === 'run').map((leg) => leg.backend); + for (const backend of ['arango', 'postgres']) { + if (!legs.some((leg) => leg.backend === backend)) problems.push(`no leg declared for ${backend}`); + else if (!ranBackends.includes(backend)) { + const leg = legs.find((l) => l.backend === backend); + problems.push(`${backend} arm is ${leg.mode}${leg.capability ? ` (${leg.capability})` : ''}: a planned or not_run arm is not a benchmark`); + } + } + const excludedArms = (report.gateReports ?? []).filter((gate) => + [gate.arango, gate.postgres].some((arm) => arm?.outcome === 'blocked' + && !(gate.exclusionReason ?? '').startsWith('live battery exclusion: '))); + if (excludedArms.length > 0) { + problems.push(`${excludedArms.length} gate report(s) carry blocked arms without a live-battery exclusion marker: ${excludedArms.map((g) => g.workloadId).join(', ')}`); + } + const measured = {}; + for (const backend of ['arango', 'postgres']) measured[backend] = 0; + for (const sample of report.samples ?? []) { + if (sample.warmup) continue; + if (sample.outcome !== 'passed') continue; + if (!(sample.backend in measured)) continue; + if (!Number.isFinite(sample.millis) || sample.millis < 0) continue; + measured[sample.backend] += 1; + } + for (const backend of ['arango', 'postgres']) { + if (measured[backend] < minSamples) { + problems.push(`${backend} produced ${measured[backend]} usable timing sample(s), need >= ${minSamples}: without real timing on both drivers there is no comparison`); + } + } + if (report.status !== 'passed') problems.push(`report status is ${report.status ?? 'unknown'}, not passed`); + if (report.plan?.publication?.run !== true) { + problems.push('the publication chain did not run: without it the arms cannot author, so no timings from such a lane are a comparison'); + } + if (report.identity?.clickhouseDisabled !== false) { + problems.push('report does not record ClickHouse as deployed: the paired lane runs the full chain on both arms and must state that honestly'); + } + return problems.length === 0 ? { status: 'pass', disclosure: null, measured } : { status: 'fail', disclosure: problems.join('; '), measured }; +} + +// renderComparisonTable turns the verified summaries into the readable +// table the acceptance gate asks for: units, median, range, and the +// conditions the numbers were taken under. It reads only report fields, +// so a reviewer can rerun the command and reproduce the same rows. +export function renderComparisonTable(report) { + const identity = report.identity ?? {}; + const rows = report.summaries ?? []; + const byWorkload = new Map(); + for (const row of rows) { + const key = row.workloadId; + if (!byWorkload.has(key)) byWorkload.set(key, {}); + byWorkload.get(key)[row.backend] = row.summary; + } + const num = (value) => (typeof value === 'number' ? value.toFixed(2) : 'n/a'); + const lines = []; + lines.push('# Paired driver benchmark — Arango/AQL vs PostgreSQL/SQL'); + lines.push(''); + lines.push(`Run \`${identity.runId ?? 'n/a'}\` — matched workload, both arms, publication chain: \`${report.plan?.publication?.run === true && identity.clickhouseDisabled === false ? 'ran for real on both arms (ClickHouse deployed)' : 'NOT run (this table cannot be a comparison)'}\`.`); + lines.push(''); + lines.push('## Conditions'); + lines.push(''); + lines.push(`- source commit: \`${identity.sourceCommit ?? 'n/a'}\``); + lines.push(`- dataset preset: \`${identity.preset ?? 'n/a'}\`, fixture digest \`${identity.datasetDigest ?? 'n/a'}\``); + lines.push(`- ArangoDB version: ${identity.arangoVersion ?? 'n/a'}; PostgreSQL version: ${identity.postgresVersion ?? 'n/a'}`); + const config = identity.config ?? {}; + lines.push(`- warmup ${config.Warmup ?? 'n/a'} per workload x backend (discarded), ${config.Iterations ?? 'n/a'} measured iterations, ${config.Rounds ?? 'n/a'} interleaved rounds, concurrency ${config.Concurrency ?? 'n/a'}`); + lines.push('- cache: steady-regime samples (warm for a spawned arm at rest, service-warm for an attached service) feed the medians; process-cold samples are reported separately and never claimed'); + lines.push(''); + lines.push('Timings are milliseconds per preview request, lower is faster. Neither driver is expected to win; a gap smaller than run-to-run variation is reported as such, not as a speedup.'); + lines.push(''); + lines.push('## Timings (ms)'); + lines.push(''); + lines.push('| workload | backend | n | median | min | max | range |'); + lines.push('| --- | --- | --- | --- | --- | --- | --- |'); + for (const [workload, perBackend] of [...byWorkload.entries()].sort()) { + for (const backend of ['arango', 'postgres']) { + const s = perBackend[backend]; + if (!s) { lines.push(`| ${workload} | ${backend} | 0 | n/a | n/a | n/a | n/a |`); continue; } + const range = (typeof s.maxMillis === 'number' && typeof s.minMillis === 'number') ? (s.maxMillis - s.minMillis).toFixed(2) : 'n/a'; + lines.push(`| ${workload} | ${backend} | ${s.count ?? 0} | ${num(s.medianMillis)} | ${num(s.minMillis)} | ${num(s.maxMillis)} | ${range} |`); + } + } + const claims = report.claims ?? []; + if (claims.length === 0) { + lines.push(''); + lines.push('No claim cleared the correctness gates plus the stability control, so no speedup is claimed. That is an acceptable outcome; the measurements above are the deliverable.'); + } + return `${lines.join('\n')}\n`; +} + + +export function buildVerifyInvocation(flags) { + const bin = flags.benchcli ?? 'go'; + const args = flags.benchcli ? ['verify', '--report', flags.report] + : ['run', './internal/dataframe/benchmark/cmd/benchcli', 'verify', '--report', flags.report]; + return { cmd: bin, args }; +} + +export function buildRunInvocation(flags) { + const bin = flags.benchcli ?? 'go'; + const args = flags.benchcli ? ['run', ...flags.runArgs] + : ['run', './internal/dataframe/benchmark/cmd/benchcli', 'run', ...flags.runArgs]; + return { cmd: bin, args }; +} + +export function formatCheckLine(id, status, detail) { + const text = detail ? ` ${detail}` : ''; + return `check ${id}: ${status}${text}`; +} + +// A run blocked by preflight (exit 3) is disclosed as not_run with the +// reason, never as a pass and never as a silent skip. Exit 1 is a failure. +export function classifyRunExit(code) { + if (code === 0) return { status: 'pass', disclosure: null }; + if (code === 3) return { status: 'not_run', disclosure: 'preflight refused the live run' }; + return { status: 'fail', disclosure: `benchcli run exited ${code}` }; +} + +export function classifyVerifyExit(code) { + if (code === 0) return { status: 'pass', disclosure: null }; + return { status: 'fail', disclosure: `benchcli verify rejected the artifact (exit ${code})` }; +} + +// readReportSummary extracts only disclosure facts for the console; the +// authoritative recomputation lives in Go. +export function readReportSummary(path) { + const raw = JSON.parse(readFileSync(path, 'utf8')); + return { + runId: raw.identity?.runId ?? null, + scope: raw.plan?.scope ?? null, + status: raw.status ?? null, + legs: (raw.plan?.legs ?? []).map((leg) => `${leg.backend}:${leg.mode}${leg.capability ? `(${leg.capability})` : ''}`), + plannedNotRun: (raw.plan?.plannedNotRun ?? []).length, + claims: (raw.claims ?? []).length, + samples: (raw.samples ?? []).length, + summaries: (raw.summaries ?? []).length, + }; +} + +// Skip-capture discipline: a planned disclosure artifact must carry zero +// measurement evidence — any sample, summary row, or claim that escaped a +// not_run leg is a FAIL, never a disclosure. Null for non-planned reports; +// the Go verifier stays the authority that the disclosure is consistent. +export function classifyPlannedDisclosure(summary) { + if (summary.scope !== 'planned') return null; + if (!summary.legs.every((leg) => leg.includes(':not_run'))) { + return { status: 'fail', disclosure: 'planned artifact declares a leg that ran' }; + } + if (summary.claims !== 0 || summary.samples !== 0 || summary.summaries !== 0) { + return { + status: 'fail', + disclosure: `planned artifact captured measurement evidence (samples=${summary.samples}, summaries=${summary.summaries}, claims=${summary.claims})`, + }; + } + return { status: 'not_run', disclosure: `planned artifact disclosed: ${summary.legs.length} not_run legs, zero timing captured` }; +} + +export function run(flags, deps = {}) { + const exec = deps.exec ?? spawnSync; + const log = deps.log ?? ((line) => console.log(line)); + const env = deps.env ?? process.env; + let failures = 0; + const live = (invocation) => exec(invocation.cmd, invocation.args, { stdio: 'inherit' }); + + if (flags.paired) { + return runPaired(flags, { exec, log, env }); + } + + if (flags.mode === 'verify') { + const result = live(buildVerifyInvocation(flags)); + const outcome = classifyVerifyExit(result.status); + log(formatCheckLine(CHECK_LIVE, outcome.status, outcome.disclosure ?? `verified ${flags.report}`)); + if (outcome.status !== 'pass') failures += 1; + return failures; + } + if (flags.mode === 'run') { + const result = live(buildRunInvocation(flags)); + const outcome = classifyRunExit(result.status); + log(formatCheckLine('live-run', outcome.status, outcome.disclosure ?? undefined)); + if (outcome.status === 'not_run') { + const reportPath = `${flags.out}/report.json`; + if (existsSync(reportPath)) { + // A planned run writes its disclosure artifact and still exits 3. + // Classify it here and let the Go verifier keep authority. + const summary = readReportSummary(reportPath); + const planned = classifyPlannedDisclosure(summary); + if (planned && planned.status === 'fail') { + log(formatCheckLine(CHECK_LIVE, 'fail', planned.disclosure)); + failures += 1; + return failures; + } + const verify = live(buildVerifyInvocation({ ...flags, report: reportPath })); + const verified = classifyVerifyExit(verify.status); + log(formatCheckLine(CHECK_LIVE, 'not_run', + `${(planned ?? { disclosure: 'disclosure artifact produced without preflight' }).disclosure}; go verify ${verified.status}`)); + if (verified.status !== 'pass') { + log(formatCheckLine(CHECK_LIVE, 'fail', verified.disclosure)); + failures += 1; + } + return failures; + } + log(formatCheckLine(CHECK_LIVE, 'not_run', 'no artifact produced; run stays planned/not_run until the granted window')); + return failures; // a refused preflight is disclosed, not a failure + } + if (outcome.status !== 'pass') { failures += 1; return failures; } + const reportPath = `${flags.out}/report.json`; + const verify = live(buildVerifyInvocation({ ...flags, report: reportPath })); + const verified = classifyVerifyExit(verify.status); + log(formatCheckLine(CHECK_LIVE, verified.status, verified.disclosure ?? `verified ${reportPath}`)); + if (verified.status !== 'pass') failures += 1; + else if (existsSync(reportPath)) log(`report summary: ${JSON.stringify(readReportSummary(reportPath))}`); + return failures; + } + // selftest: node-only logic sanity, clearly labelled. + const sample = { status: 3 }; + const classified = classifyRunExit(sample.status); + if (classified.status !== 'not_run') throw new Error('selftest: exit 3 must disclose not_run'); + log(formatCheckLine('harness-selftest', 'pass', 'node-only; live checks come from the Go verifier only')); + return failures; +} + +// runPaired is the operator-mandated comparison: one matched workload set, +// both real drivers, correctness first, then alternating timings. It +// refuses to call anything a benchmark when a service is missing, an arm +// did not run, or a driver produced no timings — the check goes red rather +// than green-by-scaffolding. The comparison table is written next to the +// verified report so the artifact set is self-explanatory. +function runPaired(flags, { exec, log, env }) { + const { arangoUrl, postgresDsn, problems } = pairedServices(env); + if (problems.length > 0) { + log(formatCheckLine(CHECK_PAIRED, 'fail', problems.join('; '))); + return 1; + } + const { database, problem } = pairedDatabaseFromDsn(postgresDsn); + if (problem) { + log(formatCheckLine(CHECK_PAIRED, 'fail', problem)); + return 1; + } + // Idempotent provisioning: the PostgreSQL backend does not create its + // own schemas, so the lane drops and re-creates its own loom_bench_* + // database when an admin DSN is supplied. A failed setup is a hard + // failure, not a silently unprovisioned arm timing out at readiness. + if ((env.LOOM_BENCH_ADMIN_DSN ?? '').trim() !== '') { + exec(buildSetupInvocation(flags, 'drop-pg', database).cmd, buildSetupInvocation(flags, 'drop-pg', database).args, { + stdio: 'inherit', env, + }); // best effort: absent database is the normal first-run case + const setup = exec(buildSetupInvocation(flags, 'setup-pg', database).cmd, buildSetupInvocation(flags, 'setup-pg', database).args, { + stdio: 'inherit', env, + }); + if (setup.status !== 0) { + log(formatCheckLine(CHECK_PAIRED, 'fail', `benchcli setup-pg exited ${setup.status}: the PostgreSQL arm cannot serve without its six-series schema ladder`)); + return 1; + } + } + // ClickHouse credentials arrive like the other services': explicit + // flags win, LOOM_BENCH_CH_USER / LOOM_BENCH_CH_PASSWORD fill what the + // lane provides, and the bare server default remains the fallback. + // (The CI service authenticates a created loom_ci user; an anonymous + // default login is refused there with code 516, which degrades the + // publication plane and keeps /readyz at 503.) + if (!flags.clickHouseUser) flags.clickHouseUser = (env.LOOM_BENCH_CH_USER ?? '').trim() || 'default'; + if (!flags.clickHousePassword) flags.clickHousePassword = (env.LOOM_BENCH_CH_PASSWORD ?? '').trim() || null; + const invocation = buildPairedInvocation(flags, { arangoUrl, postgresDsn }); + // The paired lane is its own granted window: it runs in a CI job with + // isolated services, so it sets the live gate the CLI demands. + const result = exec(invocation.cmd, invocation.args, { + stdio: 'inherit', + env: { ...env, LOOM_BENCH_LIVE: '1' }, + }); + const runOutcome = classifyRunExit(result.status); + log(formatCheckLine(`${CHECK_PAIRED}/run`, runOutcome.status, runOutcome.disclosure ?? undefined)); + const reportPath = `${flags.out}/report.json`; + if (!existsSync(reportPath)) { + log(formatCheckLine(CHECK_PAIRED, 'fail', `no report artifact at ${reportPath}; nothing was compared`)); + return 1; + } + // Authority stays with the Go verifier: a report this script could + // classify as fine but the verifier rejects is a fail, never a pass. + const verify = liveVerify(exec, flags, reportPath, log); + if (verify !== 0) { + log(formatCheckLine(CHECK_PAIRED, 'fail', `benchcli verify rejected the paired artifact (exit ${verify})`)); + return 1; + } + if (runOutcome.status !== 'pass') { + log(formatCheckLine(CHECK_PAIRED, 'fail', `paired run exited ${result.status}; a comparison needs a clean run on both drivers`)); + return 1; + } + const report = JSON.parse(readFileSync(reportPath, 'utf8')); + const paired = classifyPairedReport(report); + if (paired.status !== 'pass') { + log(formatCheckLine(CHECK_PAIRED, 'fail', paired.disclosure)); + return 1; + } + const comparisonPath = `${flags.out}/comparison.md`; + writeFileSync(comparisonPath, renderComparisonTable(report), 'utf8'); + log(formatCheckLine(CHECK_PAIRED, 'pass', + `matched battery on both drivers: arango ${paired.measured.arango} + postgres ${paired.measured.postgres} measured timings; table: ${comparisonPath}`)); + return 0; +} + +function liveVerify(exec, flags, reportPath, log) { + const invocation = buildVerifyInvocation({ ...flags, report: reportPath }); + const result = exec(invocation.cmd, invocation.args, { stdio: 'inherit' }); + const outcome = classifyVerifyExit(result.status); + log(formatCheckLine(CHECK_LIVE, outcome.status, outcome.disclosure ?? `verified ${reportPath}`)); + return result.status; +} + +const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].split('/').pop()); +if (isMain) { + try { + const flags = parseArgs(process.argv.slice(2)); + const failures = run(flags); + process.exit(failures === 0 ? 0 : 1); + } catch (err) { + console.error(`benchmark-backends: ${err.message}`); + process.exit(2); + } +} diff --git a/scripts/benchmark-backends.test.mjs b/scripts/benchmark-backends.test.mjs new file mode 100644 index 000000000..4ddaa5d4f --- /dev/null +++ b/scripts/benchmark-backends.test.mjs @@ -0,0 +1,452 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + parseArgs, + buildVerifyInvocation, + buildRunInvocation, + classifyRunExit, + classifyVerifyExit, + formatCheckLine, + readReportSummary, + run, + CHECK_LIVE, +} from './benchmark-backends.mjs'; + +test('parseArgs requires a mode', () => { + assert.throws(() => parseArgs([]), /choose --verify-only/); + assert.throws(() => parseArgs(['--verify-only']), /requires --report/); + assert.throws(() => parseArgs(['--run']), /requires --out/); + assert.throws(() => parseArgs(['--nope']), /unknown argument/); + assert.throws(() => parseArgs(['--run', '--harness-selftest']), /cannot combine/); +}); + +test('run mode collects benchcli flags after the separator', () => { + const flags = parseArgs(['--run', '--out', 'bench-out', '--', '--scope', 'postgres-only', '--window-note', 'granted by 2']); + assert.equal(flags.mode, 'run'); + assert.equal(flags.out, 'bench-out'); + assert.deepEqual(flags.runArgs, ['--scope', 'postgres-only', '--window-note', 'granted by 2']); +}); + +test('invocations use go run by default and the binary when given', () => { + const defaultVerify = buildVerifyInvocation({ report: 'r.json' }); + assert.equal(defaultVerify.cmd, 'go'); + assert.deepEqual(defaultVerify.args, ['run', './internal/dataframe/benchmark/cmd/benchcli', 'verify', '--report', 'r.json']); + const binaryRun = buildRunInvocation({ benchcli: '/tmp/benchcli', runArgs: ['--scope', 'both-arms'] }); + assert.deepEqual(binaryRun, { cmd: '/tmp/benchcli', args: ['run', '--scope', 'both-arms'] }); +}); + +test('exit classification: 0 pass, 3 disclosed not_run, anything else fail', () => { + assert.equal(classifyRunExit(0).status, 'pass'); + assert.equal(classifyRunExit(3).status, 'not_run'); + assert.match(classifyRunExit(3).disclosure, /preflight refused/); + assert.equal(classifyRunExit(1).status, 'fail'); + assert.equal(classifyRunExit(null).status, 'fail'); + assert.equal(classifyVerifyExit(0).status, 'pass'); + assert.equal(classifyVerifyExit(1).status, 'fail'); +}); + +test('a blocked live run is disclosed as not_run, never a pass', () => { + const lines = []; + const calls = []; + const deps = { + log: (line) => lines.push(line), + exec: (cmd, args) => { calls.push([cmd, args]); return { status: 3 }; }, + }; + const failures = run(parseArgs(['--run', '--out', 'out', '--', '--scope', 'postgres-only']), deps); + assert.equal(failures, 0, 'a refused preflight is disclosed, not a failure'); + assert.deepEqual(calls.length, 1, 'no verify step runs without an artifact'); + assert.ok(lines.some((l) => l.includes('check live-run: not_run'))); + assert.ok(lines.some((l) => l.includes(`check ${CHECK_LIVE}: not_run`))); + assert.ok(!lines.some((l) => l.includes(': pass'))); +}); + +test('verify-only reports the Go exit as the only authority', () => { + const lines = []; + const passing = run(parseArgs(['--verify-only', '--report', 'r.json']), { + log: (line) => lines.push(line), + exec: () => ({ status: 0 }), + }); + assert.equal(passing, 0); + assert.ok(lines.some((l) => l.includes(`check ${CHECK_LIVE}: pass`))); + + const rejectLines = []; + const failing = run(parseArgs(['--verify-only', '--report', 'r.json']), { + log: (line) => rejectLines.push(line), + exec: () => ({ status: 1 }), + }); + assert.equal(failing, 1); + assert.ok(rejectLines.some((l) => l.includes(`check ${CHECK_LIVE}: fail`))); +}); + +test('successful run verifies the produced artifact before passing', () => { + const calls = []; + const lines = []; + const failures = run(parseArgs(['--run', '--out', 'out', '--']), { + log: (line) => lines.push(line), + exec: (cmd, args) => { calls.push(args); return { status: 0 }; }, + }); + assert.equal(failures, 0); + assert.equal(calls.length, 2, 'run then verify'); + assert.deepEqual(calls[1].slice(-3), ['verify', '--report', 'out/report.json']); +}); + +test('readReportSummary surfaces scope, legs, and disclosure counts only', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bench-summary-')); + const path = join(dir, 'report.json'); + await writeFile(path, JSON.stringify({ + identity: { runId: 'bench-x' }, + status: 'passed', + plan: { + scope: 'postgres-only', + legs: [ + { backend: 'arango', mode: 'not_run', capability: 'no service' }, + { backend: 'postgres', mode: 'run' }, + ], + plannedNotRun: [{ workloadId: 'a', backend: 'arango', reason: 'leg not run' }], + }, + claims: [], + })); + const summary = readReportSummary(path); + assert.equal(summary.scope, 'postgres-only'); + assert.equal(summary.plannedNotRun, 1); + assert.deepEqual(summary.legs, ['arango:not_run(no service)', 'postgres:run']); + assert.equal(summary.claims, 0); +}); + +test('formatCheckLine is a stable single line', () => { + assert.equal(formatCheckLine('x', 'pass', 'ok'), 'check x: pass ok'); + assert.equal(formatCheckLine('x', 'fail'), 'check x: fail'); +}); + +import { classifyPlannedDisclosure } from './benchmark-backends.mjs'; + +function plannedSummary(overrides = {}) { + return { + runId: 'bench-planned-a', scope: 'planned', status: 'failed', + legs: ['arango:not_run(no ArangoDB service)', 'postgres:not_run(blocked by PG-PG-AUTH-PATH-DISCOVERY-ALLGENS)'], + plannedNotRun: 20, claims: 0, samples: 0, summaries: 0, + ...overrides, + }; +} + +test('planned disclosure: clean artifact is not_run, captured timing is fail', () => { + const clean = classifyPlannedDisclosure(plannedSummary()); + assert.equal(clean.status, 'not_run'); + assert.match(clean.disclosure, /zero timing captured/); + for (const bad of [{ samples: 1 }, { summaries: 1 }, { claims: 1 }, + { legs: ['arango:not_run(x)', 'postgres:run'] }]) { + assert.equal(classifyPlannedDisclosure(plannedSummary(bad)).status, 'fail', `skip-capture must refuse ${JSON.stringify(bad)}`); + } + assert.equal(classifyPlannedDisclosure(plannedSummary({ scope: 'postgres-only' })), null); +}); + +test('exit-3 planned artifact is verified by Go and disclosed not_run, never pass', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bench-planned-')); + await writeFile(join(dir, 'report.json'), JSON.stringify({ + identity: { runId: 'bench-planned-a' }, status: 'failed', + plan: { + scope: 'planned', + legs: [{ backend: 'arango', mode: 'not_run', capability: 'no service' }, + { backend: 'postgres', mode: 'not_run', capability: 'blocked by PG-PG-AUTH-PATH-DISCOVERY-ALLGENS' }], + plannedNotRun: [], + }, + claims: [], samples: [], summaries: [], + })); + const lines = []; + const calls = []; + const failures = run(parseArgs(['--run', '--out', dir, '--', '--scope', 'planned']), { + log: (line) => lines.push(line), + exec: (cmd, args) => { calls.push(args); return { status: args.includes('verify') ? 0 : 3 }; }, + }); + assert.equal(failures, 0); + assert.equal(calls.length, 2, 'planned artifact still goes through Go verify'); + assert.ok(lines.some((l) => l.includes(`check ${CHECK_LIVE}: not_run`) && l.includes('zero timing captured'))); + assert.ok(!lines.some((l) => l.includes(': pass'))); +}); + +test('planned artifact with escaped samples fails the check', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bench-planned-bad-')); + await writeFile(join(dir, 'report.json'), JSON.stringify({ + identity: { runId: 'bench-planned-bad' }, status: 'failed', + plan: { + scope: 'planned', + legs: [{ backend: 'arango', mode: 'not_run', capability: 'no service' }, + { backend: 'postgres', mode: 'not_run', capability: 'blocked' }], + plannedNotRun: [], + }, + claims: [], samples: [{ backend: 'postgres', millis: 12 }], summaries: [], + })); + const lines = []; + const calls = []; + const failures = run(parseArgs(['--run', '--out', dir, '--', '--scope', 'planned']), { + log: (line) => lines.push(line), + exec: (cmd, args) => { calls.push(args); return { status: 3 }; }, + }); + assert.equal(failures, 1, 'captured timing in a planned artifact is a failure'); + assert.equal(calls.length, 1, 'a failing disclosure is not verified green'); + assert.ok(lines.some((l) => l.includes(`check ${CHECK_LIVE}: fail`) && l.includes('measurement evidence'))); +}); + +// --- paired-driver benchmark (operator amendment 2026-11-13) ------------------- + +import { + pairedServices, + pairedDatabaseFromDsn, + buildPairedInvocation, + classifyPairedReport, + renderComparisonTable, + PAIRED_MIN_SAMPLES_PER_BACKEND, + CHECK_PAIRED, +} from './benchmark-backends.mjs'; + +const SHA40 = 'a'.repeat(40); +// The paired lane requires a ClickHouse service: the publication chain is +// ClickHouse-backed end to end (authoring publishes through it). +const CH_ARGS = ['--clickhouse-url', 'clickhouse://127.0.0.1:9000']; + +function pairedReportFixture(overrides = {}) { + const samples = []; + for (const backend of ['arango', 'postgres']) { + for (let i = 0; i < PAIRED_MIN_SAMPLES_PER_BACKEND; i += 1) { + samples.push({ backend, workloadId: 'select-filter', round: 1, iteration: i, warmup: false, outcome: 'passed', millis: 10 + i, cache: 'warm' }); + } + samples.push({ backend, workloadId: 'select-filter', round: 0, iteration: 0, warmup: true, outcome: 'passed', millis: 20, cache: 'warm' }); + } + return { + status: 'passed', + identity: { + runId: 'bench-paired-1', preset: 'tiny', sourceCommit: SHA40, datasetDigest: 'd'.repeat(64), + arangoVersion: '3.12.4', postgresVersion: 'PostgreSQL 18.0', clickhouseDisabled: false, + config: { Warmup: 1, Iterations: 5, Concurrency: 1, Rounds: 1 }, + }, + plan: { scope: 'both-arms', legs: [{ backend: 'arango', mode: 'run' }, { backend: 'postgres', mode: 'run' }], publication: { run: true } }, + gateReports: [{ workloadId: 'select-filter', claimable: true, arango: { outcome: 'passed' }, postgres: { outcome: 'passed' } }], + samples, + summaries: [ + { workloadId: 'select-filter', backend: 'arango', cache: 'warm', summary: { count: 5, failedCount: 0, medianMillis: 12, minMillis: 10, maxMillis: 14 } }, + { workloadId: 'select-filter', backend: 'postgres', cache: 'warm', summary: { count: 5, failedCount: 0, medianMillis: 22, minMillis: 20, maxMillis: 24 } }, + ], + claims: [], + ...overrides, + }; +} + +test('paired: missing services fail the check, never skip it', () => { + const seen = pairedServices({}); + assert.equal(seen.problems.length, 2); + const logs = []; + let spawned = false; + const failures = run(parseArgs(['--paired', '--binary', '/bin/loom', '--out', 'out', '--source-commit', SHA40, ...CH_ARGS]), { + exec: () => { spawned = true; return { status: 0 }; }, + log: (line) => logs.push(line), + env: {}, + }); + assert.equal(failures, 1); + assert.equal(spawned, false, 'benchcli must not even start without both services'); + assert.match(logs.join('\n'), new RegExp(`check ${CHECK_PAIRED}: fail`)); +}); + +test('paired: arango dsn gets a benchmark-namespaced database', () => { + const seen = pairedServices({ LOOM_TEST_ARANGO_URL: 'http://127.0.0.1:8529', LOOM_TEST_POSTGRES_URL: 'postgresql://u:p@127.0.0.1:5432/loom_bench_paired?sslmode=disable' }); + assert.deepEqual(seen.problems, []); + assert.match(seen.arangoUrl, /\/loom_bench_ci$/); +}); + +test('paired invocation: both arms, full chain, real iterations', () => { + const flags = parseArgs(['--paired', '--binary', '/bin/loom', '--out', 'out', '--profile', 'tiny', '--iterations', '10', '--warmup', '1', '--rounds', '5', '--source-commit', SHA40, ...CH_ARGS]); + const inv = buildPairedInvocation(flags, { arangoUrl: 'http://127.0.0.1:8529/loom_bench_ci', postgresDsn: 'postgresql://u:p@h:5432/loom_bench_paired' }); + const joined = inv.args.join(' '); + assert.match(joined, /--scope=both-arms/); + assert.match(joined, /--publication=run/); + assert.match(joined, /--clickhouse-enabled/); + assert.match(joined, /--clickhouse-url=clickhouse:\/\/127\.0\.0\.1:9000/); + assert.match(joined, /--clickhouse-recipe=\S+recipe\.json/); + assert.match(joined, /--clickhouse-user=default/); + assert.doesNotMatch(joined, /--clickhouse-password/); + assert.match(joined, /--iterations=10/); + assert.match(joined, /--warmup=1/); + assert.match(joined, /--rounds=5/); + assert.match(joined, /--arango-dsn=http:\/\/127\.0\.0\.1:8529\/loom_bench_ci/); +}); + +test('paired credentials: env fills ClickHouse auth, flags win', () => { + const base = ['--paired', '--binary', '/bin/loom', '--out', 'out', '--source-commit', SHA40, ...CH_ARGS]; + const seen = []; + run(parseArgs(base), { + exec: (cmd, args) => { seen.push(args.join(' ')); return { status: 2 }; }, + log: () => {}, + env: { + LOOM_TEST_ARANGO_URL: 'http://127.0.0.1:8529', + LOOM_TEST_POSTGRES_URL: 'postgresql://u:p@127.0.0.1:5432/loom_bench_paired?sslmode=disable', + LOOM_BENCH_CH_USER: 'loom_ci', LOOM_BENCH_CH_PASSWORD: 'loom_ci_password', + }, + }); + const benchcliRun = seen.find((a) => a.includes('--publication=run')); + assert.match(benchcliRun, /--clickhouse-user=loom_ci/); + assert.match(benchcliRun, /--clickhouse-password=loom_ci_password/); + const explicit = buildPairedInvocation( + parseArgs([...base, '--clickhouse-user', 'override', '--clickhouse-password', 'op']), + { arangoUrl: 'http://h/loom_bench_ci', postgresDsn: 'postgresql://u:p@h:5432/loom_bench_paired' }, + ).args.join(' '); + assert.match(explicit, /--clickhouse-user=override/); + assert.match(explicit, /--clickhouse-password=op/); +}); + +test('paired flags refuse a non-comparison', () => { + const base = ['--paired', '--binary', '/bin/loom', '--out', 'out', '--source-commit', SHA40, ...CH_ARGS]; + const without = (flag) => { + const i = base.indexOf(flag); + return [...base.slice(0, i), ...base.slice(i + 2)]; + }; + assert.throws(() => parseArgs(without('--out')), /requires --out/); + assert.throws(() => parseArgs(without('--binary')), /requires --binary/); + assert.throws(() => parseArgs(without('--source-commit')), /source-commit/); + assert.throws(() => parseArgs([...base, '--iterations', '3']), /integer >= 5/); + assert.throws(() => parseArgs([...base, '--rounds', '2']), /--rounds >= 5/); + assert.throws(() => parseArgs([...base, '--iterations', '5', '--rounds', '5']), /iterations >= 2\*--rounds/); + assert.throws(() => parseArgs([...base, '--iterations', '9', '--rounds', '5']), /split evenly/); + const defaulted = parseArgs(base); + assert.equal(defaulted.iterations, 10); + assert.equal(defaulted.rounds, 5); + assert.throws(() => parseArgs([...base, '--warmup', '0']), /warmup/); + assert.throws(() => parseArgs([...base, '--profile', 'smoke']), /--profile must be one of/); + assert.throws(() => parseArgs(without('--clickhouse-url')), /requires --clickhouse-url/); + assert.throws(() => parseArgs([...base, '--clickhouse-recipe', '']), /dataframer recipe/); +}); + +test('paired classification: only real timing on both drivers passes', () => { + assert.equal(classifyPairedReport(pairedReportFixture()).status, 'pass'); + + const oneColdArm = pairedReportFixture(); + oneColdArm.samples = oneColdArm.samples.filter((s) => s.backend !== 'postgres' || s.warmup); + assert.equal(classifyPairedReport(oneColdArm).status, 'fail', 'postgres with only warmups is not timed'); + + const notRunArm = pairedReportFixture(); + notRunArm.plan.legs = [{ backend: 'arango', mode: 'run' }, { backend: 'postgres', mode: 'not_run', capability: 'CH-x' }]; + assert.equal(classifyPairedReport(notRunArm).status, 'fail', 'a not_run arm must fail the check'); + + const plannedBoth = pairedReportFixture({ status: 'failed' }); + plannedBoth.plan.legs = [{ backend: 'arango', mode: 'not_run' }, { backend: 'postgres', mode: 'not_run' }]; + const plannedVerdict = classifyPairedReport(plannedBoth); + assert.equal(plannedVerdict.status, 'fail'); + assert.match(plannedVerdict.disclosure, /planned or not_run arm is not a benchmark/); + + const failedStatus = pairedReportFixture({ status: 'failed' }); + assert.equal(classifyPairedReport(failedStatus).status, 'fail', 'correctness failure cannot be read as fast'); + + const chDisabled = pairedReportFixture(); + chDisabled.identity.clickhouseDisabled = true; + assert.equal(classifyPairedReport(chDisabled).status, 'fail', 'a lane that records ClickHouse as disabled cannot have run the chain the battery needs'); + + const chainNotRun = pairedReportFixture(); + delete chainNotRun.plan.publication; + assert.equal(classifyPairedReport(chainNotRun).status, 'fail', 'a report whose plan does not claim the chain ran is not a comparison'); + + const blockedArm = pairedReportFixture(); + blockedArm.gateReports = [{ workloadId: 'select-filter', arango: { outcome: 'blocked' }, postgres: { outcome: 'passed' } }]; + assert.equal(classifyPairedReport(blockedArm).status, 'fail', 'a blocked arm without an exclusion marker is a fail'); + + const excludedOk = pairedReportFixture(); + excludedOk.gateReports = [ + { workloadId: 'select-filter', arango: { outcome: 'passed' }, postgres: { outcome: 'passed' } }, + { workloadId: 'derive-age-band', exclusionReason: 'live battery exclusion: unproven recode', arango: { outcome: 'blocked' }, postgres: { outcome: 'blocked' } }, + ]; + assert.equal(classifyPairedReport(excludedOk).status, 'pass', 'the two reviewer-accepted exclusions stand'); +}); + +test('paired table reports medians, ranges, units, versions, and SHA', () => { + const table = renderComparisonTable(pairedReportFixture()); + assert.match(table, /\| select-filter \| arango \| 5 \| 12\.00 \| 10\.00 \| 14\.00 \| 4\.00 \|/); + assert.match(table, /\| select-filter \| postgres \| 5 \| 22\.00 \| 20\.00 \| 24\.00 \| 4\.00 \|/); + assert.match(table, /milliseconds/); + assert.match(table, new RegExp(SHA40)); + assert.match(table, /3\.12\.4/); + assert.match(table, /PostgreSQL 18\.0/); + assert.match(table, /no speedup is claimed/i); +}); + +test('paired run end-to-end: clean both-arm report passes and writes the table', async () => { + const dir = await mkdtemp(join(tmpdir(), 'paired-')); + const report = pairedReportFixture(); + const logs = []; + const exec = (cmd, args) => { + if (args.includes('verify')) return { status: 0 }; + return { status: 0 }; + }; + const flags = parseArgs(['--paired', '--binary', '/bin/loom', '--out', dir, '--source-commit', SHA40, ...CH_ARGS]); + await writeFile(join(dir, 'report.json'), JSON.stringify(report)); + const failures = run(flags, { exec, log: (l) => logs.push(l), env: { LOOM_TEST_ARANGO_URL: 'http://h:8529', LOOM_TEST_POSTGRES_URL: 'postgresql://u:p@h:5432/loom_bench_paired' } }); + assert.equal(failures, 0, logs.join('\n')); + const { readFile } = await import('node:fs/promises'); + const table = await readFile(join(dir, 'comparison.md'), 'utf8'); + assert.match(table, /select-filter/); +}); + +test('paired run end-to-end: zero-timing report fails the check even with exit 0', async () => { + const dir = await mkdtemp(join(tmpdir(), 'paired-')); + const report = pairedReportFixture(); + report.samples = []; + report.summaries = []; + report.claims = []; + report.status = 'failed'; + const exec = () => ({ status: 0 }); + const logs = []; + const flags = parseArgs(['--paired', '--binary', '/bin/loom', '--out', dir, '--source-commit', SHA40, ...CH_ARGS]); + await writeFile(join(dir, 'report.json'), JSON.stringify(report)); + const failures = run(flags, { exec, log: (l) => logs.push(l), env: { LOOM_TEST_ARANGO_URL: 'http://h:8529', LOOM_TEST_POSTGRES_URL: 'postgresql://u:p@h:5432/loom_bench_paired' } }); + assert.equal(failures, 1); + assert.match(logs.join('\n'), /check paired-driver-benchmark: fail/); +}); + +test('paired: postgres dsn must name a loom_bench_ database', () => { + assert.equal(pairedDatabaseFromDsn('postgresql://u:p@h:5432/loom_bench_paired?sslmode=disable').problem, null); + assert.equal(pairedDatabaseFromDsn('postgresql://u:p@h:5432/loom_bench_paired?sslmode=disable').database, 'loom_bench_paired'); + assert.match(pairedDatabaseFromDsn('postgresql://u:p@h:5432/loom_test?sslmode=disable').problem, /loom_bench_/); + assert.match(pairedDatabaseFromDsn('postgresql://u:p@h:5432/loom_bench_ci?sslmode=disable').problem, /4,48/, 'two-char suffix must fail like benchcli does'); +}); + +test('paired: admin DSN drives drop-pg then setup-pg before the benchmark', () => { + const dir = '/tmp/paired-prov-test'; + const calls = []; + const flags = parseArgs(['--paired', '--binary', '/bin/loom', '--benchcli', '/bin/benchcli', '--out', dir, '--source-commit', SHA40, ...CH_ARGS]); + const exec = (cmd, args) => { calls.push([cmd, args.join(' ')]); return { status: 0 }; }; + const logs = []; + run(flags, { + exec, + log: (l) => logs.push(l), + env: { + LOOM_TEST_ARANGO_URL: 'http://h:8529', + LOOM_TEST_POSTGRES_URL: 'postgresql://u:p@h:5432/loom_bench_paired?sslmode=disable', + LOOM_BENCH_ADMIN_DSN: 'postgresql://u:p@h:5432/postgres?sslmode=disable', + }, + }); + assert.match(calls[0][1], /^drop-pg --database loom_bench_paired$/, `first call was: ${calls[0]?.[1]}`); + assert.match(calls[1][1], /^setup-pg --database loom_bench_paired$/); + assert.match(calls[2][1], /run --scope=both-arms/); +}); + +test('paired: failed setup-pg fails the check before any timing attempt', () => { + const calls = []; + const flags = parseArgs(['--paired', '--binary', '/bin/loom', '--benchcli', '/bin/benchcli', '--out', '/tmp/paired-fail', '--source-commit', SHA40, ...CH_ARGS]); + const logs = []; + const failures = run(flags, { + exec: (cmd, args) => { + calls.push(args.join(' ')); + if (args[0] === 'setup-pg') return { status: 1 }; + return { status: 0 }; + }, + log: (l) => logs.push(l), + env: { + LOOM_TEST_ARANGO_URL: 'http://h:8529', + LOOM_TEST_POSTGRES_URL: 'postgresql://u:p@h:5432/loom_bench_paired?sslmode=disable', + LOOM_BENCH_ADMIN_DSN: 'postgresql://u:p@h:5432/postgres', + }, + }); + assert.equal(failures, 1); + assert.equal(calls.some((c) => c.includes('both-arms')), false, 'no benchmark may start when provisioning failed'); + assert.match(logs.join('\n'), /setup-pg exited 1/); +}); diff --git a/scripts/package_audit.py b/scripts/package_audit.py index 9e406dd61..f6710f4e3 100644 --- a/scripts/package_audit.py +++ b/scripts/package_audit.py @@ -65,6 +65,8 @@ "internal/backends/arango/internal/catalog": "Persist and retrieve catalog facts in ArangoDB.", "internal/backends/postgres/internal/catalog": "Persist and retrieve catalog facts in PostgreSQL 18: publish-last generation-scoped resource inventory builds, bounded field-profile records with completeness and truncation metadata, leased compare-and-swap-guarded semantic inventory build state, deterministic semantic-inventory contribution rows with content-addressed replay and conflict classification, byte-order-stable keyset pages over retained resources, and authorization-scoped semantic-inventory pages and selection resolution served directly from contribution aggregates.", "internal/ci": "Exercise repository-wide Go formatting and vet gates, including injected fault detection.", + "internal/dataframe/benchmark": "Run the Arango-versus-PostgreSQL matched dataframe benchmark: deterministic fixtures, canonical bags and correctness gates evaluated before any timing is claimed, control runs, and report artifacts that a re-computing verifier must accept, with ClickHouse-backed publication legs disclosed as planned/not_run when the service is absent.", + "internal/dataframe/benchmark/cmd/benchcli": "Own the benchmark command-line entry point: fail-closed live-run preflight, planned disclosure artifacts, report verification authority, and the loom_bench PostgreSQL database lifecycle.", "internal/dataframe/columntransform": "Define closed, backend-neutral transformations over one selected output column value.", "internal/dataframe/compiler": "Orchestrate semantic planning, physical lowering, optimization, rendering, and compiled-query assembly.", "internal/dataframe/compiler/capability": "Probe which dataframe language capabilities the compiler can prove and support.",