donna 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

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 (70) hide show
  1. donna/artifacts/intro.md +39 -0
  2. donna/artifacts/research/specs/report.md +163 -0
  3. donna/artifacts/research/work/research.md +198 -0
  4. donna/artifacts/rfc/specs/request_for_change.md +270 -0
  5. donna/artifacts/rfc/work/create.md +120 -0
  6. donna/artifacts/rfc/work/do.md +109 -0
  7. donna/artifacts/rfc/work/plan.md +68 -0
  8. donna/artifacts/usage/artifacts.md +55 -12
  9. donna/artifacts/usage/cli.md +114 -39
  10. donna/artifacts/usage/worlds.md +8 -2
  11. donna/cli/__main__.py +1 -1
  12. donna/cli/commands/artifacts.py +104 -17
  13. donna/cli/commands/sessions.py +8 -8
  14. donna/cli/commands/workspaces.py +42 -0
  15. donna/cli/errors.py +18 -0
  16. donna/cli/types.py +16 -9
  17. donna/cli/utils.py +2 -2
  18. donna/core/errors.py +1 -11
  19. donna/core/result.py +5 -8
  20. donna/core/utils.py +0 -3
  21. donna/lib/__init__.py +4 -0
  22. donna/lib/sources.py +1 -1
  23. donna/lib/worlds.py +2 -2
  24. donna/machine/action_requests.py +0 -5
  25. donna/machine/artifacts.py +8 -6
  26. donna/machine/primitives.py +5 -5
  27. donna/machine/sessions.py +13 -5
  28. donna/machine/state.py +4 -4
  29. donna/machine/tasks.py +4 -18
  30. donna/machine/templates.py +4 -2
  31. donna/primitives/artifacts/specification.py +13 -2
  32. donna/primitives/artifacts/workflow.py +11 -2
  33. donna/primitives/directives/list.py +86 -0
  34. donna/primitives/directives/view.py +52 -11
  35. donna/primitives/operations/finish_workflow.py +13 -2
  36. donna/primitives/operations/output.py +87 -0
  37. donna/primitives/operations/request_action.py +3 -9
  38. donna/primitives/operations/run_script.py +2 -2
  39. donna/protocol/utils.py +22 -0
  40. donna/workspaces/artifacts.py +238 -0
  41. donna/{world → workspaces}/artifacts_discovery.py +1 -1
  42. donna/{world → workspaces}/config.py +18 -11
  43. donna/{world → workspaces}/errors.py +55 -45
  44. donna/workspaces/initialization.py +78 -0
  45. donna/{world → workspaces}/markdown.py +21 -26
  46. donna/{world → workspaces}/sources/base.py +2 -2
  47. donna/{world → workspaces}/sources/markdown.py +8 -7
  48. donna/{world → workspaces}/templates.py +4 -4
  49. donna/workspaces/tmp.py +51 -0
  50. donna/{world → workspaces}/worlds/base.py +6 -3
  51. donna/{world → workspaces}/worlds/filesystem.py +30 -10
  52. donna/{world → workspaces}/worlds/python.py +12 -9
  53. donna-0.2.2.dist-info/METADATA +463 -0
  54. donna-0.2.2.dist-info/RECORD +92 -0
  55. {donna-0.2.0.dist-info → donna-0.2.2.dist-info}/WHEEL +1 -1
  56. donna/artifacts/work/do_it.md +0 -142
  57. donna/artifacts/work/do_it_fast.md +0 -98
  58. donna/artifacts/work/planning.md +0 -245
  59. donna/cli/commands/projects.py +0 -49
  60. donna/world/artifacts.py +0 -122
  61. donna/world/initialization.py +0 -42
  62. donna/world/tmp.py +0 -33
  63. donna/world/worlds/__init__.py +0 -0
  64. donna-0.2.0.dist-info/METADATA +0 -44
  65. donna-0.2.0.dist-info/RECORD +0 -85
  66. /donna/{artifacts/work → workspaces}/__init__.py +0 -0
  67. /donna/{world → workspaces}/sources/__init__.py +0 -0
  68. /donna/{world → workspaces/worlds}/__init__.py +0 -0
  69. {donna-0.2.0.dist-info → donna-0.2.2.dist-info}/entry_points.txt +0 -0
  70. {donna-0.2.0.dist-info → donna-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,238 @@
1
+ import pathlib
2
+
3
+ from donna.core.entities import BaseEntity
4
+ from donna.core.errors import ErrorsList
5
+ from donna.core.result import Err, Ok, Result, unwrap_to_error
6
+ from donna.domain.ids import FullArtifactId, FullArtifactIdPattern, WorldId
7
+ from donna.machine.artifacts import Artifact
8
+ from donna.machine.tasks import Task, WorkUnit
9
+ from donna.workspaces import errors
10
+ from donna.workspaces.config import config
11
+ from donna.workspaces.templates import RenderMode
12
+
13
+
14
+ class ArtifactRenderContext(BaseEntity):
15
+ primary_mode: RenderMode
16
+ current_task: Task | None = None
17
+ current_work_unit: WorkUnit | None = None
18
+
19
+
20
+ class ArtifactUpdateError(errors.WorkspaceError):
21
+ cell_kind: str = "artifact_update_error"
22
+ artifact_id: FullArtifactId
23
+ path: pathlib.Path
24
+
25
+ def content_intro(self) -> str:
26
+ return f"Error updating artifact '{self.artifact_id}' from the path '{self.path}'"
27
+
28
+
29
+ class CanNotUpdateReadonlyWorld(ArtifactUpdateError):
30
+ code: str = "donna.workspaces.cannot_update_readonly_world"
31
+ message: str = "Cannot upload artifact to the read-only world `{error.world_id}`"
32
+ world_id: WorldId
33
+
34
+
35
+ class ArtifactRemoveError(errors.WorkspaceError):
36
+ cell_kind: str = "artifact_remove_error"
37
+ artifact_id: FullArtifactId
38
+
39
+ def content_intro(self) -> str:
40
+ return f"Error removing artifact '{self.artifact_id}'"
41
+
42
+
43
+ class CanNotRemoveReadonlyWorld(ArtifactRemoveError):
44
+ code: str = "donna.workspaces.cannot_remove_readonly_world"
45
+ message: str = "Cannot remove artifact from the read-only world `{error.world_id}`"
46
+ world_id: WorldId
47
+
48
+
49
+ class InputPathHasNoExtension(ArtifactUpdateError):
50
+ code: str = "donna.workspaces.input_path_has_no_extension"
51
+ message: str = "Input path has no extension to determine artifact source type"
52
+
53
+
54
+ class NoSourceForArtifactExtension(ArtifactUpdateError):
55
+ code: str = "donna.workspaces.no_source_for_artifact_extension"
56
+ message: str = "No source found for artifact extension of input path"
57
+ extension: str
58
+
59
+
60
+ class ArtifactCopyError(errors.WorkspaceError):
61
+ cell_kind: str = "artifact_copy_error"
62
+ source_id: FullArtifactId
63
+ target_id: FullArtifactId
64
+
65
+ def content_intro(self) -> str:
66
+ return f"Error copying artifact '{self.source_id}' to '{self.target_id}'"
67
+
68
+
69
+ class CanNotCopyToReadonlyWorld(ArtifactCopyError):
70
+ code: str = "donna.workspaces.cannot_copy_to_readonly_world"
71
+ message: str = "Cannot copy artifact to the read-only world `{error.world_id}`"
72
+ world_id: WorldId
73
+
74
+
75
+ class SourceArtifactHasNoExtension(ArtifactCopyError):
76
+ code: str = "donna.workspaces.source_artifact_has_no_extension"
77
+ message: str = "Source artifact has no extension to determine source type"
78
+
79
+
80
+ @unwrap_to_error
81
+ def artifact_file_extension(full_id: FullArtifactId) -> Result[str, ErrorsList]:
82
+ world = config().get_world(full_id.world_id).unwrap()
83
+ return Ok(world.file_extension_for(full_id.artifact_id).unwrap().lstrip("."))
84
+
85
+
86
+ @unwrap_to_error
87
+ def fetch_artifact(full_id: FullArtifactId, output: pathlib.Path) -> Result[None, ErrorsList]:
88
+ world = config().get_world(full_id.world_id).unwrap()
89
+ content = world.fetch_source(full_id.artifact_id).unwrap()
90
+
91
+ with output.open("wb") as f:
92
+ f.write(content)
93
+
94
+ return Ok(None)
95
+
96
+
97
+ @unwrap_to_error
98
+ def update_artifact(full_id: FullArtifactId, input: pathlib.Path) -> Result[None, ErrorsList]:
99
+ world = config().get_world(full_id.world_id).unwrap()
100
+
101
+ if world.readonly:
102
+ return Err([CanNotUpdateReadonlyWorld(artifact_id=full_id, path=input, world_id=world.id)])
103
+
104
+ source_suffix = input.suffix.lower()
105
+ content_bytes = input.read_bytes()
106
+
107
+ if not source_suffix:
108
+ return Err([InputPathHasNoExtension(artifact_id=full_id, path=input)])
109
+
110
+ source_config = config().find_source_for_extension(source_suffix)
111
+ if source_config is None:
112
+ return Err([NoSourceForArtifactExtension(artifact_id=full_id, path=input, extension=source_suffix)])
113
+
114
+ render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
115
+ test_artifact = source_config.construct_artifact_from_bytes(full_id, content_bytes, render_context).unwrap()
116
+ validation_result = test_artifact.validate_artifact()
117
+
118
+ validation_result.unwrap()
119
+ world.update(full_id.artifact_id, content_bytes, source_suffix).unwrap()
120
+
121
+ return Ok(None)
122
+
123
+
124
+ @unwrap_to_error
125
+ def copy_artifact(source_id: FullArtifactId, target_id: FullArtifactId) -> Result[None, ErrorsList]:
126
+ source_world = config().get_world(source_id.world_id).unwrap()
127
+ target_world = config().get_world(target_id.world_id).unwrap()
128
+
129
+ if target_world.readonly:
130
+ return Err(
131
+ [
132
+ CanNotCopyToReadonlyWorld(
133
+ source_id=source_id,
134
+ target_id=target_id,
135
+ world_id=target_world.id,
136
+ )
137
+ ]
138
+ )
139
+
140
+ content_bytes = source_world.fetch_source(source_id.artifact_id).unwrap()
141
+ source_extension = source_world.file_extension_for(source_id.artifact_id).unwrap()
142
+
143
+ if not source_extension:
144
+ return Err([SourceArtifactHasNoExtension(source_id=source_id, target_id=target_id)])
145
+
146
+ source_extension = source_extension.lower()
147
+ source_config = config().find_source_for_extension(source_extension)
148
+ if source_config is None:
149
+ return Err(
150
+ [
151
+ NoSourceForArtifactExtension(
152
+ artifact_id=source_id,
153
+ path=pathlib.Path(str(source_id)),
154
+ extension=source_extension,
155
+ )
156
+ ]
157
+ )
158
+
159
+ render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
160
+ test_artifact = source_config.construct_artifact_from_bytes(target_id, content_bytes, render_context).unwrap()
161
+ test_artifact.validate_artifact().unwrap()
162
+
163
+ target_world.update(target_id.artifact_id, content_bytes, source_extension).unwrap()
164
+ return Ok(None)
165
+
166
+
167
+ @unwrap_to_error
168
+ def move_artifact(source_id: FullArtifactId, target_id: FullArtifactId) -> Result[None, ErrorsList]:
169
+ copy_result = copy_artifact(source_id, target_id)
170
+ if copy_result.is_err():
171
+ return copy_result
172
+
173
+ return remove_artifact(source_id)
174
+
175
+
176
+ @unwrap_to_error
177
+ def remove_artifact(full_id: FullArtifactId) -> Result[None, ErrorsList]:
178
+ world = config().get_world(full_id.world_id).unwrap()
179
+
180
+ if world.readonly:
181
+ return Err([CanNotRemoveReadonlyWorld(artifact_id=full_id, world_id=world.id)])
182
+
183
+ world.remove(full_id.artifact_id).unwrap()
184
+ return Ok(None)
185
+
186
+
187
+ @unwrap_to_error
188
+ def load_artifact(
189
+ full_id: FullArtifactId, render_context: ArtifactRenderContext | None = None
190
+ ) -> Result[Artifact, ErrorsList]:
191
+ if render_context is None:
192
+ render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
193
+
194
+ world = config().get_world(full_id.world_id).unwrap()
195
+ return Ok(world.fetch(full_id.artifact_id, render_context).unwrap())
196
+
197
+
198
+ def list_artifacts( # noqa: CCR001
199
+ pattern: FullArtifactIdPattern,
200
+ render_context: ArtifactRenderContext | None = None,
201
+ tags: list[str] | None = None,
202
+ ) -> Result[list[Artifact], ErrorsList]:
203
+ if render_context is None:
204
+ render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
205
+
206
+ tag_filters = tags or []
207
+
208
+ artifacts: list[Artifact] = []
209
+ errors: ErrorsList = []
210
+
211
+ for world in reversed(config().worlds_instances):
212
+ for artifact_id in world.list_artifacts(pattern):
213
+ full_id = FullArtifactId((world.id, artifact_id))
214
+ artifact_result = load_artifact(full_id, render_context)
215
+ if artifact_result.is_err():
216
+ errors.extend(artifact_result.unwrap_err())
217
+ continue
218
+ artifact = artifact_result.unwrap()
219
+ if tag_filters and not _artifact_matches_tags(artifact, tag_filters):
220
+ continue
221
+ artifacts.append(artifact)
222
+
223
+ if errors:
224
+ return Err(errors)
225
+
226
+ return Ok(artifacts)
227
+
228
+
229
+ def _artifact_matches_tags(artifact: Artifact, tags: list[str]) -> bool:
230
+ if not tags:
231
+ return True
232
+
233
+ primary_result = artifact.primary_section()
234
+ if primary_result.is_err():
235
+ return False
236
+
237
+ primary = primary_result.unwrap()
238
+ return all(tag in primary.tags for tag in tags)
@@ -3,7 +3,7 @@ from functools import lru_cache
3
3
  from typing import Iterable, Protocol
4
4
 
5
5
  from donna.domain.ids import ArtifactId, FullArtifactId, FullArtifactIdPattern, WorldId
6
- from donna.world.config import config
6
+ from donna.workspaces.config import config
7
7
 
8
8
 
9
9
  class ArtifactListingNode(Protocol):
@@ -8,11 +8,11 @@ from donna.core.errors import ErrorsList
8
8
  from donna.core.result import Err, Ok, Result
9
9
  from donna.domain.ids import PythonImportPath, WorldId
10
10
  from donna.machine.primitives import resolve_primitive
11
- from donna.world import errors as world_errors
12
- from donna.world.sources.base import SourceConfig as SourceConfigValue
13
- from donna.world.sources.base import SourceConstructor
14
- from donna.world.worlds.base import World as BaseWorld
15
- from donna.world.worlds.base import WorldConstructor
11
+ from donna.workspaces import errors as world_errors
12
+ from donna.workspaces.sources.base import SourceConfig as SourceConfigValue
13
+ from donna.workspaces.sources.base import SourceConstructor
14
+ from donna.workspaces.worlds.base import World as BaseWorld
15
+ from donna.workspaces.worlds.base import WorldConstructor
16
16
 
17
17
  DONNA_DIR_NAME = ".donna"
18
18
  DONNA_CONFIG_NAME = "config.toml"
@@ -46,7 +46,7 @@ def _default_sources() -> list[SourceConfig]:
46
46
  ]
47
47
 
48
48
 
49
- def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
49
+ def _create_default_worlds() -> list[WorldConfig]:
50
50
  return [
51
51
  WorldConfig.model_validate(
52
52
  {
@@ -63,7 +63,7 @@ def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
63
63
  "kind": "donna.lib.worlds.filesystem",
64
64
  "readonly": True,
65
65
  "session": False,
66
- "path": pathlib.Path.home() / DONNA_DIR_NAME / DONNA_WORLD_HOME_DIR_NAME,
66
+ "path": f"~/{DONNA_DIR_NAME}/{DONNA_WORLD_HOME_DIR_NAME}",
67
67
  }
68
68
  ),
69
69
  WorldConfig.model_validate(
@@ -72,7 +72,7 @@ def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
72
72
  "kind": "donna.lib.worlds.filesystem",
73
73
  "readonly": False,
74
74
  "session": False,
75
- "path": project_dir / DONNA_DIR_NAME / DONNA_WORLD_PROJECT_DIR_NAME,
75
+ "path": pathlib.Path(DONNA_DIR_NAME) / DONNA_WORLD_PROJECT_DIR_NAME,
76
76
  }
77
77
  ),
78
78
  WorldConfig.model_validate(
@@ -81,14 +81,14 @@ def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
81
81
  "kind": "donna.lib.worlds.filesystem",
82
82
  "readonly": False,
83
83
  "session": True,
84
- "path": project_dir / DONNA_DIR_NAME / DONNA_WORLD_SESSION_DIR_NAME,
84
+ "path": pathlib.Path(DONNA_DIR_NAME) / DONNA_WORLD_SESSION_DIR_NAME,
85
85
  }
86
86
  ),
87
87
  ]
88
88
 
89
89
 
90
90
  def _default_worlds() -> list[WorldConfig]:
91
- return _create_default_worlds(project_dir())
91
+ return _create_default_worlds()
92
92
 
93
93
 
94
94
  class Config(BaseEntity):
@@ -152,7 +152,14 @@ class Config(BaseEntity):
152
152
  if source.kind == kind:
153
153
  return Ok(source)
154
154
 
155
- return Err([world_errors.SourceConfigNotConfigured(kind=kind)])
155
+ return Err(
156
+ [
157
+ world_errors.SourceConfigNotConfigured(
158
+ source_id=kind,
159
+ kind=kind,
160
+ )
161
+ ]
162
+ )
156
163
 
157
164
  def find_source_for_extension(self, extension: str) -> SourceConfigValue | None:
158
165
  for source in self._sources_instances:
@@ -5,64 +5,82 @@ from donna.domain.ids import ArtifactId, FullArtifactId, WorldId
5
5
 
6
6
 
7
7
  class InternalError(core_errors.InternalError):
8
- """Base class for internal errors in donna.world."""
8
+ """Base class for internal errors in donna.workspaces."""
9
9
 
10
10
 
11
- class WorldError(core_errors.EnvironmentError):
12
- cell_kind: str = "world_error"
11
+ class WorkspaceError(core_errors.EnvironmentError):
12
+ cell_kind: str = "workspace_error"
13
13
 
14
14
 
15
- class WorldConfigError(WorldError):
16
- cell_kind: str = "world_config_error"
15
+ class WorkspaceConfigError(WorkspaceError):
16
+ cell_kind: str = "workspace_config_error"
17
17
  config_path: pathlib.Path
18
18
 
19
19
  def content_intro(self) -> str:
20
- return f"Error in world config file '{self.config_path}'"
20
+ return f"Error in workspace config file '{self.config_path}'"
21
21
 
22
22
 
23
- class ConfigParseFailed(WorldConfigError):
24
- code: str = "donna.world.config_parse_failed"
23
+ class ConfigParseFailed(WorkspaceConfigError):
24
+ code: str = "donna.workspaces.config_parse_failed"
25
25
  message: str = "Failed to parse config file: {error.details}"
26
26
  details: str
27
27
 
28
28
 
29
- class ConfigValidationFailed(WorldConfigError):
30
- code: str = "donna.world.config_validation_failed"
29
+ class ConfigValidationFailed(WorkspaceConfigError):
30
+ code: str = "donna.workspaces.config_validation_failed"
31
31
  message: str = "Failed to validate config file: {error.details}"
32
32
  details: str
33
33
 
34
34
 
35
+ class WorkspaceAlreadyInitialized(WorkspaceError):
36
+ code: str = "donna.workspaces.workspace_already_initialized"
37
+ message: str = "Workspace already initialized at `{error.project_dir}`"
38
+ ways_to_fix: list[str] = [
39
+ "Continue using the existing workspace.",
40
+ "Remove the existing `.donna` directory if you want to reinitialize.",
41
+ "Choose a different project directory.",
42
+ ]
43
+ project_dir: pathlib.Path
44
+
45
+
46
+ class WorldError(WorkspaceError):
47
+ cell_kind: str = "world_error"
48
+ world_id: WorldId
49
+
50
+
35
51
  class WorldNotConfigured(WorldError):
36
- code: str = "donna.world.world_not_configured"
52
+ code: str = "donna.workspaces.world_not_configured"
37
53
  message: str = "World with id `{error.world_id}` is not configured"
38
- world_id: WorldId
39
54
 
40
55
 
41
- class SourceConfigNotConfigured(WorldError):
42
- code: str = "donna.world.source_config_not_configured"
56
+ class SourceError(WorkspaceError):
57
+ cell_kind: str = "source_error"
58
+ source_id: str
59
+
60
+
61
+ class SourceConfigNotConfigured(SourceError):
62
+ code: str = "donna.workspaces.source_config_not_configured"
43
63
  message: str = "Source config `{error.kind}` is not configured"
44
64
  kind: str
45
65
 
46
66
 
47
67
  class WorldReadonly(WorldError):
48
- code: str = "donna.world.world_readonly"
68
+ code: str = "donna.workspaces.world_readonly"
49
69
  message: str = "World `{error.world_id}` is read-only"
50
70
  ways_to_fix: list[str] = [
51
71
  "Use a world configured with readonly = false. Most likely they are `project` and `session`.",
52
72
  ]
53
- world_id: WorldId
54
73
 
55
74
 
56
75
  class WorldStateStorageUnsupported(WorldError):
57
- code: str = "donna.world.state_storage_unsupported"
76
+ code: str = "donna.workspaces.state_storage_unsupported"
58
77
  message: str = "World `{error.world_id}` does not support state storage"
59
78
  ways_to_fix: list[str] = [
60
79
  "Use the session world.",
61
80
  ]
62
- world_id: WorldId
63
81
 
64
82
 
65
- class ArtifactError(WorldError):
83
+ class ArtifactError(WorkspaceError):
66
84
  cell_kind: str = "artifact_error"
67
85
  artifact_id: ArtifactId
68
86
  world_id: WorldId
@@ -72,7 +90,7 @@ class ArtifactError(WorldError):
72
90
 
73
91
 
74
92
  class ArtifactNotFound(ArtifactError):
75
- code: str = "donna.world.artifact_not_found"
93
+ code: str = "donna.workspaces.artifact_not_found"
76
94
  message: str = "Artifact `{error.artifact_id}` does not exist in world `{error.world_id}`"
77
95
  ways_to_fix: list[str] = [
78
96
  "Check the artifact id for typos.",
@@ -81,7 +99,7 @@ class ArtifactNotFound(ArtifactError):
81
99
 
82
100
 
83
101
  class ArtifactMultipleFiles(ArtifactError):
84
- code: str = "donna.world.artifact_multiple_files"
102
+ code: str = "donna.workspaces.artifact_multiple_files"
85
103
  message: str = "Artifact `{error.artifact_id}` has multiple files in world `{error.world_id}`"
86
104
  ways_to_fix: list[str] = [
87
105
  "Keep a single source file per artifact in the world.",
@@ -89,7 +107,7 @@ class ArtifactMultipleFiles(ArtifactError):
89
107
 
90
108
 
91
109
  class UnsupportedArtifactSourceExtension(ArtifactError):
92
- code: str = "donna.world.unsupported_artifact_source_extension"
110
+ code: str = "donna.workspaces.unsupported_artifact_source_extension"
93
111
  message: str = "Unsupported artifact source extension `{error.extension}` in world `{error.world_id}`"
94
112
  ways_to_fix: list[str] = [
95
113
  "Use a supported extension for the configured sources.",
@@ -97,7 +115,7 @@ class UnsupportedArtifactSourceExtension(ArtifactError):
97
115
  extension: str
98
116
 
99
117
 
100
- class MarkdownError(WorldError):
118
+ class MarkdownError(WorkspaceError):
101
119
  cell_kind: str = "markdown_error"
102
120
  artifact_id: FullArtifactId | None = None
103
121
 
@@ -108,7 +126,7 @@ class MarkdownError(WorldError):
108
126
  return f"Error in markdown artifact '{self.artifact_id}'"
109
127
 
110
128
 
111
- class TemplateDirectiveError(WorldError):
129
+ class TemplateDirectiveError(WorkspaceError):
112
130
  cell_kind: str = "template_directive_error"
113
131
  artifact_id: FullArtifactId | None = None
114
132
 
@@ -120,14 +138,14 @@ class TemplateDirectiveError(WorldError):
120
138
 
121
139
 
122
140
  class DirectivePathIncomplete(TemplateDirectiveError):
123
- code: str = "donna.world.directive_path_incomplete"
141
+ code: str = "donna.workspaces.directive_path_incomplete"
124
142
  message: str = "Directive path must include module and directive parts, got `{error.path}`."
125
143
  ways_to_fix: list[str] = ["Use a directive path with both module and directive names."]
126
144
  path: str
127
145
 
128
146
 
129
147
  class DirectiveModuleNotImportable(TemplateDirectiveError):
130
- code: str = "donna.world.directive_module_not_importable"
148
+ code: str = "donna.workspaces.directive_module_not_importable"
131
149
  message: str = "Directive module `{error.module_path}` is not importable."
132
150
  ways_to_fix: list[str] = [
133
151
  "Check the module path for typos.",
@@ -137,7 +155,7 @@ class DirectiveModuleNotImportable(TemplateDirectiveError):
137
155
 
138
156
 
139
157
  class DirectiveNotAvailable(TemplateDirectiveError):
140
- code: str = "donna.world.directive_not_available"
158
+ code: str = "donna.workspaces.directive_not_available"
141
159
  message: str = "Directive `{error.module_path}.{error.directive_name}` is not available."
142
160
  ways_to_fix: list[str] = [
143
161
  "Check the directive name for typos.",
@@ -148,7 +166,7 @@ class DirectiveNotAvailable(TemplateDirectiveError):
148
166
 
149
167
 
150
168
  class DirectiveNotDirective(TemplateDirectiveError):
151
- code: str = "donna.world.directive_not_directive"
169
+ code: str = "donna.workspaces.directive_not_directive"
152
170
  message: str = "`{error.module_path}.{error.directive_name}` is not a directive."
153
171
  ways_to_fix: list[str] = [
154
172
  "Check the directive path for typos.",
@@ -160,7 +178,7 @@ class DirectiveNotDirective(TemplateDirectiveError):
160
178
 
161
179
 
162
180
  class DirectiveUnexpectedError(TemplateDirectiveError):
163
- code: str = "donna.world.directive_unexpected_error"
181
+ code: str = "donna.workspaces.directive_unexpected_error"
164
182
  message: str = "Unexpected error while applying directive `{error.directive_path}`: {error.details}"
165
183
  ways_to_fix: list[str] = [
166
184
  "Check the documentation for the directive to ensure correct usage.",
@@ -171,7 +189,7 @@ class DirectiveUnexpectedError(TemplateDirectiveError):
171
189
 
172
190
 
173
191
  class MarkdownUnsupportedCodeFormat(MarkdownError):
174
- code: str = "donna.world.markdown_unsupported_code_format"
192
+ code: str = "donna.workspaces.markdown_unsupported_code_format"
175
193
  message: str = "Unsupported code block format `{error.format}`"
176
194
  ways_to_fix: list[str] = [
177
195
  "Use one of the supported formats: json, yaml, yml, toml.",
@@ -180,36 +198,28 @@ class MarkdownUnsupportedCodeFormat(MarkdownError):
180
198
 
181
199
 
182
200
  class MarkdownMultipleH1Sections(MarkdownError):
183
- code: str = "donna.world.markdown_multiple_h1_sections"
201
+ code: str = "donna.workspaces.markdown_multiple_h1_sections"
184
202
  message: str = "Multiple H1 sections are not supported"
185
203
  ways_to_fix: list[str] = [
186
204
  "Keep a single H1 section in the artifact.",
187
205
  ]
188
206
 
189
207
 
190
- class MarkdownMultipleH1Titles(MarkdownError):
191
- code: str = "donna.world.markdown_multiple_h1_titles"
192
- message: str = "Multiple H1 titles are not supported"
193
- ways_to_fix: list[str] = [
194
- "Keep a single H1 title in the artifact.",
195
- ]
196
-
197
-
198
- class MarkdownH2BeforeH1Title(MarkdownError):
199
- code: str = "donna.world.markdown_h2_before_h1_title"
200
- message: str = "H2 section found before H1 title"
208
+ class MarkdownH1SectionMustBeFirst(MarkdownError):
209
+ code: str = "donna.workspaces.markdown_h1_section_must_be_first"
210
+ message: str = "H1 section must be the first section in the artifact"
201
211
  ways_to_fix: list[str] = [
202
- "Ensure the first heading is an H1 title before any H2 sections.",
212
+ "Ensure the H1 section is the first section in the artifact.",
203
213
  ]
204
214
 
205
215
 
206
216
  class MarkdownArtifactWithoutSections(MarkdownError):
207
- code: str = "donna.world.markdown_artifact_without_sections"
217
+ code: str = "donna.workspaces.markdown_artifact_without_sections"
208
218
  message: str = "Artifact MUST have at least one section"
209
219
 
210
220
 
211
221
  class PrimitiveDoesNotSupportMarkdown(MarkdownError):
212
- code: str = "donna.world.primitive_does_not_support_markdown"
222
+ code: str = "donna.workspaces.primitive_does_not_support_markdown"
213
223
  message: str = "Primitive {error.primitive_id} cannot construct artifact section from the Markdown source"
214
224
  ways_to_fix: list[str] = [
215
225
  "Ensure the section kind points to a primitive that supports Markdown sections.",
@@ -0,0 +1,78 @@
1
+ import pathlib
2
+ import tomllib
3
+
4
+ import tomli_w
5
+
6
+ from donna.core import errors as core_errors
7
+ from donna.core import utils
8
+ from donna.core.result import Err, Ok, Result, unwrap_to_error
9
+ from donna.domain.ids import WorldId
10
+ from donna.workspaces import config
11
+ from donna.workspaces import errors as world_errors
12
+
13
+
14
+ @unwrap_to_error
15
+ def initialize_runtime() -> Result[None, core_errors.ErrorsList]:
16
+ """Initialize the runtime environment for the application.
17
+
18
+ This function MUST be called before any other operations.
19
+ """
20
+ project_dir = utils.discover_project_dir(config.DONNA_DIR_NAME).unwrap()
21
+
22
+ config.project_dir.set(project_dir)
23
+
24
+ config_dir = project_dir / config.DONNA_DIR_NAME
25
+
26
+ config.config_dir.set(config_dir)
27
+
28
+ config_path = config_dir / config.DONNA_CONFIG_NAME
29
+
30
+ if not config_path.exists():
31
+ config.config.set(config.Config())
32
+ return Ok(None)
33
+
34
+ try:
35
+ data = tomllib.loads(config_path.read_text(encoding="utf-8"))
36
+ except tomllib.TOMLDecodeError as e:
37
+ return Err([world_errors.ConfigParseFailed(config_path=config_path, details=str(e))])
38
+
39
+ try:
40
+ loaded_config = config.Config.model_validate(data)
41
+ except Exception as e:
42
+ return Err([world_errors.ConfigValidationFailed(config_path=config_path, details=str(e))])
43
+
44
+ config.config.set(loaded_config)
45
+
46
+ return Ok(None)
47
+
48
+
49
+ @unwrap_to_error
50
+ def initialize_workspace(project_dir: pathlib.Path) -> Result[None, core_errors.ErrorsList]:
51
+ """Initialize the physical workspace for the project (`.donna` directory)."""
52
+ project_dir = project_dir.resolve()
53
+ workspace_dir = project_dir / config.DONNA_DIR_NAME
54
+
55
+ if workspace_dir.exists():
56
+ return Err([world_errors.WorkspaceAlreadyInitialized(project_dir=project_dir)])
57
+
58
+ config.project_dir.set(project_dir)
59
+ config.config_dir.set(workspace_dir)
60
+
61
+ workspace_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ default_config = config.Config()
64
+ config.config.set(default_config)
65
+
66
+ config_path = workspace_dir / config.DONNA_CONFIG_NAME
67
+ config_path.write_text(
68
+ tomli_w.dumps(default_config.model_dump(mode="json")),
69
+ encoding="utf-8",
70
+ )
71
+
72
+ project_world = default_config.get_world(WorldId(config.DONNA_WORLD_PROJECT_DIR_NAME)).unwrap()
73
+ project_world.initialize()
74
+
75
+ session_world = default_config.get_world(WorldId(config.DONNA_WORLD_SESSION_DIR_NAME)).unwrap()
76
+ session_world.initialize()
77
+
78
+ return Ok(None)