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.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- 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
|