metaobjects 0.9.0__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 (181) hide show
  1. metaobjects/__init__.py +75 -0
  2. metaobjects/agent_context/__init__.py +55 -0
  3. metaobjects/agent_context/_content/README.md +14 -0
  4. metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
  5. metaobjects/agent_context/_content/servers/java.meta.json +5 -0
  6. metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
  7. metaobjects/agent_context/_content/servers/python.meta.json +5 -0
  8. metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
  9. metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
  10. metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
  11. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
  12. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
  13. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
  14. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
  15. metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
  16. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
  17. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
  18. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
  19. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
  20. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
  21. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
  22. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
  23. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
  24. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
  25. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
  26. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
  27. metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
  28. metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
  29. metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
  30. metaobjects/agent_context/assemble.py +133 -0
  31. metaobjects/agent_context/content_root.py +54 -0
  32. metaobjects/agent_context/scaffold.py +191 -0
  33. metaobjects/agent_context/types.py +44 -0
  34. metaobjects/attr_class_map.py +23 -0
  35. metaobjects/cli.py +696 -0
  36. metaobjects/codegen/__init__.py +0 -0
  37. metaobjects/codegen/config.py +11 -0
  38. metaobjects/codegen/constants.py +13 -0
  39. metaobjects/codegen/extract_delegate_emitter.py +384 -0
  40. metaobjects/codegen/extract_schema_emitter.py +139 -0
  41. metaobjects/codegen/format.py +31 -0
  42. metaobjects/codegen/fr010_field_mapping.py +220 -0
  43. metaobjects/codegen/generator.py +62 -0
  44. metaobjects/codegen/generator_registry.py +163 -0
  45. metaobjects/codegen/generators/__init__.py +0 -0
  46. metaobjects/codegen/generators/entity_model.py +263 -0
  47. metaobjects/codegen/generators/extractor_generator.py +317 -0
  48. metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
  49. metaobjects/codegen/generators/m2m_codegen.py +192 -0
  50. metaobjects/codegen/generators/output_parser_generator.py +272 -0
  51. metaobjects/codegen/generators/output_prompt_generator.py +192 -0
  52. metaobjects/codegen/generators/payload_vo_generator.py +672 -0
  53. metaobjects/codegen/generators/render_helper_generator.py +451 -0
  54. metaobjects/codegen/generators/router_generator.py +635 -0
  55. metaobjects/codegen/generators/template_generator.py +70 -0
  56. metaobjects/codegen/generators/tph_plan.py +120 -0
  57. metaobjects/codegen/generators/trace_helper_generator.py +336 -0
  58. metaobjects/codegen/instance_artifacts.py +15 -0
  59. metaobjects/codegen/output_format_spec_emitter.py +79 -0
  60. metaobjects/codegen/overwrite_policy.py +27 -0
  61. metaobjects/codegen/runner.py +110 -0
  62. metaobjects/codegen/runtime/__init__.py +6 -0
  63. metaobjects/codegen/runtime/filter_parser.py +193 -0
  64. metaobjects/codegen/type_map.py +84 -0
  65. metaobjects/core_types.py +809 -0
  66. metaobjects/datatype.py +19 -0
  67. metaobjects/documentation/__init__.py +28 -0
  68. metaobjects/documentation/doc_constants.py +20 -0
  69. metaobjects/documentation/doc_provider.py +20 -0
  70. metaobjects/documentation/doc_schema.py +24 -0
  71. metaobjects/errors.py +124 -0
  72. metaobjects/loader/__init__.py +0 -0
  73. metaobjects/loader/merge.py +287 -0
  74. metaobjects/loader/meta_data_loader.py +245 -0
  75. metaobjects/loader/sources/__init__.py +24 -0
  76. metaobjects/loader/sources/directory_source.py +50 -0
  77. metaobjects/loader/sources/file_source.py +41 -0
  78. metaobjects/loader/sources/meta_data_source.py +67 -0
  79. metaobjects/loader/sources/uri_source.py +56 -0
  80. metaobjects/loader/validate_discriminator.py +181 -0
  81. metaobjects/loader/validate_field_readonly.py +146 -0
  82. metaobjects/loader/validate_source_parameter_ref.py +159 -0
  83. metaobjects/loader/validate_source_physical_names.py +140 -0
  84. metaobjects/loader/validation_passes.py +1513 -0
  85. metaobjects/meta/__init__.py +1 -0
  86. metaobjects/meta/core/__init__.py +0 -0
  87. metaobjects/meta/core/attr/__init__.py +0 -0
  88. metaobjects/meta/core/attr/attr_constants.py +31 -0
  89. metaobjects/meta/core/attr/meta_attr.py +136 -0
  90. metaobjects/meta/core/field/__init__.py +0 -0
  91. metaobjects/meta/core/field/field_constants.py +105 -0
  92. metaobjects/meta/core/field/meta_field.py +76 -0
  93. metaobjects/meta/core/identity/__init__.py +0 -0
  94. metaobjects/meta/core/identity/identity_constants.py +19 -0
  95. metaobjects/meta/core/identity/meta_identity.py +8 -0
  96. metaobjects/meta/core/object/__init__.py +0 -0
  97. metaobjects/meta/core/object/meta_object.py +65 -0
  98. metaobjects/meta/core/object/meta_object_aware.py +43 -0
  99. metaobjects/meta/core/object/object_class_registry.py +56 -0
  100. metaobjects/meta/core/object/object_constants.py +13 -0
  101. metaobjects/meta/core/object/object_extract.py +400 -0
  102. metaobjects/meta/core/object/value_object.py +70 -0
  103. metaobjects/meta/core/relationship/__init__.py +0 -0
  104. metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
  105. metaobjects/meta/core/relationship/meta_relationship.py +54 -0
  106. metaobjects/meta/core/relationship/relationship_constants.py +51 -0
  107. metaobjects/meta/core/validator/__init__.py +0 -0
  108. metaobjects/meta/core/validator/validator_constants.py +18 -0
  109. metaobjects/meta/meta_data.py +206 -0
  110. metaobjects/meta/meta_root.py +8 -0
  111. metaobjects/meta/persistence/__init__.py +0 -0
  112. metaobjects/meta/persistence/db/__init__.py +1 -0
  113. metaobjects/meta/persistence/db/db_constants.py +41 -0
  114. metaobjects/meta/persistence/db/db_provider.py +60 -0
  115. metaobjects/meta/persistence/origin/__init__.py +0 -0
  116. metaobjects/meta/persistence/origin/meta_origin.py +8 -0
  117. metaobjects/meta/persistence/origin/origin_constants.py +20 -0
  118. metaobjects/meta/persistence/source/__init__.py +0 -0
  119. metaobjects/meta/persistence/source/meta_source.py +137 -0
  120. metaobjects/meta/persistence/source/source_constants.py +115 -0
  121. metaobjects/meta/presentation/__init__.py +0 -0
  122. metaobjects/meta/presentation/layout/__init__.py +0 -0
  123. metaobjects/meta/presentation/layout/layout_constants.py +13 -0
  124. metaobjects/meta/presentation/layout/meta_layout.py +8 -0
  125. metaobjects/meta/presentation/view/__init__.py +0 -0
  126. metaobjects/meta/presentation/view/meta_view.py +8 -0
  127. metaobjects/meta/presentation/view/view_constants.py +22 -0
  128. metaobjects/meta/template/__init__.py +0 -0
  129. metaobjects/meta/template/meta_template.py +46 -0
  130. metaobjects/meta/template/template_constants.py +112 -0
  131. metaobjects/meta/template/template_provider.py +43 -0
  132. metaobjects/parser.py +380 -0
  133. metaobjects/parser_yaml.py +82 -0
  134. metaobjects/provider.py +111 -0
  135. metaobjects/py.typed +0 -0
  136. metaobjects/registry.py +210 -0
  137. metaobjects/registry_manifest.py +223 -0
  138. metaobjects/render/__init__.py +74 -0
  139. metaobjects/render/email_document.py +14 -0
  140. metaobjects/render/escapers.py +109 -0
  141. metaobjects/render/extract/__init__.py +59 -0
  142. metaobjects/render/extract/coerce.py +279 -0
  143. metaobjects/render/extract/extract.py +211 -0
  144. metaobjects/render/extract/extract_map.py +61 -0
  145. metaobjects/render/extract/json_forgiving_reader.py +203 -0
  146. metaobjects/render/extract/locate.py +65 -0
  147. metaobjects/render/extract/normalize.py +96 -0
  148. metaobjects/render/extract/strip.py +20 -0
  149. metaobjects/render/extract/types.py +332 -0
  150. metaobjects/render/extract/xml_forgiving_reader.py +162 -0
  151. metaobjects/render/filesystem_provider.py +51 -0
  152. metaobjects/render/prompt/__init__.py +32 -0
  153. metaobjects/render/prompt/output_format_renderer.py +340 -0
  154. metaobjects/render/prompt/output_format_spec.py +28 -0
  155. metaobjects/render/prompt/prompt_field.py +29 -0
  156. metaobjects/render/prompt/prompt_overrides.py +29 -0
  157. metaobjects/render/prompt/prompt_style.py +38 -0
  158. metaobjects/render/renderer.py +358 -0
  159. metaobjects/render/verify.py +266 -0
  160. metaobjects/runtime/__init__.py +39 -0
  161. metaobjects/runtime/llm_recorder.py +210 -0
  162. metaobjects/runtime/n2m_resolver.py +155 -0
  163. metaobjects/runtime/object_manager.py +715 -0
  164. metaobjects/runtime/tph.py +50 -0
  165. metaobjects/serializer_json.py +172 -0
  166. metaobjects/shared/__init__.py +0 -0
  167. metaobjects/shared/base_types.py +16 -0
  168. metaobjects/shared/separators.py +4 -0
  169. metaobjects/shared/structural.py +9 -0
  170. metaobjects/source/__init__.py +79 -0
  171. metaobjects/source/error_source.py +266 -0
  172. metaobjects/source/json_path.py +106 -0
  173. metaobjects/source/semantic_diff.py +98 -0
  174. metaobjects/source/yaml_positions.py +174 -0
  175. metaobjects/super_resolve.py +128 -0
  176. metaobjects/yaml_desugar.py +481 -0
  177. metaobjects-0.9.0.dist-info/METADATA +97 -0
  178. metaobjects-0.9.0.dist-info/RECORD +181 -0
  179. metaobjects-0.9.0.dist-info/WHEEL +4 -0
  180. metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
  181. metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
@@ -0,0 +1,27 @@
1
+ # Working with MetaObjects in this project
2
+
3
+ > {{stackLine}}
4
+
5
+ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the durable
6
+ spine; generated code is the disposable artifact. Regenerate with `{{codegenCommand}}`.
7
+
8
+ ## Principles
9
+ - Pattern-derivable from metadata = codegen, never hand-write (FKs, CRUD, validators, finders).
10
+ - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).
11
+ - Use the generated constants for any string that names metadata.
12
+ - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases.
13
+
14
+ ## Authoring rules you must not violate
15
+ - Nodes are fused-key maps: `{"<type>.<subType>": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
16
+ - Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
17
+ - An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
18
+ - Package paths use `::` (`acme::common::id`).
19
+
20
+ ## Going deeper (Claude Code)
21
+ For authoring, codegen, runtime/UI, prompts, or verify work, use the matching
22
+ `metaobjects-*` skill — its body links the `references/<lang>.md` fragment installed
23
+ for this project's stack.
24
+
25
+ These `metaobjects-*` skills are plain, inspectable Markdown reference docs (no tools
26
+ or hooks) generated by MetaObjects for this project's stack — safe to read and edit;
27
+ re-run the agent-context scaffold to refresh them.
@@ -0,0 +1,133 @@
1
+ """The pure agent-context assembler.
2
+
3
+ Port of ``server/typescript/packages/sdk/src/agent-context/assemble.ts``. Given
4
+ the content tree and a resolved :class:`Stack`, produce the ``(path, contents)``
5
+ files the consumer project receives — byte-identical to the TS reference.
6
+
7
+ BYTE-IDENTITY: every file but the two always-on documents is a verbatim copy.
8
+ We read with ``Path.read_bytes().decode("utf-8")`` (NOT ``open(... )`` text mode)
9
+ so Python never translates newlines, and emit ``str`` whose UTF-8 encoding is the
10
+ original bytes. The only computed content is the two template substitutions.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ from .types import (
21
+ CLIENT_FRAMEWORKS,
22
+ MIGRATION_TOKEN,
23
+ SERVER_LANGS,
24
+ SKILL_NAMES,
25
+ Stack,
26
+ )
27
+
28
+ #: ``{{key}}`` template-variable pattern (word chars only, matching the TS regex).
29
+ _TEMPLATE_VAR = re.compile(r"\{\{(\w+)\}\}")
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class AssembledFile:
34
+ """A file the assembler emits, ``path`` relative to the consumer project root."""
35
+
36
+ path: str
37
+ contents: str
38
+
39
+
40
+ def make_stack(servers: list[str], clients: list[str]) -> Stack:
41
+ """Build a :class:`Stack`: dedupe + canonical-order the inputs, derive tokens.
42
+
43
+ Unknown entries are dropped (the canonical orderings are the allow-list); the
44
+ resulting orderings are exactly :data:`SERVER_LANGS` / :data:`CLIENT_FRAMEWORKS`
45
+ filtered to the requested set, matching the TS ``makeStack``.
46
+ """
47
+ s = tuple(x for x in SERVER_LANGS if x in servers)
48
+ c = tuple(x for x in CLIENT_FRAMEWORKS if x in clients)
49
+ tokens = frozenset({*s, *c, MIGRATION_TOKEN})
50
+ return Stack(servers=s, clients=c, tokens=tokens)
51
+
52
+
53
+ def _read_text(path: Path) -> str:
54
+ """Read a file as UTF-8 with NO newline translation (byte-faithful)."""
55
+ return path.read_bytes().decode("utf-8")
56
+
57
+
58
+ def _read_server_meta(content_root: Path, server: str) -> dict[str, str] | None:
59
+ """Load ``servers/<server>.meta.json``, or ``None`` if absent."""
60
+ p = content_root / "servers" / f"{server}.meta.json"
61
+ if not p.exists():
62
+ return None
63
+ return json.loads(_read_text(p))
64
+
65
+
66
+ def _stack_line(content_root: Path, stack: Stack) -> tuple[str, str]:
67
+ """Compute ``(stackLine, codegenCommand)`` for the always-on template.
68
+
69
+ ``codegenCommand`` is the FIRST server's ``codegenCommand`` (or ``"meta gen"``
70
+ if there is no primary server, or its meta file is absent).
71
+ """
72
+ primary = stack.servers[0] if stack.servers else None
73
+ meta = _read_server_meta(content_root, primary) if primary else None
74
+ server_part = ", ".join(stack.servers) + " server" if stack.servers else "no server"
75
+ client_part = ", ".join(stack.clients) + " client" if stack.clients else "no client"
76
+ line = f"Stack: {server_part}, {client_part}; migrations are TS."
77
+ codegen_command = meta["codegenCommand"] if meta else "meta gen"
78
+ return line, codegen_command
79
+
80
+
81
+ def _apply_template(tpl: str, variables: dict[str, str]) -> str:
82
+ """Replace every ``{{key}}``; raise on an unknown key (matches TS)."""
83
+
84
+ def repl(match: re.Match[str]) -> str:
85
+ key = match.group(1)
86
+ if key not in variables:
87
+ raise ValueError(f"agent-context: unknown template variable {{{{{key}}}}}")
88
+ return variables[key]
89
+
90
+ return _TEMPLATE_VAR.sub(repl, tpl)
91
+
92
+
93
+ def assemble(content_root: Path, stack: Stack) -> list[AssembledFile]:
94
+ """Assemble the consumer files for a resolved stack. Pure given the content tree.
95
+
96
+ Output is sorted by path ascending — the stable order the conformance gate and
97
+ the TS reference both produce.
98
+ """
99
+ out: list[AssembledFile] = []
100
+
101
+ # 1. Always-on (AGENTS.md + CLAUDE.md, identical contents).
102
+ tpl = _read_text(content_root / "templates" / "always-on.md.mustache")
103
+ line, codegen_command = _stack_line(content_root, stack)
104
+ always_on = _apply_template(
105
+ tpl, {"stackLine": line, "codegenCommand": codegen_command}
106
+ )
107
+ out.append(AssembledFile(".metaobjects/AGENTS.md", always_on))
108
+ out.append(AssembledFile(".metaobjects/CLAUDE.md", always_on))
109
+
110
+ # 2. Skills: body + only the references whose token is in the stack.
111
+ for skill in SKILL_NAMES:
112
+ skill_dir = content_root / "skills" / skill
113
+ body = _read_text(skill_dir / "SKILL.md")
114
+ out.append(AssembledFile(f".claude/skills/{skill}/SKILL.md", body))
115
+
116
+ ref_dir = skill_dir / "references"
117
+ if ref_dir.is_dir():
118
+ tokens = sorted(
119
+ p.stem
120
+ for p in ref_dir.iterdir()
121
+ if p.is_file() and p.suffix == ".md" and p.stem in stack.tokens
122
+ )
123
+ for token in tokens:
124
+ out.append(
125
+ AssembledFile(
126
+ f".claude/skills/{skill}/references/{token}.md",
127
+ _read_text(ref_dir / f"{token}.md"),
128
+ )
129
+ )
130
+
131
+ # Stable order: by path.
132
+ out.sort(key=lambda f: f.path)
133
+ return out
@@ -0,0 +1,54 @@
1
+ """Resolve the ``agent-context/`` content tree the assembler reads.
2
+
3
+ Two resolution sources (matching the TS reference's bundled-then-monorepo walk):
4
+
5
+ 1. A bundled copy shipped inside the installed wheel, at
6
+ ``metaobjects/agent_context/_content/`` (vendored at build time by the custom
7
+ hatch build hook ``hatch_build.py``). This is the published path.
8
+ 2. A dev fallback: walk up from this module to the monorepo root and use its
9
+ top-level ``agent-context/`` directory.
10
+
11
+ A directory is a valid content root iff it holds the authoring skill body.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+
18
+ #: Where the bundled copy lands inside the package (see pyproject force-include).
19
+ _BUNDLED = Path(__file__).resolve().parent / "_content"
20
+
21
+
22
+ def _is_content_root(directory: Path) -> bool:
23
+ """A directory is a valid content root iff it holds the authoring skill body."""
24
+ return (directory / "skills" / "metaobjects-authoring" / "SKILL.md").is_file()
25
+
26
+
27
+ def resolve_agent_context_root(override: Path | None = None) -> Path:
28
+ """Resolve the content tree.
29
+
30
+ - If ``override`` is given, it must be a valid content root (else raise).
31
+ - Else: prefer the bundled copy beside this module (published path); fall back
32
+ to a monorepo ``agent-context/`` found by walking up from this module (dev).
33
+ """
34
+ if override is not None:
35
+ if _is_content_root(override):
36
+ return override
37
+ raise FileNotFoundError(
38
+ f"agent-context content not found at override: {override}"
39
+ )
40
+
41
+ if _is_content_root(_BUNDLED):
42
+ return _BUNDLED
43
+
44
+ here = Path(__file__).resolve()
45
+ for parent in here.parents:
46
+ candidate = parent / "agent-context"
47
+ if _is_content_root(candidate):
48
+ return candidate
49
+
50
+ raise FileNotFoundError(
51
+ "agent-context content not found — looked for a bundled copy beside the "
52
+ "package (metaobjects/agent_context/_content) and a monorepo "
53
+ "`agent-context/` walking up from this module."
54
+ )
@@ -0,0 +1,191 @@
1
+ """Scaffold planning + sidecar manifest for the agent-context writer.
2
+
3
+ Port of ``server/typescript/packages/sdk/src/agent-context/scaffold.ts``. Pure:
4
+ all filesystem access is via a ``read_current`` callback so the planning logic is
5
+ testable without touching disk.
6
+
7
+ A file is safe to overwrite iff it is absent, or its on-disk sha256 still equals
8
+ the hash the prior manifest recorded (the user hasn't hand-edited it). A
9
+ hand-edited file is preserved — the fresh contents go to ``<path>.new`` instead.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ from collections.abc import Callable
16
+ from dataclasses import dataclass, field
17
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
18
+
19
+ from .assemble import AssembledFile
20
+ from .types import Stack
21
+
22
+ #: Consumer-relative path of the sidecar manifest that tracks scaffolded files.
23
+ AGENT_CONTEXT_MANIFEST_PATH = ".metaobjects/.agent-context.json"
24
+
25
+
26
+ def installed_metaobjects_version() -> str:
27
+ """The installed ``metaobjects`` distribution version, or ``"0.0.0"`` if absent.
28
+
29
+ Resolved idiomatically via :func:`importlib.metadata.version`; a
30
+ ``PackageNotFoundError`` (e.g. running straight from a source checkout that
31
+ was never installed) falls back to ``"0.0.0"`` — mirroring the TS reference's
32
+ fallback so the stamp/nudge never crashes.
33
+ """
34
+ try:
35
+ return _pkg_version("metaobjects")
36
+ except PackageNotFoundError:
37
+ return "0.0.0"
38
+
39
+
40
+ @dataclass
41
+ class Manifest:
42
+ """Tracks what the assembler last wrote, so re-runs can detect hand-edits."""
43
+
44
+ version: int
45
+ servers: list[str]
46
+ clients: list[str]
47
+ #: consumer-relative path → sha256 of the contents as last scaffolded.
48
+ files: dict[str, str]
49
+ #: The MetaObjects version that last scaffolded this context. Drives the
50
+ #: staleness nudge (an upgrade can leave the copied-in skills/docs stale).
51
+ #: Optional for back-compat with manifests written before version tracking.
52
+ #: Serialized as ``generatedBy`` — the SAME key as the TS reference, so a
53
+ #: polyglot repo can cross-read the manifest regardless of which port wrote it.
54
+ generated_by: str | None = None
55
+
56
+ def to_json(self) -> dict[str, object]:
57
+ out: dict[str, object] = {"version": self.version}
58
+ if self.generated_by is not None:
59
+ out["generatedBy"] = self.generated_by
60
+ out["servers"] = list(self.servers)
61
+ out["clients"] = list(self.clients)
62
+ out["files"] = dict(self.files)
63
+ return out
64
+
65
+ @staticmethod
66
+ def from_json(data: dict[str, object]) -> "Manifest":
67
+ files_raw = data.get("files", {})
68
+ files = (
69
+ {str(k): str(v) for k, v in files_raw.items()}
70
+ if isinstance(files_raw, dict)
71
+ else {}
72
+ )
73
+ servers = data.get("servers", [])
74
+ clients = data.get("clients", [])
75
+ generated_by = data.get("generatedBy")
76
+ return Manifest(
77
+ version=int(data.get("version", 1)), # type: ignore[arg-type]
78
+ servers=[str(x) for x in servers] if isinstance(servers, list) else [],
79
+ clients=[str(x) for x in clients] if isinstance(clients, list) else [],
80
+ files=files,
81
+ generated_by=str(generated_by) if generated_by is not None else None,
82
+ )
83
+
84
+
85
+ @dataclass
86
+ class _Write:
87
+ path: str
88
+ contents: str
89
+
90
+
91
+ @dataclass
92
+ class _Conflict:
93
+ path: str
94
+ new_path: str
95
+ contents: str
96
+
97
+
98
+ @dataclass
99
+ class ScaffoldDecision:
100
+ """The outcome of planning a (re-)scaffold."""
101
+
102
+ #: files to (over)write at their own path: new, or unmodified-since-last-scaffold.
103
+ writes: list[_Write] = field(default_factory=list)
104
+ #: hand-edited files: write the fresh contents to ``<path>.new``, leave the original.
105
+ conflicts: list[_Conflict] = field(default_factory=list)
106
+ #: the manifest to persist after writing.
107
+ manifest: Manifest | None = None
108
+ #: paths the prior manifest tracked that are no longer assembled — reported, never deleted.
109
+ removed: list[str] = field(default_factory=list)
110
+
111
+
112
+ def hash_contents(s: str) -> str:
113
+ """sha256 hex of the UTF-8 bytes of ``s`` (matches the TS digest)."""
114
+ return hashlib.sha256(s.encode("utf-8")).hexdigest()
115
+
116
+
117
+ def plan_scaffold(
118
+ stack: Stack,
119
+ assembled: list[AssembledFile],
120
+ prior: Manifest | None,
121
+ read_current: Callable[[str], str | None],
122
+ generated_by: str | None = None,
123
+ ) -> ScaffoldDecision:
124
+ """Decide what to write for a (re-)scaffold (pure; FS via ``read_current``).
125
+
126
+ ``generated_by`` is the MetaObjects version doing the scaffold — stamped into
127
+ the persisted manifest so a later ``gen``/``verify`` can detect staleness.
128
+ """
129
+ writes: list[_Write] = []
130
+ conflicts: list[_Conflict] = []
131
+ files: dict[str, str] = {}
132
+
133
+ for f in assembled:
134
+ files[f.path] = hash_contents(f.contents)
135
+ current = read_current(f.path)
136
+ if current is None:
137
+ writes.append(_Write(path=f.path, contents=f.contents))
138
+ continue
139
+ prior_hash = prior.files.get(f.path) if prior else None
140
+ if prior_hash is not None and hash_contents(current) == prior_hash:
141
+ writes.append(_Write(path=f.path, contents=f.contents)) # refresh
142
+ else:
143
+ conflicts.append(
144
+ _Conflict(
145
+ path=f.path, new_path=f"{f.path}.new", contents=f.contents
146
+ )
147
+ )
148
+
149
+ assembled_paths = {f.path for f in assembled}
150
+ removed = (
151
+ [p for p in prior.files if p not in assembled_paths] if prior else []
152
+ )
153
+
154
+ return ScaffoldDecision(
155
+ writes=writes,
156
+ conflicts=conflicts,
157
+ manifest=Manifest(
158
+ version=1,
159
+ servers=list(stack.servers),
160
+ clients=list(stack.clients),
161
+ files=files,
162
+ generated_by=generated_by,
163
+ ),
164
+ removed=removed,
165
+ )
166
+
167
+
168
+ def agent_context_staleness(
169
+ manifest: dict[str, object] | None, current_version: str
170
+ ) -> str | None:
171
+ """One-line nudge if the scaffolded agent context predates the install.
172
+
173
+ Returns ``None`` when there is nothing to say — no agent context here, or it
174
+ is in sync — and a one-line advisory message otherwise. Pure + advisory:
175
+ never raises, never blocks, never writes.
176
+
177
+ The comparison is **exact equality** on purpose: ANY drift nudges (a
178
+ re-scaffold is cheap + idempotent). Do NOT "fix" this into a semver compare —
179
+ a prerelease/build-metadata difference is still a reason to refresh.
180
+ """
181
+ if manifest is None:
182
+ return None # no agent context here → nothing to nudge
183
+ generated_by = manifest.get("generatedBy")
184
+ if generated_by == current_version:
185
+ return None # in sync
186
+ frm = generated_by if generated_by else "an older MetaObjects"
187
+ return (
188
+ f"MetaObjects agent context was generated by {frm}; "
189
+ f"you're on {current_version}. Re-run 'metaobjects agent-docs' to "
190
+ f"refresh the .claude/skills docs."
191
+ )
@@ -0,0 +1,44 @@
1
+ """Stack vocabulary for the agent-context assembler.
2
+
3
+ These constants are the cross-port contract — they must match the TypeScript
4
+ reference (``server/typescript/packages/sdk/src/agent-context/types.ts``) exactly:
5
+ the SERVER/CLIENT orderings determine dedupe order, and the skill list + its
6
+ order is load-bearing for byte-identical output.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ #: Server languages, in canonical dedupe order.
14
+ SERVER_LANGS: tuple[str, ...] = ("typescript", "java", "kotlin", "csharp", "python")
15
+
16
+ #: Client frameworks, in canonical dedupe order.
17
+ CLIENT_FRAMEWORKS: tuple[str, ...] = ("react", "tanstack", "angular")
18
+
19
+ #: Always-present token: schema migrations are TS-owned for every port (ADR-0015).
20
+ MIGRATION_TOKEN = "migration"
21
+
22
+ #: The five skills, in the exact emit order (matches the TS reference).
23
+ SKILL_NAMES: tuple[str, ...] = (
24
+ "metaobjects-authoring",
25
+ "metaobjects-codegen",
26
+ "metaobjects-runtime-ui",
27
+ "metaobjects-prompts",
28
+ "metaobjects-verify",
29
+ )
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Stack:
34
+ """The resolved tech-stack of a consumer project.
35
+
36
+ - ``servers`` — deduped, in :data:`SERVER_LANGS` order.
37
+ - ``clients`` — deduped, in :data:`CLIENT_FRAMEWORKS` order.
38
+ - ``tokens`` — ``servers ∪ clients ∪ {"migration"}``, the install-selection
39
+ set used to choose which reference fragments to emit.
40
+ """
41
+
42
+ servers: tuple[str, ...]
43
+ clients: tuple[str, ...]
44
+ tokens: frozenset[str]
@@ -0,0 +1,23 @@
1
+ """Dependency-free registry: attr subtype -> MetaAttr class. Breaks the meta-data import cycle."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable
5
+
6
+ _ATTR_CLASSES: dict[str, Callable[..., object]] = {}
7
+ _FALLBACK: Callable[..., object] | None = None
8
+
9
+
10
+ def register_attr_class(sub_type: str, ctor: Callable[..., object]) -> None:
11
+ _ATTR_CLASSES[sub_type] = ctor
12
+
13
+
14
+ def register_fallback_attr_class(ctor: Callable[..., object]) -> None:
15
+ global _FALLBACK
16
+ _FALLBACK = ctor
17
+
18
+
19
+ def attr_class_for(sub_type: str) -> Callable[..., object]:
20
+ ctor = _ATTR_CLASSES.get(sub_type, _FALLBACK)
21
+ if ctor is None:
22
+ raise RuntimeError("attr classes not registered (import metaobjects.core_types first)")
23
+ return ctor