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.
- flo_lang-0.1.0/PKG-INFO +165 -0
- flo_lang-0.1.0/README.md +150 -0
- flo_lang-0.1.0/pyproject.toml +99 -0
- flo_lang-0.1.0/src/flo/__init__.py +12 -0
- flo_lang-0.1.0/src/flo/adapters/__init__.py +41 -0
- flo_lang-0.1.0/src/flo/adapters/composition.py +211 -0
- flo_lang-0.1.0/src/flo/adapters/models.py +20 -0
- flo_lang-0.1.0/src/flo/adapters/yaml_loader.py +26 -0
- flo_lang-0.1.0/src/flo/compiler/__init__.py +7 -0
- flo_lang-0.1.0/src/flo/compiler/analysis/__init__.py +20 -0
- flo_lang-0.1.0/src/flo/compiler/analysis/movement.py +451 -0
- flo_lang-0.1.0/src/flo/compiler/analysis/scc.py +121 -0
- flo_lang-0.1.0/src/flo/compiler/compile.py +251 -0
- flo_lang-0.1.0/src/flo/compiler/ir/__init__.py +19 -0
- flo_lang-0.1.0/src/flo/compiler/ir/enums.py +38 -0
- flo_lang-0.1.0/src/flo/compiler/ir/models.py +99 -0
- flo_lang-0.1.0/src/flo/compiler/ir/validate.py +525 -0
- flo_lang-0.1.0/src/flo/core/__init__.py +137 -0
- flo_lang-0.1.0/src/flo/core/cli.py +268 -0
- flo_lang-0.1.0/src/flo/core/cli_args.py +117 -0
- flo_lang-0.1.0/src/flo/export/__init__.py +40 -0
- flo_lang-0.1.0/src/flo/export/ingredients_export.py +7 -0
- flo_lang-0.1.0/src/flo/export/json_export.py +107 -0
- flo_lang-0.1.0/src/flo/export/materials_export.py +100 -0
- flo_lang-0.1.0/src/flo/export/movement_export.py +73 -0
- flo_lang-0.1.0/src/flo/export/options.py +48 -0
- flo_lang-0.1.0/src/flo/pipeline.py +214 -0
- flo_lang-0.1.0/src/flo/render/__init__.py +25 -0
- flo_lang-0.1.0/src/flo/render/_graphviz_dot_common.py +550 -0
- flo_lang-0.1.0/src/flo/render/_graphviz_dot_flow.py +11 -0
- flo_lang-0.1.0/src/flo/render/_graphviz_dot_flowchart.py +58 -0
- flo_lang-0.1.0/src/flo/render/_graphviz_dot_spaghetti.py +551 -0
- flo_lang-0.1.0/src/flo/render/_graphviz_dot_sppm.py +250 -0
- flo_lang-0.1.0/src/flo/render/_graphviz_dot_swimlane.py +180 -0
- flo_lang-0.1.0/src/flo/render/_sppm_themes.py +74 -0
- flo_lang-0.1.0/src/flo/render/graphviz_dot.py +12 -0
- flo_lang-0.1.0/src/flo/render/options.py +139 -0
- flo_lang-0.1.0/src/flo/services/__init__.py +51 -0
- flo_lang-0.1.0/src/flo/services/errors.py +106 -0
- flo_lang-0.1.0/src/flo/services/graphviz.py +60 -0
- flo_lang-0.1.0/src/flo/services/io.py +47 -0
- flo_lang-0.1.0/src/flo/services/logging.py +84 -0
- flo_lang-0.1.0/src/flo/services/telemetry.py +148 -0
flo_lang-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
flo_lang-0.1.0/README.md
ADDED
|
@@ -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.
|