Repository navigation
feat(cli,docs): add TypeScript CLI implementation and macro architecture proposals - #2613
jacobsimionato wants to merge 12 commits into
Conversation
…talog fluent builders
…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
…lder.v0_9 package
…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
7a66ff5 to
31ddebd
Compare
…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)
…der imports in TypeScript CLI and proposals
684ee62 to
4b69e36
Compare
There was a problem hiding this comment.
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.
| argsDictItems.push( | ||
| ` if ${pyParam} is not None:\n args["${pName}"] = ${pyParam}.to_dict() if hasattr(${pyParam}, "to_dict") else ${pyParam}`, | ||
| ); |
There was a problem hiding this comment.
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.
| 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 + ')' | |
| ); |
| 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'; | ||
| } |
There was a problem hiding this comment.
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.
| 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
- 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.
| 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}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
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);
}
}| 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; | ||
| } |
There was a problem hiding this comment.
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;
}3cda285 to
f169ce2
Compare
bcfb6b3 to
59da919
Compare
b6d1706 to
f3df8ec
Compare
|
Closing as we have Dart builders so we don't need TypeScript builders. |
Summary
Introduces the TypeScript implementation of the
@a2ui/clideveloper 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
javascript/a2ui_cli/):"javascript/a2ui_cli".build,lint,lint:fix,test,format,format:check).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
yarn workspace @a2ui/cli test(28/28 passed)yarn workspace @a2ui/cli lint(clean)yarn workspace @a2ui/cli format:check(clean)