flo-lang 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 (43) hide show
  1. flo_lang-0.1.0/PKG-INFO +165 -0
  2. flo_lang-0.1.0/README.md +150 -0
  3. flo_lang-0.1.0/pyproject.toml +99 -0
  4. flo_lang-0.1.0/src/flo/__init__.py +12 -0
  5. flo_lang-0.1.0/src/flo/adapters/__init__.py +41 -0
  6. flo_lang-0.1.0/src/flo/adapters/composition.py +211 -0
  7. flo_lang-0.1.0/src/flo/adapters/models.py +20 -0
  8. flo_lang-0.1.0/src/flo/adapters/yaml_loader.py +26 -0
  9. flo_lang-0.1.0/src/flo/compiler/__init__.py +7 -0
  10. flo_lang-0.1.0/src/flo/compiler/analysis/__init__.py +20 -0
  11. flo_lang-0.1.0/src/flo/compiler/analysis/movement.py +451 -0
  12. flo_lang-0.1.0/src/flo/compiler/analysis/scc.py +121 -0
  13. flo_lang-0.1.0/src/flo/compiler/compile.py +251 -0
  14. flo_lang-0.1.0/src/flo/compiler/ir/__init__.py +19 -0
  15. flo_lang-0.1.0/src/flo/compiler/ir/enums.py +38 -0
  16. flo_lang-0.1.0/src/flo/compiler/ir/models.py +99 -0
  17. flo_lang-0.1.0/src/flo/compiler/ir/validate.py +525 -0
  18. flo_lang-0.1.0/src/flo/core/__init__.py +137 -0
  19. flo_lang-0.1.0/src/flo/core/cli.py +268 -0
  20. flo_lang-0.1.0/src/flo/core/cli_args.py +117 -0
  21. flo_lang-0.1.0/src/flo/export/__init__.py +40 -0
  22. flo_lang-0.1.0/src/flo/export/ingredients_export.py +7 -0
  23. flo_lang-0.1.0/src/flo/export/json_export.py +107 -0
  24. flo_lang-0.1.0/src/flo/export/materials_export.py +100 -0
  25. flo_lang-0.1.0/src/flo/export/movement_export.py +73 -0
  26. flo_lang-0.1.0/src/flo/export/options.py +48 -0
  27. flo_lang-0.1.0/src/flo/pipeline.py +214 -0
  28. flo_lang-0.1.0/src/flo/render/__init__.py +25 -0
  29. flo_lang-0.1.0/src/flo/render/_graphviz_dot_common.py +550 -0
  30. flo_lang-0.1.0/src/flo/render/_graphviz_dot_flow.py +11 -0
  31. flo_lang-0.1.0/src/flo/render/_graphviz_dot_flowchart.py +58 -0
  32. flo_lang-0.1.0/src/flo/render/_graphviz_dot_spaghetti.py +551 -0
  33. flo_lang-0.1.0/src/flo/render/_graphviz_dot_sppm.py +250 -0
  34. flo_lang-0.1.0/src/flo/render/_graphviz_dot_swimlane.py +180 -0
  35. flo_lang-0.1.0/src/flo/render/_sppm_themes.py +74 -0
  36. flo_lang-0.1.0/src/flo/render/graphviz_dot.py +12 -0
  37. flo_lang-0.1.0/src/flo/render/options.py +139 -0
  38. flo_lang-0.1.0/src/flo/services/__init__.py +51 -0
  39. flo_lang-0.1.0/src/flo/services/errors.py +106 -0
  40. flo_lang-0.1.0/src/flo/services/graphviz.py +60 -0
  41. flo_lang-0.1.0/src/flo/services/io.py +47 -0
  42. flo_lang-0.1.0/src/flo/services/logging.py +84 -0
  43. flo_lang-0.1.0/src/flo/services/telemetry.py +148 -0
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.3
2
+ Name: flo-lang
3
+ Version: 0.1.0
4
+ Summary: FLO reference implementation
5
+ Requires-Dist: structlog>=22.0
6
+ Requires-Dist: opentelemetry-api>=1.20.0
7
+ Requires-Dist: opentelemetry-sdk>=1.20.0
8
+ Requires-Dist: click>=8.1
9
+ Requires-Dist: rich>=13.0
10
+ Requires-Dist: pydantic>=2.0
11
+ Requires-Dist: pyyaml>=6.0
12
+ Requires-Dist: jsonschema>=4.26.0
13
+ Requires-Python: >=3.14
14
+ Description-Content-Type: text/markdown
15
+
16
+ # FLO
17
+
18
+ FLO is a declarative language for modeling organizational flow.
19
+
20
+ It allows you to define processes in a minimal, versioned format and
21
+ compile them into a canonical graph representation (FLO IR) for
22
+ visualization and analysis.
23
+
24
+ User documentation: `docs/User_Manual.md`
25
+
26
+ ------------------------------------------------------------------------
27
+
28
+ ## Example
29
+
30
+ ``` yaml
31
+ spec_version: "0.1"
32
+
33
+ process:
34
+ id: onboarding_v1
35
+ name: Client Onboarding
36
+ version: 1
37
+ owner:
38
+ id: ops_mgr
39
+ name: Ops Manager
40
+ business_units:
41
+ - id: sales
42
+ name: Sales
43
+ - id: ops
44
+ name: Operations
45
+
46
+ steps:
47
+ - id: start
48
+ kind: start
49
+ name: Start
50
+
51
+ - id: collect_docs
52
+ kind: task
53
+ name: Collect Documents
54
+ lane: sales
55
+
56
+ - id: verify
57
+ kind: task
58
+ name: Verify Documents
59
+ lane: ops
60
+
61
+ - id: approved
62
+ kind: decision
63
+ name: Approved?
64
+ outcomes:
65
+ yes: finish
66
+ no: collect_docs
67
+
68
+ - id: finish
69
+ kind: end
70
+ name: Complete
71
+ ```
72
+
73
+ ------------------------------------------------------------------------
74
+
75
+ ## What FLO Provides
76
+
77
+ - Deterministic compilation to FLO IR\
78
+ - Structural and semantic validation\
79
+ - Graph projections: flowchart, swimlane, spaghetti map, SPPM\
80
+ - DOT and JSON exports\
81
+ - Ingredients list and movement report exports\
82
+ - Stable foundation for analytics
83
+
84
+ ------------------------------------------------------------------------
85
+
86
+ ## What FLO Does Not Provide
87
+
88
+ - Workflow execution\
89
+ - Task scheduling\
90
+ - Orchestration\
91
+ - Simulation engines (v0.x)
92
+
93
+ ------------------------------------------------------------------------
94
+
95
+ ## Architecture
96
+
97
+ - `src/flo/` --- compiler, validators, renderers, and CLI\
98
+ - `schema/` --- JSON schemas for FLO IR and types\
99
+ - `examples/` --- canonical reference examples\
100
+ - `tests/` --- unit, integration, and conformance tests\
101
+ - `docs/` --- user manual and design documents
102
+
103
+ Downstream projects depend on FLO IR.
104
+
105
+ ------------------------------------------------------------------------
106
+
107
+ ## Source of Truth Hierarchy
108
+
109
+ - Structural contract SSOT: `schema/flo_ir.json`
110
+ - Semantic SSOT: `docs/design/IR.md`
111
+ - User-facing summary: `README.md`
112
+
113
+ Hierarchy policy and update workflow are defined in
114
+ `docs/design/SSOT_Hierarchy.md`.
115
+
116
+ ------------------------------------------------------------------------
117
+
118
+ ## Current Semantic Constraints (v0.1)
119
+
120
+ - Exactly one `start` node.
121
+ - At least one `end` node.
122
+ - All edge endpoints must resolve to declared node IDs.
123
+ - Every non-`start` node must have at least one predecessor.
124
+ - Every non-`end` node must have at least one successor.
125
+ - Every node must be reachable from `start`.
126
+ - Every node must be able to reach at least one `end` node.
127
+ - `decision` nodes must have at least two outgoing transitions.
128
+
129
+ ------------------------------------------------------------------------
130
+
131
+ ## Versioning
132
+
133
+ FLO follows semantic versioning at the spec level.
134
+
135
+ - v0.x: rapid iteration\
136
+ - v1.0: language stability
137
+
138
+ ------------------------------------------------------------------------
139
+
140
+ ## Philosophy
141
+
142
+ FLO treats processes as first-class artifacts:
143
+
144
+ - Explicit\
145
+ - Versioned\
146
+ - Portable\
147
+ - Validatable
148
+
149
+ It is a small language by design.
150
+
151
+ ------------------------------------------------------------------------
152
+
153
+ ## Schema Contract & Migration
154
+
155
+ As of v0.1 the compiler emits a schema-shaped canonical IR and the
156
+ runtime enforces that contract. The compiler must set `IR.schema_aligned`
157
+ to `True` and `IR.to_dict()` will produce the top-level mapping with
158
+ `process`, `nodes`, and `edges` fields required by the authoritative
159
+ JSON Schema in `schema/flo_ir.json`.
160
+
161
+ If you are upgrading from an earlier development version that relied on
162
+ an internal IR translator, update any custom compiler integrations to
163
+ emit the schema-shaped IR directly. The translator was intentionally
164
+ removed; CI now validates example outputs using the same schema and
165
+ will fail when the contract is violated.
@@ -0,0 +1,150 @@
1
+ # FLO
2
+
3
+ FLO is a declarative language for modeling organizational flow.
4
+
5
+ It allows you to define processes in a minimal, versioned format and
6
+ compile them into a canonical graph representation (FLO IR) for
7
+ visualization and analysis.
8
+
9
+ User documentation: `docs/User_Manual.md`
10
+
11
+ ------------------------------------------------------------------------
12
+
13
+ ## Example
14
+
15
+ ``` yaml
16
+ spec_version: "0.1"
17
+
18
+ process:
19
+ id: onboarding_v1
20
+ name: Client Onboarding
21
+ version: 1
22
+ owner:
23
+ id: ops_mgr
24
+ name: Ops Manager
25
+ business_units:
26
+ - id: sales
27
+ name: Sales
28
+ - id: ops
29
+ name: Operations
30
+
31
+ steps:
32
+ - id: start
33
+ kind: start
34
+ name: Start
35
+
36
+ - id: collect_docs
37
+ kind: task
38
+ name: Collect Documents
39
+ lane: sales
40
+
41
+ - id: verify
42
+ kind: task
43
+ name: Verify Documents
44
+ lane: ops
45
+
46
+ - id: approved
47
+ kind: decision
48
+ name: Approved?
49
+ outcomes:
50
+ yes: finish
51
+ no: collect_docs
52
+
53
+ - id: finish
54
+ kind: end
55
+ name: Complete
56
+ ```
57
+
58
+ ------------------------------------------------------------------------
59
+
60
+ ## What FLO Provides
61
+
62
+ - Deterministic compilation to FLO IR\
63
+ - Structural and semantic validation\
64
+ - Graph projections: flowchart, swimlane, spaghetti map, SPPM\
65
+ - DOT and JSON exports\
66
+ - Ingredients list and movement report exports\
67
+ - Stable foundation for analytics
68
+
69
+ ------------------------------------------------------------------------
70
+
71
+ ## What FLO Does Not Provide
72
+
73
+ - Workflow execution\
74
+ - Task scheduling\
75
+ - Orchestration\
76
+ - Simulation engines (v0.x)
77
+
78
+ ------------------------------------------------------------------------
79
+
80
+ ## Architecture
81
+
82
+ - `src/flo/` --- compiler, validators, renderers, and CLI\
83
+ - `schema/` --- JSON schemas for FLO IR and types\
84
+ - `examples/` --- canonical reference examples\
85
+ - `tests/` --- unit, integration, and conformance tests\
86
+ - `docs/` --- user manual and design documents
87
+
88
+ Downstream projects depend on FLO IR.
89
+
90
+ ------------------------------------------------------------------------
91
+
92
+ ## Source of Truth Hierarchy
93
+
94
+ - Structural contract SSOT: `schema/flo_ir.json`
95
+ - Semantic SSOT: `docs/design/IR.md`
96
+ - User-facing summary: `README.md`
97
+
98
+ Hierarchy policy and update workflow are defined in
99
+ `docs/design/SSOT_Hierarchy.md`.
100
+
101
+ ------------------------------------------------------------------------
102
+
103
+ ## Current Semantic Constraints (v0.1)
104
+
105
+ - Exactly one `start` node.
106
+ - At least one `end` node.
107
+ - All edge endpoints must resolve to declared node IDs.
108
+ - Every non-`start` node must have at least one predecessor.
109
+ - Every non-`end` node must have at least one successor.
110
+ - Every node must be reachable from `start`.
111
+ - Every node must be able to reach at least one `end` node.
112
+ - `decision` nodes must have at least two outgoing transitions.
113
+
114
+ ------------------------------------------------------------------------
115
+
116
+ ## Versioning
117
+
118
+ FLO follows semantic versioning at the spec level.
119
+
120
+ - v0.x: rapid iteration\
121
+ - v1.0: language stability
122
+
123
+ ------------------------------------------------------------------------
124
+
125
+ ## Philosophy
126
+
127
+ FLO treats processes as first-class artifacts:
128
+
129
+ - Explicit\
130
+ - Versioned\
131
+ - Portable\
132
+ - Validatable
133
+
134
+ It is a small language by design.
135
+
136
+ ------------------------------------------------------------------------
137
+
138
+ ## Schema Contract & Migration
139
+
140
+ As of v0.1 the compiler emits a schema-shaped canonical IR and the
141
+ runtime enforces that contract. The compiler must set `IR.schema_aligned`
142
+ to `True` and `IR.to_dict()` will produce the top-level mapping with
143
+ `process`, `nodes`, and `edges` fields required by the authoritative
144
+ JSON Schema in `schema/flo_ir.json`.
145
+
146
+ If you are upgrading from an earlier development version that relied on
147
+ an internal IR translator, update any custom compiler integrations to
148
+ emit the schema-shaped IR directly. The translator was intentionally
149
+ removed; CI now validates example outputs using the same schema and
150
+ will fail when the contract is violated.
@@ -0,0 +1,99 @@
1
+ [project]
2
+ name = "flo-lang"
3
+ version = "0.1.0"
4
+ description = "FLO reference implementation"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ # Runtime dependencies
8
+ dependencies = [
9
+ "structlog>=22.0",
10
+ "opentelemetry-api>=1.20.0",
11
+ "opentelemetry-sdk>=1.20.0",
12
+ "click>=8.1",
13
+ "rich>=13.0",
14
+ "pydantic>=2.0",
15
+ "PyYAML>=6.0",
16
+ "jsonschema>=4.26.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+
21
+ [build-system]
22
+ requires = ["uv_build>=0.10.4,<0.11.0"]
23
+ build-backend = "uv_build"
24
+
25
+ [tool.uv.build-backend]
26
+ module-name = "flo"
27
+ module-root = "src"
28
+
29
+ [project.scripts]
30
+ flo = "flo.core.cli:main"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pre-commit>=4.5.1",
35
+ "pydocstyle>=6.3.0",
36
+ "pyright>=1.1.408",
37
+ "pytest>=9.0.2",
38
+ "ruff>=0.15.2",
39
+ "vulture>=2.14",
40
+ "radon>=5.1.0",
41
+ "pytest",
42
+ "pytest-cov>=4.0",
43
+ "import-linter>=2.10",
44
+ ]
45
+
46
+ [tool.importlinter]
47
+ # Import-linter rules for enforcing layer boundaries in the FLO project.
48
+ # Note: import-linter expects a specific schema; this section encodes the
49
+ # intended rules in pyproject.toml so CI and tooling can be wired to the
50
+ # enforcement tool. Adjust keys below to match the import-linter version used.
51
+
52
+ # The top-level package that import-linter should treat as the project's
53
+ # root importable package. This must match the importable package name
54
+ # (e.g. the `flo` package under `src/flo`).
55
+ root_package = "flo"
56
+
57
+ [tool.importlinter.layers]
58
+ # Logical layer -> package prefix
59
+ # Keep layer names small and map to specific package prefixes.
60
+ core = "flo.core"
61
+ adapters = "flo.adapters"
62
+ compiler = "flo.compiler"
63
+ render = "flo.render"
64
+ services = "flo.services"
65
+
66
+ [[tool.importlinter.rules]]
67
+ name = "no_core_deps"
68
+ description = "Core/CLI must not be imported by domain layers."
69
+ modules = ["flo.core"]
70
+ forbidden = ["flo.compiler", "flo.adapters", "flo.render", "flo.services"]
71
+
72
+ [[tool.importlinter.rules]]
73
+ name = "no_compiler_to_core_logging"
74
+ description = "Compiler and adapters must not import core or logging setup."
75
+ modules = ["flo.compiler", "flo.adapters"]
76
+ forbidden = ["flo.core", "flo.services.logging"]
77
+
78
+ [[tool.importlinter.rules]]
79
+ name = "compiler_ir_independent"
80
+ description = "Compiler IR package must be independent of higher-level concerns."
81
+ modules = ["flo.compiler.ir"]
82
+ forbidden = ["flo.core", "flo.adapters", "flo.render", "flo.services"]
83
+
84
+ [[tool.importlinter.rules]]
85
+ name = "render_optional"
86
+ description = "Renderers should only depend on compiler layers and services, not on core internals."
87
+ modules = ["flo.render"]
88
+ forbidden = ["flo.core", "flo.services.logging"]
89
+
90
+ [[tool.importlinter.rules]]
91
+ name = "no_domain_import_core"
92
+ description = "Domain layers must not import core entrypoint modules."
93
+ modules = ["flo.adapters", "flo.compiler", "flo.render", "flo.services"]
94
+ forbidden = ["flo.core"]
95
+
96
+ [tool.coverage.report]
97
+ omit = [
98
+ "src/flo/main.py",
99
+ ]
@@ -0,0 +1,12 @@
1
+ """FLO package public surface.
2
+
3
+ Keep the package import lightweight: importing the top-level package should
4
+ not import CLI-related heavy dependencies (like `click`). Use the console
5
+ entrypoint (`flo`) or import from `flo.core.cli` explicitly where needed,
6
+ instead of exposing CLI modules at package import time.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__: list[str] = []
12
+
@@ -0,0 +1,41 @@
1
+ """Adapters package: YAML/other input adapters for FLO."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict
6
+ import yaml
7
+
8
+
9
+ from .yaml_loader import load_adapter_from_yaml
10
+ from .composition import resolve_includes
11
+
12
+
13
+ def parse_adapter(content: str, source_path: str | None = None) -> Dict[str, Any]:
14
+ """Parse adapter content and return a validated mapping.
15
+
16
+ This dispatcher currently supports YAML input via
17
+ `load_adapter_from_yaml`. The loader returns an `AdapterModel`
18
+ which we convert to a plain dict (`model_dump`) so downstream
19
+ compiler code can continue to operate on mappings.
20
+ """
21
+ try:
22
+ model = load_adapter_from_yaml(content)
23
+ except ValueError:
24
+ # If the content is YAML mapping-shaped FLO, return it directly.
25
+ # Otherwise preserve the previous permissive text fallback.
26
+ parsed = yaml.safe_load(content)
27
+ if isinstance(parsed, dict):
28
+ return resolve_includes(parsed, source_path=source_path)
29
+ return {"name": "parsed", "content": content}
30
+
31
+ # `model` supports `model_dump()` for Pydantic compatibility
32
+ try:
33
+ return model.model_dump()
34
+ except Exception:
35
+ # Fallback: if the model does not implement `model_dump`, try
36
+ # converting via `dict()` for compatibility with lightweight
37
+ # fallback models.
38
+ return dict(model)
39
+
40
+
41
+ __all__ = ["load_adapter_from_yaml", "parse_adapter"]
@@ -0,0 +1,211 @@
1
+ """Composition helpers for include-based FLO source documents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ _RESOURCE_KEYS = ("materials", "equipment", "locations", "workers")
11
+ _LIST_KEYS = ("steps", "transitions", "edges", "lanes")
12
+
13
+
14
+ def resolve_includes(document: dict[str, Any], source_path: str | None = None) -> dict[str, Any]:
15
+ """Resolve include directives and return a composed mapping.
16
+
17
+ Include syntax:
18
+
19
+ includes:
20
+ - relative/or/absolute/path.yaml
21
+ """
22
+ root_path = Path(source_path).resolve() if source_path else None
23
+ return _compose_document(document=document, current_path=root_path, include_stack=[])
24
+
25
+
26
+ def _compose_document(
27
+ document: dict[str, Any],
28
+ current_path: Path | None,
29
+ include_stack: list[Path],
30
+ ) -> dict[str, Any]:
31
+ include_paths = _normalize_include_entries(document)
32
+ composed: dict[str, Any] = {}
33
+
34
+ for include_ref in include_paths:
35
+ include_path = _resolve_include_path(include_ref=include_ref, current_path=current_path)
36
+ include_doc = _load_include_mapping(include_path=include_path, include_stack=include_stack)
37
+ nested = _compose_document(
38
+ document=include_doc,
39
+ current_path=include_path,
40
+ include_stack=[*include_stack, include_path],
41
+ )
42
+ composed = _merge_documents(base=composed, incoming=nested)
43
+
44
+ local = dict(document)
45
+ local.pop("include", None)
46
+ local.pop("includes", None)
47
+ composed = _merge_documents(base=composed, incoming=local)
48
+
49
+ _validate_unique_ids(composed)
50
+ return composed
51
+
52
+
53
+ def _normalize_include_entries(document: dict[str, Any]) -> list[str]:
54
+ include_value = document.get("includes")
55
+ if include_value is None:
56
+ include_value = document.get("include")
57
+
58
+ if include_value is None:
59
+ return []
60
+
61
+ if isinstance(include_value, str):
62
+ text = include_value.strip()
63
+ return [text] if text else []
64
+
65
+ if not isinstance(include_value, list):
66
+ raise ValueError("include/includes must be a string or list of strings")
67
+
68
+ out: list[str] = []
69
+ for idx, entry in enumerate(include_value):
70
+ if not isinstance(entry, str) or not entry.strip():
71
+ raise ValueError(f"includes[{idx}] must be a non-empty string path")
72
+ out.append(entry.strip())
73
+ return out
74
+
75
+
76
+ def _resolve_include_path(include_ref: str, current_path: Path | None) -> Path:
77
+ raw = Path(include_ref)
78
+ if raw.is_absolute():
79
+ return raw.resolve()
80
+
81
+ base_dir = current_path.parent if current_path is not None else Path.cwd()
82
+ return (base_dir / raw).resolve()
83
+
84
+
85
+ def _load_include_mapping(include_path: Path, include_stack: list[Path]) -> dict[str, Any]:
86
+ if include_path in include_stack:
87
+ chain = " -> ".join(str(path) for path in [*include_stack, include_path])
88
+ raise ValueError(f"include cycle detected: {chain}")
89
+
90
+ if not include_path.exists():
91
+ raise ValueError(f"include file not found: {include_path}")
92
+
93
+ try:
94
+ content = include_path.read_text(encoding="utf-8")
95
+ except OSError as exc:
96
+ raise ValueError(f"unable to read include file '{include_path}': {exc}") from exc
97
+
98
+ parsed = yaml.safe_load(content)
99
+ if not isinstance(parsed, dict):
100
+ raise ValueError(f"include file '{include_path}' must contain a YAML mapping")
101
+
102
+ return parsed
103
+
104
+
105
+ def _merge_documents(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
106
+ merged = dict(base)
107
+
108
+ for key, value in incoming.items():
109
+ if key in _LIST_KEYS:
110
+ merged[key] = _merge_list_values(key=key, base_value=merged.get(key), incoming_value=value)
111
+ continue
112
+
113
+ if key in _RESOURCE_KEYS:
114
+ merged[key] = _merge_resource_values(key=key, base_value=merged.get(key), incoming_value=value)
115
+ continue
116
+
117
+ if key == "process":
118
+ merged[key] = _merge_process(base_value=merged.get(key), incoming_value=value)
119
+ continue
120
+
121
+ if key == "spec_version":
122
+ merged.setdefault(key, value)
123
+ continue
124
+
125
+ merged[key] = value
126
+
127
+ return merged
128
+
129
+
130
+ def _merge_list_values(key: str, base_value: Any, incoming_value: Any) -> list[Any]:
131
+ if incoming_value is None:
132
+ return list(base_value) if isinstance(base_value, list) else []
133
+ if not isinstance(incoming_value, list):
134
+ raise ValueError(f"{key} must be a list")
135
+
136
+ out: list[Any] = []
137
+ if isinstance(base_value, list):
138
+ out.extend(base_value)
139
+ elif base_value is not None:
140
+ raise ValueError(f"{key} must be a list")
141
+
142
+ out.extend(incoming_value)
143
+ return out
144
+
145
+
146
+ def _merge_resource_values(key: str, base_value: Any, incoming_value: Any) -> Any:
147
+ if base_value is None:
148
+ return incoming_value
149
+ if incoming_value is None:
150
+ return base_value
151
+
152
+ if isinstance(base_value, list) and isinstance(incoming_value, list):
153
+ return [*base_value, *incoming_value]
154
+
155
+ if isinstance(base_value, dict) and isinstance(incoming_value, dict):
156
+ merged = dict(base_value)
157
+ for child_key, child_value in incoming_value.items():
158
+ if child_key == "name":
159
+ merged.setdefault("name", child_value)
160
+ continue
161
+ if child_key in merged:
162
+ merged[child_key] = _merge_resource_values(
163
+ key=f"{key}.{child_key}",
164
+ base_value=merged[child_key],
165
+ incoming_value=child_value,
166
+ )
167
+ else:
168
+ merged[child_key] = child_value
169
+ return merged
170
+
171
+ raise ValueError(f"{key} include merge type mismatch: expected list/list or dict/dict")
172
+
173
+
174
+ def _merge_process(base_value: Any, incoming_value: Any) -> Any:
175
+ if base_value is None:
176
+ return incoming_value
177
+ if incoming_value is None:
178
+ return base_value
179
+ if not isinstance(base_value, dict) or not isinstance(incoming_value, dict):
180
+ raise ValueError("process must be an object when present")
181
+
182
+ merged = dict(base_value)
183
+ for key, value in incoming_value.items():
184
+ if key == "metadata" and isinstance(merged.get("metadata"), dict) and isinstance(value, dict):
185
+ merged["metadata"] = {**merged["metadata"], **value}
186
+ continue
187
+ merged[key] = value
188
+ return merged
189
+
190
+
191
+ def _validate_unique_ids(document: dict[str, Any]) -> None:
192
+ _ensure_unique_id_list(document=document, key="steps")
193
+ _ensure_unique_id_list(document=document, key="lanes")
194
+
195
+
196
+ def _ensure_unique_id_list(document: dict[str, Any], key: str) -> None:
197
+ value = document.get(key)
198
+ if not isinstance(value, list):
199
+ return
200
+
201
+ seen: set[str] = set()
202
+ for idx, item in enumerate(value):
203
+ if not isinstance(item, dict):
204
+ continue
205
+ item_id = item.get("id")
206
+ if item_id is None:
207
+ continue
208
+ item_id_text = str(item_id)
209
+ if item_id_text in seen:
210
+ raise ValueError(f"duplicate {key[:-1]} id '{item_id_text}' detected at {key}[{idx}]")
211
+ seen.add(item_id_text)
@@ -0,0 +1,20 @@
1
+ """Adapter models for FLO inputs.
2
+
3
+ Prefer Pydantic v2 models for runtime validation and parsing. If
4
+ Pydantic isn't available the module will still import but some helper
5
+ callers may choose a dict-based fallback.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pydantic import BaseModel
11
+
12
+
13
+ class AdapterModel(BaseModel):
14
+ """Pydantic-backed AdapterModel used for adapter inputs."""
15
+
16
+ name: str
17
+ content: str
18
+
19
+ # Pydantic v2 provides `model_validate` and `model_dump` so callers
20
+ # can use the same API as before without needing a runtime fallback.