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,108 @@
|
|
|
1
|
+
# Java parser-on-receipt
|
|
2
|
+
|
|
3
|
+
For every `template.output`, `codegen-spring`'s `SpringOutputParserGenerator` emits
|
|
4
|
+
a **typed parser** that validates an LLM/raw response against the template's
|
|
5
|
+
`@payloadRef` payload record. This is the receive side only — codegen emits **no**
|
|
6
|
+
provider/LLM-call layer; you compose the call yourself.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
- Wire the generator
|
|
10
|
+
- What it emits
|
|
11
|
+
- The three-step consumer pattern
|
|
12
|
+
- Recommended LLM caller (bring-your-own)
|
|
13
|
+
- Drift gate
|
|
14
|
+
|
|
15
|
+
## Wire the generator
|
|
16
|
+
|
|
17
|
+
Add `SpringOutputParserGenerator` (alongside `SpringPayloadGenerator`, which emits
|
|
18
|
+
the payload record it parses into) to the Maven plugin's `<generators>` list:
|
|
19
|
+
|
|
20
|
+
```xml
|
|
21
|
+
<generator>
|
|
22
|
+
<classname>com.metaobjects.generator.spring.SpringPayloadGenerator</classname>
|
|
23
|
+
<args><outputDir>${project.build.directory}/generated-sources/java</outputDir></args>
|
|
24
|
+
</generator>
|
|
25
|
+
<generator>
|
|
26
|
+
<classname>com.metaobjects.generator.spring.SpringOutputParserGenerator</classname>
|
|
27
|
+
<args><outputDir>${project.build.directory}/generated-sources/java</outputDir></args>
|
|
28
|
+
</generator>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What it emits
|
|
32
|
+
|
|
33
|
+
Per `template.output`, `mvn metaobjects:generate` writes a `<Name>Parser` class with a static
|
|
34
|
+
`parse` method returning the `@payloadRef` payload record. The strict path throws
|
|
35
|
+
`com.fasterxml.jackson.core.JsonProcessingException` on malformed input:
|
|
36
|
+
|
|
37
|
+
```java
|
|
38
|
+
// generated <Name>Parser.java (shape)
|
|
39
|
+
public final class NpcResponseParser {
|
|
40
|
+
private NpcResponseParser() { } // no instances
|
|
41
|
+
|
|
42
|
+
public static NpcResponsePayload parse(String text) throws JsonProcessingException {
|
|
43
|
+
// Jackson-backed: validates the text against the payload record
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For `@format: json|xml` outputs the generator also emits a tolerant best-effort
|
|
49
|
+
variant (`extractLenient(...)` returning an `ExtractionResult<NpcResponsePayload>`
|
|
50
|
+
from `com.metaobjects.render.extract`) for cases where you want classification
|
|
51
|
+
rather than a throw. The payload record itself comes from `SpringPayloadGenerator`
|
|
52
|
+
— the parser is a companion to it, so the parser and payload VO can't silently
|
|
53
|
+
drift.
|
|
54
|
+
|
|
55
|
+
## The three-step consumer pattern
|
|
56
|
+
|
|
57
|
+
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
58
|
+
here) → parse the response with the generated parser:
|
|
59
|
+
|
|
60
|
+
```java
|
|
61
|
+
String promptText = Renderer.render(/* template ref + payload + provider */);
|
|
62
|
+
String llmResponse = myLlmClient.call(promptText); // YOUR code — no generated provider
|
|
63
|
+
|
|
64
|
+
NpcResponsePayload npc = NpcResponseParser.parse(llmResponse); // throws on bad shape
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Recommended LLM caller (bring-your-own)
|
|
68
|
+
|
|
69
|
+
`codegen-spring` emits **no** provider/LLM-call layer and never will — calling is a
|
|
70
|
+
commodity the ecosystem already solves, and a maintained vendor wrapper would chase
|
|
71
|
+
SDK churn (ADR-0024). You bring the caller; MetaObjects owns the typed render →
|
|
72
|
+
parse (above) → record. For the call step use the idiomatic JVM library:
|
|
73
|
+
|
|
74
|
+
```java
|
|
75
|
+
// Spring AI (recommended) — provider-agnostic ChatClient; YOUR code, no generated provider
|
|
76
|
+
String response = chatClient.prompt()
|
|
77
|
+
.system(systemText)
|
|
78
|
+
.user(promptText)
|
|
79
|
+
.call()
|
|
80
|
+
.content();
|
|
81
|
+
|
|
82
|
+
NpcResponsePayload npc = NpcResponseParser.parse(response); // the generated parser, above
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Recommended: Spring AI** (`ChatClient`) — Spring-native, provider-agnostic
|
|
86
|
+
(Anthropic / OpenAI / Azure / Bedrock / Ollama), fits an OMDB + Spring-tx app.
|
|
87
|
+
Non-Spring JVM apps: **LangChain4j** (`ChatLanguageModel.generate(prompt)`) is the
|
|
88
|
+
equivalent one-call seam.
|
|
89
|
+
|
|
90
|
+
> The typed-trace recorder + render→call→record convenience loop ship in TypeScript
|
|
91
|
+
> today; the JVM port is planned (ADR-0024). Until then the call is your code and the
|
|
92
|
+
> generated parser is the typed receive side.
|
|
93
|
+
|
|
94
|
+
## Drift gate
|
|
95
|
+
|
|
96
|
+
The render module's static `Verify.check(String templateText, List<PayloadField> fields, VerifyOptions options)`
|
|
97
|
+
walks a Mustache template's tokens against a payload field tree and returns a
|
|
98
|
+
`List<VerifyError>` — each `{{...}}` reference that doesn't resolve against the
|
|
99
|
+
payload fields yields an error (empty list = no drift). `VerifyOptions.empty()`
|
|
100
|
+
supplies a fully-defaulted options record; `VerifyOptions(provider, requiredSlots,
|
|
101
|
+
requiredTags)` adds partial resolution and required-slot/-tag checks. For the
|
|
102
|
+
output-format fragment specifically, `Verify.checkOutputPrompt(String fragment,
|
|
103
|
+
List<String> requiredFieldNames)` returns a `List<VerifyError>` for every required
|
|
104
|
+
field name absent from the rendered fragment. Assert the returned list is empty in a
|
|
105
|
+
JUnit test in the Maven `test` phase to fail the build on prompt/payload drift. The
|
|
106
|
+
`metaobjects:verify` codegen-drift goal additionally catches a stale committed
|
|
107
|
+
parser. The emitted parser imports Jackson (`com.fasterxml.jackson`), normally
|
|
108
|
+
already on a Spring Boot classpath.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Kotlin parser-on-receipt
|
|
2
|
+
|
|
3
|
+
For every `template.output`, `codegen-kotlin`'s `KotlinOutputParserGenerator` emits
|
|
4
|
+
a **typed parser** that validates an LLM/raw response against the template's
|
|
5
|
+
`@payloadRef` payload data class. This is the receive side only — codegen emits
|
|
6
|
+
**no** provider/LLM-call layer; you compose the call yourself. The payload data
|
|
7
|
+
class itself comes from `KotlinPayloadGenerator` (a `@Serializable data class`), so
|
|
8
|
+
the parser and the payload VO can't silently drift.
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
- Wire the generators
|
|
12
|
+
- What it emits
|
|
13
|
+
- The three-step consumer pattern
|
|
14
|
+
- Consumer dependency
|
|
15
|
+
- Recommended LLM caller (bring-your-own)
|
|
16
|
+
- Drift gate
|
|
17
|
+
|
|
18
|
+
## Wire the generators
|
|
19
|
+
|
|
20
|
+
Add `KotlinOutputParserGenerator` (alongside `KotlinPayloadGenerator`, which emits
|
|
21
|
+
the payload it parses into) to the Maven plugin's `<generators>` list:
|
|
22
|
+
|
|
23
|
+
```xml
|
|
24
|
+
<generator>
|
|
25
|
+
<classname>com.metaobjects.generator.kotlin.KotlinPayloadGenerator</classname>
|
|
26
|
+
<args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
|
|
27
|
+
</generator>
|
|
28
|
+
<generator>
|
|
29
|
+
<classname>com.metaobjects.generator.kotlin.KotlinOutputParserGenerator</classname>
|
|
30
|
+
<args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
|
|
31
|
+
</generator>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What it emits
|
|
35
|
+
|
|
36
|
+
Per `template.output`, `mvn metaobjects:generate` writes a `<Name>Parser.kt`
|
|
37
|
+
`object` with a dual API matching kotlinx.serialization's exception model plus the
|
|
38
|
+
Kotlin stdlib `Result<T>` convention:
|
|
39
|
+
|
|
40
|
+
```kotlin
|
|
41
|
+
// generated <Name>Parser.kt (shape)
|
|
42
|
+
object NpcResponseParser {
|
|
43
|
+
private val json: Json = Json { ignoreUnknownKeys = false }
|
|
44
|
+
|
|
45
|
+
/** Throws kotlinx.serialization.SerializationException on bad input. */
|
|
46
|
+
fun parseNpcResponse(text: String): NpcResponsePayload =
|
|
47
|
+
json.decodeFromString<NpcResponsePayload>(text)
|
|
48
|
+
|
|
49
|
+
/** Result-style — does not throw. */
|
|
50
|
+
fun safeParseNpcResponse(text: String): Result<NpcResponsePayload> =
|
|
51
|
+
runCatching { parseNpcResponse(text) }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For `@format: json|xml` outputs the generator additionally emits a **tolerant**
|
|
56
|
+
best-effort variant — `extractLenient(...)` returning an
|
|
57
|
+
`ExtractionResult<NpcResponseExtracted>` (from `com.metaobjects.render.extract`) for
|
|
58
|
+
cases where you want a classified per-field report rather than a throw. There are
|
|
59
|
+
two overloads: a self-contained one (scalars/enums only; nested components stay
|
|
60
|
+
null) and a `extractLenient(loader, text)` overload that delegates to the runtime
|
|
61
|
+
`MetaObjectExtractor` to fully populate nested-object and array-of-object
|
|
62
|
+
components. The lenient mirror type (`<Name>Extracted`) uses nullable fields per
|
|
63
|
+
the Kotlin null-safety port — a missing/malformed component is `null`, not a throw.
|
|
64
|
+
|
|
65
|
+
## The three-step consumer pattern
|
|
66
|
+
|
|
67
|
+
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
68
|
+
here) → parse the response with the generated parser:
|
|
69
|
+
|
|
70
|
+
```kotlin
|
|
71
|
+
val response: String = myLlmClient.complete(promptText) // YOUR code — no generated provider
|
|
72
|
+
|
|
73
|
+
// Throwing path — propagate to your error handler
|
|
74
|
+
val npc = NpcResponseParser.parseNpcResponse(response)
|
|
75
|
+
|
|
76
|
+
// Or Result-style
|
|
77
|
+
NpcResponseParser.safeParseNpcResponse(response)
|
|
78
|
+
.onSuccess { npc -> /* use it */ }
|
|
79
|
+
.onFailure { ex -> log.warn("LLM returned malformed payload", ex) }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Consumer dependency
|
|
83
|
+
|
|
84
|
+
The emitted parser imports `kotlinx.serialization.json.Json` and calls
|
|
85
|
+
`Json.decodeFromString<T>(text)`. The `kotlinx-serialization-core` artifact alone
|
|
86
|
+
(which `@Serializable` needs) does NOT include the JSON format — add the JSON
|
|
87
|
+
artifact + the serialization plugin:
|
|
88
|
+
|
|
89
|
+
```kotlin
|
|
90
|
+
plugins { kotlin("plugin.serialization") version "1.9.x" }
|
|
91
|
+
dependencies {
|
|
92
|
+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.x")
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Recommended LLM caller (bring-your-own)
|
|
97
|
+
|
|
98
|
+
`codegen-kotlin` emits **no** provider/LLM-call layer and never will — calling is a
|
|
99
|
+
commodity the ecosystem already solves (ADR-0024). You bring the caller; MetaObjects
|
|
100
|
+
owns the typed render → parse (above) → record. For the call step use the idiomatic
|
|
101
|
+
JVM library — Kotlin calls either directly:
|
|
102
|
+
|
|
103
|
+
```kotlin
|
|
104
|
+
// Spring AI (recommended) — provider-agnostic ChatClient; YOUR code, no generated provider
|
|
105
|
+
val response: String = chatClient.prompt()
|
|
106
|
+
.system(systemText)
|
|
107
|
+
.user(promptText)
|
|
108
|
+
.call()
|
|
109
|
+
.content()
|
|
110
|
+
|
|
111
|
+
val npc = NpcResponseParser.parseNpcResponse(response) // the generated parser, above
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Recommended: Spring AI** (`ChatClient`) for a Spring app, or **LangChain4j**
|
|
115
|
+
(`ChatLanguageModel.generate(prompt)`) for non-Spring JVM — both provider-agnostic,
|
|
116
|
+
both a one-call seam Kotlin uses idiomatically.
|
|
117
|
+
|
|
118
|
+
> The typed-trace recorder + render→call→record convenience loop ship in TypeScript
|
|
119
|
+
> today; the JVM port is planned (ADR-0024). Until then the call is your code and the
|
|
120
|
+
> generated parser is the typed receive side.
|
|
121
|
+
|
|
122
|
+
## Drift gate
|
|
123
|
+
|
|
124
|
+
The render engine is the Java `metaobjects-render` module (Kotlin wraps it). Its
|
|
125
|
+
static `Verify.check(...)` walks a Mustache template's tokens against the payload
|
|
126
|
+
field tree and returns a list of errors for any `{{...}}` reference that doesn't
|
|
127
|
+
resolve against the payload — empty list = no drift. Assert it is empty in a JUnit
|
|
128
|
+
test in the Maven `test` phase to fail the build on prompt/payload drift. The
|
|
129
|
+
`metaobjects:verify` codegen-drift goal additionally catches a stale committed
|
|
130
|
+
parser.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Python parser-on-receipt
|
|
2
|
+
|
|
3
|
+
For every `template.output`, the `output-parser` generator (run via `metaobjects gen`)
|
|
4
|
+
emits a **typed parser** that validates an LLM/raw response against the template's
|
|
5
|
+
`@payloadRef` payload Pydantic model. This is the receive side only — codegen emits
|
|
6
|
+
**no** provider/LLM-call layer; you compose the call yourself. The payload class comes
|
|
7
|
+
from the sibling `payload` generator, so the parser and the payload VO can't silently
|
|
8
|
+
drift.
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
- Wire the generators
|
|
12
|
+
- What it emits
|
|
13
|
+
- The three-step consumer pattern
|
|
14
|
+
- Recommended LLM caller (bring-your-own)
|
|
15
|
+
- Consumer dependency
|
|
16
|
+
- Drift gate
|
|
17
|
+
|
|
18
|
+
## Wire the generators
|
|
19
|
+
|
|
20
|
+
Select `output-parser` (the `payload` generator that emits the `<Name>Payload` it
|
|
21
|
+
parses into runs alongside it):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
metaobjects gen ./metadata --out ./generated --generators payload,output-parser
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`metaobjects gen --list` shows every generator name; the programmatic
|
|
28
|
+
`run_gen(..., generators=[...])` path takes the same names.
|
|
29
|
+
|
|
30
|
+
## What it emits
|
|
31
|
+
|
|
32
|
+
Per `template.output`, `metaobjects gen` writes one `<template_name>_output_parser.py`
|
|
33
|
+
with a single throw-only entry point. Python uses one API (not TS's
|
|
34
|
+
`parse`/`safeParse`) because raising `pydantic.ValidationError` is the idiomatic
|
|
35
|
+
failure — the pydantic / Instructor / FastAPI norm; a Result-style wrapper would be
|
|
36
|
+
un-Pythonic.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# generated <template_name>_output_parser.py (shape)
|
|
40
|
+
from .npc_response_payload import NpcResponsePayload # the @payloadRef VO (Pydantic v2 BaseModel)
|
|
41
|
+
|
|
42
|
+
def parse_npc_response(text: str) -> NpcResponsePayload:
|
|
43
|
+
"""Validates text against the payload model.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
pydantic.ValidationError: when the input does not match the schema.
|
|
47
|
+
"""
|
|
48
|
+
...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For `@format: json|xml` outputs the generator additionally emits a **tolerant**
|
|
52
|
+
best-effort variant — `extract_lenient_<name>(text) -> ExtractionResult[<Name>PayloadExtracted]`
|
|
53
|
+
(from the `metaobjects` render `extract` engine) for cases where you want a classified
|
|
54
|
+
per-field report rather than a raise. The lenient mirror (`<Name>PayloadExtracted`)
|
|
55
|
+
uses `Optional[...]` fields — a missing/malformed component is `None`, not a raise.
|
|
56
|
+
|
|
57
|
+
## The three-step consumer pattern
|
|
58
|
+
|
|
59
|
+
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
60
|
+
here) → parse the response with the generated parser:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pydantic import ValidationError
|
|
64
|
+
from .npc_response_output_parser import parse_npc_response
|
|
65
|
+
|
|
66
|
+
text = my_llm_client.complete(prompt_text) # YOUR code — no generated provider
|
|
67
|
+
try:
|
|
68
|
+
npc = parse_npc_response(text) # raises pydantic.ValidationError on bad shape
|
|
69
|
+
except ValidationError as exc:
|
|
70
|
+
log.warning("LLM returned malformed payload: %s", exc)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Recommended LLM caller (bring-your-own)
|
|
74
|
+
|
|
75
|
+
`metaobjects gen` emits **no** provider/LLM-call layer and never will — calling is a
|
|
76
|
+
commodity the ecosystem already solves (ADR-0024). You bring the caller; MetaObjects
|
|
77
|
+
owns the typed render → parse (above) → record. For the call step use the idiomatic
|
|
78
|
+
Python library:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from litellm import completion # LiteLLM — recommended
|
|
82
|
+
|
|
83
|
+
resp = completion(
|
|
84
|
+
model="anthropic/claude-3-5-sonnet",
|
|
85
|
+
messages=[{"role": "system", "content": system_text},
|
|
86
|
+
{"role": "user", "content": prompt_text}],
|
|
87
|
+
)
|
|
88
|
+
text = resp.choices[0].message.content
|
|
89
|
+
|
|
90
|
+
npc = parse_npc_response(text) # the generated parser, above
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Recommended: LiteLLM** — one OpenAI-shaped `completion()` over 100+ providers; the
|
|
94
|
+
raw text feeds straight into MetaObjects' typed parser/extract. If you want the *LLM*
|
|
95
|
+
to enforce the typed shape instead, **Instructor** or **Pydantic-AI** return a
|
|
96
|
+
validated Pydantic model — but that overlaps MetaObjects' own typed `extract`, so pick
|
|
97
|
+
one boundary, not both.
|
|
98
|
+
|
|
99
|
+
> The typed-trace recorder + render→call→record convenience loop ship in TypeScript
|
|
100
|
+
> today; the Python port is planned (ADR-0024). Until then the call is your code and
|
|
101
|
+
> the generated parser is the typed receive side.
|
|
102
|
+
|
|
103
|
+
## Consumer dependency
|
|
104
|
+
|
|
105
|
+
The generated parser imports `pydantic` (v2 — `model_validate_json`); `pip install
|
|
106
|
+
pydantic`. The tolerant `extract_lenient_*` variant pulls the `metaobjects` render
|
|
107
|
+
`extract` engine, already in the `metaobjects` package.
|
|
108
|
+
|
|
109
|
+
## Drift gate
|
|
110
|
+
|
|
111
|
+
The render module's `verify(template_text, fields, *, provider=None,
|
|
112
|
+
required_slots=None, required_tags=None) -> list[VerifyError]` walks a Mustache
|
|
113
|
+
template's tokens against the payload field tree — each `{{...}}` reference that
|
|
114
|
+
doesn't resolve yields a `VerifyError` (empty list = no drift). Assert it is empty in a
|
|
115
|
+
pytest test to fail the build on prompt/payload drift; `metaobjects verify`
|
|
116
|
+
additionally catches a stale committed parser.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# TypeScript parser-on-receipt
|
|
2
|
+
|
|
3
|
+
For every `template.output` in your metadata, the `outputParser()` generator (from
|
|
4
|
+
`@metaobjectsdev/codegen-ts/generators`) emits a **typed parser** that validates an
|
|
5
|
+
LLM/raw response against the template's `@payloadRef` payload value-object. This is
|
|
6
|
+
the receive side; codegen emits **no** provider/LLM-call layer — you compose the
|
|
7
|
+
call yourself.
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
- Wire the generator
|
|
11
|
+
- What it emits
|
|
12
|
+
- The three-step consumer pattern
|
|
13
|
+
- Recommended LLM caller (bring-your-own)
|
|
14
|
+
- Drift gate
|
|
15
|
+
- See which fields a template consumes
|
|
16
|
+
|
|
17
|
+
## Wire the generator
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// metaobjects.config.ts
|
|
21
|
+
import { defineConfig } from "@metaobjectsdev/cli";
|
|
22
|
+
import { entityFile, queriesFile, barrel, promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators";
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
outDir: "src/generated",
|
|
26
|
+
generators: [
|
|
27
|
+
entityFile(), queriesFile(), barrel(),
|
|
28
|
+
promptRender(), // render<Name>() per template.prompt (the send side)
|
|
29
|
+
outputParser(), // parse*/safeParse* per template.output (the receive side)
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`outputParser({ outDir: "src/generated/outputs", target: "default" })` are the
|
|
35
|
+
options.
|
|
36
|
+
|
|
37
|
+
## What it emits
|
|
38
|
+
|
|
39
|
+
Per `template.output` (say named `NpcResponseOutput`, `@payloadRef:
|
|
40
|
+
"NpcResponsePayload"`), `meta gen` writes a self-contained `NpcResponseOutput.output.ts`
|
|
41
|
+
with a Zod schema + a dual API:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { z } from "zod";
|
|
45
|
+
|
|
46
|
+
const NpcResponseOutputSchema = z.object({
|
|
47
|
+
name: z.string(),
|
|
48
|
+
level: z.number().int(),
|
|
49
|
+
role: z.unknown(), // field.enum is not value-constrained in the strict parser → z.unknown()
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type NpcResponseOutputData = z.infer<typeof NpcResponseOutputSchema>;
|
|
53
|
+
export type NpcResponseOutputValidationError = z.ZodError;
|
|
54
|
+
|
|
55
|
+
/** Throws ZodError on bad input. */
|
|
56
|
+
export function parseNpcResponseOutput(text: string): NpcResponseOutputData { /* ... */ }
|
|
57
|
+
|
|
58
|
+
/** Result-style; never throws. */
|
|
59
|
+
export function safeParseNpcResponseOutput(text: string):
|
|
60
|
+
| { success: true; data: NpcResponseOutputData }
|
|
61
|
+
| { success: false; error: NpcResponseOutputValidationError } { /* ... */ }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The dual API mirrors Zod's idiomatic shape: `parse*` throws a `ZodError`,
|
|
65
|
+
`safeParse*` returns a discriminated union. The emitted `<Name>Data` type is
|
|
66
|
+
structurally identical to the `promptRender()` payload VO, so you can pass values
|
|
67
|
+
between the render and parse sides interchangeably.
|
|
68
|
+
|
|
69
|
+
Field-type → Zod mapping: `field.string` → `z.string()`; `field.int`/`long`/`short`/`byte`
|
|
70
|
+
→ `z.number().int()`; `field.double`/`float` → `z.number()`; `field.boolean` →
|
|
71
|
+
`z.boolean()`; `field.object` (with `@objectRef`) → a nested `z.object({...})`;
|
|
72
|
+
`isArray: true` → wrapped in `z.array(...)`. Any subtype outside this scalar set —
|
|
73
|
+
including `field.enum` — falls through to `z.unknown()` in the strict
|
|
74
|
+
`parse*`/`safeParse*` schema (the value-constrained `z.enum([...])` form is emitted
|
|
75
|
+
in the entity insert/update schemas, not in this output parser; the lenient extract
|
|
76
|
+
path carries the enum-as-string handling).
|
|
77
|
+
|
|
78
|
+
## The three-step consumer pattern
|
|
79
|
+
|
|
80
|
+
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
81
|
+
here) → parse the response with the generated parser:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { renderNpcPrompt } from "./generated/prompts";
|
|
85
|
+
import { parseNpcResponseOutput, safeParseNpcResponseOutput } from "./generated/NpcResponseOutput.output";
|
|
86
|
+
|
|
87
|
+
const promptText = renderNpcPrompt(payload, textProvider);
|
|
88
|
+
const llmResponse = await myLlmProvider.call(promptText); // YOUR code — no generated provider
|
|
89
|
+
|
|
90
|
+
// Throwing path:
|
|
91
|
+
const npc = parseNpcResponseOutput(llmResponse);
|
|
92
|
+
|
|
93
|
+
// Result-style:
|
|
94
|
+
const r = safeParseNpcResponseOutput(llmResponse);
|
|
95
|
+
if (!r.success) log.warn("malformed LLM payload", r.error);
|
|
96
|
+
else handle(r.data);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Recommended LLM caller (bring-your-own)
|
|
100
|
+
|
|
101
|
+
MetaObjects generates **no** provider/LLM-call layer and never will — the call is a
|
|
102
|
+
commodity the ecosystem already solves, and a maintained vendor wrapper would just
|
|
103
|
+
chase SDK churn (ADR-0024). You bring the caller; MetaObjects owns the typed
|
|
104
|
+
render → parse → record. For the call step, plug the idiomatic library into the
|
|
105
|
+
one-method `LlmClient` seam from `@metaobjectsdev/ai-runtime`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { generateText } from "ai"; // Vercel AI SDK — recommended
|
|
109
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
110
|
+
import type { LlmClient } from "@metaobjectsdev/ai-runtime";
|
|
111
|
+
|
|
112
|
+
export const llm: LlmClient = {
|
|
113
|
+
async complete({ prompt, model, system }) {
|
|
114
|
+
const { text } = await generateText({ model: anthropic(model), system, prompt });
|
|
115
|
+
return { body: text }; // also map usage/model/finishReason from the result → cost + token columns
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Recommended: the Vercel AI SDK (`ai`)** — provider-agnostic (`generateText` /
|
|
121
|
+
`generateObject` over Anthropic / OpenAI / Google / …), first-class structured
|
|
122
|
+
output, the de-facto TS standard. Single-provider apps can implement `complete`
|
|
123
|
+
directly over `@anthropic-ai/sdk` / `openai`; heavy chains can use LangChain.js —
|
|
124
|
+
the seam is one method either way.
|
|
125
|
+
|
|
126
|
+
With a client in hand, the generated `record<Entity>` helper (the `trace-helper`
|
|
127
|
+
generator) and `@metaobjectsdev/ai-runtime`'s `callLlm` do render → call → **typed
|
|
128
|
+
trace persist** in one call, recording request/response as typed value objects in
|
|
129
|
+
your own DB. The parser above is the standalone receive side if you don't want the
|
|
130
|
+
recorder.
|
|
131
|
+
|
|
132
|
+
## Drift gate
|
|
133
|
+
|
|
134
|
+
`meta verify` walks every `template.output`'s `@payloadRef` resolution and fails the
|
|
135
|
+
build (exit 1, `(output)`-prefixed diagnostic) if a reference can't be resolved —
|
|
136
|
+
catching payload-VO ↔ parser drift at build time. The emitted parser imports `zod`;
|
|
137
|
+
it's usually already a dependency (Drizzle / `runtime-ts` lean on it), else
|
|
138
|
+
`npm i zod`.
|
|
139
|
+
|
|
140
|
+
## See which fields a template consumes
|
|
141
|
+
|
|
142
|
+
Run `meta docs` to emit the model surface to `./docs` — one page per
|
|
143
|
+
`template.output` at `docs/<Template>.md`. Each template page has a
|
|
144
|
+
`## Template source` section that shows the Mustache source with every `{{var}}`
|
|
145
|
+
linked to that field's doc page (`docs/<Owner>.md#field-<name>`), plus a variables
|
|
146
|
+
table — so you can see exactly which payload fields a template reads. Those links are
|
|
147
|
+
build-time drift-gated against the render `verify()` engine, so a link can't claim a
|
|
148
|
+
field that `verify()` would reject. (`meta docs` is the single door for all docs —
|
|
149
|
+
it's a command, not a `meta gen` generator; the model surface here lands in `./docs`,
|
|
150
|
+
the SDK/API surface under `./docs/api`.)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: metaobjects-runtime-ui
|
|
3
|
+
description: Use when wiring MetaObjects generated code into an app: runtime queries/CRUD, REST routes, and the web client (forms, grids, filters).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Wiring MetaObjects runtime + web client
|
|
7
|
+
|
|
8
|
+
This skill is the procedure for putting generated code to work: querying/persisting
|
|
9
|
+
through the runtime tier, serving the REST contract, and consuming it from the
|
|
10
|
+
universal web client. It is port-agnostic — the cross-cutting contracts are here;
|
|
11
|
+
the server- and client-specific imports/types live in reference fragments (pointed
|
|
12
|
+
to at the bottom).
|
|
13
|
+
|
|
14
|
+
## The runtime return-type contract
|
|
15
|
+
|
|
16
|
+
A port's runtime returns **native in-process language types**, never wire strings.
|
|
17
|
+
Query the runtime and you get back the language's natural type for each field:
|
|
18
|
+
|
|
19
|
+
- `field.decimal` → the language's exact-decimal type (`BigDecimal` / `decimal` /
|
|
20
|
+
`Decimal`). Decimal is **lossless end-to-end** — no float round-tripping.
|
|
21
|
+
- temporal fields → native date/instant types.
|
|
22
|
+
- `field.object` (jsonb) → a native map/object.
|
|
23
|
+
- **TypeScript is the documented outlier: `field.decimal` is a `string`** in the
|
|
24
|
+
TS runtime (JS has no native exact decimal), preserving precision as text.
|
|
25
|
+
|
|
26
|
+
Wire canonicalization (integer minor units for currency, ISO-8601 strings for
|
|
27
|
+
temporals, canonical hex for UUID) is applied only at the **serialization
|
|
28
|
+
boundary** — when a row leaves over HTTP — never inside the runtime query path. So:
|
|
29
|
+
compute with native types in-process; the HTTP layer handles wire encoding.
|
|
30
|
+
|
|
31
|
+
## Generated repo / query helpers take the context as a parameter
|
|
32
|
+
|
|
33
|
+
Generated finders and CRUD helpers **do not** reach for a module-level `db`
|
|
34
|
+
singleton. They take the persistence context (connection / session / data-access
|
|
35
|
+
handle) as an explicit **parameter**. You own the connection's lifecycle and pass
|
|
36
|
+
it in:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
const rows = await findPostsForAuthor(db, authorId); // db is passed, not imported
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This keeps generated code free of global state, makes it testable, and lets one
|
|
43
|
+
process talk to multiple databases (multi-tenant, read-replica). Construct/own the
|
|
44
|
+
context in your app; thread it through every generated call.
|
|
45
|
+
|
|
46
|
+
## The REST contract
|
|
47
|
+
|
|
48
|
+
Generated (or hand-written) routes speak one cross-port HTTP contract so the same
|
|
49
|
+
universal web client serves any backend language.
|
|
50
|
+
|
|
51
|
+
### URL grammar
|
|
52
|
+
|
|
53
|
+
`apiPrefix` (default `/api`, set in project config) flows to both the server routes
|
|
54
|
+
and the client fetch URLs. `<entity>` is lowercased + pluralized (`Author` →
|
|
55
|
+
`authors`).
|
|
56
|
+
|
|
57
|
+
| Verb | Path | Purpose |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `GET` | `/<apiPrefix>/<entity>?filter[...][...]=...&sort=...&limit=N&offset=N&withCount=1` | List |
|
|
60
|
+
| `GET` | `/<apiPrefix>/<entity>/:id` | Get by id |
|
|
61
|
+
| `POST` | `/<apiPrefix>/<entity>` | Create (201) |
|
|
62
|
+
| `PATCH` | `/<apiPrefix>/<entity>/:id` | Update (partial) |
|
|
63
|
+
| `PUT` | `/<apiPrefix>/<entity>/:id` | Update (replace) — optional |
|
|
64
|
+
| `DELETE` | `/<apiPrefix>/<entity>/:id` | Delete (204) |
|
|
65
|
+
|
|
66
|
+
### Filter operators by field subtype
|
|
67
|
+
|
|
68
|
+
Filters use a bracketed qs: `filter[<field>][<op>]=<value>`. A bare
|
|
69
|
+
`filter[<field>]=<value>` is sugar for `eq`. The operator set is **gated by field
|
|
70
|
+
subtype** via the generated `<Entity>FilterAllowlist` — an unsupported operator for
|
|
71
|
+
a field → HTTP 400.
|
|
72
|
+
|
|
73
|
+
| Operator | Strings | Numbers / Dates | Booleans |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `eq`, `ne`, `isNull` | yes | yes | `eq` + `isNull` only |
|
|
76
|
+
| `in`, `like` | yes | `in` only | – |
|
|
77
|
+
| `gt`, `gte`, `lt`, `lte` | – | yes | – |
|
|
78
|
+
|
|
79
|
+
These eight (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) are the whole
|
|
80
|
+
closed set — every port implements these and only these.
|
|
81
|
+
|
|
82
|
+
### Sort + pagination
|
|
83
|
+
|
|
84
|
+
- `sort=<field>:asc|desc` — single sort key; the field must be in
|
|
85
|
+
`<Entity>SortAllowlist`.
|
|
86
|
+
- `limit=N` / `offset=N` — page size + offset, identical across every endpoint.
|
|
87
|
+
- `withCount=1` — switches the list response from `[<row>...]` to
|
|
88
|
+
`{ rows: [...], total: N }` (grids always send it).
|
|
89
|
+
|
|
90
|
+
### Wire format
|
|
91
|
+
|
|
92
|
+
JSON bodies (`application/json; charset=utf-8`). Single-row responses (`GET /:id`,
|
|
93
|
+
`POST`, `PATCH`, `PUT`) have **no envelope** — the body is the row. Type encodings:
|
|
94
|
+
|
|
95
|
+
| Field type | JSON | Notes |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `field.string` / `field.uuid` / `field.enum` | string | UUID is canonical hex `8-4-4-4-12` |
|
|
98
|
+
| `field.int` / `field.long` / `field.double` | number | `long` may be string on overflow (per-port) |
|
|
99
|
+
| `field.boolean` | boolean | |
|
|
100
|
+
| `field.date` | string | ISO 8601 `YYYY-MM-DD` |
|
|
101
|
+
| `field.timestamp` | string | ISO 8601 with timezone |
|
|
102
|
+
| `field.currency` | **integer minor units** | cents/yen; float arithmetic forbidden; server never formats |
|
|
103
|
+
| `field.object` | object | per the sub-object schema |
|
|
104
|
+
|
|
105
|
+
The currency invariant is load-bearing: integer minor units on the wire, always.
|
|
106
|
+
Formatting happens client-side with locale-aware code.
|
|
107
|
+
|
|
108
|
+
Errors: non-2xx returns `{ "error": "<short_code>", "message"?: "..." }`. 400 for
|
|
109
|
+
validation/filter-parser errors, 404 for not-found (`{ "error": "not_found" }`),
|
|
110
|
+
5xx implementation-defined. Treat any 4xx as user-facing, any 5xx as retryable.
|
|
111
|
+
|
|
112
|
+
## The EntityFetcher browser contract
|
|
113
|
+
|
|
114
|
+
The web client never calls `fetch` directly. Every generated hook delegates to a
|
|
115
|
+
single `EntityFetcher` you supply once at the app root:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
export type EntityFetcher = <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Your fetcher resolves the `path` (always starting with `apiPrefix`) to a full URL,
|
|
122
|
+
attaches auth (cookie / bearer / API key) per your policy, parses the JSON, and
|
|
123
|
+
throws on non-2xx (the hooks rely on the throw for error state). Provide it via the
|
|
124
|
+
fetcher-provider at the tree root; every generated hook reads it from context. The
|
|
125
|
+
generated grid and form components, filter-qs serializer, and cell renderers all
|
|
126
|
+
sit on top of this one seam.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
For this project's runtime + web-client specifics, read every `references/*.md` file in this skill's directory (one per server language and client framework in this project's stack).
|