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.
Files changed (142) hide show
  1. novaflow_types-0.1.0/.gitignore +32 -0
  2. novaflow_types-0.1.0/.vscode/settings.json +5 -0
  3. novaflow_types-0.1.0/PKG-INFO +37 -0
  4. novaflow_types-0.1.0/README.md +28 -0
  5. novaflow_types-0.1.0/generate_camera_registry_schema.py +46 -0
  6. novaflow_types-0.1.0/generate_comm_kv_schema.py +76 -0
  7. novaflow_types-0.1.0/generate_comm_registries.py +139 -0
  8. novaflow_types-0.1.0/generate_global_type_catalog.py +185 -0
  9. novaflow_types-0.1.0/generate_node_constants.py +312 -0
  10. novaflow_types-0.1.0/generate_structural_id_golden.py +330 -0
  11. novaflow_types-0.1.0/generate_tree_schema.py +76 -0
  12. novaflow_types-0.1.0/generate_type_structural_ids.py +108 -0
  13. novaflow_types-0.1.0/postprocess_core_types.py +56 -0
  14. novaflow_types-0.1.0/postprocess_data_types.py +138 -0
  15. novaflow_types-0.1.0/postprocess_tree_types.py +210 -0
  16. novaflow_types-0.1.0/pyproject.toml +40 -0
  17. novaflow_types-0.1.0/src/novaflow/types/__init__.py +9 -0
  18. novaflow_types-0.1.0/src/novaflow/types/comm/__init__.py +0 -0
  19. novaflow_types-0.1.0/src/novaflow/types/comm/camera_registry.py +72 -0
  20. novaflow_types-0.1.0/src/novaflow/types/comm/cloudevent.py +167 -0
  21. novaflow_types-0.1.0/src/novaflow/types/comm/execution_state.py +22 -0
  22. novaflow_types-0.1.0/src/novaflow/types/comm/heartbeat.py +11 -0
  23. novaflow_types-0.1.0/src/novaflow/types/comm/kv_keys.py +185 -0
  24. novaflow_types-0.1.0/src/novaflow/types/comm/programming.py +471 -0
  25. novaflow_types-0.1.0/src/novaflow/types/comm/response_payload.py +87 -0
  26. novaflow_types-0.1.0/src/novaflow/types/comm/run_hooks.py +60 -0
  27. novaflow_types-0.1.0/src/novaflow/types/comm/status.py +19 -0
  28. novaflow_types-0.1.0/src/novaflow/types/comm/subjects.py +75 -0
  29. novaflow_types-0.1.0/src/novaflow/types/core/__init__.py +17 -0
  30. novaflow_types-0.1.0/src/novaflow/types/core/action.py +1083 -0
  31. novaflow_types-0.1.0/src/novaflow/types/core/base.py +24 -0
  32. novaflow_types-0.1.0/src/novaflow/types/core/blackboard.py +90 -0
  33. novaflow_types-0.1.0/src/novaflow/types/core/service.py +81 -0
  34. novaflow_types-0.1.0/src/novaflow/types/core/shape_diff.py +72 -0
  35. novaflow_types-0.1.0/src/novaflow/types/core/structural_id.py +439 -0
  36. novaflow_types-0.1.0/src/novaflow/types/data/__init__.py +42 -0
  37. novaflow_types-0.1.0/src/novaflow/types/data/depth_image.py +66 -0
  38. novaflow_types-0.1.0/src/novaflow/types/data/dof_check.py +250 -0
  39. novaflow_types-0.1.0/src/novaflow/types/data/image.py +35 -0
  40. novaflow_types-0.1.0/src/novaflow/types/data/joints.py +23 -0
  41. novaflow_types-0.1.0/src/novaflow/types/data/json_data.py +32 -0
  42. novaflow_types-0.1.0/src/novaflow/types/data/mobile.py +95 -0
  43. novaflow_types-0.1.0/src/novaflow/types/data/motion_item.py +127 -0
  44. novaflow_types-0.1.0/src/novaflow/types/data/motion_routine.py +607 -0
  45. novaflow_types-0.1.0/src/novaflow/types/data/motion_target.py +51 -0
  46. novaflow_types-0.1.0/src/novaflow/types/data/nova_openapi/26.3.0/public.openapi.json +20077 -0
  47. novaflow_types-0.1.0/src/novaflow/types/data/nova_spec.py +203 -0
  48. novaflow_types-0.1.0/src/novaflow/types/data/pointcloud.py +38 -0
  49. novaflow_types-0.1.0/src/novaflow/types/data/pose.py +27 -0
  50. novaflow_types-0.1.0/src/novaflow/types/data/reference.py +35 -0
  51. novaflow_types-0.1.0/src/novaflow/types/data/registry.py +286 -0
  52. novaflow_types-0.1.0/src/novaflow/types/data/vector3d.py +12 -0
  53. novaflow_types-0.1.0/src/novaflow/types/nova_client.py +43 -0
  54. novaflow_types-0.1.0/src/novaflow/types/tree/__init__.py +89 -0
  55. novaflow_types-0.1.0/src/novaflow/types/tree/bounds.py +100 -0
  56. novaflow_types-0.1.0/src/novaflow/types/tree/io_action_naming.py +39 -0
  57. novaflow_types-0.1.0/src/novaflow/types/tree/migrate_v2_to_v3.py +1299 -0
  58. novaflow_types-0.1.0/src/novaflow/types/tree/naming.py +44 -0
  59. novaflow_types-0.1.0/src/novaflow/types/tree/nodes.py +1074 -0
  60. novaflow_types-0.1.0/src/novaflow/types/tree/references.py +212 -0
  61. novaflow_types-0.1.0/src/novaflow/types/tree/tree.py +320 -0
  62. novaflow_types-0.1.0/tests/__init__.py +0 -0
  63. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/globals_with_repeated.v2.json +15 -0
  64. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/globals_with_repeated.v3.json +13 -0
  65. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/folge11_globals.975cf7a5..804d24fb.json +1919 -0
  66. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase1_globals.804d24fb.json +67 -0
  67. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase1_globals.975cf7a5.UNRECOGNIZED.json +34 -0
  68. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase2b_globals.json +71 -0
  69. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase3_globals.v1.json +75 -0
  70. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase3_globals.v2.json +71 -0
  71. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase4_globals.v1.json +75 -0
  72. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/historical_corpus/phase4_globals.v2.json +71 -0
  73. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/io_service_setio_tree.v2.json +90 -0
  74. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/io_service_setio_tree.v3.json +106 -0
  75. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/move_with_io_gates.v2.json +24 -0
  76. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/move_with_io_gates.v3.json +30 -0
  77. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_boolean_controller.v2.json +9 -0
  78. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_boolean_controller.v3.json +9 -0
  79. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_float_busio.v2.json +8 -0
  80. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_float_busio.v3.json +9 -0
  81. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_integer_controller.v2.json +9 -0
  82. novaflow_types-0.1.0/tests/data/migrate_v2_to_v3/set_io_integer_controller.v3.json +9 -0
  83. novaflow_types-0.1.0/tests/served_type_id_golden.json +196 -0
  84. novaflow_types-0.1.0/tests/test_R2_01_tree_bounds.py +211 -0
  85. novaflow_types-0.1.0/tests/test_R3_03_safety_floats.py +150 -0
  86. novaflow_types-0.1.0/tests/test_any_type_support.py +93 -0
  87. novaflow_types-0.1.0/tests/test_behavior_tree_model.py +1373 -0
  88. novaflow_types-0.1.0/tests/test_boundary_extra_ignore.py +67 -0
  89. novaflow_types-0.1.0/tests/test_bump_tree_version.py +242 -0
  90. novaflow_types-0.1.0/tests/test_camera_registry.py +114 -0
  91. novaflow_types-0.1.0/tests/test_check_tree_version_stragglers.py +233 -0
  92. novaflow_types-0.1.0/tests/test_comm_kv_keys.py +94 -0
  93. novaflow_types-0.1.0/tests/test_comm_programming.py +203 -0
  94. novaflow_types-0.1.0/tests/test_comm_subjects.py +57 -0
  95. novaflow_types-0.1.0/tests/test_condition_node_rename.py +51 -0
  96. novaflow_types-0.1.0/tests/test_controller_aggregate.py +146 -0
  97. novaflow_types-0.1.0/tests/test_custom_composite_types.py +218 -0
  98. novaflow_types-0.1.0/tests/test_dof_check.py +334 -0
  99. novaflow_types-0.1.0/tests/test_enum_support.py +237 -0
  100. novaflow_types-0.1.0/tests/test_generate_comm_registries.py +65 -0
  101. novaflow_types-0.1.0/tests/test_generate_global_type_catalog.py +88 -0
  102. novaflow_types-0.1.0/tests/test_generate_type_structural_ids.py +58 -0
  103. novaflow_types-0.1.0/tests/test_io_action_naming.py +85 -0
  104. novaflow_types-0.1.0/tests/test_io_reference.py +166 -0
  105. novaflow_types-0.1.0/tests/test_iodescription_shadow_removed.py +30 -0
  106. novaflow_types-0.1.0/tests/test_jointtrajectory_shadow_removed.py +33 -0
  107. novaflow_types-0.1.0/tests/test_migrate_v2_to_v3.py +1341 -0
  108. novaflow_types-0.1.0/tests/test_migrate_v2_to_v3_historical_corpus.py +83 -0
  109. novaflow_types-0.1.0/tests/test_mobile_refs.py +69 -0
  110. novaflow_types-0.1.0/tests/test_motion_routine_final_pose.py +82 -0
  111. novaflow_types-0.1.0/tests/test_motion_routine_io_condition.py +104 -0
  112. novaflow_types-0.1.0/tests/test_motion_routine_start_joint_position.py +147 -0
  113. novaflow_types-0.1.0/tests/test_motion_routine_terminal.py +59 -0
  114. novaflow_types-0.1.0/tests/test_nesting_matrix.py +208 -0
  115. novaflow_types-0.1.0/tests/test_no_metadata_sidecar_anywhere.py +102 -0
  116. novaflow_types-0.1.0/tests/test_no_nova_type_tag.py +81 -0
  117. novaflow_types-0.1.0/tests/test_no_trajectory_globals_metadata.py +99 -0
  118. novaflow_types-0.1.0/tests/test_node_kind_contract.py +139 -0
  119. novaflow_types-0.1.0/tests/test_node_kind_generator.py +74 -0
  120. novaflow_types-0.1.0/tests/test_node_level_fields.py +125 -0
  121. novaflow_types-0.1.0/tests/test_node_roles.py +137 -0
  122. novaflow_types-0.1.0/tests/test_nova_client.py +37 -0
  123. novaflow_types-0.1.0/tests/test_nova_spec_diff.py +188 -0
  124. novaflow_types-0.1.0/tests/test_nova_structural_parity.py +36 -0
  125. novaflow_types-0.1.0/tests/test_nova_type_parity.py +169 -0
  126. novaflow_types-0.1.0/tests/test_open_generic_types.py +265 -0
  127. novaflow_types-0.1.0/tests/test_postprocess_data_types.py +123 -0
  128. novaflow_types-0.1.0/tests/test_pydantic_json_schema_overrides.py +44 -0
  129. novaflow_types-0.1.0/tests/test_served_type_id_golden.py +105 -0
  130. novaflow_types-0.1.0/tests/test_shape_diff.py +73 -0
  131. novaflow_types-0.1.0/tests/test_shipped_trees_v3_conformance.py +61 -0
  132. novaflow_types-0.1.0/tests/test_shipped_type_id.py +183 -0
  133. novaflow_types-0.1.0/tests/test_structural_id.py +544 -0
  134. novaflow_types-0.1.0/tests/test_structural_id_conformance.py +58 -0
  135. novaflow_types-0.1.0/tests/test_structural_id_spec_invariant.py +233 -0
  136. novaflow_types-0.1.0/tests/test_structural_nesting.py +107 -0
  137. novaflow_types-0.1.0/tests/test_type_id_registry.py +139 -0
  138. novaflow_types-0.1.0/tests/test_type_id_stability.py +191 -0
  139. novaflow_types-0.1.0/tests/test_vendored_spec.py +31 -0
  140. novaflow_types-0.1.0/tools/nova_spec_diff.py +291 -0
  141. novaflow_types-0.1.0/tools/vendored_spec.py +64 -0
  142. 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,5 @@
1
+ {
2
+ "editor.defaultFormatter": "charliermarsh.ruff",
3
+ "editor.formatOnPaste": true,
4
+ "editor.formatOnSave": true
5
+ }
@@ -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()