Skip to content

feat(cli,docs): add TypeScript CLI implementation and macro architecture proposals - #2613

Closed
jacobsimionato wants to merge 12 commits into
feat/dart-cli-conformancefrom
feat/typescript-cli-proposals
Closed

jacobsimionato wants to merge 12 commits into
feat/dart-cli-conformancefrom
feat/typescript-cli-proposals

Conversation

@jacobsimionato

Copy link
Copy Markdown
Collaborator

Summary

Introduces the TypeScript implementation of the @a2ui/cli developer tool, registered in Yarn workspaces, alongside architecture proposals for macros and fluent builders.

Layer 5 (final layer) of the builder and macro stack (#2571), building on the Dart CLI and conformance test suite from Layer 4 (#2521).

Architecture Context (Issue #2571)

Includes the formal specification proposals tracked in #2571 detailing Pydantic v2 fluent builders, open enums, strict authoring validation, and the Phase 2 roadmap for single-pass AST deserialization (WrapValidator, unlinked subtree preservation, and edge case mitigation).

Key Components

  • TypeScript CLI Package (javascript/a2ui_cli/):
    • Registered in root Yarn Berry workspaces under "javascript/a2ui_cli".
    • Built with Commander and TypeScript flat ESLint config.
    • Implements the canonical script targets (build, lint, lint:fix, test, format, format:check).
    • Implements catalog analyzer and Pydantic Python code generator with parity to the Dart implementation.
  • Specification Proposals (specification/proposals/macros/):
    • macros_and_typesafe_builders.md: Core proposal for client-side macro expansion, slot coercion, and catalog composition.
    • pydantic_fluent_builders_and_deserialization.md: Proposal detailing the migration from dataclasses to Pydantic v2 models, extra attributes support, and deserialization.

Verification

  • CLI Tests: yarn workspace @a2ui/cli test (28/28 passed)
  • Linter: yarn workspace @a2ui/cli lint (clean)
  • Formatting: yarn workspace @a2ui/cli format:check (clean)

…streamline catalog models

- Switch enums from open unions (Literal | str) to strict Literals for edit-time and runtime validation
- Add arbitrary_types_allowed=False to ComponentBuilderNode and primitives
- Use validate_by_name=True in ConfigDict per modern Pydantic conventions
- Strip redundant to_dict() boilerplate from generated component classes and value models
- Rename basic.py to basic_catalog.py
- Use native Pydantic model_dump for AccessibilityAttributes, FunctionCall, CheckRule, and DynamicChildList
- Update README examples to reflect automatic Pydantic property handling
- Add builder conformance test suite and golden JSON outputs verifying all API features
- Add unit tests verifying rejection of unknown enums, typos, missing required params, and compiler failure via mypy
…package

- Extract version-agnostic tree runtime (ComponentBuilderNode, ComponentRef, IdAllocator, ComponentTree, flatten_component_tree) into a2ui.builder.core
- Implement version-specific wire models (Action, FunctionCall, envelopes) under a2ui.builder.v0_9
- Relocate basic catalog to a2ui.builder.v0_9.catalogs.basic with explicit versioned imports
- Remove unversioned latest-defaulting facades to ensure zero breaking changes across protocol evolutions
- Add PEP 561 py.typed marker to a2ui_agent
…sioned builder imports

- Dynamically extract catalog protocol version from protocolVersion, version, or catalogId/schema URL in Catalog.fromJson
- Default PythonEmitter baseImport to a2ui.builder.v<version> based on catalog specVersion
- Remove hardcoded default for --base-import flag in codegen command
- Update CLI conformance expectations in codegen.yaml to reference a2ui.builder.v0_9
@jacobsimionato
jacobsimionato force-pushed the feat/dart-cli-conformance branch from 7a66ff5 to 31ddebd Compare September 10, 2026 20:44
…ing and loose deserialization

- Update pydantic_fluent_builders_and_deserialization.md to document strict Literal enums for authoring safety
- Document two-constructor architecture separating strict __init__ authoring from secondary Phase 2 deserialization constructors
- Update CLI generation guide to reflect Dart CLI (dart/a2ui_cli)
@jacobsimionato
jacobsimionato force-pushed the feat/typescript-cli-proposals branch from 684ee62 to 4b69e36 Compare September 10, 2026 20:44

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the @a2ui/cli package, a developer tool and type-safe component generator that parses catalog schemas to emit Python builder modules. It includes a catalog analyzer, a Python emitter, modular CLI commands, comprehensive tests, and architectural proposals for macros and Pydantic-based builders. The review feedback highlights several critical improvements for the Python emitter and analyzer: recursively serializing nested function arguments, implementing a robust 'version at least' check for forward-compatible base imports, sorting generated class properties to place required fields before optional ones to avoid Python type errors, and explicitly handling 'integer' and 'float' return types in the catalog analyzer.

Comment on lines +275 to +277
argsDictItems.push(
` if ${pyParam} is not None:\n args["${pName}"] = ${pyParam}.to_dict() if hasattr(${pyParam}, "to_dict") else ${pyParam}`,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Function arguments can be complex structures such as lists of components (e.g., SlotList) or nested dictionaries. Simply checking hasattr(pyParam, 'to_dict') will fail to serialize components nested inside lists or dictionaries because the list/dict itself does not have a to_dict method.

Using the helper function _serialize_prop recursively serializes any nested components, lists, or dictionaries, ensuring consistent and correct serialization for all function arguments.

Suggested change
argsDictItems.push(
` if ${pyParam} is not None:\n args["${pName}"] = ${pyParam}.to_dict() if hasattr(${pyParam}, "to_dict") else ${pyParam}`,
);
argsDictItems.push(
' if ' + pyParam + ' is not None:\n args["' + pName + '"] = _serialize_prop(' + pyParam + ')'
);

Comment on lines +55 to +61
export function resolveDefaultBaseImport(specVersion: string): string {
const clean = specVersion.startsWith('v') ? specVersion.slice(1) : specVersion;
if (clean.startsWith('1.')) {
return 'a2ui.builder.v1_0';
}
return 'a2ui.builder.v0_9';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

When checking protocol or specification versions for feature availability, use a 'version at least' check instead of strict prefix matching or equality to ensure forward compatibility with future versions (e.g., major versions >= 1, or version strings like v1 without a trailing dot).

Currently, clean.startsWith('1.') will fail for a major version of 1 (without minor version) or any future major version like 2.0.

Suggested change
export function resolveDefaultBaseImport(specVersion: string): string {
const clean = specVersion.startsWith('v') ? specVersion.slice(1) : specVersion;
if (clean.startsWith('1.')) {
return 'a2ui.builder.v1_0';
}
return 'a2ui.builder.v0_9';
}
export function resolveDefaultBaseImport(specVersion: string): string {
const clean = specVersion.startsWith('v') ? specVersion.slice(1) : specVersion;
const major = parseInt(clean.split('.')[0], 10);
if (!isNaN(major) && major >= 1) {
return 'a2ui.builder.v1_0';
}
return 'a2ui.builder.v0_9';
}
References
  1. When checking protocol or specification versions for feature availability, use a 'version at least' check (e.g., >= or a helper like _is_version_at_least_1_0) instead of strict equality (e.g., == 'v1.0') to ensure forward compatibility with future versions.

Comment on lines +223 to +243
for (const [pName, prop] of comp.properties.entries()) {
if (pName === 'component') continue;
const pyName = sanitizeIdent(pName);
const pyType = typeToPython(prop.type);

if (prop.isRequired) {
lines.push(` ${pyName}: ${pyType}`);
} else {
let defaultStr = 'None';
if (prop.defaultValue !== undefined) {
if (typeof prop.defaultValue === 'string') {
defaultStr = `"${prop.defaultValue}"`;
} else if (typeof prop.defaultValue === 'boolean') {
defaultStr = prop.defaultValue ? 'True' : 'False';
} else if (typeof prop.defaultValue === 'number') {
defaultStr = String(prop.defaultValue);
}
}
lines.push(` ${pyName}: Optional[${pyType}] = ${defaultStr}`);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Python, defining a non-default argument (required field) after a default argument (optional field with a default value) in a class definition can cause static type checkers (like Pyright or Mypy) to flag type errors or synthesize invalid __init__ signatures.

To prevent this, sort the component properties so that all required properties are defined before optional properties in the generated Python class.

    const sortedProps = [...comp.properties.entries()]
      .filter(([pName]) => pName !== 'component')
      .sort((a, b) => {
        if (a[1].isRequired && !b[1].isRequired) return -1;
        if (!a[1].isRequired && b[1].isRequired) return 1;
        return 0;
      });

    for (const [pName, prop] of sortedProps) {
      const pyName = sanitizeIdent(pName);
      const pyType = typeToPython(prop.type);

      if (prop.isRequired) {
        lines.push('    ' + pyName + ': ' + pyType);
      } else {
        let defaultStr = 'None';
        if (prop.defaultValue !== undefined) {
          if (typeof prop.defaultValue === 'string') {
            defaultStr = '"' + prop.defaultValue + '"';
          } else if (typeof prop.defaultValue === 'boolean') {
            defaultStr = prop.defaultValue ? 'True' : 'False';
          } else if (typeof prop.defaultValue === 'number') {
            defaultStr = String(prop.defaultValue);
          }
        }
        lines.push('    ' + pyName + ': Optional[' + pyType + '] = ' + defaultStr);
      }
    }

Comment on lines +175 to +188
switch (fn.returnType) {
case 'string':
returnTypeDesc = {kind: 'primitive', primitive: 'string'};
break;
case 'number':
returnTypeDesc = {kind: 'primitive', primitive: 'float'};
break;
case 'boolean':
returnTypeDesc = {kind: 'primitive', primitive: 'boolean'};
break;
default:
returnTypeDesc = {kind: 'primitive', primitive: 'any'};
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The switch statement for fn.returnType currently does not handle 'integer' or 'float' return types, causing them to fall back to 'any'. Since PrimitiveKind supports 'integer' and 'float', adding explicit cases for them ensures more precise type descriptors are generated.

      switch (fn.returnType) {
        case 'string':
          returnTypeDesc = {kind: 'primitive', primitive: 'string'};
          break;
        case 'number':
        case 'float':
          returnTypeDesc = {kind: 'primitive', primitive: 'float'};
          break;
        case 'integer':
          returnTypeDesc = {kind: 'primitive', primitive: 'integer'};
          break;
        case 'boolean':
          returnTypeDesc = {kind: 'primitive', primitive: 'boolean'};
          break;
        default:
          returnTypeDesc = {kind: 'primitive', primitive: 'any'};
          break;
      }

@jacobsimionato
jacobsimionato force-pushed the feat/dart-cli-conformance branch 6 times, most recently from 3cda285 to f169ce2 Compare September 22, 2026 05:06
@jacobsimionato
jacobsimionato force-pushed the feat/dart-cli-conformance branch 6 times, most recently from bcfb6b3 to 59da919 Compare September 23, 2026 00:23
@jacobsimionato
jacobsimionato force-pushed the feat/dart-cli-conformance branch 12 times, most recently from b6d1706 to f3df8ec Compare September 24, 2026 05:06
@jacobsimionato

Copy link
Copy Markdown
Collaborator Author

Closing as we have Dart builders so we don't need TypeScript builders.

@github-project-automation github-project-automation Bot moved this from Todo to Done in A2UI Sep 28, 2026
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