novaflow-types 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novaflow_types-0.1.0/.gitignore +32 -0
- novaflow_types-0.1.0/.vscode/settings.json +5 -0
- novaflow_types-0.1.0/PKG-INFO +37 -0
- novaflow_types-0.1.0/README.md +28 -0
- novaflow_types-0.1.0/generate_camera_registry_schema.py +46 -0
- novaflow_types-0.1.0/generate_comm_kv_schema.py +76 -0
- novaflow_types-0.1.0/generate_comm_registries.py +139 -0
- novaflow_types-0.1.0/generate_global_type_catalog.py +185 -0
- novaflow_types-0.1.0/generate_node_constants.py +312 -0
- novaflow_types-0.1.0/generate_structural_id_golden.py +330 -0
- novaflow_types-0.1.0/generate_tree_schema.py +76 -0
- novaflow_types-0.1.0/generate_type_structural_ids.py +108 -0
- novaflow_types-0.1.0/postprocess_core_types.py +56 -0
- novaflow_types-0.1.0/postprocess_data_types.py +138 -0
- novaflow_types-0.1.0/postprocess_tree_types.py +210 -0
- novaflow_types-0.1.0/pyproject.toml +40 -0
- novaflow_types-0.1.0/src/novaflow/types/__init__.py +9 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/__init__.py +0 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/camera_registry.py +72 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/cloudevent.py +167 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/execution_state.py +22 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/heartbeat.py +11 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/kv_keys.py +185 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/programming.py +471 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/response_payload.py +87 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/run_hooks.py +60 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/status.py +19 -0
- novaflow_types-0.1.0/src/novaflow/types/comm/subjects.py +75 -0
- novaflow_types-0.1.0/src/novaflow/types/core/__init__.py +17 -0
- novaflow_types-0.1.0/src/novaflow/types/core/action.py +1083 -0
- novaflow_types-0.1.0/src/novaflow/types/core/base.py +24 -0
- novaflow_types-0.1.0/src/novaflow/types/core/blackboard.py +90 -0
- novaflow_types-0.1.0/src/novaflow/types/core/service.py +81 -0
- novaflow_types-0.1.0/src/novaflow/types/core/shape_diff.py +72 -0
- novaflow_types-0.1.0/src/novaflow/types/core/structural_id.py +439 -0
- novaflow_types-0.1.0/src/novaflow/types/data/__init__.py +42 -0
- novaflow_types-0.1.0/src/novaflow/types/data/depth_image.py +66 -0
- novaflow_types-0.1.0/src/novaflow/types/data/dof_check.py +250 -0
- novaflow_types-0.1.0/src/novaflow/types/data/image.py +35 -0
- novaflow_types-0.1.0/src/novaflow/types/data/joints.py +23 -0
- novaflow_types-0.1.0/src/novaflow/types/data/json_data.py +32 -0
- novaflow_types-0.1.0/src/novaflow/types/data/mobile.py +95 -0
- novaflow_types-0.1.0/src/novaflow/types/data/motion_item.py +127 -0
- novaflow_types-0.1.0/src/novaflow/types/data/motion_routine.py +607 -0
- novaflow_types-0.1.0/src/novaflow/types/data/motion_target.py +51 -0
- novaflow_types-0.1.0/src/novaflow/types/data/nova_openapi/26.3.0/public.openapi.json +20077 -0
- novaflow_types-0.1.0/src/novaflow/types/data/nova_spec.py +203 -0
- novaflow_types-0.1.0/src/novaflow/types/data/pointcloud.py +38 -0
- novaflow_types-0.1.0/src/novaflow/types/data/pose.py +27 -0
- novaflow_types-0.1.0/src/novaflow/types/data/reference.py +35 -0
- novaflow_types-0.1.0/src/novaflow/types/data/registry.py +286 -0
- novaflow_types-0.1.0/src/novaflow/types/data/vector3d.py +12 -0
- novaflow_types-0.1.0/src/novaflow/types/nova_client.py +43 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/__init__.py +89 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/bounds.py +100 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/io_action_naming.py +39 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/migrate_v2_to_v3.py +1299 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/naming.py +44 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/nodes.py +1074 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/references.py +212 -0
- novaflow_types-0.1.0/src/novaflow/types/tree/tree.py +320 -0
- novaflow_types-0.1.0/tests/__init__.py +0 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/globals_with_repeated.v2.json +15 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/globals_with_repeated.v3.json +13 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/folge11_globals.975cf7a5..804d24fb.json +1919 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase1_globals.804d24fb.json +67 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase1_globals.975cf7a5.UNRECOGNIZED.json +34 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase2b_globals.json +71 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase3_globals.v1.json +75 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase3_globals.v2.json +71 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase4_globals.v1.json +75 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase4_globals.v2.json +71 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/io_service_setio_tree.v2.json +90 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/io_service_setio_tree.v3.json +106 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/move_with_io_gates.v2.json +24 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/move_with_io_gates.v3.json +30 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_boolean_controller.v2.json +9 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_boolean_controller.v3.json +9 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_float_busio.v2.json +8 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_float_busio.v3.json +9 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_integer_controller.v2.json +9 -0
- novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_integer_controller.v3.json +9 -0
- novaflow_types-0.1.0/tests/served_type_id_golden.json +196 -0
- novaflow_types-0.1.0/tests/test_R2_01_tree_bounds.py +211 -0
- novaflow_types-0.1.0/tests/test_R3_03_safety_floats.py +150 -0
- novaflow_types-0.1.0/tests/test_any_type_support.py +93 -0
- novaflow_types-0.1.0/tests/test_behavior_tree_model.py +1373 -0
- novaflow_types-0.1.0/tests/test_boundary_extra_ignore.py +67 -0
- novaflow_types-0.1.0/tests/test_bump_tree_version.py +242 -0
- novaflow_types-0.1.0/tests/test_camera_registry.py +114 -0
- novaflow_types-0.1.0/tests/test_check_tree_version_stragglers.py +233 -0
- novaflow_types-0.1.0/tests/test_comm_kv_keys.py +94 -0
- novaflow_types-0.1.0/tests/test_comm_programming.py +203 -0
- novaflow_types-0.1.0/tests/test_comm_subjects.py +57 -0
- novaflow_types-0.1.0/tests/test_condition_node_rename.py +51 -0
- novaflow_types-0.1.0/tests/test_controller_aggregate.py +146 -0
- novaflow_types-0.1.0/tests/test_custom_composite_types.py +218 -0
- novaflow_types-0.1.0/tests/test_dof_check.py +334 -0
- novaflow_types-0.1.0/tests/test_enum_support.py +237 -0
- novaflow_types-0.1.0/tests/test_generate_comm_registries.py +65 -0
- novaflow_types-0.1.0/tests/test_generate_global_type_catalog.py +88 -0
- novaflow_types-0.1.0/tests/test_generate_type_structural_ids.py +58 -0
- novaflow_types-0.1.0/tests/test_io_action_naming.py +85 -0
- novaflow_types-0.1.0/tests/test_io_reference.py +166 -0
- novaflow_types-0.1.0/tests/test_iodescription_shadow_removed.py +30 -0
- novaflow_types-0.1.0/tests/test_jointtrajectory_shadow_removed.py +33 -0
- novaflow_types-0.1.0/tests/test_migrate_v2_to_v3.py +1341 -0
- novaflow_types-0.1.0/tests/test_migrate_v2_to_v3_historical_corpus.py +83 -0
- novaflow_types-0.1.0/tests/test_mobile_refs.py +69 -0
- novaflow_types-0.1.0/tests/test_motion_routine_final_pose.py +82 -0
- novaflow_types-0.1.0/tests/test_motion_routine_io_condition.py +104 -0
- novaflow_types-0.1.0/tests/test_motion_routine_start_joint_position.py +147 -0
- novaflow_types-0.1.0/tests/test_motion_routine_terminal.py +59 -0
- novaflow_types-0.1.0/tests/test_nesting_matrix.py +208 -0
- novaflow_types-0.1.0/tests/test_no_metadata_sidecar_anywhere.py +102 -0
- novaflow_types-0.1.0/tests/test_no_nova_type_tag.py +81 -0
- novaflow_types-0.1.0/tests/test_no_trajectory_globals_metadata.py +99 -0
- novaflow_types-0.1.0/tests/test_node_kind_contract.py +139 -0
- novaflow_types-0.1.0/tests/test_node_kind_generator.py +74 -0
- novaflow_types-0.1.0/tests/test_node_level_fields.py +125 -0
- novaflow_types-0.1.0/tests/test_node_roles.py +137 -0
- novaflow_types-0.1.0/tests/test_nova_client.py +37 -0
- novaflow_types-0.1.0/tests/test_nova_spec_diff.py +188 -0
- novaflow_types-0.1.0/tests/test_nova_structural_parity.py +36 -0
- novaflow_types-0.1.0/tests/test_nova_type_parity.py +169 -0
- novaflow_types-0.1.0/tests/test_open_generic_types.py +265 -0
- novaflow_types-0.1.0/tests/test_postprocess_data_types.py +123 -0
- novaflow_types-0.1.0/tests/test_pydantic_json_schema_overrides.py +44 -0
- novaflow_types-0.1.0/tests/test_served_type_id_golden.py +105 -0
- novaflow_types-0.1.0/tests/test_shape_diff.py +73 -0
- novaflow_types-0.1.0/tests/test_shipped_trees_v3_conformance.py +61 -0
- novaflow_types-0.1.0/tests/test_shipped_type_id.py +183 -0
- novaflow_types-0.1.0/tests/test_structural_id.py +544 -0
- novaflow_types-0.1.0/tests/test_structural_id_conformance.py +58 -0
- novaflow_types-0.1.0/tests/test_structural_id_spec_invariant.py +233 -0
- novaflow_types-0.1.0/tests/test_structural_nesting.py +107 -0
- novaflow_types-0.1.0/tests/test_type_id_registry.py +139 -0
- novaflow_types-0.1.0/tests/test_type_id_stability.py +191 -0
- novaflow_types-0.1.0/tests/test_vendored_spec.py +31 -0
- novaflow_types-0.1.0/tools/nova_spec_diff.py +291 -0
- novaflow_types-0.1.0/tools/vendored_spec.py +64 -0
- novaflow_types-0.1.0/uv.lock +1428 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
**/kubeconfig*
|
|
2
|
+
**/.env
|
|
3
|
+
**/.venv
|
|
4
|
+
**/__pycache__/
|
|
5
|
+
**/.ruff_cache/
|
|
6
|
+
.DS_Store
|
|
7
|
+
logs/
|
|
8
|
+
|
|
9
|
+
__pycache__
|
|
10
|
+
py_test
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
|
|
13
|
+
node_modules
|
|
14
|
+
profiling-data/
|
|
15
|
+
|
|
16
|
+
.opencode
|
|
17
|
+
.claude/settings.local.json
|
|
18
|
+
.claude/worktrees/
|
|
19
|
+
.claude/scheduled_tasks.lock
|
|
20
|
+
|
|
21
|
+
# Debug / ad-hoc data dumps
|
|
22
|
+
globals_dump*.json
|
|
23
|
+
wandelbox-4qq9rq.yaml
|
|
24
|
+
|
|
25
|
+
# Working-scratch investigations — fine to write into docs/investigations/
|
|
26
|
+
# locally, but the tree must not be committed. Load-bearing facts go inline
|
|
27
|
+
# at the citation site (ADR / spec / code docstring); see AGENTS.md
|
|
28
|
+
# § Response Length.
|
|
29
|
+
docs/investigations/
|
|
30
|
+
|
|
31
|
+
# Playwright MCP session scratch (console logs + page snapshots)
|
|
32
|
+
.playwright-mcp/
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: novaflow-types
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Python: >=3.11
|
|
5
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
6
|
+
Requires-Dist: pydantic>=2.0
|
|
7
|
+
Requires-Dist: wandelbots-api-client==26.3.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# novaflow-types
|
|
11
|
+
|
|
12
|
+
Pydantic models for the NovaFlow behavior-tree platform — the shared
|
|
13
|
+
vocabulary used across the stack: tree nodes (`BehaviorTree`, `ActionNode`,
|
|
14
|
+
sequences, decorators, …), robotics data types (`Pose`, `Joints`,
|
|
15
|
+
`MotionTarget`, …), action I/O schemas, and the structural type identity
|
|
16
|
+
(content-hashed `type_id`) that decides node compatibility.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install novaflow-types
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Typical uses:
|
|
23
|
+
|
|
24
|
+
- Build NovaFlow behavior trees programmatically in Python and serialize
|
|
25
|
+
them to the JSON the Forest API imports (`BehaviorTree.model_dump_json()`).
|
|
26
|
+
- Author custom action services — this package comes in transitively with
|
|
27
|
+
the `wandelbots-novaflow` runtime SDK.
|
|
28
|
+
|
|
29
|
+
The package also ships the committed Nova API spec it validates types
|
|
30
|
+
against; the pinned `wandelbots-api-client` version is the single source
|
|
31
|
+
of truth for Nova type identity across the stack.
|
|
32
|
+
|
|
33
|
+
## Versioning
|
|
34
|
+
|
|
35
|
+
Pre-1.0: this package tracks internal NovaFlow development. Breaking
|
|
36
|
+
changes can happen on any minor release — **pin your version**. Published
|
|
37
|
+
versions are immutable; nothing breaks under you until you upgrade.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# novaflow-types
|
|
2
|
+
|
|
3
|
+
Pydantic models for the NovaFlow behavior-tree platform — the shared
|
|
4
|
+
vocabulary used across the stack: tree nodes (`BehaviorTree`, `ActionNode`,
|
|
5
|
+
sequences, decorators, …), robotics data types (`Pose`, `Joints`,
|
|
6
|
+
`MotionTarget`, …), action I/O schemas, and the structural type identity
|
|
7
|
+
(content-hashed `type_id`) that decides node compatibility.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install novaflow-types
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Typical uses:
|
|
14
|
+
|
|
15
|
+
- Build NovaFlow behavior trees programmatically in Python and serialize
|
|
16
|
+
them to the JSON the Forest API imports (`BehaviorTree.model_dump_json()`).
|
|
17
|
+
- Author custom action services — this package comes in transitively with
|
|
18
|
+
the `wandelbots-novaflow` runtime SDK.
|
|
19
|
+
|
|
20
|
+
The package also ships the committed Nova API spec it validates types
|
|
21
|
+
against; the pinned `wandelbots-api-client` version is the single source
|
|
22
|
+
of truth for Nova type identity across the stack.
|
|
23
|
+
|
|
24
|
+
## Versioning
|
|
25
|
+
|
|
26
|
+
Pre-1.0: this package tracks internal NovaFlow development. Breaking
|
|
27
|
+
changes can happen on any minor release — **pin your version**. Published
|
|
28
|
+
versions are immutable; nothing breaks under you until you upgrade.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate JSON Schema for the ``registry_cameras`` KV wire payload.
|
|
3
|
+
|
|
4
|
+
Read-side half of the wire-contract symmetry (#251) for camera feeds: connectors
|
|
5
|
+
publish ``CameraRegistryEntry`` payloads into the ``registry_cameras`` KV bucket;
|
|
6
|
+
this emits that Pydantic model as JSON Schema so canopy validates each entry on
|
|
7
|
+
READ via ajv. Both ends share ONE contract, generated from the Pydantic source,
|
|
8
|
+
so the schema can't drift from what producers write.
|
|
9
|
+
|
|
10
|
+
Usage: uv run generate_camera_registry_schema.py ../json/camera_registry.schema.json
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from novaflow.types.comm.camera_registry import CameraRegistryEntry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate() -> dict:
|
|
21
|
+
"""Return the serialization-mode JSON Schema for a registry_cameras entry."""
|
|
22
|
+
# mode="serialization" → the shape actually written to KV (model_dump),
|
|
23
|
+
# which is what canopy parses and must validate.
|
|
24
|
+
schema = CameraRegistryEntry.model_json_schema(mode="serialization")
|
|
25
|
+
schema["$comment"] = (
|
|
26
|
+
"AUTO-GENERATED from the CameraRegistryEntry Pydantic model "
|
|
27
|
+
"(novaflow-schemas) — do NOT hand-edit; run scripts/generate-types.sh. "
|
|
28
|
+
"Read-side validation contract for canopy (#251)."
|
|
29
|
+
)
|
|
30
|
+
return schema
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> None:
|
|
34
|
+
schema = generate()
|
|
35
|
+
if len(sys.argv) > 1:
|
|
36
|
+
output_path = Path(sys.argv[1])
|
|
37
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
with open(output_path, "w") as f:
|
|
39
|
+
json.dump(schema, f, indent=2)
|
|
40
|
+
print(f"Camera-registry schema written to {output_path}", file=sys.stderr)
|
|
41
|
+
else:
|
|
42
|
+
print(json.dumps(schema, indent=2))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate JSON Schema for the per-run KV *state* payloads canopy reads.
|
|
3
|
+
|
|
4
|
+
This is the read-side half of the wire-contract symmetry (#251): the action /
|
|
5
|
+
control-plane already validate these payloads against their Pydantic models on
|
|
6
|
+
WRITE; this emits the same models as JSON Schema so canopy can validate them on
|
|
7
|
+
READ via ajv. Both ends then share ONE contract — generated from the Pydantic
|
|
8
|
+
source, so the schema can't drift from what the writers produce.
|
|
9
|
+
|
|
10
|
+
Output is a map keyed by the field name canopy sees after stripping the
|
|
11
|
+
``{run_id}.`` prefix → that field's JSON Schema:
|
|
12
|
+
|
|
13
|
+
{"cursor": {...}, "scrub_state": {...}, "terminal": {...}, "speed": {...}}
|
|
14
|
+
|
|
15
|
+
Only the *state* signals canopy reads are included. ``progress`` is
|
|
16
|
+
intentionally omitted: its KV body is ``{item_index, state}`` and canopy
|
|
17
|
+
derives the index from the *key* (``progress.{N}``), so there is no dedicated
|
|
18
|
+
KV model to validate against (``ProgressEvent`` is the richer *subject* model).
|
|
19
|
+
|
|
20
|
+
Usage: uv run generate_comm_kv_schema.py ../json/comm_kv.schema.json
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from novaflow.types.comm.programming import (
|
|
28
|
+
CursorEvent,
|
|
29
|
+
ProgrammingTerminalBody,
|
|
30
|
+
ScrubStateEvent,
|
|
31
|
+
SpeedRequest,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Field name (as canopy sees it, post ``{run_id}.`` strip) → Pydantic model.
|
|
35
|
+
# Keep in lockstep with the writers (cursor_cb / state_cb / _publish_session_
|
|
36
|
+
# terminal / the speed KV write) and the read dispatch in
|
|
37
|
+
# canopy/src/hooks/handleProgrammingSessionKvEntry.ts.
|
|
38
|
+
_KV_FIELD_MODELS = {
|
|
39
|
+
"cursor": CursorEvent,
|
|
40
|
+
"scrub_state": ScrubStateEvent,
|
|
41
|
+
"terminal": ProgrammingTerminalBody,
|
|
42
|
+
"speed": SpeedRequest,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate() -> dict:
|
|
47
|
+
"""Return ``{field: json_schema}`` for every validated KV state signal."""
|
|
48
|
+
out: dict = {
|
|
49
|
+
"//": (
|
|
50
|
+
"AUTO-GENERATED from Pydantic models (novaflow-schemas) — do NOT "
|
|
51
|
+
"hand-edit; run scripts/generate-types.sh. Read-side validation "
|
|
52
|
+
"contract for canopy (#251); serialization mode (matches the bytes "
|
|
53
|
+
"on the wire)."
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
for field, model in _KV_FIELD_MODELS.items():
|
|
57
|
+
# mode="serialization" → the shape that's actually written to KV
|
|
58
|
+
# (``model_dump``), which is what canopy parses and must validate.
|
|
59
|
+
out[field] = model.model_json_schema(mode="serialization")
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main() -> None:
|
|
64
|
+
schema = generate()
|
|
65
|
+
if len(sys.argv) > 1:
|
|
66
|
+
output_path = Path(sys.argv[1])
|
|
67
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with open(output_path, "w") as f:
|
|
69
|
+
json.dump(schema, f, indent=2)
|
|
70
|
+
print(f"Comm-KV schema written to {output_path}", file=sys.stderr)
|
|
71
|
+
else:
|
|
72
|
+
print(json.dumps(schema, indent=2))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate the TypeScript twins of the comm subject + KV-key registries from
|
|
3
|
+
their Python source (#251).
|
|
4
|
+
|
|
5
|
+
``subjects.py`` and ``kv_keys.py`` are the single source of truth for the
|
|
6
|
+
per-run NATS subject + KV-key strings. Their TS twins were previously
|
|
7
|
+
HAND-mirrored and kept in sync only by twin tests — their own docstrings
|
|
8
|
+
promised a codegen pipeline that was never built. This is that pipeline: it
|
|
9
|
+
reflects each Python helper and emits the equivalent TS, so the twins can't
|
|
10
|
+
drift (the twin tests become a *verification* of the generated output, not a
|
|
11
|
+
manual-sync chore).
|
|
12
|
+
|
|
13
|
+
Emits two files:
|
|
14
|
+
- commSubjects.ts ← novaflow.types.comm.subjects
|
|
15
|
+
- programmingKvKeys.ts ← novaflow.types.comm.kv_keys
|
|
16
|
+
|
|
17
|
+
Rules (kept deliberately simple, matching the existing twins):
|
|
18
|
+
* module-level UPPER_CASE ``str`` constant → ``export const NAME = "...";``
|
|
19
|
+
* module-level function defined here, returns str → reflected TS function
|
|
20
|
+
(called with ``__param__`` sentinels; result becomes a template literal;
|
|
21
|
+
param types mapped str→string, int→number; names snake→camel).
|
|
22
|
+
* everything else (purge-suffix tuples, imports) is Python-only → skipped.
|
|
23
|
+
|
|
24
|
+
Usage: uv run generate_comm_registries.py <out_dir>
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import inspect
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from types import ModuleType
|
|
31
|
+
|
|
32
|
+
from novaflow.types.comm import kv_keys as kv_keys_mod
|
|
33
|
+
from novaflow.types.comm import subjects as subjects_mod
|
|
34
|
+
|
|
35
|
+
# Keys cover both resolved types and their string names — modules using
|
|
36
|
+
# ``from __future__ import annotations`` (PEP 563) expose annotations as
|
|
37
|
+
# strings even with ``eval_str=True`` unavailable.
|
|
38
|
+
_PY_TO_TS_TYPE = {str: "string", int: "number", "str": "string", "int": "number"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _camel(snake: str) -> str:
|
|
42
|
+
head, *rest = snake.split("_")
|
|
43
|
+
return head + "".join(w[:1].upper() + w[1:] for w in rest)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Prettier's default print width. We emit already-wrapped output so the
|
|
47
|
+
# committed twins are byte-identical to the generator with NO external
|
|
48
|
+
# formatter in the loop (deterministic across toolchains; #251 review).
|
|
49
|
+
_PRINT_WIDTH = 80
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _emit_function(name: str, fn) -> str | None:
|
|
53
|
+
try:
|
|
54
|
+
sig = inspect.signature(fn, eval_str=True)
|
|
55
|
+
except Exception:
|
|
56
|
+
sig = inspect.signature(fn)
|
|
57
|
+
params = list(sig.parameters.values())
|
|
58
|
+
sentinels = {p.name: f"__{p.name}__" for p in params}
|
|
59
|
+
try:
|
|
60
|
+
template = fn(*sentinels.values())
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
63
|
+
if not isinstance(template, str):
|
|
64
|
+
return None
|
|
65
|
+
for p in params:
|
|
66
|
+
template = template.replace(sentinels[p.name], "${" + _camel(p.name) + "}")
|
|
67
|
+
ts_param_list = [
|
|
68
|
+
f"{_camel(p.name)}: {_PY_TO_TS_TYPE.get(p.annotation, 'string')}"
|
|
69
|
+
for p in params
|
|
70
|
+
]
|
|
71
|
+
decl = f"export function {_camel(name)}({', '.join(ts_param_list)}): string {{"
|
|
72
|
+
if len(decl) <= _PRINT_WIDTH:
|
|
73
|
+
sig_block = decl
|
|
74
|
+
else:
|
|
75
|
+
# Mirror prettier: one param per line, indented, trailing comma.
|
|
76
|
+
wrapped = "".join(f" {p},\n" for p in ts_param_list)
|
|
77
|
+
sig_block = (
|
|
78
|
+
f"export function {_camel(name)}(\n{wrapped}): string {{"
|
|
79
|
+
)
|
|
80
|
+
return f"{sig_block}\n return `{template}`;\n}}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _header(py_module: str) -> str:
|
|
84
|
+
# No trailing newline: blocks are joined with a blank line, so a
|
|
85
|
+
# trailing "\n" here would produce a double blank after the header.
|
|
86
|
+
return (
|
|
87
|
+
"// AUTO-GENERATED — DO NOT EDIT.\n"
|
|
88
|
+
f"// Source: {py_module}\n"
|
|
89
|
+
"// Regenerate via: bash scripts/generate-types.sh\n"
|
|
90
|
+
"// (TS twin of the Python subject/KV-key registry — #251 codegen.)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def emit_module(mod: ModuleType) -> str:
|
|
95
|
+
blocks: list[str] = [_header(mod.__name__)]
|
|
96
|
+
for name, obj in vars(mod).items():
|
|
97
|
+
if name.startswith("_"):
|
|
98
|
+
continue
|
|
99
|
+
if isinstance(obj, str) and name.isupper():
|
|
100
|
+
blocks.append(f'export const {name} = "{obj}";')
|
|
101
|
+
elif inspect.isfunction(obj) and getattr(obj, "__module__", None) == mod.__name__:
|
|
102
|
+
ts = _emit_function(name, obj)
|
|
103
|
+
if ts is None:
|
|
104
|
+
# A public, module-defined helper we cannot reflect. Failing
|
|
105
|
+
# loud beats silently dropping a wire helper from the TS twin
|
|
106
|
+
# (which would drift unnoticed until a downstream consumer
|
|
107
|
+
# broke). Make it Python-only with a leading underscore, or
|
|
108
|
+
# teach _emit_function to handle it.
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
f"cannot reflect public helper {name!r} in {mod.__name__} "
|
|
111
|
+
"for TS codegen (returned non-str or raised). Prefix it "
|
|
112
|
+
"with '_' if it is Python-only, or extend _emit_function."
|
|
113
|
+
)
|
|
114
|
+
blocks.append(ts)
|
|
115
|
+
return "\n\n".join(blocks) + "\n"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_OUTPUTS = {
|
|
119
|
+
"commSubjects.ts": subjects_mod,
|
|
120
|
+
"programmingKvKeys.ts": kv_keys_mod,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> None:
|
|
125
|
+
if len(sys.argv) < 2:
|
|
126
|
+
print("usage: generate_comm_registries.py <out_dir>", file=sys.stderr)
|
|
127
|
+
raise SystemExit(2)
|
|
128
|
+
out_dir = Path(sys.argv[1])
|
|
129
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
for filename, mod in _OUTPUTS.items():
|
|
131
|
+
# Pin UTF-8: the header carries a non-ASCII em-dash, and the platform
|
|
132
|
+
# default encoding (cp1252 on Windows) would mangle it → drift between
|
|
133
|
+
# a Windows dev and Linux CI. See AGENTS.md cross-platform rules.
|
|
134
|
+
(out_dir / filename).write_text(emit_module(mod), encoding="utf-8")
|
|
135
|
+
print(f"Wrote {out_dir / filename}", file=sys.stderr)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate canopy's registry-derived catalog of selectable global types (#420).
|
|
3
|
+
|
|
4
|
+
canopy's globals "create new global" form used to hardcode a 7-item type list
|
|
5
|
+
(``String``/``Float``/``Int``/``Bool``/``Pose``/``Joints``/``Any``) with
|
|
6
|
+
hand-rolled defaults and schemas. That list (a) used **non-canonical** primitive
|
|
7
|
+
names (``"String"`` resolves to NO structural id — ``"str"`` is the registry's
|
|
8
|
+
name) so a global created with it failed closed when wired into a flow, and
|
|
9
|
+
(b) couldn't offer the ~30 *other* registered Nova types (``Vector3d``,
|
|
10
|
+
``LimitsOverride``, ``BlendingAuto/Position``, ``CubicSplineParameter``,
|
|
11
|
+
``MotionRoutine*``, ``ApproachJoints*``, the IO specs, …).
|
|
12
|
+
|
|
13
|
+
This generator emits one entry per registered type, keyed by its **canonical**
|
|
14
|
+
registry name, with:
|
|
15
|
+
* ``jsonSchema`` — ``TypeAdapter(py_type).json_schema()`` (the type's schema,
|
|
16
|
+
which ActionInputField dispatches its editor from), and
|
|
17
|
+
* ``defaultValue`` — a best-effort, total (never-raises) schema default the
|
|
18
|
+
value editor seeds with and then refines.
|
|
19
|
+
|
|
20
|
+
So the editor's dropdown == the registered (Nova) types with canonical names,
|
|
21
|
+
and a global created from it stores a type expression that *resolves* (its
|
|
22
|
+
structural id is non-null), instead of failing closed.
|
|
23
|
+
|
|
24
|
+
Emits one file: ``globalTypeCatalog.ts``.
|
|
25
|
+
|
|
26
|
+
Usage: uv run generate_global_type_catalog.py <out_dir>
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from pydantic import TypeAdapter
|
|
35
|
+
|
|
36
|
+
from novaflow.types.data.registry import NOVAFLOW_TYPE_REGISTRY
|
|
37
|
+
|
|
38
|
+
_HEADER = (
|
|
39
|
+
"// AUTO-GENERATED — DO NOT EDIT.\n"
|
|
40
|
+
"// Source: novaflow.types.data.registry.NOVAFLOW_TYPE_REGISTRY\n"
|
|
41
|
+
"// Regenerate via: bash scripts/generate-types.sh\n"
|
|
42
|
+
"// (canopy's registry-derived catalog of selectable global types — #420.\n"
|
|
43
|
+
"// Drives the globals editor's type dropdown, per-type JSON schema, and\n"
|
|
44
|
+
"// default value from the SAME registry control-plane uses, with the\n"
|
|
45
|
+
"// canonical type names — so a created global stores a resolvable type.)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_INTERFACE = (
|
|
49
|
+
"export interface GlobalTypeCatalogEntry {\n"
|
|
50
|
+
" /** Canonical registry type name (e.g. \"str\", \"Pose\", \"Vector3d\"). */\n"
|
|
51
|
+
" name: string;\n"
|
|
52
|
+
" /** The type's JSON schema (ActionInputField dispatches its editor off this). */\n"
|
|
53
|
+
" jsonSchema: Record<string, unknown>;\n"
|
|
54
|
+
" /** Best-effort seed value for a fresh scalar global of this type. */\n"
|
|
55
|
+
" defaultValue: unknown;\n"
|
|
56
|
+
"}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
_HELPER = """\
|
|
60
|
+
const _BY_NAME: Record<string, GlobalTypeCatalogEntry> = Object.fromEntries(
|
|
61
|
+
GLOBAL_TYPE_CATALOG.map((e) => [e.name, e]),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Look up a catalog entry by its canonical registry name. Returns undefined
|
|
66
|
+
* for an unregistered name (the caller falls back to a null default / a
|
|
67
|
+
* pass-through schema).
|
|
68
|
+
*/
|
|
69
|
+
export function globalTypeEntry(
|
|
70
|
+
name: string,
|
|
71
|
+
): GlobalTypeCatalogEntry | undefined {
|
|
72
|
+
return _BY_NAME[name];
|
|
73
|
+
}"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_ref(schema: dict[str, Any], defs: dict[str, Any]) -> dict[str, Any]:
|
|
77
|
+
"""Resolve a single local ``$ref`` (``#/$defs/Name``) against ``defs``.
|
|
78
|
+
|
|
79
|
+
Only the local ``#/$defs/`` shape Pydantic emits is handled; anything else
|
|
80
|
+
(or a missing target) is returned unchanged so the caller degrades to the
|
|
81
|
+
empty/unresolved → ``null`` branch rather than raising."""
|
|
82
|
+
ref = schema.get("$ref")
|
|
83
|
+
if not isinstance(ref, str) or not ref.startswith("#/$defs/"):
|
|
84
|
+
return schema
|
|
85
|
+
return defs.get(ref[len("#/$defs/") :], {})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _default_for(schema: Any, defs: dict[str, Any]) -> Any:
|
|
89
|
+
"""Total (never raises) recursive JSON-Schema default.
|
|
90
|
+
|
|
91
|
+
* ``"string"`` → ``""``; ``"number"``/``"integer"`` → ``0``;
|
|
92
|
+
``"boolean"`` → ``False``; ``"array"`` → ``[]``.
|
|
93
|
+
* ``"object"`` → ``{prop: default(propSchema)}`` over ``properties`` (local
|
|
94
|
+
``$ref`` resolved against ``$defs``).
|
|
95
|
+
* ``anyOf``/``oneOf`` → default of the first member.
|
|
96
|
+
* ``null`` / ``Any`` (``True`` / empty ``{}``) / unresolved → ``None``.
|
|
97
|
+
|
|
98
|
+
Best-effort: the value editor refines it. Kept total so a future type with
|
|
99
|
+
an exotic schema can never break codegen."""
|
|
100
|
+
if not isinstance(schema, dict) or not schema:
|
|
101
|
+
# ``None`` schema, ``True`` (Any), or empty ``{}`` → no default.
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
schema = _resolve_ref(schema, defs)
|
|
105
|
+
if not isinstance(schema, dict) or not schema:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
for key in ("anyOf", "oneOf"):
|
|
109
|
+
members = schema.get(key)
|
|
110
|
+
if isinstance(members, list) and members:
|
|
111
|
+
return _default_for(members[0], defs)
|
|
112
|
+
|
|
113
|
+
t = schema.get("type")
|
|
114
|
+
if t == "string":
|
|
115
|
+
return ""
|
|
116
|
+
if t in ("number", "integer"):
|
|
117
|
+
return 0
|
|
118
|
+
if t == "boolean":
|
|
119
|
+
return False
|
|
120
|
+
if t == "array":
|
|
121
|
+
# Respect a fixed/min length so a constrained array (e.g. ``Vector3d`` /
|
|
122
|
+
# ``RotationVector`` — minItems=maxItems=3) gets a structurally VALID
|
|
123
|
+
# default: a min-length array of the item default (``[0, 0, 0]``), not
|
|
124
|
+
# an empty ``[]`` that violates the schema and breaks fixed-length
|
|
125
|
+
# editors (a 3-axis Pose/Vector editor that indexes [0..2]). A
|
|
126
|
+
# variable-length array (no ``minItems`` — e.g. ``Joints`` / ``list[T]``)
|
|
127
|
+
# legitimately stays ``[]``.
|
|
128
|
+
min_items = schema.get("minItems") or 0
|
|
129
|
+
items_schema = schema.get("items")
|
|
130
|
+
if min_items and isinstance(items_schema, dict):
|
|
131
|
+
return [_default_for(items_schema, defs) for _ in range(min_items)]
|
|
132
|
+
return []
|
|
133
|
+
if t == "null":
|
|
134
|
+
return None
|
|
135
|
+
if t == "object":
|
|
136
|
+
props = schema.get("properties")
|
|
137
|
+
if isinstance(props, dict):
|
|
138
|
+
return {k: _default_for(v, defs) for k, v in props.items()}
|
|
139
|
+
return {}
|
|
140
|
+
# No (or unrecognised) ``type`` and no union → unresolved.
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _entry(name: str, py_type: Any) -> dict[str, Any]:
|
|
145
|
+
"""The catalog entry (name + json schema + default) for one registry type."""
|
|
146
|
+
schema = TypeAdapter(py_type).json_schema()
|
|
147
|
+
defs = schema.get("$defs", {}) if isinstance(schema, dict) else {}
|
|
148
|
+
return {
|
|
149
|
+
"name": name,
|
|
150
|
+
"jsonSchema": schema,
|
|
151
|
+
"defaultValue": _default_for(schema, defs),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def emit() -> str:
|
|
156
|
+
"""The full globalTypeCatalog.ts content (deterministic, pre-formatted)."""
|
|
157
|
+
rows: list[str] = []
|
|
158
|
+
for name in sorted(NOVAFLOW_TYPE_REGISTRY):
|
|
159
|
+
entry = _entry(name, NOVAFLOW_TYPE_REGISTRY[name])
|
|
160
|
+
# json.dumps with sorted keys for a stable, deterministic serialization
|
|
161
|
+
# that doesn't depend on dict insertion order across toolchains.
|
|
162
|
+
rows.append(" " + json.dumps(entry, sort_keys=True) + ",")
|
|
163
|
+
catalog = "\n".join(rows)
|
|
164
|
+
const = (
|
|
165
|
+
"export const GLOBAL_TYPE_CATALOG: GlobalTypeCatalogEntry[] = [\n"
|
|
166
|
+
f"{catalog}\n"
|
|
167
|
+
"];"
|
|
168
|
+
)
|
|
169
|
+
return f"{_HEADER}\n\n{_INTERFACE}\n\n{const}\n\n{_HELPER}\n"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main() -> None:
|
|
173
|
+
if len(sys.argv) < 2:
|
|
174
|
+
print("usage: generate_global_type_catalog.py <out_dir>", file=sys.stderr)
|
|
175
|
+
raise SystemExit(2)
|
|
176
|
+
out = Path(sys.argv[1]) / "globalTypeCatalog.ts"
|
|
177
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
# Pin UTF-8: the header carries a non-ASCII em-dash; cp1252 (Windows
|
|
179
|
+
# default) would mangle it → drift between a Windows dev and Linux CI.
|
|
180
|
+
out.write_text(emit(), encoding="utf-8")
|
|
181
|
+
print(f"Wrote {out}", file=sys.stderr)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|