diff --git a/docs/source/development/callable_artifact_authoring.rst b/docs/source/development/callable_artifact_authoring.rst index 431e72d622..b928a6dc6e 100644 --- a/docs/source/development/callable_artifact_authoring.rst +++ b/docs/source/development/callable_artifact_authoring.rst @@ -79,8 +79,9 @@ runtime loader or default value for it. Image outputs that retain the current stack's axes should use ``MainFlowStackOutputSpec.output(name, ImageArtifactType, ...)``. The compiler -binds its lineage to the current image input. Declare the image before any -trailing table outputs so the returned image participates in main flow. +binds their lineage to the current image input. Declare main-flow images before +trailing typed artifacts; multiple images share one aligned canonical return +slot, as described under **Return diagnostic images without flattening the ABI**. For a cropped subset, return ``SelectedPlaneImageOutput(array, source_indices)`` in that image slot; the source indices must match the array's leading axis. It is an array-compatible runtime payload: ``numpy.asarray(result)`` exposes @@ -235,14 +236,13 @@ normal plate compilation/execution supplies image-source and runtime context. :name: callable-artifact-reference-check from openhcs.core.callable_contract import CallableContract - from openhcs.core.runtime_output_matching import RuntimeReturnedOutputMatcher from openhcs.core.steps import FunctionStep fixture = np.zeros((8, 8), dtype=np.uint16) fixture[2:6, 3:7] = 1 returned = inspect_label_fixture(fixture) contract = CallableContract.from_callable(inspect_label_fixture) - matched = RuntimeReturnedOutputMatcher(contract, returned).resolve() + matched = contract.resolve_returned_output(returned) assert np.array_equal(matched[FIXTURE_IMAGE.ref()], fixture) assert matched[FIXTURE_LABELS.ref()].dtype == np.int32 assert matched[FIXTURE_ROWS.ref()].row_mappings() == ( @@ -267,8 +267,154 @@ import, so its dataclass schema is valid in the custom execution namespace as well as in an ordinary module. Persist the source when it must survive a new process; session-only registration does not make a spawned worker import it. +Materialize typed 3D centres as feature-bearing Points +------------------------------------------------------------ + +Points are a materialization of ``MeasurementsArtifactType``, not a missing +``PointsArtifactType`` or a centre-voxel image. Attach ``PointROIOptions`` to the +same object-measurement output as ``CsvOptions``. The existing writer projects +the contextualized table into source-bearing point ROIs; reopening that archive +uses the native Points route and retains row features, including object identity. +A CSV alone is not that native geometry declaration. + +This declaration block is for an operation that already produces labels and +3D centre rows. It specifies their ABI, not how to detect or choose a centre: + +.. code-block:: python + :name: callable-artifact-points-reference + + from dataclasses import dataclass + + from openhcs.core.artifacts import ( + ImageArtifactType, MainFlowStackOutputSpec, MeasurementsArtifactType, + ObjectLabelsArtifactType, ObjectMeasurementSubjectRelation, + ) + from openhcs.core.measurement_row_materialization import ( + DataclassMeasurementColumnarRows, + ) + from openhcs.core.runtime_measurements import ( + ObjectCoreMeasurementFeature, RuntimeMeasurementFeatureOwner, + ) + from openhcs.processing.materialization import ( + CsvOptions, MaterializationSpec, PointROIOptions, + ) + + class CentreFeatureOwner(RuntimeMeasurementFeatureOwner): + @classmethod + def owns_measurement_feature_name(cls, feature_name: str) -> bool: + return any(feature.feature_name == feature_name + for feature in ObjectCoreMeasurementFeature) + + @classmethod + def owns_primary_measurement_feature_name(cls, feature_name: str) -> bool: + return cls.owns_measurement_feature_name(feature_name) + + @dataclass(frozen=True) + class CentreRow: + object_label: int + center_z: float + center_y: float + center_x: float + + CENTRE_IMAGE = MainFlowStackOutputSpec.output("centre_image", ImageArtifactType) + CENTRE_LABELS = MainFlowStackOutputSpec.output( + "centre_labels", ObjectLabelsArtifactType, + ) + CENTRE_ROWS = MainFlowStackOutputSpec.output( + "centre_rows", MeasurementsArtifactType, + measurement_feature_owner=CentreFeatureOwner, + relations=(ObjectMeasurementSubjectRelation( + source=CENTRE_LABELS.ref(), id_field="object_label", + ),), + materialization=MaterializationSpec( + CsvOptions(), + PointROIOptions( + z_feature=ObjectCoreMeasurementFeature.CENTER_Z, + y_feature=ObjectCoreMeasurementFeature.CENTER_Y, + x_feature=ObjectCoreMeasurementFeature.CENTER_X, + ), + ), + ) + +On the existing centre-producing callable, declare +``@artifact_outputs(CENTRE_IMAGE, CENTRE_LABELS, CENTRE_ROWS)`` and return +``image, labels, DataclassMeasurementColumnarRows(rows, row_type=CentreRow)``. +Use the existing memory/processing decorators, as in the executable reference; +``PURE_3D`` is appropriate only for an actual volumetric calculation. Retain +the complete source bindings and compiled ZYX grouping: the decorator alone +does not establish axis order. The row's representative rule (peak, body centre +or another task-defined location) remains the analysis contract, not the writer. + +Coordinates are floating source-grid Z/Y/X locations, not calibrated world +coordinates and not a manually rescaled display array. Keep the full declared +source Z domain, including planes without objects, and its source paths, +spatial frame and voxel spacing through the compiled source/subject relations. +The writer rejects missing source provenance, missing/foreign coordinate fields, +duplicate object IDs and non-finite coordinates. It currently rejects an empty +Points archive; empty measurement rows retain their table schema but do not +establish successful point materialization. + +Fractional Z is geometry. An integer navigation slice, a rounded centre-voxel +marker image and a fractional point table describe different things. Do not +round the table to fit a viewer slice or infer its Z domain from occupied +labels/rounded markers. Compare the persisted table/archive and native Points +at matched raw coordinates, full Z extent and orthogonal views; inspect object +IDs/features and source calibration, not just point count. Verify that the +installed version exposes the required point producer/domain and viewer geometry +contracts before relying on automatic streaming or archive reopening. Declaration +and direct-call checks do not prove that live path. + +Return diagnostic images without flattening the ABI +------------------------------------------------------------ + +``MainFlowStackOutputSpec`` declares source lineage; it does not give every +Image its own outer tuple slot. ``CallableContract`` groups the consecutive +leading main-flow Image declarations into one canonical return slot. Labels, +measurements and other remaining artifact declarations each have one trailing +slot in their exact order. Inspect ``canonical_return_output_specs`` and +``trailing_return_output_specs`` before extending a callable's returns. + +For example, to extend the executable 2D reference with an aligned diagnostic, +keep its labels/rows declarations and replace its image declarations/decorator: + +.. code-block:: python + :name: callable-artifact-diagnostic-reference + + from openhcs.core.aligned_image_payload import ( + AlignedImageSliceContext, pack_aligned_image_outputs, + ) + + FIXTURE_DIAGNOSTIC = MainFlowStackOutputSpec.output( + "fixture_diagnostic", ImageArtifactType, + ) + + @numpy(contract=ProcessingContract.PURE_2D) + @artifact_outputs( + FIXTURE_IMAGE, FIXTURE_DIAGNOSTIC, FIXTURE_LABELS, FIXTURE_ROWS, + ) + def inspect_label_fixture_with_diagnostic(image: np.ndarray): + image, labels, rows = inspect_label_fixture(image) + image_specs = (FIXTURE_IMAGE, FIXTURE_DIAGNOSTIC) + main_images = pack_aligned_image_outputs( + (image, (labels > 0).astype(np.uint8)), + slice_contexts=AlignedImageSliceContext.main_flow_for_artifact_specs( + image_specs, + ), + ) + return main_images, labels, rows + +The diagnostic here is only the synthetic fixture's foreground indicator. +The helper retains exact named slice contexts; it is not ``np.stack`` of +scientific Z planes and introduces no new return codec. With five leading +main-flow Images followed by three typed artifacts, the outer return has four +positions (one aligned image bundle plus three artifacts), not eight. A +``trailing return count`` error is repaired against this declared partition, +not by dropping useful diagnostics or weakening the matcher. Validate/compile +the corrected complete document and inspect its materialization/streaming plan; +this ABI repair does not itself settle a viewer or validate the analysis. + Consume a nominal artifact input -------------------------------- +-------------------------------- An artifact's input ABI is not necessarily its raw output representation. ``ObjectLabelsArtifactType`` accepts integer labels from a producer, but supplies @@ -334,7 +480,7 @@ the corrected complete document through the ordinary route. This establishes technical input compatibility, not object identity or biological accuracy. Summarize declared measurements once per plate ---------------------------------------------- +---------------------------------------------- A terminal ``PLATE`` callable receives ``RuntimeArtifactBatch``, not an image or a directory of CSVs. The parent executes it once after the compiled axes diff --git a/docs/source/development/mcp_knowledge_base_manifest.json b/docs/source/development/mcp_knowledge_base_manifest.json index 249ca9afd6..03674c18d3 100644 --- a/docs/source/development/mcp_knowledge_base_manifest.json +++ b/docs/source/development/mcp_knowledge_base_manifest.json @@ -709,7 +709,7 @@ "document_id": "openhcs_callable_artifact_authoring", "section_count": 0, "source_path": "docs/source/development/callable_artifact_authoring.rst", - "summary": "Canonical callable and artifact authoring reference with executable nominal input annotations and artifact bindings, earliest compile-error repair, typed NumPy main flow, object labels, dataclass ColumnarRows, RuntimeMeasurementFeatureOwner, exact subject/source relations, CSV and ROI materialization, custom registration namespace boundaries, and a terminal PLATE summary using RuntimeArtifactBatch.", + "summary": "Canonical callable and artifact authoring reference with executable nominal input annotations and artifact bindings, earliest compile-error repair, aligned diagnostic-image returns, typed NumPy main flow, object labels, dataclass ColumnarRows, RuntimeMeasurementFeatureOwner, exact subject/source relations, CSV and fractional 3D Points materialization, custom registration namespace boundaries, and a terminal PLATE summary using RuntimeArtifactBatch.", "tags": [ "custom function artifacts", "callable artifact authoring", @@ -726,6 +726,8 @@ "ObjectMeasurementSubjectRelation", "materialization", "ROIOptions", + "PointROIOptions fractional centres", + "aligned diagnostic image returns", "CsvOptions", "exact imports decorators", "custom registration namespace" diff --git a/docs/validation/typed-centroid-authoring-20261005.rst b/docs/validation/typed-centroid-authoring-20261005.rst new file mode 100644 index 0000000000..8cfce6d6c5 --- /dev/null +++ b/docs/validation/typed-centroid-authoring-20261005.rst @@ -0,0 +1,117 @@ +Typed centroid and multi-image authoring checkpoint +================================================== + +Owner and scope +--------------- + +Singer owns this documentation-only change. Planck explicitly released the +callable-artifact authoring and custom-function reference documentation; Planck +retains #748 geometry and its frozen engineering fixtures. No production, +installed package, live author, or scientific input is changed here. + +The canonical owner is ``docs/source/development/callable_artifact_authoring.rst``. +The custom-function skill reference routes to that manifest-declared document; +``build_mcp_knowledge_assets.py`` copies both declared sources into package output. +It does not convert this RST into a second Markdown artifact manual. + +Determining source contracts +---------------------------- + +``PointROIOptions`` in ``processing/materialization/options.py`` selects nominal +runtime measurement features. The registered ``_write_point_roi_zip`` writer in +``processing/materialization/core.py`` requires a contextualized measurement +table, object subject, owned coordinate fields, and exact source provenance. +It retains fractional Z through ``ROIFractionalZ`` and binds the archive through +``ROIArchiveSourceMetadata``. There is no new Points artifact declaration. + +The read-only engineering494 ``point_volume_fixture_494.py`` is pinned at SHA256 +``c883251abc009e9d2c7b82a3dde3b4fe4a66529b6c8117b6bf1bfd887fdc2935``. +Its typed feature owner, subject relation, and CSV plus PointROIOptions supply +the existing declaration example, not an assay algorithm or recommended settings. + +``CallableContract.__post_init__`` separates consecutive main-flow Image specs +into canonical return specs; ``resolve_returned_output`` expects those images +in one named ``AlignedImageStack`` return slot. Each remaining typed artifact +has one trailing slot. A flat tuple of diagnostic Images is not this ABI. +``pack_aligned_image_outputs`` and ``AlignedImageSliceContext`` own its packing. + +Review and readiness +-------------------- + +Read current skill-creator, NRA, authoritative refactor-audit archive, pattern +README and boundary patterns, Diataxis and use-openhcs. Relevant BOUND-2/4/8 +risks are hand-decoding rows, flattening nominal output families, and inventing +an image/CSV substitute for the materialization contract. Existing owners remain +unchanged; this is not a structural refactor or global NRA proof. + +Source reflection, executable declaration/ABI checks and real knowledge/package +projection follow the coherent documentation edit. Native #748 geometry/reopen +qualification remains with Planck/Dewey and is not claimed by these source checks. + +Implemented owner/consumer change +-------------------------------- + +The RST adds one declaration-only centre row/feature owner and measurement output +with CSV plus PointROIOptions; detection is deliberately left to the existing +analysis callable. The original engineering fixture is not edited or copied as +a new detector. The named subject binds the exact labels declaration. The generic +writer accepts this independently named CentreRow/CentreFeatureOwner declaration +without consumer edits, retaining fractional geometry, row features and the full +four-plane source domain. Missing source/field, duplicate-ID and empty-archive +guards remain unchanged. No Points artifact, schema mirror or custom codec is added. + +The existing executable image/labels/rows reference gains a diagnostic variant +using the original aligned image packer and named slice-context owner. Its two +Images share one canonical slot and labels/rows retain two trailing slots; the +original matcher rejects the flattened return. Five canonical Images plus three +trailing typed artifacts likewise means four outer return positions, not eight. +This identifies the reported return-count error as an ABI mismatch, not evidence +that diagnostics are unavailable or that a viewer geometry guard must change. + +One obsolete direct-check consumer of the deleted RuntimeReturnedOutputMatcher +is migrated to CallableContract.resolve_returned_output. One stale fixture cache +clearing call is deleted: resolved_callable_type_hints now owns fresh resolution, +not that removed cache. No compatibility facade or alternative matcher is added. +Two pre-existing short RST heading underlines are corrected so the existing +knowledge section parser ends each section at its actual boundary. The packaged +custom-function reference links these two sections once, with no duplicated ABI +example or new SKILL entrypoint. Manifest discovery metadata names this same +canonical document; the original package builder copies its exact bytes. + +Actual qualification and preserved failures +------------------------------------------ + +All controls use the existing run_installed_tests.py entrypoint, paired interpreter +and read-only qualified receiving14 runtime/dependencies. Six declaration/source +owners (artifacts, callable contract, runtime measurements, aligned image payload, +options and tabular CPP) byte-match current main. The backing writer includes +unmerged #748 PointROIOutput; native streaming/reopen is explicitly NOT inferred +from this disk/source contract proof. No install/build/catalogue/native/viewer or +provider request occurs. Current live authors and their instructions are untouched. + +Original controls01:13PASS/3FAIL,7.02s,377724KiB max RSS, swaps0. Failures were +the new fixture assuming .csv instead of CsvOptions' declared _details.csv, +the pre-existing removed cache API call, and an undersized new RST heading. +Original controls02:15PASS/1FAIL,7.11s,372296KiB max RSS, swaps0. All executable +ABI/materialization tests passed; section retrieval still included later content +because the pre-existing input/plate section underlines were too short. +Final affected controls03:3PASS,6.42s,364996KiB max RSS, swaps0. No larger bound, +parser patch or assertion weakening: correct canonical section boundaries now +retrieve both sections untruncated at10000chars; whole-document retrieval remains +untruncated at30000chars. All16 distinct original controls are qualified by02/03; +this is not reported as a single clean16-test final run. + +Real KnowledgeBaseService retrieval/search, exact RST/skill package projection, +isolated managed-skill sync and unchanged second sync pass. The original +skill-creator quick validator reports ``Skill is valid!``. No production changes, +scientific inputs/reference answers, settings recommendations, source calibration +invention or current-author coaching. git diff --check passes on this change; +eight unrelated foreign gitlinks and prior untracked evidence remain untouched. + +Raw numbered stdout/stderr and original generated centroid CSV/ROI archive are +preserved in the byte-exact qualification archive adjacent to this receipt. +Its15members compare byte-exact to source; archive SHA256 is +``9cc10ec8c226b8c4d79c599aa6d2f670de7de8ca7432d828c501914cc451c6c9``. +CI is deferred. Documentation/contract exposure is ready for review and future +ordinary package delivery; #748's native geometry/selected-source acceptance +remains the explicit independent scope boundary, not a documentation delivery hold. diff --git a/docs/validation/typed-centroid-authoring-20261005.tar.gz b/docs/validation/typed-centroid-authoring-20261005.tar.gz new file mode 100644 index 0000000000..f1b2a5ace4 Binary files /dev/null and b/docs/validation/typed-centroid-authoring-20261005.tar.gz differ diff --git a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md index 597b4555f5..6e7a7fd8ec 100644 --- a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md +++ b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md @@ -20,6 +20,12 @@ Record the missing operation and expected input axes, dtype, units, memory backend, outputs and empty-input behaviour before writing source. For centre detection, specify what defines a centre, coordinate order/origin, label identity and whether a count covers one plane or the whole volume. +For native feature-bearing 3-D Points, retrieve **Materialize typed 3D centres +as feature-bearing Points** in `openhcs_callable_artifact_authoring`: existing +`MeasurementsArtifactType` plus `PointROIOptions`, not a new Points artifact or +rounded centre-voxel image. When adding diagnostic Images, use that document's +**Return diagnostic images without flattening the ABI** section for the aligned +canonical image slot and trailing typed outputs. Before writing source, verify the intended imports, decorators, helper types and enum members against the actual public declarations for the installed diff --git a/tests/unit/agent/test_callable_artifact_reference.py b/tests/unit/agent/test_callable_artifact_reference.py index 965d416f87..ab292178bc 100644 --- a/tests/unit/agent/test_callable_artifact_reference.py +++ b/tests/unit/agent/test_callable_artifact_reference.py @@ -59,6 +59,8 @@ DOCUMENT_ID = "openhcs_callable_artifact_authoring" REFERENCE_BLOCKS = ( "callable-artifact-reference", + "callable-artifact-points-reference", + "callable-artifact-diagnostic-reference", "callable-artifact-input-reference", "callable-artifact-plate-reference", ) @@ -173,6 +175,91 @@ def test_reference_executes_and_compiles_actual_function_step(reference_namespac assert not namespace["FixtureFeatureOwner"].owns_measurement_feature_name("unknown") +def test_diagnostic_reference_uses_one_canonical_slot_and_exact_trailing_artifacts( + reference_namespace, +): + namespace = reference_namespace + function = namespace["inspect_label_fixture_with_diagnostic"] + contract = CallableContract.from_callable(function) + pixels = np.zeros((8, 8), dtype=np.uint16) + pixels[2:6, 3:7] = 7 + returned = function(pixels) + assert len(returned) == 3 + assert contract.canonical_return_output_specs.names() == ( + "fixture_image", "fixture_diagnostic", + ) + assert contract.trailing_return_output_specs.names() == ( + "fixture_labels", "fixture_object_rows", + ) + matched = contract.resolve_returned_output(returned) + np.testing.assert_array_equal(matched[namespace["FIXTURE_IMAGE"].ref()], pixels) + np.testing.assert_array_equal( + matched[namespace["FIXTURE_DIAGNOSTIC"].ref()], pixels > 0, + ) + assert matched[namespace["FIXTURE_ROWS"].ref()].row_mappings() == ( + {"slice_index": 0, "object_label": 7, "pixel_count": 16}, + ) + with pytest.raises(ValueError, match="trailing return count"): + contract.resolve_returned_output((pixels, pixels > 0, returned[1], returned[2])) + + +def test_points_declaration_materializes_fractional_geometry_and_full_source_domain( + reference_namespace, tmp_path, +): + from openhcs.core.roi_point_metadata import ROIFractionalZ + from openhcs.core.roi_source_metadata import ROIArchiveSourceMetadata + from openhcs.core.runtime_measurements import MeasurementTable + from openhcs.core.source_image_provenance import SourceImageProvenancePlanes + from polystore.roi import PointShape + + namespace = reference_namespace + spec = namespace["CENTRE_ROWS"] + labels = namespace["CENTRE_LABELS"] + plan = ArtifactOutputPlan( + name=spec.name, path="/synthetic/centre_rows", artifact_type=spec.artifact_type, + relations=spec.relations, + ) + assert plan.object_subject_binding().source == labels.ref() + row = namespace["CentreRow"](7, 1.5, 1.25, 2.5) + carrier = namespace["DataclassMeasurementColumnarRows"]( + (row,), row_type=namespace["CentreRow"], + ) + # Direct materialization supplies the same nominal context normally owned + # by runtime contextualization, without inventing an archive/CSV writer. + table = MeasurementTable( + name=spec.name, rows=carrier, subject=plan.measurement_subject(), + measurement_feature_owner=namespace["CentreFeatureOwner"], + source_path="/synthetic/volume.tif", + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/synthetic/volume.tif",) * 4, + component_metadata=tuple( + {"well": "A01", "site": 1, "channel": 1, "z_index": z, "timepoint": 1} + for z in range(4) + ), + ), + ) + materialize( + spec.materialization, data=table, path=str(tmp_path / "centres"), + filemanager=FileManager({"disk": DiskStorageBackend()}), + backends=["disk"], backend_kwargs={}, + ) + csv_options = next( + option for option in spec.materialization.outputs + if isinstance(option, namespace["CsvOptions"]) + ) + assert (tmp_path / ("centres" + csv_options.filename_suffix)).read_text().splitlines() == [ + "object_label,center_z,center_y,center_x", "7,1.5,1.25,2.5", + ] + rois = load_rois_from_zip(tmp_path / "centres_points.roi.zip") + assert rois[0].shapes == [PointShape(y=1.25, x=2.5)] + assert rois[0].metadata["object_label"] == 7 + assert ROIFractionalZ.decode(rois[0].metadata) == ROIFractionalZ(1.5) + metadata = ROIArchiveSourceMetadata.decode(rois) + domain = ROIFractionalZ.source_component_domain(rois, metadata) + assert tuple(plane["z_index"] for plane in domain) == (0, 1, 2, 3) + assert metadata.source_path == "/synthetic/volume.tif" + + def test_input_reference_compiles_nominal_binding_and_repairs_wrong_annotation( reference_namespace, ): @@ -202,13 +289,10 @@ def test_input_reference_compiles_nominal_binding_and_repairs_wrong_annotation( original_annotation = raw.__annotations__["objects"] try: raw.__annotations__["objects"] = np.ndarray - from openhcs.core.pipeline.function_contracts import resolved_callable_type_hints - resolved_callable_type_hints.cache_clear() with pytest.raises(TypeError, match="does not accept object_labels artifact payloads"): compile_function_pattern(function, {plan.ref(): plan}, {}) finally: raw.__annotations__["objects"] = original_annotation - resolved_callable_type_hints.cache_clear() compile_function_pattern(function, {plan.ref(): plan}, {}) @@ -490,6 +574,9 @@ def test_packaged_knowledge_projection_retains_executable_reference(tmp_path): ) assert "def inspect_label_fixture(" in document.content assert "from openhcs.core.memory import numpy" in document.content + for query in ("PointROIOptions fractional centres", "aligned diagnostic image returns"): + hits = service.search(KnowledgeBaseSearchRequest(query=query, limit=10)) + assert DOCUMENT_ID in {hit.document.document_id for hit in hits.hits} for block in REFERENCE_BLOCKS: for line in _reference_block(block).splitlines(): if line.strip(): @@ -502,3 +589,18 @@ def test_packaged_knowledge_projection_retains_executable_reference(tmp_path): source_path = "packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md" assert (destination / source_path).read_bytes() == (ROOT / source_path).read_bytes() assert "Summarize declared measurements once per" in route.content + for section_id in ( + "materialize-typed-3d-centres-as-feature-bearing-points", + "return-diagnostic-images-without-flattening-the-abi", + ): + selected = service.get_document(KnowledgeBaseDocumentRequest.from_fields( + document_id=DOCUMENT_ID, section_id=section_id, max_chars=10_000, + )) + assert not selected.errors and not selected.truncated + assert selected.selected_section_id == section_id + original = KnowledgeBaseService(repo_root=ROOT).get_document( + KnowledgeBaseDocumentRequest.from_fields( + document_id=DOCUMENT_ID, section_id=section_id, max_chars=10_000, + ) + ) + assert selected.content == original.content