Skip to content

feat!: prepare Ack 2.0.0-beta.1 release - #220

Closed
leoafarias wants to merge 29 commits into
mainfrom
leoafarias/ack-2-0-prep-review
Closed

leoafarias wants to merge 29 commits into
mainfrom
leoafarias/ack-2-0-prep-review

Conversation

@leoafarias

@leoafarias leoafarias commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

🎉 Announcing Ack 2.0.0-beta.1

Ack 2.0 is a clean-slate architectural release focused on one annotation, one generated part file, bidirectional codecs, full JSON Schema Draft 2020-12 conformance, and zero 1.x baggage.

Every package in the workspace moves together to 2.0.0-beta.1:

  • ack 2.0.0-beta.1
  • ack_generator 2.0.0-beta.1
  • ack_firebase_ai 2.0.0-beta.1
  • ack_json_schema_builder 2.0.0-beta.1
  • ack_mcp_dart 2.0.0-beta.1

✨ What's New in Ack 2.0

1. One Annotation & One Generated Part File (@Schemable() + .g.dart)

  • Single-part code generation: ack_generator now emits models, schemas, value-equality mixins (_$TypeAck), and runtime JSON helpers directly into part '<file>.g.dart'; via source_gen:combining_builder.
  • No json_serializable dependency required: @Schemable() owns fromJson / toJson generation directly against Ack's validated runtime representation, while still coexisting seamlessly in the same .g.dart file if your library also uses @JsonSerializable() on other classes.
  • @Schemable() everywhere: Whether you write schema-first definitions (@Schemable() final userSchema = Ack.object({...});) or class-first models (@Schemable() final class User with _$UserAck { ... }), @Schemable() is the single entry point.
  • Type-owned schema ergonomics: Class-first models generate a private backing schema (_<className>Schema) alongside the <ClassName>Schema facade so you can bind static final schema = _userSchema; directly on your class without repeating the class name.
  • Custom schema resolution (@Schemable(schemas: [...]) & @AckField(schema: ...)): Attach custom AckSchema<Boundary, Runtime> instances to third-party or value types at the field, class, or library level, or expose static final schema on your own types for automatic discovery.

2. Unified @Validate.* Constraint Namespace (Single Import)

  • All field and constructor-parameter validation annotations now live under the unified @Validate.* namespace exported directly from package:ack/ack.dart—no separate package:ack/annotations.dart or package:ack/format_annotations.dart import needed, and zero naming collisions with dart:core (Uri, DateTime):
    • Numeric: @Validate.min(...), @Validate.max(...), @Validate.multipleOf(...), @Validate.positive(), @Validate.negative()
    • String & Format: @Validate.minLength(...), @Validate.maxLength(...), @Validate.matches(...), @Validate.notEmpty(), @Validate.email(), @Validate.url(), @Validate.uri(), @Validate.uuid(), @Validate.date(), @Validate.dateTime(), @Validate.ip(), @Validate.ipv4(), @Validate.ipv6()
    • Collection: @Validate.minItems(...), @Validate.maxItems(...), @Validate.uniqueItems() / @Validate.unique()
  • Works directly on fields, initializing formal parameters (this.x), and Dart 3.11+ primary constructor parameters (final class User(@Validate.email() final String email) with _$UserAck).

3. Bidirectional Codecs & Two-Way String Normalizers

  • CodecSchema<Boundary, Runtime> replaces one-way transforms as the core type-conversion mechanism, supporting both parse / safeParse (decode) and encode / safeEncode (encode).
  • Two-way string normalizers: .trim(), .toLowerCase(), and .toUpperCase() now stay on StringSchema, execute in declaration order alongside constraints/refinements, round-trip through encode(), and export cleanly as type: "string" in JSON Schema without x-transformed: true.

4. Sealed AckSchema & SchemaError Hierarchies

  • Both AckSchema<Boundary, Runtime> and SchemaError (TypeMismatchError, SchemaConstraintsError, SchemaNestedError, SchemaValidationError, JsonSchemaValidationError) are now sealed, enabling exhaustive switch expressions across schemas and validation errors without wildcard fallbacks.

5. Full JSON Schema Draft 2020-12 Runtime & .schema.json Codegen

  • Ack.fromJsonSchema(...) compiles JSON Schema Draft 2020-12 (and Draft-07) documents into executable AckSchema instances with full support for $id, $anchor, $ref, $dynamicAnchor, $dynamicRef, allOf/anyOf/oneOf/not, if/then/else, dependentSchemas/dependentRequired, prefixItems/items/contains/minContains/maxContains, unevaluatedProperties/unevaluatedItems, propertyNames, patternProperties, and RFC-compliant format assertions (assertFormats: true).
  • toJsonSchema() & toSchemaModel() preserve Draft 2020-12 by default, with explicit toJsonSchemaDraft7() / toSchemaModelDraft7() helpers when lowering to legacy Draft-07 consumers.
  • .schema.json code generator (ack_generator:ack_json_schema & generateAckSchemaLibrary): Compile .schema.json assets directly into @Schemable() .schema.dart libraries (which ack_generator:ack_generator compiles into .schema.g.dart models and schemas).

🔥 Breaking Changes & Removals (What Is No Longer Here)

To keep Ack 2.0 lean and unambiguous ("one way to do it"), all deprecated 1.x shims and legacy APIs have been removed:

Removed 1.x API / Artifact Ack 2.0 Replacement
package:ack_annotations/ack_annotations.dart import 'package:ack/ack.dart'; (re-exports @Schemable, @AckField, @Validate)
package:ack/format_annotations.dart Removed; use @Validate.dateTime(), @Validate.uri(), @Validate.ip(), etc. from package:ack/ack.dart
@Min, @Max, @Email, @NotEmpty, @MinLength, @MaxLength, @Matches, @Url, @Uuid, @Date, @MinItems, @MaxItems, @UniqueItems, @Positive, @Negative, @MultipleOf Removed; use @Validate.min(...), @Validate.max(...), @Validate.email(), @Validate.notEmpty(), etc.
@JsonKey(name: ...) on @Schemable classes Removed; use @AckField(name: '...')
@AckModel(), @AckType(), @AckInfer(), @Discriminated() @Schemable() (with discriminatorKey / discriminatorValue for unions)
part '<file>.ack.dart'; & ack_models / ack_model_json builders part '<file>.g.dart'; (ack_generator:ack_generator via source_gen:combining_builder)
SchemaResult.getOrNull() / getOrThrow() / getErrors() result.valueOrNull / switch (result) { case Ok(:final value): ... case Fail(:final error): ... }
AckSchema.validate() / validateOrThrow() / tryParse() schema.safeParse(input) / schema.parse(input)
Ack.int() / Ack.bool() / Ack.enumString() / Ack.enumCodec() / Ack.discriminated() Ack.integer() / Ack.boolean() / Ack.literal() or Ack.enumValues() / Ack.discriminatedObject()
.nullable(false) / .optional(false) positional booleans & .defaultValue(...) .nullable(value: false) / .optional(value: false) & .withDefault(...)
.strict() / .passthrough() on ObjectSchema Ack.object({...}, additionalProperties: false | true)
registerAckModelTool(...) in ack_mcp_dart registerAckTool(...) with <Model>.schema
convertAckSchemaModelToBuilder(...) in ack_json_schema_builder schema.toJsonSchemaBuilder()

🛠️ Migration Quick-Start (1.x $\rightarrow$ 2.0)

// Before (1.x)
import 'package:ack/ack.dart';
import 'package:ack/annotations.dart';

part 'user.ack.dart';
part 'user.g.dart';

@AckModel()
class User {
  @Email()
  final String email;
  @Min(0)
  final int age;

  const User({required this.email, required this.age});
}
// After (2.0 — Dart 3.11+ Primary Constructor)
import 'package:ack/ack.dart';

part 'user.g.dart';

@Schemable()
final class User(
  @Validate.email() final String email, {
  @Validate.min(0) this.age = 0,
}) with _$UserAck {
  static final schema = _userSchema;
}

✅ Verification & Consistency Audit

  • 5-Stage Reference Design & Code Simplification Audit: Removed 1,100+ net lines of legacy 1.x shims, test-only schemas from production lib/, unused runtime dependencies, and duplicated generator AST/emitter code.
  • Static analysis: fvm dart analyze --fatal-infos passes with 0 issues across all 6 workspace packages (ack, ack_generator, ack_firebase_ai, ack_json_schema_builder, ack_mcp_dart, example).
  • Tests: Full unit, conformance, and build_runner integration suites pass across ack (1,972 tests), ack_generator (679 tests), ack_json_schema_builder (52 tests), ack_mcp_dart (63 tests), ack_firebase_ai (98 tests), example, and test/scripts (60 tests).

…ep-review

# Conflicts:
#	packages/ack/lib/src/constraints/comparison_constraint.dart
#	packages/ack/lib/src/json_schema/json_schema_compiler.dart
#	packages/ack/lib/src/schemas/imported_json_schema.dart
#	packages/ack/test/fixtures/json_schema_import/README.md
#	packages/ack/test/json_schema_format_import_test.dart
#	packages/ack/test/json_schema_import_api_test.dart
#	packages/ack/test/json_schema_import_test.dart
#	packages/ack_json_schema_builder/test/from_json_schema_builder_test.dart

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ace93ba5c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


@override
Map<String, Object?> toJsonSchema() {
final body = {...schema, ..._common.toJson()};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve source allOf when layering conflicting constraints

When an imported schema already contains allOf and a fluent/custom constraint conflicts with another root keyword, _applyConstraints stores the conflicting keyword in extensions['allOf']; this spread then replaces the source document's original allOf. For example, a source containing allOf: [{'maxLength': 10}] and minLength: 2, followed by a constraint exporting minLength: 3, loses the maxLength assertion after toJsonSchema(), so the exported schema accepts values the runtime schema rejects. Merge the generated branch with the source allOf, or preserve it in a separate conjunctive wrapper.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T20:19:47.739093Z b5045ff New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcaf31d981

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -9,4 +9,6 @@ const publishableAckPackages = <String>[

/// First releases for packages added after the original workspace baseline.
/// Earlier versions have no published API to compare against.
const ackPackageFirstReleases = <String, String>{'ack_mcp_dart': '1.3.0'};
const ackPackageFirstReleases = <String, String>{
'ack_mcp_dart': '2.0.0-beta.1',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the actual ack_mcp_dart first-release version

Keep this value at 1.3.0: ack_mcp_dart was already part of the coordinated 1.3.0–1.6.2 releases, as the repository history and PUBLISHING.md confirm. With 2.0.0-beta.1 here, the API compatibility job skips this package whenever it uses the configured 1.6.2 baseline, so breaking changes in the MCP adapter can pass both PR and release preflight without being examined.

Useful? React with 👍 / 👎.

Comment on lines +336 to +338
return baseFieldsEqual(other) &&
assertFormats == other.assertFormats &&
deepEquals(rawSchema, other.rawSchema);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include referenced resources in imported-schema equality

Compare the compiled resource inputs, not only the root document. Two calls such as Ack.fromJsonSchema({'$ref': 'value.json'}, documents: {valueUri: {'type': 'string'}}) and the same root with valueUri mapped to {'type': 'integer'} currently compare equal and have the same hash even though they accept different values; differing baseUri values can cause the same problem for relative references. This can make schema-keyed caches or collections reuse behavior from a non-equivalent imported schema.

Useful? React with 👍 / 👎.

Comment on lines +394 to +396
if (node['additionalProperties'] == false) {
node['additionalProperties'] = true;
widened.add(pointer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Widen unevaluatedProperties in preserve mode

Handle unevaluatedProperties: false as well as additionalProperties: false. For a Draft 2020-12 model that closes an object with unevaluatedProperties, selecting unknown_properties: preserve leaves that assertion unchanged, so generated validated models still reject additive unknown keys despite this option promising to accept and retain them on every object. This is especially relevant for composed schemas, where unevaluatedProperties is the standard way to close properties declared across branches.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5608c4fb2c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +458 to +460
if (node.allOf case final branches?) {
for (var i = 0; i < branches.length; i++) {
final result = _evaluateImportedNode(branches[i], value, currentScope);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor custom dialects that omit the applicator vocabulary

When a supplied custom dialect declares only the core vocabulary and omits the applicator vocabulary, keywords such as allOf must have no assertion behavior, but this evaluator executes them unconditionally. For example, a schema using that dialect with allOf: [false] rejects every instance even though allOf is unknown under the declared dialect; _configureDialect tracks validation and format vocabularies but never gates applicator or unevaluated keywords. Track those vocabularies and evaluate their keywords only when the dialect includes them.

Useful? React with 👍 / 👎.

Comment on lines +147 to +150
for (final external in _externalRoots) {
if (external.documentUri != external.baseUri &&
resolved.removeFragment() == external.documentUri) {
exported[keyword] = external.baseUri

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the root retrieval alias during export

When the imported root has a canonical $id different from its baseUri and an external resource refers back to the root through that retrieval URI, this loop rewrites aliases only for _externalRoots. The exported root is identified solely by its canonical $id, while the external resource retains its reference to the now-unregistered root retrieval URI, so Ack.fromJsonSchema(schema.toJsonSchema()) fails with an unresolved reference. Include the root's document/base URI pair in this alias canonicalization as well.

Useful? React with 👍 / 👎.

Comment on lines +444 to +446
final dialect = resources[node.dialectUri];
if (dialect == null) {
_fail(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register the advertised Draft-7 dialect

Every document declaring http(s)://json-schema.org/draft-07/schema(#) now reaches this missing-resource branch because only the 2020-12 meta-schemas are registered. This makes all explicit Draft-7 imports fail with unsupported_dialect, even though this commit's public guide still documents Draft-7 as accepted and the previous importer supported these URI variants. Register or normalize the Draft-7 dialect before performing this lookup.

Useful? React with 👍 / 👎.

Comment on lines +25 to +26
bool _isSupportedJsonSchemaFormat(String format) =>
_jsonSchemaFormats.contains(format);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore asserted numeric format support

The supported-format lookup no longer contains int32, int64, uint32, uint64, float, or double, although these formats remain documented and were enforced by the previous importer. Consequently, Ack.fromJsonSchema({'type': 'integer', 'format': 'int32'}, assertFormats: true) throws unsupported_format instead of compiling a validator, and the numeric evaluation path has no replacement range checks. Add the numeric formats back to both recognition and numeric validation.

Useful? React with 👍 / 👎.

Comment on lines +97 to +98
return _isFullDate(value.substring(0, 10)) &&
_isFullTime(value.substring(11));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unannounced leap seconds in date-time values

For date-time format assertions, this composition validates only the calendar date and whether :60 maps to UTC minute 23:59; it never checks the bundled announced-leap-second dates. Thus an input such as 2030-06-30T23:59:60Z is now accepted, while the documented behavior and the existing native Ack.string().datetime() validator reject it. Reuse isValidRfc3339DateTime here so imported and native schemas retain the same leap-second semantics.

Useful? React with 👍 / 👎.

///
/// Runs in declaration order with any constraints and refinements chained
/// before or after this call, on both `parse` and `encode`.
StringSchema trim() => _withNormalizer(_StringNormalizer.trim);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid exporting post-normalization constraints as-is

When a constraint is chained after this normalizer, runtime validation applies it to the normalized value but JSON Schema export still applies the keyword to the original boundary string. For example, Ack.string().trim().minLength(3) rejects ' a ' after trimming it to length 1, while its exported {type: 'string', minLength: 3} accepts the five-character input. This makes schemas advertised to MCP, Firebase, and other JSON Schema consumers disagree with Ack; omit or warn about constraints whose meaning is changed by an earlier normalizer.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5045ff22b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +401 to +405
for (final key in mapKeywords) {
if (rewritten[key] case final Map<String, Object?> children) {
rewritten[key] = children.map(
(name, child) => MapEntry(name, visit(child, currentBase)),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Traverse indexed extension-container schemas during relocation

When a nullable imported root reaches a schema through a JSON Pointer in a nonstandard container, this traversal visits only the standard schema-bearing keywords. The nullable rewrite moves that container under /anyOf/0, but nested $ref or $dynamicRef values inside the explicitly indexed schema keep their old root pointers; for example, a catalog entry referring to #/catalog/target makes Ack.fromJsonSchema(schema.nullable().toJsonSchema()) fail with an unresolved reference. Traverse the compiler-indexed schema locations as well.

Useful? React with 👍 / 👎.

for (final key in [r'$ref', r'$dynamicRef']) {
final reference = rewritten[key];
if (reference is! String) continue;
final resolved = currentBase.resolve(reference);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve dormant malformed references during nullable export

When an imported schema contains a malformed reference in an unreachable $defs entry, the compiler intentionally leaves it dormant and ordinary export preserves it, but adding .nullable() makes this unconditional resolve visit that entry and throw FormatException. Consequently schema.nullable().toJsonSchema() fails for a schema that imported successfully; malformed dormant references should remain unchanged during relocation, as they are in export2020Document.

Useful? React with 👍 / 👎.

Comment on lines +836 to +839
int? integral(Object? value) =>
value is num && value == value.truncate() ? value.toInt() : null;
if (node['minimum'] case final num n) {
buffer.write('.min(${integral(n) ?? n.ceil()})');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back before clamping integer bounds

When a .schema.json integer model has a bound outside Dart's integer range, such as minimum: 1e30, toInt() clamps the value instead of reporting that it is unrepresentable. Typed generation therefore emits approximately Ack.integer().min(9223372036854775807), which accepts a representable maximum integer even though the source schema rejects it; fractional bounds can clamp through ceil() or floor() as well. Check representability for all integer bounds and select the validated-value fallback instead.

Useful? React with 👍 / 👎.

Comment on lines +516 to +521
if (keys.contains(r'$ref')) {
if (keys.length > 1) {
_fail(pointer, 'Keywords next to "\$ref" are not supported.');
return null;
}
return _ref(node[r'$ref'], '$pointer/\$ref', owner);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply use-site descriptions to referenced fields

When a typed .schema.json property uses $ref with a sibling description, this branch returns the referenced schema expression without applying the local describe wrapper. The generated field documentation and toJsonSchema() therefore lose the use-site description (or expose the referenced model's description instead), even though Draft 2020-12 applies annotation siblings alongside $ref; wrap the referenced expression while retaining its dependency metadata.

Useful? React with 👍 / 👎.

@leoafarias

Copy link
Copy Markdown
Collaborator Author

Superseded by the dedicated release/2.0 branch (2.0.0-alpha.1 at b4c4f053c83b3d9828a7eccac4649a1d9672f8b8).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant