framework-m-studio 0.2.3__tar.gz → 0.3.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.
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/.gitignore +15 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/CHANGELOG.md +28 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/PKG-INFO +7 -2
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/README.md +3 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/pyproject.toml +30 -3
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/__init__.py +6 -1
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/app.py +56 -11
- framework_m_studio-0.3.0/src/framework_m_studio/checklist_parser.py +421 -0
- framework_m_studio-0.3.0/src/framework_m_studio/cli/__init__.py +752 -0
- framework_m_studio-0.3.0/src/framework_m_studio/cli/build.py +421 -0
- framework_m_studio-0.3.0/src/framework_m_studio/cli/dev.py +214 -0
- framework_m_studio-0.3.0/src/framework_m_studio/cli/new.py +754 -0
- framework_m_studio-0.3.0/src/framework_m_studio/cli/quality.py +157 -0
- framework_m_studio-0.3.0/src/framework_m_studio/cli/studio.py +159 -0
- framework_m_studio-0.3.0/src/framework_m_studio/cli/utility.py +50 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/codegen/generator.py +6 -2
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/codegen/parser.py +101 -4
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/codegen/templates/doctype.py.jinja2 +19 -10
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/codegen/test_generator.py +6 -2
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/discovery.py +15 -5
- framework_m_studio-0.3.0/src/framework_m_studio/docs_generator.py +614 -0
- framework_m_studio-0.3.0/src/framework_m_studio/protocol_scanner.py +435 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/routes.py +39 -11
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/CodePreview.tsx +36 -8
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/FieldEditor.tsx +53 -1
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/pages/doctypes/edit.tsx +111 -70
- framework_m_studio-0.3.0/tests/test_api_routes.py +209 -0
- framework_m_studio-0.3.0/tests/test_build.py +362 -0
- framework_m_studio-0.3.0/tests/test_build_extended.py +591 -0
- framework_m_studio-0.3.0/tests/test_dev.py +403 -0
- framework_m_studio-0.3.0/tests/test_discovery.py +416 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_docs_generator.py +180 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_generator.py +77 -11
- framework_m_studio-0.3.0/tests/test_new_cli.py +608 -0
- framework_m_studio-0.3.0/tests/test_parser.py +420 -0
- framework_m_studio-0.3.0/tests/test_protocol_scanner.py +184 -0
- framework_m_studio-0.3.0/tests/test_quality_cli.py +412 -0
- framework_m_studio-0.3.0/tests/test_studio.py +262 -0
- framework_m_studio-0.3.0/tests/test_utility_cli.py +71 -0
- framework_m_studio-0.2.3/src/framework_m_studio/cli.py +0 -247
- framework_m_studio-0.2.3/src/framework_m_studio/docs_generator.py +0 -318
- framework_m_studio-0.2.3/src/framework_m_studio/static/assets/index-BJ5Noua8.js +0 -171
- framework_m_studio-0.2.3/src/framework_m_studio/static/assets/index-CnPUX2YK.css +0 -1
- framework_m_studio-0.2.3/src/framework_m_studio/static/favicon.ico +0 -0
- framework_m_studio-0.2.3/src/framework_m_studio/static/index.html +0 -40
- framework_m_studio-0.2.3/studio_ui/public/favicon.ico +0 -0
- framework_m_studio-0.2.3/tests/test_api_routes.py +0 -53
- framework_m_studio-0.2.3/tests/test_discovery.py +0 -212
- framework_m_studio-0.2.3/tests/test_parser.py +0 -203
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/docs/api/index.md +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/docs/user-guide.md +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/codegen/__init__.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/codegen/templates/test_doctype.py.jinja2 +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/codegen/transformer.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/git/__init__.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/git/adapter.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/git/github_provider.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/git/protocol.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/py.typed +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/sdk_generator.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/src/framework_m_studio/workspace.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/.gitignore +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/.npmrc +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/Dockerfile +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/README.MD +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/eslint.config.js +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/index.html +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/package.json +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/pnpm-lock.yaml +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/App.css +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/App.test.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/App.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/AutoForm.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/AutoTable.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/ControllerEditor.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/ERDView.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/LayoutDesigner.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/ModuleExplorer.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/NestedEditorDrawer.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/SandboxPreview.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/TableFieldEditor.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/breadcrumb/index.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/fields/DefaultFieldComponents.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/fields/index.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/fields/resolveFieldComponent.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/layout/index.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/components/menu/index.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/index.css +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/index.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/pages/doctypes/index.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/pages/doctypes/list.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/pages/index.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/providers/ThemeContext.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/providers/constants.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/providers/data.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/providers/theme.tsx +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/providers/useTheme.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/registry/defaultComponents.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/registry/fieldComponents.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/registry/index.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/setupTests.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/utils/mockDataGenerator.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/src/vite-env.d.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/tsconfig.json +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/tsconfig.node.json +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/vite.config.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/studio_ui/vitest.config.ts +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/__init__.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/conftest.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_cli.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_git_adapter.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_git_protocol.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_github_provider.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_sdk_generator.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_transformer.py +0 -0
- {framework_m_studio-0.2.3 → framework_m_studio-0.3.0}/tests/test_workspace.py +0 -0
|
@@ -77,6 +77,9 @@ docker-compose.override.yml
|
|
|
77
77
|
# Studio UI built assets
|
|
78
78
|
apps/studio/src/framework_m_studio/static/
|
|
79
79
|
|
|
80
|
+
# Desk UI built assets (bundled with framework-m package)
|
|
81
|
+
libs/framework-m/src/framework_m/static/assets/
|
|
82
|
+
|
|
80
83
|
#doctypes
|
|
81
84
|
doctypes/*.py
|
|
82
85
|
apps/studio/src/doctypes/*.py
|
|
@@ -105,3 +108,15 @@ yarn-error.log*
|
|
|
105
108
|
.idea/
|
|
106
109
|
*.sublime-project
|
|
107
110
|
*.sublime-workspace
|
|
111
|
+
|
|
112
|
+
# Generated Documentation
|
|
113
|
+
docs/developer/generated/
|
|
114
|
+
docs/machine/
|
|
115
|
+
|
|
116
|
+
# Website
|
|
117
|
+
website/.docusaurus/
|
|
118
|
+
website/build/
|
|
119
|
+
website/node_modules/
|
|
120
|
+
|
|
121
|
+
# GitLab Pages
|
|
122
|
+
public/
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## framework-m-studio v0.3.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- support namespaced app, test directory and dev.py moved to studio (37beae6)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## framework-m-studio v0.2.8
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- add ruff code style badge to README (c2482ea)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## framework-m-studio v0.2.7
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
- add Python version and license badges to README (90d1adf)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## framework-m-studio v0.2.6
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
- format and lint (090018f)
|
|
29
|
+
|
|
30
|
+
|
|
3
31
|
## framework-m-studio v0.2.3
|
|
4
32
|
|
|
5
33
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: framework-m-studio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Framework M Studio - Visual DocType Builder & Developer Tools
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/castlecraft/framework-m
|
|
6
6
|
Project-URL: Documentation, https://gitlab.com/castlecraft/framework-m#readme
|
|
@@ -17,7 +17,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
|
-
Requires-Dist: framework-m
|
|
20
|
+
Requires-Dist: framework-m-core>=0.5.0
|
|
21
|
+
Requires-Dist: framework-m>=0.4.2
|
|
22
|
+
Requires-Dist: honcho>=1.1.0
|
|
21
23
|
Requires-Dist: jinja2>=3.1.0
|
|
22
24
|
Requires-Dist: libcst>=1.0.0
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
@@ -27,7 +29,10 @@ Description-Content-Type: text/markdown
|
|
|
27
29
|
Visual DocType builder and developer tools for Framework M.
|
|
28
30
|
|
|
29
31
|
[](https://badge.fury.io/py/framework-m-studio)
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
34
|
[](https://gitlab.com/castlecraft/framework-m/-/pipelines)
|
|
35
|
+
[](https://github.com/astral-sh/ruff)
|
|
31
36
|
|
|
32
37
|
## Overview
|
|
33
38
|
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
Visual DocType builder and developer tools for Framework M.
|
|
4
4
|
|
|
5
5
|
[](https://badge.fury.io/py/framework-m-studio)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
8
|
[](https://gitlab.com/castlecraft/framework-m/-/pipelines)
|
|
9
|
+
[](https://github.com/astral-sh/ruff)
|
|
7
10
|
|
|
8
11
|
## Overview
|
|
9
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "framework-m-studio"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Framework M Studio - Visual DocType Builder & Developer Tools"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -21,12 +21,19 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
dependencies = [
|
|
24
|
-
"framework-m",
|
|
24
|
+
"framework-m-core>=0.5.0",
|
|
25
|
+
"framework-m>=0.4.2",
|
|
25
26
|
# Code generation
|
|
26
27
|
"libcst>=1.0.0",
|
|
27
28
|
"jinja2>=3.1.0",
|
|
29
|
+
# Process management for dev command
|
|
30
|
+
"honcho>=1.1.0",
|
|
28
31
|
]
|
|
29
32
|
|
|
33
|
+
[tool.uv.sources]
|
|
34
|
+
framework-m-core = { workspace = true }
|
|
35
|
+
framework-m = { workspace = true }
|
|
36
|
+
|
|
30
37
|
[dependency-groups]
|
|
31
38
|
dev = [
|
|
32
39
|
"pytest>=8.0.0",
|
|
@@ -41,11 +48,26 @@ dev = [
|
|
|
41
48
|
"twine>=5.0.0",
|
|
42
49
|
]
|
|
43
50
|
|
|
44
|
-
[project.entry-points."
|
|
51
|
+
[project.entry-points."framework_m_core.cli_commands"]
|
|
45
52
|
# These commands override/extend base CLI when framework-m-studio is installed
|
|
46
53
|
codegen = "framework_m_studio.cli:codegen_app"
|
|
47
54
|
studio = "framework_m_studio.cli:studio_app"
|
|
48
55
|
docs = "framework_m_studio.cli:docs_app"
|
|
56
|
+
"docs:generate" = "framework_m_studio.cli:docs_generate"
|
|
57
|
+
"docs:export" = "framework_m_studio.cli:docs_export"
|
|
58
|
+
"docs:adr" = "framework_m_studio.cli:docs_adr"
|
|
59
|
+
"docs:rfc" = "framework_m_studio.cli:docs_rfc"
|
|
60
|
+
new = "framework_m_studio.cli.new:new_app_command"
|
|
61
|
+
"new:doctype" = "framework_m_studio.cli.new:new_doctype_command"
|
|
62
|
+
"new:app" = "framework_m_studio.cli.new:new_app_command"
|
|
63
|
+
test = "framework_m_studio.cli.quality:test_command"
|
|
64
|
+
lint = "framework_m_studio.cli.quality:lint_command"
|
|
65
|
+
format = "framework_m_studio.cli.quality:format_command"
|
|
66
|
+
typecheck = "framework_m_studio.cli.quality:typecheck_command"
|
|
67
|
+
console = "framework_m_studio.cli.utility:console_command"
|
|
68
|
+
routes = "framework_m_studio.cli.utility:routes_command"
|
|
69
|
+
dev = "framework_m_studio.cli.dev:dev_command"
|
|
70
|
+
|
|
49
71
|
|
|
50
72
|
[project.urls]
|
|
51
73
|
Homepage = "https://gitlab.com/castlecraft/framework-m"
|
|
@@ -92,12 +114,17 @@ explicit_package_bases = true
|
|
|
92
114
|
exclude = [
|
|
93
115
|
"tests/",
|
|
94
116
|
".venv/",
|
|
117
|
+
"src/doctypes/",
|
|
95
118
|
]
|
|
96
119
|
|
|
97
120
|
[[tool.mypy.overrides]]
|
|
98
121
|
module = "framework_m.core.fields.registry"
|
|
99
122
|
ignore_missing_imports = true
|
|
100
123
|
|
|
124
|
+
[[tool.mypy.overrides]]
|
|
125
|
+
module = "honcho.*"
|
|
126
|
+
ignore_missing_imports = true
|
|
127
|
+
|
|
101
128
|
[tool.ruff]
|
|
102
129
|
line-length = 88
|
|
103
130
|
target-version = "py312"
|
|
@@ -11,6 +11,11 @@ runtime lightweight. Install as a dev dependency.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
import importlib.metadata
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
__version__ = importlib.metadata.version("framework-m-studio")
|
|
18
|
+
except importlib.metadata.PackageNotFoundError:
|
|
19
|
+
__version__ = "0.0.0"
|
|
15
20
|
|
|
16
21
|
__all__ = ["__version__"]
|
|
@@ -43,19 +43,64 @@ async def api_root() -> dict[str, Any]:
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def _get_spa_response(path: str) -> Response[
|
|
47
|
-
"""Helper to serve SPA files.
|
|
46
|
+
def _get_spa_response(path: str) -> Response[bytes | dict[str, str]]:
|
|
47
|
+
"""Helper to serve SPA files.
|
|
48
|
+
|
|
49
|
+
Returns HTML content for missing builds, or serves actual static files
|
|
50
|
+
when the SPA is built. This is the standard approach for SPAs - serving
|
|
51
|
+
HTML directly without requiring a template engine configuration.
|
|
52
|
+
"""
|
|
48
53
|
if not STATIC_DIR.exists():
|
|
49
54
|
# Development mode: no built assets yet
|
|
55
|
+
# Return HTML placeholder directly (no template engine needed)
|
|
56
|
+
html_content = """<!DOCTYPE html>
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="UTF-8">
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
61
|
+
<title>Framework M Studio - Build Required</title>
|
|
62
|
+
<style>
|
|
63
|
+
body {
|
|
64
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
65
|
+
max-width: 600px;
|
|
66
|
+
margin: 100px auto;
|
|
67
|
+
padding: 20px;
|
|
68
|
+
line-height: 1.6;
|
|
69
|
+
}
|
|
70
|
+
h1 { color: #333; }
|
|
71
|
+
code {
|
|
72
|
+
background: #f4f4f4;
|
|
73
|
+
padding: 2px 6px;
|
|
74
|
+
border-radius: 3px;
|
|
75
|
+
font-family: monospace;
|
|
76
|
+
}
|
|
77
|
+
.api-links { margin-top: 30px; }
|
|
78
|
+
.api-links a {
|
|
79
|
+
display: block;
|
|
80
|
+
margin: 10px 0;
|
|
81
|
+
color: #0066cc;
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
}
|
|
84
|
+
.api-links a:hover { text-decoration: underline; }
|
|
85
|
+
</style>
|
|
86
|
+
</head>
|
|
87
|
+
<body>
|
|
88
|
+
<h1>Studio UI Not Built</h1>
|
|
89
|
+
<p>The Studio UI assets have not been built yet.</p>
|
|
90
|
+
<p>To build the UI, run:</p>
|
|
91
|
+
<pre><code>cd apps/studio/studio_ui && pnpm install && pnpm build</code></pre>
|
|
92
|
+
|
|
93
|
+
<div class="api-links">
|
|
94
|
+
<h3>Available API Endpoints:</h3>
|
|
95
|
+
<a href="/studio/api/health">Health Check</a>
|
|
96
|
+
<a href="/studio/api/doctypes">DocTypes API</a>
|
|
97
|
+
<a href="/studio/api/field-types">Field Types API</a>
|
|
98
|
+
</div>
|
|
99
|
+
</body>
|
|
100
|
+
</html>"""
|
|
50
101
|
return Response(
|
|
51
|
-
content=
|
|
52
|
-
|
|
53
|
-
"hint": "Run: cd apps/studio/studio_ui && pnpm build",
|
|
54
|
-
"api_health": "/studio/api/health",
|
|
55
|
-
"api_doctypes": "/studio/api/doctypes",
|
|
56
|
-
"api_field_types": "/studio/api/field-types",
|
|
57
|
-
},
|
|
58
|
-
media_type="application/json",
|
|
102
|
+
content=html_content.encode("utf-8"),
|
|
103
|
+
media_type="text/html; charset=utf-8",
|
|
59
104
|
)
|
|
60
105
|
|
|
61
106
|
# Check for actual file
|
|
@@ -125,7 +170,7 @@ async def list_field_types() -> dict[str, Any]:
|
|
|
125
170
|
Uses FieldRegistry for dynamic discovery.
|
|
126
171
|
"""
|
|
127
172
|
try:
|
|
128
|
-
from
|
|
173
|
+
from framework_m_standard.adapters.db.field_registry import FieldRegistry
|
|
129
174
|
|
|
130
175
|
types_list = []
|
|
131
176
|
for type_info in FieldRegistry.get_instance().get_all_types():
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Checklist Parser - Extract features from Phase checklists.
|
|
2
|
+
|
|
3
|
+
This module parses markdown checklist files to extract:
|
|
4
|
+
- Completed features ([x])
|
|
5
|
+
- Feature metadata (phase, category, description)
|
|
6
|
+
- Implementation status
|
|
7
|
+
|
|
8
|
+
Used for generating:
|
|
9
|
+
- Features documentation page
|
|
10
|
+
- Release notes from version diffs
|
|
11
|
+
- Progress tracking
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ChecklistItem:
|
|
24
|
+
"""A single checklist item from a phase."""
|
|
25
|
+
|
|
26
|
+
phase: str
|
|
27
|
+
section: str
|
|
28
|
+
description: str
|
|
29
|
+
completed: bool
|
|
30
|
+
subsection: str | None = None
|
|
31
|
+
line_number: int = 0
|
|
32
|
+
indent_level: int = 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class PhaseInfo:
|
|
37
|
+
"""Information about a phase from its checklist."""
|
|
38
|
+
|
|
39
|
+
phase_id: str
|
|
40
|
+
title: str
|
|
41
|
+
objective: str
|
|
42
|
+
items: list[ChecklistItem] = field(default_factory=list)
|
|
43
|
+
completion_percentage: float = 0.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_checklist_file(filepath: Path) -> PhaseInfo:
|
|
47
|
+
"""Parse a phase checklist markdown file.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
filepath: Path to the checklist markdown file.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
PhaseInfo with extracted items and metadata.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> phase = parse_checklist_file(Path("checklists/phase-01-core-kernel.md"))
|
|
57
|
+
>>> print(f"{phase.title}: {phase.completion_percentage:.0f}% complete")
|
|
58
|
+
Phase 01: Core Kernel & Interfaces: 95% complete
|
|
59
|
+
"""
|
|
60
|
+
content = filepath.read_text()
|
|
61
|
+
lines = content.split("\n")
|
|
62
|
+
|
|
63
|
+
# Extract phase metadata from filename and first heading
|
|
64
|
+
phase_id = _extract_phase_id(filepath.name)
|
|
65
|
+
title = ""
|
|
66
|
+
objective = ""
|
|
67
|
+
current_section = "Unknown"
|
|
68
|
+
current_subsection: str | None = None
|
|
69
|
+
items: list[ChecklistItem] = []
|
|
70
|
+
|
|
71
|
+
for line_num, line in enumerate(lines, 1):
|
|
72
|
+
# Extract title from first H1
|
|
73
|
+
if line.startswith("# ") and not title:
|
|
74
|
+
title = line[2:].strip()
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
# Extract objective
|
|
78
|
+
if line.startswith("**Objective**:"):
|
|
79
|
+
objective = line.split(":", 1)[1].strip()
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Track current section (## headers)
|
|
83
|
+
if line.startswith("## "):
|
|
84
|
+
current_section = line[3:].strip()
|
|
85
|
+
current_subsection = None
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Track subsections (### headers)
|
|
89
|
+
if line.startswith("### "):
|
|
90
|
+
current_subsection = line[4:].strip()
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Parse checklist items
|
|
94
|
+
if match := re.match(r"^(\s*)- \[([ x])\] (.+)$", line):
|
|
95
|
+
indent = match.group(1)
|
|
96
|
+
is_checked = match.group(2) == "x"
|
|
97
|
+
description = match.group(3).strip()
|
|
98
|
+
|
|
99
|
+
items.append(
|
|
100
|
+
ChecklistItem(
|
|
101
|
+
phase=phase_id,
|
|
102
|
+
section=current_section,
|
|
103
|
+
subsection=current_subsection,
|
|
104
|
+
description=description,
|
|
105
|
+
completed=is_checked,
|
|
106
|
+
line_number=line_num,
|
|
107
|
+
indent_level=len(indent),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Calculate completion percentage
|
|
112
|
+
completed_count = sum(1 for item in items if item.completed)
|
|
113
|
+
total_count = len(items)
|
|
114
|
+
completion_percentage = (
|
|
115
|
+
(completed_count / total_count * 100) if total_count > 0 else 0.0
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return PhaseInfo(
|
|
119
|
+
phase_id=phase_id,
|
|
120
|
+
title=title,
|
|
121
|
+
objective=objective,
|
|
122
|
+
items=items,
|
|
123
|
+
completion_percentage=completion_percentage,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _extract_phase_id(filename: str) -> str:
|
|
128
|
+
"""Extract phase ID from filename.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
filename: Checklist filename (e.g., "phase-01-core-kernel.md")
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Phase ID (e.g., "01")
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> _extract_phase_id("phase-01-core-kernel.md")
|
|
138
|
+
'01'
|
|
139
|
+
>>> _extract_phase_id("phase-11-package-split.md")
|
|
140
|
+
'11'
|
|
141
|
+
"""
|
|
142
|
+
match = re.search(r"phase-(\d+[a-z]?)", filename)
|
|
143
|
+
return match.group(1) if match else "unknown"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def scan_all_checklists(checklists_dir: Path) -> list[PhaseInfo]:
|
|
147
|
+
"""Scan all checklist files in a directory.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
checklists_dir: Directory containing phase-*.md files.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of PhaseInfo objects, sorted by phase ID.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> phases = scan_all_checklists(Path("checklists"))
|
|
157
|
+
>>> for phase in phases:
|
|
158
|
+
... print(f"{phase.phase_id}: {phase.completion_percentage:.0f}%")
|
|
159
|
+
"""
|
|
160
|
+
checklist_files = sorted(checklists_dir.glob("phase-*.md"))
|
|
161
|
+
phases = [parse_checklist_file(f) for f in checklist_files]
|
|
162
|
+
return sorted(phases, key=lambda p: p.phase_id)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def group_by_category(items: list[ChecklistItem]) -> dict[str, list[ChecklistItem]]:
|
|
166
|
+
"""Group checklist items by section.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
items: List of checklist items.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dictionary mapping section names to items.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> items = phase.items
|
|
176
|
+
>>> grouped = group_by_category(items)
|
|
177
|
+
>>> for category, category_items in grouped.items():
|
|
178
|
+
... print(f"{category}: {len(category_items)} items")
|
|
179
|
+
"""
|
|
180
|
+
groups: dict[str, list[ChecklistItem]] = {}
|
|
181
|
+
for item in items:
|
|
182
|
+
section_key = f"{item.section}"
|
|
183
|
+
if item.subsection:
|
|
184
|
+
section_key = f"{item.section} > {item.subsection}"
|
|
185
|
+
|
|
186
|
+
if section_key not in groups:
|
|
187
|
+
groups[section_key] = []
|
|
188
|
+
groups[section_key].append(item)
|
|
189
|
+
|
|
190
|
+
return groups
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def filter_completed(items: list[ChecklistItem]) -> list[ChecklistItem]:
|
|
194
|
+
"""Filter to only completed items.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
items: List of checklist items.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of completed items only.
|
|
201
|
+
"""
|
|
202
|
+
return [item for item in items if item.completed]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def generate_features_summary(phases: list[PhaseInfo]) -> str:
|
|
206
|
+
"""Generate a markdown summary of all features.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
phases: List of phase information objects.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Markdown-formatted feature summary.
|
|
213
|
+
|
|
214
|
+
Example:
|
|
215
|
+
>>> phases = scan_all_checklists(Path("checklists"))
|
|
216
|
+
>>> summary = generate_features_summary(phases)
|
|
217
|
+
>>> print(summary)
|
|
218
|
+
"""
|
|
219
|
+
lines = [
|
|
220
|
+
"# Framework M Features",
|
|
221
|
+
"",
|
|
222
|
+
"Auto-generated feature list from phase checklists.",
|
|
223
|
+
"",
|
|
224
|
+
"## Overview",
|
|
225
|
+
"",
|
|
226
|
+
"| Phase | Title | Completion |",
|
|
227
|
+
"|-------|-------|------------|",
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
# Overview table
|
|
231
|
+
for phase in phases:
|
|
232
|
+
completion_bar = _progress_bar(phase.completion_percentage)
|
|
233
|
+
lines.append(
|
|
234
|
+
f"| {phase.phase_id} | [{phase.title}](#{_slugify(phase.title)}) | {completion_bar} {phase.completion_percentage:.0f}% |"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
lines.extend(["", "---", ""])
|
|
238
|
+
|
|
239
|
+
# Detailed sections
|
|
240
|
+
for phase in phases:
|
|
241
|
+
lines.append(f"## {phase.title}")
|
|
242
|
+
lines.append("")
|
|
243
|
+
lines.append(f"**Phase**: {phase.phase_id}")
|
|
244
|
+
lines.append(f"**Objective**: {phase.objective}")
|
|
245
|
+
lines.append(f"**Status**: {phase.completion_percentage:.0f}% Complete")
|
|
246
|
+
lines.append("")
|
|
247
|
+
|
|
248
|
+
# Group by category
|
|
249
|
+
grouped = group_by_category(phase.items)
|
|
250
|
+
for category, items in grouped.items():
|
|
251
|
+
completed = [i for i in items if i.completed]
|
|
252
|
+
pending = [i for i in items if not i.completed]
|
|
253
|
+
|
|
254
|
+
lines.append(f"### {category}")
|
|
255
|
+
lines.append("")
|
|
256
|
+
lines.append(
|
|
257
|
+
f"**Progress**: {len(completed)}/{len(items)} ({len(completed) / len(items) * 100:.0f}%)"
|
|
258
|
+
)
|
|
259
|
+
lines.append("")
|
|
260
|
+
|
|
261
|
+
if completed:
|
|
262
|
+
lines.append("**Completed:**")
|
|
263
|
+
lines.append("")
|
|
264
|
+
for item in completed:
|
|
265
|
+
lines.append(f"- ✅ {item.description}")
|
|
266
|
+
lines.append("")
|
|
267
|
+
|
|
268
|
+
if pending:
|
|
269
|
+
lines.append("**Pending:**")
|
|
270
|
+
lines.append("")
|
|
271
|
+
for item in pending:
|
|
272
|
+
lines.append(f"- ⏳ {item.description}")
|
|
273
|
+
lines.append("")
|
|
274
|
+
|
|
275
|
+
lines.append("---")
|
|
276
|
+
lines.append("")
|
|
277
|
+
|
|
278
|
+
return "\n".join(lines)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _progress_bar(percentage: float, width: int = 10) -> str:
|
|
282
|
+
"""Generate a text progress bar.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
percentage: Completion percentage (0-100).
|
|
286
|
+
width: Width of the progress bar in characters.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Unicode progress bar string.
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
>>> _progress_bar(75, 10)
|
|
293
|
+
'████████░░'
|
|
294
|
+
"""
|
|
295
|
+
filled = int(percentage / 100 * width)
|
|
296
|
+
empty = width - filled
|
|
297
|
+
return "█" * filled + "░" * empty
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _slugify(text: str) -> str:
|
|
301
|
+
"""Convert text to URL-friendly slug.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
text: Text to slugify.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Lowercase slug with hyphens.
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
>>> _slugify("Core Kernel & Interfaces")
|
|
311
|
+
'core-kernel--interfaces'
|
|
312
|
+
"""
|
|
313
|
+
slug = text.lower()
|
|
314
|
+
slug = re.sub(r"[^\w\s-]", "", slug)
|
|
315
|
+
slug = re.sub(r"[-\s]+", "-", slug)
|
|
316
|
+
return slug.strip("-")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def compare_versions(
|
|
320
|
+
old_items: list[ChecklistItem],
|
|
321
|
+
new_items: list[ChecklistItem],
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""Compare two versions of checklist items to find changes.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
old_items: Checklist items from old version.
|
|
327
|
+
new_items: Checklist items from new version.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Dictionary with newly_completed, newly_added, and removed items.
|
|
331
|
+
|
|
332
|
+
Example:
|
|
333
|
+
>>> changes = compare_versions(old_phase.items, new_phase.items)
|
|
334
|
+
>>> print(f"Newly completed: {len(changes['newly_completed'])}")
|
|
335
|
+
"""
|
|
336
|
+
# Create lookup by description for comparison
|
|
337
|
+
old_map = {item.description: item for item in old_items}
|
|
338
|
+
new_map = {item.description: item for item in new_items}
|
|
339
|
+
|
|
340
|
+
newly_completed = []
|
|
341
|
+
newly_added = []
|
|
342
|
+
removed = []
|
|
343
|
+
|
|
344
|
+
# Find newly completed items
|
|
345
|
+
for desc, new_item in new_map.items():
|
|
346
|
+
old_item = old_map.get(desc)
|
|
347
|
+
if old_item and not old_item.completed and new_item.completed:
|
|
348
|
+
newly_completed.append(new_item)
|
|
349
|
+
elif not old_item:
|
|
350
|
+
newly_added.append(new_item)
|
|
351
|
+
|
|
352
|
+
# Find removed items
|
|
353
|
+
for desc in old_map:
|
|
354
|
+
if desc not in new_map:
|
|
355
|
+
removed.append(old_map[desc])
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
"newly_completed": newly_completed,
|
|
359
|
+
"newly_added": newly_added,
|
|
360
|
+
"removed": removed,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def generate_release_notes(changes: dict[str, Any], version: str) -> str:
|
|
365
|
+
"""Generate release notes from checklist changes.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
changes: Dictionary from compare_versions().
|
|
369
|
+
version: Version string (e.g., "1.0.0").
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Markdown-formatted release notes.
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
>>> changes = compare_versions(old_items, new_items)
|
|
376
|
+
>>> notes = generate_release_notes(changes, "1.0.0")
|
|
377
|
+
"""
|
|
378
|
+
lines = [
|
|
379
|
+
f"# Release {version}",
|
|
380
|
+
"",
|
|
381
|
+
"## What's New",
|
|
382
|
+
"",
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
newly_completed = changes.get("newly_completed", [])
|
|
386
|
+
newly_added = changes.get("newly_added", [])
|
|
387
|
+
|
|
388
|
+
if newly_completed:
|
|
389
|
+
# Group by phase
|
|
390
|
+
by_phase: dict[str, list[ChecklistItem]] = {}
|
|
391
|
+
for item in newly_completed:
|
|
392
|
+
if item.phase not in by_phase:
|
|
393
|
+
by_phase[item.phase] = []
|
|
394
|
+
by_phase[item.phase].append(item)
|
|
395
|
+
|
|
396
|
+
lines.append("### Completed Features")
|
|
397
|
+
lines.append("")
|
|
398
|
+
|
|
399
|
+
for phase_id in sorted(by_phase.keys()):
|
|
400
|
+
items = by_phase[phase_id]
|
|
401
|
+
lines.append(f"#### Phase {phase_id}")
|
|
402
|
+
lines.append("")
|
|
403
|
+
for item in items:
|
|
404
|
+
section_info = f"{item.section}"
|
|
405
|
+
if item.subsection:
|
|
406
|
+
section_info += f" > {item.subsection}"
|
|
407
|
+
lines.append(f"- **{section_info}**: {item.description}")
|
|
408
|
+
lines.append("")
|
|
409
|
+
|
|
410
|
+
if newly_added:
|
|
411
|
+
lines.append("### New Checklist Items")
|
|
412
|
+
lines.append("")
|
|
413
|
+
for item in newly_added:
|
|
414
|
+
lines.append(f"- {item.description}")
|
|
415
|
+
lines.append("")
|
|
416
|
+
|
|
417
|
+
if not newly_completed and not newly_added:
|
|
418
|
+
lines.append("_No new features in this release._")
|
|
419
|
+
lines.append("")
|
|
420
|
+
|
|
421
|
+
return "\n".join(lines)
|