From f98a984a3bb28153f261f440e56230f0b2199632 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Mon, 26 Oct 2026 11:48:12 -0400 Subject: [PATCH] docs: align 2.0 integrations and converter contracts --- .github/workflows/ci.yml | 3 + PUBLISHING.md | 5 +- analysis_options.yaml | 2 +- docs/getting-started/installation.mdx | 4 +- docs/getting-started/migration-guide.mdx | 10 +-- .../creating-schema-converter-packages.mdx | 78 ++++++++++++------- docs/guides/ecosystem-integrations.mdx | 70 +++++++++++------ docs/guides/schema-converter-quickstart.mdx | 27 ++++--- llms.txt | 2 +- packages/ack_firebase_ai/CHANGELOG.md | 7 ++ packages/ack_firebase_ai/README.md | 7 +- .../ack_firebase_ai/analysis_options.yaml | 2 + packages/ack_mcp_dart/CHANGELOG.md | 7 ++ packages/ack_mcp_dart/README.md | 4 +- scripts/stage_min_sdk_workspace.dart | 10 +-- 15 files changed, 153 insertions(+), 85 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28e87c82..fca741dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,10 +46,13 @@ jobs: run: | dart test --platform chrome \ test/schemas/numeric_platform_parity_test.dart \ + test/schemas/duration_range_test.dart \ + test/schemas/nullability_export_contract_test.dart \ test/schemas/extensions/numeric_extensions_test.dart \ test/schemas/extensions/list_schema_extensions_test.dart \ test/utils/collection_utils_test.dart \ test/json_schema_import_test.dart \ + test/json_schema_vocabulary_contract_test.dart \ test/json_schema_import_api_test.dart - name: Test JSON Schema import bridge in Chrome diff --git a/PUBLISHING.md b/PUBLISHING.md index d1a4e064..8c690a47 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -36,8 +36,9 @@ For example, packages at 1.3.0 can still declare `ack: ^1.2.0` when they use onl Pub workspaces manage one local dependency resolution, not package versions. Keep hosted version constraints on sibling dependencies and `resolution: workspace` in members; do not add local path overrides to publishable manifests. -Keep explicit workspace paths because glob patterns require Dart 3.11 and this -repository supports Dart 3.9. The root and `ack_example` are private and are not +Keep the explicit workspace paths aligned with the package list. The supported +floor is Dart 3.11.0 (Flutter 3.41.2 for the Firebase adapter); preflight checks +those versions separately. The root and `ack_example` are private and are not part of the five-package release. References: [Melos versioning](https://melos.invertase.dev/commands/version), diff --git a/analysis_options.yaml b/analysis_options.yaml index c80f7ac5..1f25b11c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -36,7 +36,7 @@ dart_code_metrics: prefer-prefixed-global-constants: false avoid-returning-widgets: false prefer-trailing-comma: false - # Enum dot-shorthands require Dart 3.10; the workspace SDK floor is 3.9. + # Keep explicit enum qualification as the workspace style. prefer-shorthands-with-enums: false # Schema classes extend a non-const AckSchema base, so they cannot declare # const constructors. diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 102a32ce..ada4a01e 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -91,4 +91,6 @@ Import `package:ack/ack.dart` and you're ready — the [Quickstart Tutorial](./q ## Requirements -- Dart SDK: `>=3.9.0 <4.0.0` for `ack` and `ack_generator` +- Dart SDK: `>=3.11.0 <4.0.0` for all Ack packages. +- Flutter: `>=3.41.2` when using `ack_firebase_ai`. +- Dart 3.12 or later when your models use primary-constructor syntax. diff --git a/docs/getting-started/migration-guide.mdx b/docs/getting-started/migration-guide.mdx index f0c092f2..627b8ef0 100644 --- a/docs/getting-started/migration-guide.mdx +++ b/docs/getting-started/migration-guide.mdx @@ -12,10 +12,10 @@ Ack is organized into five coordinated packages that share one version number an | Package | Role | Key Capabilities | | --- | --- | --- | -| **`ack`** | Core runtime & annotations | • Fluent schema builders: `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.number()`, `Ack.boolean()`, `Ack.list()`, `Ack.map()`, `Ack.object()`, `Ack.any()`, `Ack.literal()`, `Ack.enumString()`, `Ack.enumValues()`, `Ack.anyOf()`, `Ack.discriminated()`, `Ack.lazy()`
• Bidirectional `CodecSchema`: `Ack.date()`, `Ack.datetime()`, `Ack.uri()`, `Ack.duration()`, `.codec()`, `.transform()`
• Two-way string normalizers: `.trim()`, `.toLowerCase()`, `.toUpperCase()`
• `JsonMaybe` (`JsonAbsent` / `JsonValue`) for tri-state `PATCH` payloads
• Full JSON Schema Draft 2020-12 runtime (`Ack.fromJsonSchema()`) and exporter (`toJsonSchema()`, `toSchemaModel()`, `toJsonSchemaDraft7()`)
• Sealed `SchemaResult` (`Ok` / `Fail`) and sealed `SchemaError` hierarchy
• Unified codegen annotations in `package:ack/ack.dart`: `@Schemable()`, `@AckKey()`, and `@Validate.*` | +| **`ack`** | Core runtime & annotations | • Fluent schema builders: `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.number()`, `Ack.boolean()`, `Ack.list()`, `Ack.map()`, `Ack.object()`, `Ack.any()`, `Ack.literal()`, `Ack.enumString()`, `Ack.enumValues()`, `Ack.anyOf()`, `Ack.discriminated()`, `Ack.lazy()`
• Bidirectional `CodecSchema`: `Ack.date()`, `Ack.datetime()`, `Ack.uri()`, `Ack.duration()`, `.codec()`, `.transform()`
• Two-way string normalizers: `.trim()`, `.toLowerCase()`, `.toUpperCase()`
• `JsonMaybe` (`JsonAbsent` / `JsonValue`) for tri-state `PATCH` payloads
• JSON Schema Draft 2020-12 import and runtime validation (`Ack.fromJsonSchema()`); native schemas export Draft-7, while `toJsonSchema()` preserves imported 2020-12 resources (`toJsonSchemaDraft7()` explicitly lowers them)
• Sealed `SchemaResult` (`Ok` / `Fail`) and sealed `SchemaError` hierarchy
• Unified codegen annotations in `package:ack/ack.dart`: `@Schemable()`, `@AckKey()`, and `@Validate.*` | | **`ack_generator`** | Two-way code generator | • **Schema-first (`@Schemable()` on a top-level schema)**: Generates an immutable model class, `Model.schema`, `parse`/`safeParse`, `fromJson`/`toJson`/`safeToJson`, typed `copyWith`, and deep `==`/`hashCode` in `.g.dart`
• **Class-first (`@Schemable()` on a `final class` or `sealed class`)**: Supports standard and Dart 3.12+ primary constructors; generates `_Schema`, `Schema`, and `_$Ack` in `.g.dart`
• **`.schema.json` codegen (`ack_generator:ack_json_schema`)**: Compiles `lib/**/*.schema.json` assets into `.schema.dart` + `.schema.g.dart` | -| **`ack_mcp_dart`** | MCP tool adapter | • `server.registerAckTool()`, `registerAckInputTool()`, `registerAckOutputTool()`, and `result.decodeAckOutput()` for `mcp_dart` | -| **`ack_firebase_ai`** | Firebase AI (Gemini) adapter | • `schema.toFirebaseAiSchema()` for Gemini structured output and tool declarations | +| **`ack_mcp_dart`** | MCP tool input adapter | • `server.registerAckTool(name, input: schema, callback: ...)`, `schema.toMcpToolInputSchema()`, and `parseMcpToolArguments(...)`; callbacks return `CallToolResult` and own output validation | +| **`ack_firebase_ai`** | Firebase AI (Gemini) adapter | • `schema.toFirebaseAiResponseJsonSchema()` for the map-based `GenerationConfig.responseJsonSchema` field | | **`ack_json_schema_builder`** | `json_schema_builder` bridge | • Bidirectional `schema.toJsonSchemaBuilder()` and `jsbSchema.toAckSchema()` | --- @@ -24,7 +24,7 @@ Ack is organized into five coordinated packages that share one version number an | Change | Introduced | What Changed | Why It Changed | | --- | --- | --- | --- | -| **Annotations merged into `package:ack`** | `1.7.0` | `ack_annotations` was retired. `@Schemable()`, `@AckKey()`, and `@Validate.*` ship directly in `package:ack/ack.dart`. | Eliminated version skew between `ack` and a separate annotations package and reduced model files to a single `import 'package:ack/ack.dart';`. | +| **Annotations merged into `package:ack`** | `1.7.0` | `ack_annotations` was retired and model/validation annotations moved into `ack`. In 2.0, the public annotation API is `@Schemable()`, `@AckKey()`, and `@Validate.*` from `package:ack/ack.dart`. | Eliminated version skew between `ack` and a separate annotations package and reduced model files to a single `import 'package:ack/ack.dart';`. | | **Unified `@Schemable()` annotation** | `1.7.0` | `@Schemable()` drives both schema-first and class-first generation, replacing `@AckInfer()`, `@AckModel()`, and `@AckType()`. | One annotation and one mental model whether you start from an `AckSchema` or a hand-written Dart `class`. | | **Unified `@Validate.*` validation namespace** | `2.0.0` | All field and parameter validation annotations—including presence (`@Validate.required()`, `@Validate.optional()`), nullability (`@Validate.notNull()`), bounds (`@Validate.min(0)`), formats (`@Validate.email()`, `@Validate.dateTime()`, `@Validate.uri()`), and collection rules (`@Validate.notEmpty()`, `@Validate.uniqueItems()`)—live under `@Validate.*` in `package:ack/ack.dart`. | Top-level 1.x classes (`@Required`, `@Email`, `@Uuid`, `@Date`, `@Uri`, `@DateTime`, `@Pattern`) collided with `package:meta` / Flutter (`Required`), `dart:core` (`Uri`, `DateTime`, `Pattern`), and `package:uuid`, forcing import prefixes (`as ack`). `@Validate.*` has zero symbol collisions and one canonical name per validation rule. | | **Single `.g.dart` part & direct JSON emitter** | `2.0.0` | `ack_generator:ack_generator` is a `SharedPartBuilder` that emits `.g.dart` directly with no `json_serializable` or `json_annotation` dependency. | In 1.x, every model required two `part` directives (`.ack.dart` + `.ack.g.dart`) and a second `json_serializable` pass. Direct emission cuts build steps in half, uses standard `part '.g.dart';`, and coexists with Freezed or Riverpod in the same file. | @@ -93,7 +93,7 @@ All top-level validation and presence classes were removed in `2.0.0`. Replace t | `Ack.enumCodec(MyEnum.values)` | `2.0.0` | `Ack.enumValues(MyEnum.values)` | | `StandardSchemaV1` (`standard_schema.dart`) | `2.0.0` | `AckSchema` + `SchemaResult` | | `schema.toMap()` | `2.0.0` | `schema.toJsonSchema()` or `schema.toSchemaModel()` | -| `server.registerAckModelTool(...)` (`ack_mcp_dart`) | `2.0.0` | `server.registerAckTool(name, inputSchema: Model.schema, ...)` | +| `server.registerAckModelTool(...)` (`ack_mcp_dart`) | `2.0.0` | `server.registerAckTool(name, input: Model.schema, callback: ...)`; return a `CallToolResult` from the callback | | `convertAckSchemaModelToBuilder(model)` (`ack_json_schema_builder`) | `2.0.0` | `schema.toJsonSchemaBuilder()` | | `Ack.fromJsonSchema(...)` enforcing `format` by default | `2.0.0` | Pass `Ack.fromJsonSchema(schema, assertFormats: true)` (or declare `format-assertion` in `$vocabulary`) when format assertions are required | | `schema.toJsonSchema()` lowering imported 2020-12 schemas to Draft-7 | `2.0.0` | `schema.toJsonSchema()` now preserves Draft 2020-12 resources by default; call `schema.toJsonSchemaDraft7()` when Draft-7 lowering is required | diff --git a/docs/guides/creating-schema-converter-packages.mdx b/docs/guides/creating-schema-converter-packages.mdx index 26b67119..023b1f44 100644 --- a/docs/guides/creating-schema-converter-packages.mdx +++ b/docs/guides/creating-schema-converter-packages.mdx @@ -39,10 +39,11 @@ final jsonSchema = schema.toJsonSchema(); `AckSchemaModel` describes the boundary shape, constraints, export-safe defaults, discriminator metadata, and warnings that adapters can reuse for non-JSON targets. JSON-map adapters can call `schema.toJsonSchema()` directly. -Imported schemas use the existing `AckAllOfSchemaModel` and -`AckRefSchemaModel` variants, with their preserved definition bodies attached -to the root model. They do not introduce an import-only variant into the sealed -model hierarchy. +Imported JSON Schema resources can surface as `AckRawSchemaModel`, directly or +inside composition models, preserving their dialect and resource semantics. +Non-JSON adapters must implement support for those resources or reject them +explicitly. Native lazy schemas surface as `AckRefSchemaModel`; supporting +references also requires resolving their definitions, not just emitting a name. Adapters should not traverse `AckSchema` subclasses or parse rendered JSON Schema output as their source of truth for non-JSON formats. @@ -141,9 +142,9 @@ repository: https://github.com/btwld/ack issue_tracker: https://github.com/btwld/ack/issues environment: - sdk: '>=3.9.0 <4.0.0' + sdk: '>=3.11.0 <4.0.0' # Add flutter if target SDK requires it - # flutter: '>=3.35.0' + # flutter: '>=3.41.2' dependencies: ack: ^2.0.0-alpha.1 @@ -343,6 +344,9 @@ class SchemaConverter { AckRefSchemaModel() => throw UnsupportedError( 'Reference schemas (Ack.lazy) are not supported by this target.', ), + AckRawSchemaModel() => throw UnsupportedError( + 'Imported JSON Schema resources are not supported by this target.', + ), }; } @@ -351,7 +355,7 @@ class SchemaConverter { // ======================================================================== static _convertString(AckStringSchemaModel schema) { - final enumValues = schema.allowedStringValues; + final enumValues = schema.enumValues; if (enumValues != null) { return _buildEnumSchema(enumValues, schema); } @@ -399,7 +403,8 @@ class SchemaConverter { static _convertObject(AckObjectSchemaModel schema) { final properties = {}; - for (final entry in schema.properties?.entries ?? const []) { + for (final entry in + schema.properties?.entries ?? const >[]) { properties[entry.key] = _convertModel(entry.value); } @@ -1193,6 +1198,9 @@ TargetSchema _convert(AckSchemaModel schema) { AckAllOfSchemaModel() => _convertAllOf(schema), AckNullSchemaModel() => TargetSchema.nullValue(), AckRefSchemaModel() => TargetSchema.ref(schema.refName), + AckRawSchemaModel() => throw UnsupportedError( + 'Implement target-specific support for imported JSON Schema resources.', + ), }; } @@ -1246,23 +1254,24 @@ TargetSchema _convert(AckSchema schema) { ### Handling Transformed Schemas -`toSchemaModel()` projects transformed schemas from their boundary input schema -and preserves adapter-safe metadata such as description, nullability, and -`x-transformed`. +`toSchemaModel()` projects codecs and transforms from their boundary input +schema. It describes external data, not the decoded Dart runtime value. +Decoder, encoder, and refinement callbacks still belong to Ack runtime +validation; they cannot be reproduced by a target schema renderer. -**Option 1: Render projected boundary model** (Recommended) +**Render the projected boundary model**: ```dart TargetSchema _convertTransformed(AckSchemaModel schema) { return _convert(schema); } ``` -**Option 2: Reject explicit transformed marker** (If target cannot tolerate transforms) -```dart -if (schema.extensions['x-transformed'] == true) { - throw UnsupportedError('Transformed Ack schemas are not supported here.'); -} -``` +Native transforms do not emit an `x-transformed` detection marker. An imported +document may preserve that extension as ordinary annotation data, but it is not +a reliable way to identify native callbacks. Validate returned data with the +original Ack schema, and inspect export warnings for omitted runtime checks. +Use a bidirectional `.codec()` when the application also needs runtime-to-wire +encoding; `.transform()` is parse-only. ### Handling Discriminated Unions @@ -1375,6 +1384,12 @@ class OpenApiSchemaConverter { **Target**: GraphQL Schema Definition Language +This illustration supports object roots with scalar fields and arrays of those +scalars. Field names and `typeName` must already be valid GraphQL identifiers. +Nested objects, enums, compositions, references, and imported raw resources are +rejected until their target definitions are implemented. It is not a lossless +translation of Ack constraints; for example, GraphQL `Int` has its own range. + ```dart // lib/src/converter.dart import 'package:ack/ack.dart'; @@ -1396,15 +1411,20 @@ class GraphQlSchemaConverter { if (schema is! AckObjectSchemaModel) { throw UnsupportedError('Only object schema models can be converted.'); } + if (schema.properties?.isNotEmpty != true) { + throw UnsupportedError('GraphQL object definitions need at least one field.'); + } buffer.writeln('type $typeName {'); final required = schema.required ?? const []; - for (final entry in schema.properties?.entries ?? const []) { + for (final entry in + schema.properties?.entries ?? const >[]) { final fieldName = entry.key; final fieldSchema = entry.value; final gqlType = _getGraphQLType(fieldSchema); - final nullableSuffix = required.contains(fieldName) ? '!' : ''; + final nullableSuffix = + required.contains(fieldName) && !fieldSchema.nullable ? '!' : ''; if (fieldSchema.description != null) { buffer.writeln(' """${fieldSchema.description}"""'); @@ -1417,22 +1437,20 @@ class GraphQlSchemaConverter { static String _getGraphQLType(AckSchemaModel schema) { return switch (schema) { - AckStringSchemaModel(allowedStringValues: final values) - when values != null && values.isNotEmpty => - _generateEnumType(schema), + AckStringSchemaModel(enumValues: final values) + when values != null => + throw UnsupportedError('Generate enum definitions before using enums.'), AckStringSchemaModel() => 'String', AckIntegerSchemaModel() => 'Int', AckNumberSchemaModel() => 'Float', AckBooleanSchemaModel() => 'Boolean', - AckArraySchemaModel(items: final item?) => '[${_getGraphQLType(item)}]', - _ => 'String', // Fallback + AckArraySchemaModel(items: final item?) => + '[${_getGraphQLType(item)}${item.nullable ? '' : '!'}]', + _ => throw UnsupportedError( + 'Only scalar fields and arrays of supported scalars are implemented.', + ), }; } - - static String _generateEnumType(AckStringSchemaModel schema) { - // Would need to generate enum definitions separately - return 'EnumType'; - } } ``` diff --git a/docs/guides/ecosystem-integrations.mdx b/docs/guides/ecosystem-integrations.mdx index eb14b660..b0fb8a60 100644 --- a/docs/guides/ecosystem-integrations.mdx +++ b/docs/guides/ecosystem-integrations.mdx @@ -2,12 +2,12 @@ title: Ecosystem Integrations --- -Every `AckSchema` (and every `@Schemable()` model via `Model.schema`) can be reused directly across Ack's official adapter packages: +Reuse an `AckSchema` (or a `@Schemable()` model's `Model.schema`) across Ack's official adapter packages. Each adapter has its own supported schema shapes; MCP tool inputs, for example, must render as a non-null object root. | Adapter Package | Target Ecosystem | Primary Entry Point | | --- | --- | --- | -| **`ack_mcp_dart`** | [Model Context Protocol (`mcp_dart`)](https://pub.dev/packages/mcp_dart) | `server.registerAckTool(...)`, `result.decodeAckOutput(...)` | -| **`ack_firebase_ai`** | [Firebase AI Logic (`firebase_ai` / Gemini)](https://pub.dev/packages/firebase_ai) | `schema.toFirebaseAiSchema()` | +| **`ack_mcp_dart`** | [Model Context Protocol (`mcp_dart`)](https://pub.dev/packages/mcp_dart) | `server.registerAckTool(...)`, `schema.toMcpToolInputSchema()` | +| **`ack_firebase_ai`** | [Firebase AI Logic (`firebase_ai` / Gemini)](https://pub.dev/packages/firebase_ai) | `schema.toFirebaseAiResponseJsonSchema()` | | **`ack_json_schema_builder`** | [`json_schema_builder`](https://pub.dev/packages/json_schema_builder) | `schema.toJsonSchemaBuilder()`, `jsbSchema.toAckSchema()` | --- @@ -49,7 +49,7 @@ final class WeatherReport with _$WeatherReportAck { ## 2. MCP Tool Servers (`ack_mcp_dart`) -`ack_mcp_dart` bridges `AckSchema` with `McpServer` from `package:mcp_dart/mcp_dart.dart`. It exports the tool's JSON Schema automatically, validates incoming tool arguments before your callback runs, validates structured tool output, and formats validation failures as MCP `CallToolResult(isError: true)` responses. +`ack_mcp_dart` bridges `AckSchema` with `McpServer` from `package:mcp_dart/mcp_dart.dart`. It exports the tool's input JSON Schema, validates and decodes incoming arguments before your callback runs, and formats Ack validation failures as MCP `CallToolResult(isError: true)` responses. MCP's structural validation runs first; its failures do not reach the Ack callback. ### Install @@ -57,43 +57,56 @@ final class WeatherReport with _$WeatherReportAck { dart pub add ack ack_mcp_dart mcp_dart ``` -### Register a typed input + output tool +### Register a typed input tool -Pass `WeatherQuery.schema` and `WeatherReport.schema` directly to `server.registerAckTool`: +Pass `WeatherQuery.schema` through `input`. The callback receives a `WeatherQuery` and returns a `CallToolResult`; output validation and serialization belong to the callback: ```dart +import 'dart:convert'; + import 'package:ack_mcp_dart/ack_mcp_dart.dart'; import 'package:mcp_dart/mcp_dart.dart'; import 'weather_models.dart'; void registerWeatherTools(McpServer server) { - server.registerAckTool( + server.registerAckTool( 'get_weather', description: 'Fetch the current weather for a city.', - inputSchema: WeatherQuery.schema, - outputSchema: WeatherReport.schema, - callback: (query, extra) async { - // `query` is already validated and decoded into a `WeatherQuery` - return WeatherReport( + input: WeatherQuery.schema, + callback: (query, extra) { + final report = WeatherReport( city: query.city, temperatureC: 21.5, ); + // Generated toJson validates and encodes the report. + final json = report.toJson(); + return CallToolResult( + content: [TextContent(text: jsonEncode(json))], + structuredContent: json, + ); }, ); } ``` -Other registration and client helpers: -- `server.registerAckInputTool(name, inputSchema: ..., callback: ...)` — validates input only and returns a `CallToolResult`. -- `server.registerAckOutputTool(name, outputSchema: ..., callback: ...)` — takes no input arguments and validates structured output. -- `callToolResult.decodeAckOutput(WeatherReport.schema)` — validates and decodes `structuredContent` on the client side into a `SchemaResult`. +For manual registration, use `schema.toMcpToolInputSchema()`, +`parseMcpToolArguments(schema, args)`, and +`ackValidationErrorToCallToolResult(error, toolName: name)`. On the client, +validate structured output explicitly with +`WeatherReport.schema.safeParse(result.structuredContent)`. +The adapter does not register or advertise output schemas; use `mcp_dart` +directly when those are required. --- ## 3. Firebase AI / Gemini Structured Outputs (`ack_firebase_ai`) -`ack_firebase_ai` converts any `AckSchema` into a `firebase_ai` `Schema` for Gemini structured JSON output or function calling, then lets you parse the model's response back through the same Ack schema. +`ack_firebase_ai` renders an Ack schema as the JSON-compatible map accepted by +`GenerationConfig.responseJsonSchema`. Use a model that supports that field, +initialize Firebase in your app, and validate the response with the same Ack +schema. This is not a converter to Firebase's native `Schema` type or a function +declaration helper. ### Install @@ -111,25 +124,30 @@ import 'package:firebase_ai/firebase_ai.dart'; import 'weather_models.dart'; -Future askGeminiWeather(String prompt) async { +Future askGeminiWeather( + String prompt, { + required String modelName, +}) async { final model = FirebaseAI.googleAI().generativeModel( - model: 'gemini-2.5-flash', + model: modelName, generationConfig: GenerationConfig( responseMimeType: 'application/json', - // Convert the Ack schema to a Firebase AI Schema - responseSchema: WeatherReport.schema.toFirebaseAiSchema(), + responseJsonSchema: WeatherReport.schema.toFirebaseAiResponseJsonSchema(), ), ); final response = await model.generateContent([Content.text(prompt)]); - final jsonMap = jsonDecode(response.text!); + final jsonMap = jsonDecode(response.text ?? 'null'); // Validate and decode Gemini's response into WeatherReport - return WeatherReport.schema.parse(jsonMap); + return WeatherReportSchema.fromJson(jsonMap); } ``` -> **Note:** Gemini's OpenAPI-based schema subset does not enforce every keyword (such as regex `pattern` or custom `.refine()` callbacks) during generation. `toFirebaseAiSchema()` appends human-readable constraint descriptions so the model has full context, and `WeatherReport.schema.parse(...)` enforces the full Ack contract on the response. +The adapter returns `schema.toJsonSchema()` unchanged. It does not append +constraint descriptions or adapt keywords to a provider-specific subset. +Response schemas guide generation; Ack validation remains authoritative for +constraints and refinements that the provider does not enforce. --- @@ -153,7 +171,7 @@ import 'package:json_schema_builder/json_schema_builder.dart' as jsb; import 'weather_models.dart'; void main() { - // 1. AckSchema -> json_schema_builder Schema (preserves Draft 2020-12 by default) + // Native Ack schemas render as Draft-7; imported 2020-12 resources are preserved. final jsb.Schema builderSchema = WeatherQuery.schema.toJsonSchemaBuilder(); // Need legacy Draft-7 definitions lowering instead? @@ -165,6 +183,8 @@ void main() { required: ['city'], ).toAckSchema(assertFormats: true); + print(builderSchema.value); + print(draft7Schema.value); final valid = importedValidator.parse({'city': 'Lisbon'}); print(valid); } diff --git a/docs/guides/schema-converter-quickstart.mdx b/docs/guides/schema-converter-quickstart.mdx index cc8bbddd..14f556c6 100644 --- a/docs/guides/schema-converter-quickstart.mdx +++ b/docs/guides/schema-converter-quickstart.mdx @@ -37,8 +37,8 @@ version: 1.0.0-beta.1 repository: https://github.com/btwld/ack environment: - sdk: '>=3.9.0 <4.0.0' - # flutter: '>=3.35.0' # Uncomment if needed + sdk: '>=3.11.0 <4.0.0' + # flutter: '>=3.41.2' # Uncomment if needed dependencies: ack: ^2.0.0-alpha.1 @@ -58,8 +58,6 @@ dev_dependencies: /// schema converter for Ack validation library. library; -import 'package:ack/ack.dart'; - export 'src/extension.dart'; ``` @@ -100,7 +98,7 @@ class SchemaConverter { static Map _convertSchema(AckSchemaModel schema) { return switch (schema) { - AckStringSchemaModel(allowedStringValues: final values) + AckStringSchemaModel(enumValues: final values) when values != null => _convertEnum(schema), AckStringSchemaModel() => _convertString(schema), @@ -113,9 +111,12 @@ class SchemaConverter { AckOneOfSchemaModel() => _convertOneOf(schema), AckAllOfSchemaModel() => _convertAllOf(schema), AckNullSchemaModel() => {'type': 'null'}, - AckRefSchemaModel(refName: final name) => { - r'$ref': '#/definitions/$name', - }, + AckRefSchemaModel() => throw UnsupportedError( + 'References need target-specific definition resolution.', + ), + AckRawSchemaModel() => throw UnsupportedError( + 'Imported JSON Schema resources need target-specific support.', + ), }; } @@ -175,7 +176,8 @@ class SchemaConverter { return { 'type': 'object', 'properties': { - for (final entry in s.properties?.entries ?? const []) + for (final entry in + s.properties?.entries ?? const >[]) entry.key: _convertSchema(entry.value), }, if (s.required != null && s.required!.isNotEmpty) 'required': s.required, @@ -202,7 +204,7 @@ class SchemaConverter { 'type': 'string', if (s.description != null) 'description': s.description, if (s.nullable) 'nullable': true, - 'enum': s.allowedStringValues, + 'enum': s.enumValues, }; } @@ -243,6 +245,11 @@ class SchemaConverter { } ``` +This non-JSON target scaffold deliberately rejects references and imported raw +resources rather than emitting unresolved references or discarding their +dialect semantics. If your target accepts JSON Schema maps directly, forward +`schema.toJsonSchema()` instead; that path preserves definitions and resources. + ## 6. Basic Tests (15 minutes) **`test/to__schema_test.dart`**: diff --git a/llms.txt b/llms.txt index f79f7a60..0c4c9b7a 100644 --- a/llms.txt +++ b/llms.txt @@ -305,5 +305,5 @@ validates through the union's effective branch. | `@AckField(description: ...)` | `/// @description ...` doc tag | | `@AckField(presence: ...)` | `@Validate.optional()`, `@Validate.required()`, `@Validate.notNull()`, or `JsonMaybe` | | `Ack.enumCodec(MyEnum.values)` | `Ack.enumValues(MyEnum.values)` | -| `registerAckModelTool(...)` (`ack_mcp_dart`) | `server.registerAckTool(name, inputSchema: Model.schema, ...)` | +| `registerAckModelTool(...)` (`ack_mcp_dart`) | `server.registerAckTool(name, input: Model.schema, callback: ...)` | | `convertAckSchemaModelToBuilder(model)` (`ack_json_schema_builder`) | `schema.toJsonSchemaBuilder()` | diff --git a/packages/ack_firebase_ai/CHANGELOG.md b/packages/ack_firebase_ai/CHANGELOG.md index 7890114b..44babe91 100644 --- a/packages/ack_firebase_ai/CHANGELOG.md +++ b/packages/ack_firebase_ai/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +### Documentation + +* Clarify that the adapter forwards `toJsonSchema()` unchanged to the map-based + `responseJsonSchema` field, preserving imported resource dialects. + ## 2.0.0-alpha.1 ### Breaking diff --git a/packages/ack_firebase_ai/README.md b/packages/ack_firebase_ai/README.md index 458a184c..aa186449 100644 --- a/packages/ack_firebase_ai/README.md +++ b/packages/ack_firebase_ai/README.md @@ -80,9 +80,10 @@ Future main() async { ## Schema Mapping -The adapter returns ACK's generic Draft-7 JSON Schema renderer output. That -means adapter behavior matches `AckSchema.toJsonSchema()`, which renders -`AckSchema.toSchemaModel().toJsonSchema()`. +The adapter returns `AckSchema.toJsonSchema()` unchanged. Native Ack schemas +use the Draft-7 representation; imported Draft 2020-12 schemas preserve their +resources and dialect. The adapter does not lower or filter provider-specific +keywords. | ACK schema | JSON Schema output | | --- | --- | diff --git a/packages/ack_firebase_ai/analysis_options.yaml b/packages/ack_firebase_ai/analysis_options.yaml index 72cd0fb5..f1f1ae59 100644 --- a/packages/ack_firebase_ai/analysis_options.yaml +++ b/packages/ack_firebase_ai/analysis_options.yaml @@ -3,6 +3,8 @@ include: ../../analysis_options.yaml analyzer: + exclude: + - build/** language: # This package enforces stricter typing than the workspace default. strict-casts: true diff --git a/packages/ack_mcp_dart/CHANGELOG.md b/packages/ack_mcp_dart/CHANGELOG.md index a0ea9b6a..74ddb72d 100644 --- a/packages/ack_mcp_dart/CHANGELOG.md +++ b/packages/ack_mcp_dart/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +### Documentation + +* Correct the Dart SDK floor and document the current typed-input registration + API and callback-owned output validation in the ecosystem guide. + ## 2.0.0-alpha.1 ### Breaking diff --git a/packages/ack_mcp_dart/README.md b/packages/ack_mcp_dart/README.md index 3974bb16..09d9a71b 100644 --- a/packages/ack_mcp_dart/README.md +++ b/packages/ack_mcp_dart/README.md @@ -140,9 +140,9 @@ New emitted keywords require revisiting this dialect assumption. | Component | Supported / exercised | | --- | --- | -| Dart | `>=3.9.0 <4.0.0` | +| Dart | `>=3.11.0 <4.0.0` | | ACK | `^2.0.0-alpha.1` | -| mcp_dart | `^2.4.2` (tests resolved against 2.4.2) | +| mcp_dart | `^2.4.2` | | MCP stable discovery | `2026-07-28` | | MCP initialization | `2025-11-25`, `2025-06-18` | diff --git a/scripts/stage_min_sdk_workspace.dart b/scripts/stage_min_sdk_workspace.dart index 46a45eac..3f07adbe 100644 --- a/scripts/stage_min_sdk_workspace.dart +++ b/scripts/stage_min_sdk_workspace.dart @@ -9,11 +9,11 @@ import 'src/staging.dart'; /// Copies the repository into a pure-Dart workspace that resolves on the /// minimum Dart SDK. /// -/// The complete workspace cannot resolve on Dart 3.9, because `flutter_test` -/// pins `test_api 0.7.6` while the root `test` constraint and the Analyzer 10 -/// dependency of `ack_generator` require a newer `test_api`. Removing the -/// Flutter workspace members leaves a resolvable workspace that still covers -/// every pure-Dart package. The Flutter packages get their own staged lane +/// The complete workspace cannot resolve on the minimum SDK, because +/// `flutter_test` pins an older `test_api` than the analyzer dependency of +/// `ack_generator` allows. Removing the Flutter workspace members leaves a +/// resolvable workspace that still covers every pure-Dart package. The Flutter +/// packages get their own staged lane /// through `scripts/stage_package.dart`. /// /// Usage: