fix(zfa build): scaffold build.yaml when missing, fail loudly when zorphy builder unregistered (#276) - #282
Conversation
…rphy builder unregistered (#276)
|
@coderabbitai review |
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthrough
ChangesBuild preflight and output validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Developer
participant ZfaBuild
participant BuildYamlGuard
participant build_runner
participant ProjectFiles
Developer->>ZfaBuild: run zfa build
ZfaBuild->>BuildYamlGuard: check or scaffold build.yaml
BuildYamlGuard->>ProjectFiles: read or write build.yaml
BuildYamlGuard-->>ZfaBuild: return configuration status
ZfaBuild->>build_runner: run configured build
build_runner->>ProjectFiles: write generated outputs
ZfaBuild->>ProjectFiles: scan annotated sources and outputs
ZfaBuild-->>Developer: report build result or warning
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/src/commands/build_command.dart (1)
42-44: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPrevent side effects in dry-run mode.
dryRunchanges output text but does not stop normal build execution.
lib/src/commands/build_command.dart#L42-L44: do not clean the build cache whendryRunis true.lib/src/commands/build_command.dart#L75-L75: return after dry-run reporting instead of invokingbuild_runner.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/src/commands/build_command.dart` around lines 42 - 44, Update lib/src/commands/build_command.dart lines 42-44 so _cleanBuildCache() runs only when clean is enabled and dryRun is false; update line 75 so the dry-run reporting path returns before invoking build_runner. Use the existing build command flow and dryRun flag, preserving normal execution when dryRun is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/src/commands/build_command.dart`:
- Around line 143-176: Update _hasGeneratedOutputs and _warnIfNoOutputsGenerated
to correlate generated outputs with each `@Zorphy/`@ZorphyMixin-annotated source,
rather than treating any lib .zorphy.dart or .g.dart file as sufficient. Ensure
sources missing their expected part output or excluded by build.yaml still
trigger the warning, while preserving the best-effort non-fatal behavior; add
coverage for an unrelated existing generated file unless .g.dart is guaranteed
to be exclusively produced by Zorphy.
In `@lib/src/commands/build_yaml_guard.dart`:
- Around line 54-57: Update BuildYamlGuard.scaffold and
BuildCommand._ensureBuildYaml to create build.yaml exclusively: after the
initial missing-file check, have scaffold call File.create(exclusive: true)
before writing content, so existing files are never overwritten. Catch
PathExistsException in _ensureBuildYaml, rerun the guard/check, and only write
when the file was confirmed absent during the original check.
- Around line 66-71: Update BuildYamlGuard._registersZorphyBuilder to inspect
only targets.$default.builders and require zorphy:zorphy to be enabled,
accepting generate_for as the fallback configuration. Reject disabled
registrations and unrelated root-level zorphy:zorphy keys, and add tests
covering both cases.
In `@test/commands/build_command_test.dart`:
- Around line 35-46: Update the setUpAll setup around zfaBin and
useCompiledBinary to always invoke the checkout entrypoint at bin/zfa.dart,
removing the preference for $HOME/.local/bin/zfa. If startup performance
requires a compiled binary, build that binary from the current checkout before
assigning it to zfaBin.
- Around line 80-82: Update all five subprocess-output sites in
test/commands/build_command_test.dart (anchor lines 80-82 and sibling lines
99-101, 133-135, 162-164, and 200-201): start stdout and stderr stream
collectors before awaiting either result, then await both collectors
concurrently; in the malformed-build test, collect stderr before awaiting
exitCode.
---
Outside diff comments:
In `@lib/src/commands/build_command.dart`:
- Around line 42-44: Update lib/src/commands/build_command.dart lines 42-44 so
_cleanBuildCache() runs only when clean is enabled and dryRun is false; update
line 75 so the dry-run reporting path returns before invoking build_runner. Use
the existing build command flow and dryRun flag, preserving normal execution
when dryRun is false.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a2d0da6f-d24a-4d87-8e90-5e913add47a5
📒 Files selected for processing (4)
lib/src/commands/build_command.dartlib/src/commands/build_yaml_guard.darttest/commands/build_command_test.darttest/commands/build_yaml_guard_test.dart
| /// Warns (non-fatally) when build_runner exited 0 but produced no `.zorphy` | ||
| /// / `.g.dart` outputs despite `@Zorphy`-annotated sources being present. | ||
| /// Catches misconfigurations the pre-flight check can't detect statically. | ||
| void _warnIfNoOutputsGenerated() { | ||
| try { | ||
| final hasZorphySources = _hasZorphyAnnotatedSources(); | ||
| final hasOutputs = _hasGeneratedOutputs(); | ||
| if (hasZorphySources && !hasOutputs) { | ||
| print( | ||
| '\n⚠️ build_runner wrote 0 outputs although @Zorphy sources exist.\n' | ||
| ' Check build.yaml registers `zorphy:zorphy` for lib/src/** and\n' | ||
| ' that the annotated files are under the configured generate_for\n' | ||
| ' glob. Run `zfa setup` to regenerate a known-good build.yaml.', | ||
| ); | ||
| } | ||
| } catch (_) { | ||
| // Best-effort safety net — never fail the build from this path. | ||
| } | ||
| } | ||
|
|
||
| bool _hasGeneratedOutputs() { | ||
| final libDir = Directory('lib'); | ||
| if (!libDir.existsSync()) return false; | ||
| bool found = false; | ||
| for (final entity in libDir.listSync(recursive: true)) { | ||
| if (entity is File) { | ||
| final name = p.basename(entity.path); | ||
| if (name.endsWith('.zorphy.dart') || name.endsWith('.g.dart')) { | ||
| found = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| return found; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
What output file names does the zorphy builder generate for @Zorphy sources, and how does it interact with source_gen combining_builder outputs?
💡 Result:
The Zorphy builder generates code for classes annotated with @Zorphy by producing files with the.zorphy.dart extension [1][2]. Additionally, classes annotated with @Zorphy2 generate files with the.zorphy2.dart extension [1][3]. Regarding interaction with source_gen's combining_builder: The Zorphy builder functions as part of the standard Dart build_runner process [1][4]. While Zorphy utilizes source_gen in its implementation, it operates by generating specific part files (.zorphy.dart) associated with the source code [1][2]. The.zorphy2.dart files are designed to build prior to the main Zorphy generation [1][3]. In the context of build_runner, these generated part files are typically handled by the combining_builder (or similar mechanisms in the build system) to merge the generated code into the final project structure [5].
Citations:
- 1: https://github.com/arrrrny/zorphy
- 2: https://pub.dev/packages/zorphy_annotation
- 3: https://pub.dev/packages/zorphy
- 4: https://pub.dev/packages/zorphy/versions/1.7.0
- 5: https://pub.dev/packages/zorphy/versions
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
fd -a 'build_command\.dart|build\.yaml|pubspec\.yaml' . | sed 's#^\./##'
echo "== build_command outline =="
ast-grep outline lib/src/commands/build_command.dart --view expanded || true
echo "== relevant build_command lines =="
sed -n '1,240p' lib/src/commands/build_command.dart | cat -n
echo "== search zorphy annotations/build usage =="
rg -n "`@Zorphy`|zorphy|\.zorphy|zorphy_annotation|zorphy_builder|build_runner|generate_for|build.yaml" -S .Repository: arrrrny/zuraffa
Length of output: 50371
🌐 Web query:
zorphy pub.dev source_gen builder .zorphy.dart annotation part files
💡 Result:
Zorphy is a code generation package for Dart and Flutter that automates the creation of boilerplate code such as copyWith methods, JSON serialization, equality operators, toString methods, and patch mechanisms [1][2][3]. To use Zorphy with your Dart/Flutter projects, you must include the required dependencies in your pubspec.yaml file [1][2]: dependencies: zorphy_annotation: ^1.7.0 dev_dependencies: zorphy: ^1.7.0 build_runner: ^2.4.0 As Zorphy utilizes the Dart source_gen and build_runner infrastructure, it requires you to define a part file in your source code [3][4]. By convention, Zorphy uses the.zorphy.dart file extension for these generated parts [3]. To implement this in a file (e.g., user.dart), you must: 1. Import the zorphy_annotation package [3]. 2. Declare the part directive pointing to the.zorphy.dart file [3]. 3. Annotate your classes with the appropriate Zorphy annotations (e.g., @Zorphy) to trigger the code generation [5][3]. Example structure: import 'package:zorphy_annotation/zorphy_annotation.dart'; part 'user.zorphy.dart'; @Zorphy abstract class User { String get name; int get age; } After setting up your files, run the build_runner command in your terminal to generate the necessary code [1][3]: dart run build_runner build
Citations:
- 1: https://pub.dev/packages/zorphy
- 2: https://pub.dev/packages/zorphy/versions/1.8.10
- 3: https://pub.dev/packages/zorphy_annotation
- 4: https://pub.dev/packages/source_gen
- 5: https://pub.dev/documentation/zorphy/latest/zorphy/
🌐 Web query:
zorphy_annotation AnnotationSourceGenerator .zorphy.dart generated file extension
💡 Result:
In the Zorphy code generation package for Dart and Flutter, the.zorphy.dart file extension is the standard naming convention for generated part files [1][2][3]. When you annotate an abstract class with @Zorphy, you must include a part directive in your source file that matches the filename with a.zorphy.dart extension (e.g., if your file is named user.dart, you should add part 'user.zorphy.dart';) [1][3][4]. The Zorphy generator then creates the corresponding code—such as constructors, copyWith methods, JSON serialization, and equality operators—in that generated file [1][5]. Additionally, the package uses the.zorphy2.dart extension for code generated from the @Zorphy2 annotation, which is designed to build before the primary Zorphy generator [3][4].
Citations:
- 1: https://pub.dev/packages/zorphy_annotation
- 2: https://pub.dev/documentation/zorphy_annotation/latest/
- 3: https://github.com/arrrrny/zorphy
- 4: https://pub.dev/packages/zorphy
- 5: https://pub.dev/documentation/zorphy/latest/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== build.yaml =="
cat -n build.yaml
echo "== build_command_test relevant section =="
fd 'build_command_test\.dart' test -a | while read -r f; do
echo "--- $f"
sed -n '1,260p' "$f" | cat -n
done
echo "== deterministic path probe from lib/src/commands/build_command.dart text =="
python3 - <<'PY'
from pathlib import Path
import re
src = Path('lib/src/commands/build_command.dart').read_text()
print("has _hasGeneratedOutputs:", '_hasGeneratedOutputs()' in src)
print("generator accepts .zorphy.dart:", ".zorphy.dart" in src)
print("generator accepts .g.dart:", ".g.dart" in src)
print("skips annotated files:", ".zorphy.dart' || name.endsWith('.g.dart')" in src)
print("checks annotated `@Zorphy` or `@ZorphyMixin`:", '`@Zorphy`" || src.contains("`@ZorphyMixin`")' in src)
print("uses hasZorphySources and !hasOutputs:", 'hasZorphySources && !hasOutputs' in src)
m = re.search(r"bool _hasGeneratedOutputs\(\).*?\n \}", src, re.S)
if not m:
raise SystemExit("_hasGeneratedOutputs not found")
print("implementation:")
print(m.group(0), end="")
m = re.search(r"bool _hasZorphyAnnotatedSources\(\).*?\n \}", src, re.S)
if not m:
raise SystemExit("_hasZorphyAnnotatedSources not found")
print("implementation:")
print(m.group(0), end="")
print("\n== possible config that keeps unrelated output while missing zorphy output ==")
print("build.yaml contains zorphy:zorphy:", ":zorphy:" in Path('build.yaml').read_text())
print("build.yaml generate_for entries:")
for line in Path('build.yaml').read_text().splitlines():
if 'generate_for' in line or 'builder' in line or re.search(r'^\s+src:', line):
print(line)
PY
echo "== existing tests mentioning generated output warning =="
rg -n "wrote 0 outputs|`@Zorphy` sources|generated output|hasGeneratedOutputs|_warnIfNoOutputsGenerated" test lib -SRepository: arrrrny/zuraffa
Length of output: 13655
🌐 Web query:
zorphy source_gen builder .g.dart output extension @ZorphyMixin``
💡 Result:
The Zorphy package uses the source_gen framework to generate code [1][2][3]. According to the project's documentation, Zorphy generates files with the extension .zorphy.dart (and .zorphy2.dart for specific annotations) rather than the standard .g.dart extension typically used by SharedPartBuilder [1][2]. Regarding @ZorphyMixin, while the @Zorphy annotation is the primary mechanism for generating immutable data classes, Zorphy's build system is integrated into build_runner and configured in build.yaml [1][2][3]. The generator produces code that includes standard Dart features like copyWith, JSON serialization, equality, and toString [1][4]. If you are encountering issues related to .g.dart output extensions, it is important to note that source_gen strongly recommends that only SharedPartBuilder uses the .g.dart extension to avoid conflicts [5][6][7]. Because Zorphy utilizes its own dedicated builder and file extensions (.zorphy.dart), it operates independently of the standard .g.dart output process [1][2]. Ensure your build.yaml correctly targets the zorphy builder, which is typically configured as follows: targets: $default: builders: zorphy|zorphy: enabled: true If you are manually implementing a mixin to work with Zorphy-generated classes, you should ensure that your mixin is applied to the class before the code generation runs, or that it is properly imported in the file that includes the part directive for the Zorphy-generated file [1][4].
Citations:
- 1: https://github.com/arrrrny/zorphy
- 2: https://pub.dev/documentation/zorphy/latest/
- 3: https://pub.dev/packages/zorphy/versions/1.8.10
- 4: https://pub.dev/packages/zorphy_annotation
- 5: https://github.com/dart-lang/source_gen
- 6: https://pub.dev/documentation/source_gen/
- 7: https://pub.dev/documentation/source_gen/latest/index.html
Correlate outputs with the annotated source set.
_hasGeneratedOutputs() treats any .zorphy.dart or .g.dart file under lib as a Zorphy-generated output. If another builder generates a file while a @Zorphy / @ZorphyMixin source lacks its part '...zorphy.dart' or was filtered by build.yaml, the warning is suppressed. Check outputs per annotated source set and add a test with an unrelated existing generated file, unless .g.dart is already covered by another generator in every supported project scenario.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/src/commands/build_command.dart` around lines 143 - 176, Update
_hasGeneratedOutputs and _warnIfNoOutputsGenerated to correlate generated
outputs with each `@Zorphy/`@ZorphyMixin-annotated source, rather than treating
any lib .zorphy.dart or .g.dart file as sufficient. Ensure sources missing their
expected part output or excluded by build.yaml still trigger the warning, while
preserving the best-effort non-fatal behavior; add coverage for an unrelated
existing generated file unless .g.dart is guaranteed to be exclusively produced
by Zorphy.
| static Future<void> scaffold({String? projectRoot}) async { | ||
| final root = projectRoot ?? Directory.current.path; | ||
| final file = File(p.join(root, 'build.yaml')); | ||
| await file.writeAsString(DependencyWirer.buildYamlContent); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
According to the official Dart documentation, how can File creation be made exclusive so a write does not overwrite a file created concurrently?
💡 Result:
According to the official Dart documentation, the recommended way to create a file exclusively—ensuring a write does not overwrite a file if it has been created concurrently—is to use the exclusive parameter of the File.create method [1]. When you call File.create with exclusive: true, the operation will fail if the file already exists [1]. In this case, the returned Future will complete with a PathExistsException [1]. If exclusive is false (the default), the file will be created if it does not exist, but existing files will be left untouched [1]. Example usage: dart import 'dart:io'; Future<void> createExclusive(String path) async { final file = File(path); try { await file.create(exclusive: true); print('File created successfully.'); } on PathExistsException { print('File already exists; skipping creation to avoid overwrite.'); } catch (e) { print('An error occurred: $e'); } } Note that Dart's file locking mechanisms (via RandomAccessFile.lock) are advisory and do not prevent other processes or parts of the system from opening, truncating, or deleting a file if they do not also check for those locks [2][3]. Therefore, for preventing concurrent overwrites during initial creation, using the atomic exclusive option in File.create is the intended mechanism [1].
Citations:
- 1: https://api.dart.dev/dart-io/File/create.html
- 2: https://api.dart.dev/stable/dart-io/RandomAccessFile/lock.html
- 3: FileLock.exclusive lock mode doesn't prevent from truncating a file to zero length dart-lang/sdk#41011
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)build_yaml_guard\.dart$|build|yaml' || true
echo "== outline =="
ast-grep outline lib/src/commands/build_yaml_guard.dart --view expanded || true
echo "== relevant file =="
cat -n lib/src/commands/build_yaml_guard.dart
echo "== callers / usage =="
rg -n "BuildYamlGuard|_ensureBuildYaml|scaffold\\(|ensureBuildYaml" lib test 2>/dev/null || trueRepository: arrrrny/zuraffa
Length of output: 13091
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== build_command relevant sections =="
cat -n lib/src/commands/build_command.dart | sed -n '1,180p'
echo "== tests outline/head =="
cat -n test/commands/build_yaml_guard_test.dart | sed -n '1,190p'
echo "== behavioral probe: Dart File.create write/overwrite semantics =="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat > "$tmpdir/semantics_probe.dart" <<'DART'
import 'dart:io';
Future<void> main() async {
final file = File('$tmpdir/build.yaml');
await file.writeAsString('initial');
print('initial=${file.readAsStringSync()}');
await file.writeAsString('overwritten');
print('after_writeAsString=${file.readAsStringSync()}');
try {
await file.create(exclusive: true);
print('exclusive_create=no exception');
} on PathExistsException catch (e) {
print('exclusive_create=PathExistsException: ${e.message}');
} catch (e) {
print('exclusive_create=other ${e.runtimeType}: $e');
}
}
DART
if command -v dart >/dev/null 2>&1; then
dart "$tmpdir/semantics_probe.dart"
else
echo "dart not available"
fiRepository: arrrrny/zuraffa
Length of output: 12810
Make build.yaml scaffolding create the file exclusively.
BuildCommand._ensureBuildYaml() checks build.yaml, then BuildYamlGuard.scaffold() calls writeAsString at a later async point. Dart File.writeAsString() overwrites an existing file. Use File.create(exclusive: true) before writing, re-run the guard on PathExistsException, and only write after confirming the file was missed in the original check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/src/commands/build_yaml_guard.dart` around lines 54 - 57, Update
BuildYamlGuard.scaffold and BuildCommand._ensureBuildYaml to create build.yaml
exclusively: after the initial missing-file check, have scaffold call
File.create(exclusive: true) before writing content, so existing files are never
overwritten. Catch PathExistsException in _ensureBuildYaml, rerun the
guard/check, and only write when the file was confirmed absent during the
original check.
| static bool _registersZorphyBuilder(String contents) { | ||
| if (contents.isEmpty) return false; | ||
| // Match `zorphy:zorphy` possibly followed by `:` (mapping) or whitespace, | ||
| // at the start of a line (builder keys are map keys under `builders:`). | ||
| final re = RegExp(r'^\s*zorphy:zorphy\s*:?', multiLine: true); | ||
| return re.hasMatch(contents); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For build_runner build.yaml configuration, how is a builder enabled or disabled under targets.$default.builders, and does a zorphy:zorphy key outside that map activate the builder?
💡 Result:
In a build_runner build.yaml configuration, builders are enabled or disabled within the targets.$default.builders map by setting the enabled property [1][2][3]. Setting enabled: false explicitly disables the builder for that specific target [4][2]. Conversely, setting enabled: true ensures the builder is active [2][3]. If this key is omitted, the builder's state is determined by its default auto_apply configuration [2][3]. Regarding the zorphy:zorphy key, placing it outside the targets section (e.g., at the root of the build.yaml) does not activate or configure the builder. The build_runner configuration strictly expects builder-specific settings to be defined within the targets section [1][2]. In the case of the zorphy package, the common usage shown in documentation (e.g., targets: $default: builders: zorphy:zorphy: { enabled: true }) is the correct way to register and enable that specific builder [5][6][7][8]. Any configuration key placed outside the structure defined by the build_config specification is ignored by the build_runner process [1][2].
Citations:
- 1: https://github.com/dart-lang/build/blob/master/docs/build_yaml_format.md
- 2: https://github.com/dart-lang/build/blob/master/build_config/README.md
- 3: https://pub.dev/documentation/build_config/
- 4: Question: how to configure
build.yamlto "disable" provided/inherited builder? dart-lang/build#3595 - 5: https://pub.dev/documentation/zorphy/latest/
- 6: https://pub.dev/packages/zorphy
- 7: https://github.com/arrrrny/zorphy
- 8: https://pub.dev/packages/zorphy/versions/1.7.0
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Files matching build_yaml_guard:\n'
fd -a 'build_yaml_guard\.dart$|zorphy|build\.yaml' . | sed 's#^\./##' | head -100
printf '\nbuild_yaml_guard outline:\n'
ast-grep outline lib/src/commands/build_yaml_guard.dart --view expanded || true
printf '\nRelevant build_yaml_guard contents:\n'
cat -n lib/src/commands/build_yaml_guard.dart
printf '\nSearch for BuildYamlGuard usages and tests:\n'
rg -n "BuildYamlGuard|build.yaml|zorphy:zorphy|_registersZorphyBuilder|BuildYamlStatus" .Repository: arrrrny/zuraffa
Length of output: 21524
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Current build.yaml:\n'
cat -n build.yaml
printf '\nbuild_command relevant contents:\n'
sed -n '40,165p' lib/src/commands/build_command.dart | cat -n
printf '\nExisting build_yaml_guard tests:\n'
cat -n test/commands/build_yaml_guard_test.dart
printf '\nBehavior simulation:_registersZorphyBuilder matches these configurations?\n'
python3 - <<'PY'
import re
import tempfile
os, makedirs, chdir = __import__('os'), __import__('os').makedirs, __import__('os').chdir
re = __import__('re')
rx = re.compile(r'^\s*zorphy:zorphy\s*:?', re.MULTILINE)
configs = {
'root zorphy key': 'zorphy:zorphy:\n enabled: true\n',
'disabled under default target builder': '''targets:
$default:
builders:
zorphy:zorphy:
enabled: false
generate_for:
- lib/src/**
''',
'enabled under default target builder': '''targets:
$default:
builders:
zorphy:zorphy:
enabled: true
generate_for:
- lib/src/**
- test/**
''',
'root builder key with disabled mapping inside': '''zorphy:zorphy:
enabled: false
generate_for:
- lib/src/**
''',
}
for name, s in configs.items():
print(f'{name}: {bool(rx.search(s))}')
PY
printf '\nFlutter/Dart package analyzer availability:\n'
if command -v flutter >/dev/null 2>&1; then
flutter --version 2>&1 | sed -n '1,5p'
fi
if command -v dart >/dev/null 2>&1; then
dart --version 2>&1 | sed -n '1,5p'
fiRepository: arrrrny/zuraffa
Length of output: 12286
Validate the active Zorphy builder configuration.
BuildYamlGuard._registersZorphyBuilder() returns BuildYamlStatus.ok for disabled or root-level zorphy:zorphy keys, while build_runner only enables builders under targets.$default.builders. Reject those cases by requiring targets.$default.builders.zorphy:zorphy with enabled: true, or use generate_for as a fallback. Add tests for disabled target registration and unrelated root zorphy:zorphy keys.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/src/commands/build_yaml_guard.dart` around lines 66 - 71, Update
BuildYamlGuard._registersZorphyBuilder to inspect only targets.$default.builders
and require zorphy:zorphy to be enabled, accepting generate_for as the fallback
configuration. Reject disabled registrations and unrelated root-level
zorphy:zorphy keys, and add tests covering both cases.
| setUpAll(() async { | ||
| final homeDir = Platform.environment['HOME'] ?? ''; | ||
| final compiledBin = path.join(homeDir, '.local', 'bin', 'zfa'); | ||
| final compiledExists = File(compiledBin).existsSync(); | ||
| if (compiledExists) { | ||
| zfaBin = compiledBin; | ||
| useCompiledBinary = true; | ||
| } else { | ||
| final projectRoot = await findProjectRoot(); | ||
| zfaBin = path.join(projectRoot, 'bin', 'zfa.dart'); | ||
| useCompiledBinary = false; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Run the command from the checkout.
This test uses $HOME/.local/bin/zfa when it exists. That binary can belong to another revision. The test can then pass or fail without testing this PR.
Always invoke the checkout entrypoint. Build a test binary from the checkout only if startup time requires it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/commands/build_command_test.dart` around lines 35 - 46, Update the
setUpAll setup around zfaBin and useCompiledBinary to always invoke the checkout
entrypoint at bin/zfa.dart, removing the preference for $HOME/.local/bin/zfa. If
startup performance requires a compiled binary, build that binary from the
current checkout before assigning it to zfaBin.
| final stdout = await proc.stdout.transform(systemEncoding.decoder).join(); | ||
| final stderr = await proc.stderr.transform(systemEncoding.decoder).join(); | ||
| final code = await proc.exitCode; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
According to Dart Process documentation, should stdout and stderr be consumed concurrently to prevent a child process from blocking on full output pipes?
💡 Result:
Yes, according to Dart documentation and recommended best practices, stdout and stderr must be consumed concurrently to prevent a child process from blocking [1][2][3][4]. The underlying communication for stdout and stderr uses pipes with limited capacity [1][2][3]. If a child process writes to these pipes faster than they are being read—or if one stream is left unread while the other is filled—the child process will block, waiting for the pipe buffer to have available space [1][2][3]. If this happens, the child process will never finish, and any code waiting for the process to exit will deadlock [1][2][4]. To avoid this, you should ensure that both streams are being actively consumed, such as by using listen or other asynchronous consumption methods, before or while waiting for the process to complete [5][4]. For example, you can register listeners for both streams or use methods that process both streams in parallel to ensure that neither pipe buffer becomes full and stalls the subprocess [4]. Simply reading one stream to completion before starting to read the other is a common cause of deadlocks [4].
Citations:
- 1: https://api.dart.dev/dart-io/Process/stdout.html
- 2: https://api.dart.dev/dart-io/Process-class.html
- 3: https://api.flutter.dev/flutter/dart-io/Process/stderr.html
- 4: https://zetcode.com/dart/process/
- 5: Process.start() gets blocked when read stderr before stdout dart-lang/sdk#50674
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file snippets =="
sed -n '65,105p;125,170p;190,210p' test/commands/build_command_test.dart
echo
echo "== process.stdout/stderr usages in target file =="
rg -n "proc\.stdout|proc\.stderr|exitCode|Process\.start" test/commands/build_command_test.dartRepository: arrrrny/zuraffa
Length of output: 5477
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== analyzer availability and package config =="
dart --version 2>&1 || true
if [ -f pubspec.yaml ]; then
sed -n '1,120p' pubspec.yaml
fi
echo
echo "== full target test function headers =="
argparse parse test/commands/build_command_test.dart || true
fd -a build_command_test.dart . | head -10
echo
echo "== stream transformation structure =="
python3 - <<'PY'
from pathlib import Path
p = Path('test/commands/build_command_test.dart')
s = p.read_text()
for i, line in enumerate(s.splitlines(), 1):
if 'await proc.stdout.transform(systemEncoding.decoder).join()' in line:
start=max(1,i-2); end=min(len(s.splitlines()), i+4)
print(f"\n-- occurrences around line {i} --")
for n in range(start,end+1):
print(f"{n}: {s.splitlines()[n-1]}")
PYRepository: arrrrny/zuraffa
Length of output: 3870
Drain stdout and stderr concurrently.
Dart subprocess pipes have limited buffers, so awaiting stdout to completion before consuming stderr can make the child process block. Start both stream collectors before awaiting either result in these tests, and collect stderr before awaiting exitCode in the malformed-build test.
📍 Affects 1 file
test/commands/build_command_test.dart#L80-L82(this comment)test/commands/build_command_test.dart#L99-L101test/commands/build_command_test.dart#L133-L135test/commands/build_command_test.dart#L162-L164test/commands/build_command_test.dart#L200-L201
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/commands/build_command_test.dart` around lines 80 - 82, Update all five
subprocess-output sites in test/commands/build_command_test.dart (anchor lines
80-82 and sibling lines 99-101, 133-135, 162-164, and 200-201): start stdout and
stderr stream collectors before awaiting either result, then await both
collectors concurrently; in the malformed-build test, collect stderr before
awaiting exitCode.
Closes #276
When build.yaml is absent,
zfa buildnow scaffolds one registering the zorphy builder (self-healing). When build.yaml exists but omitszorphy:zorphy,zfa buildfails loudly with an actionable error and exit 1 instead of silently reporting success with 0 outputs.Root cause
BuildCommandrandart run build_runner buildand reported success whenever the process exited 0 — but with nobuild.yaml(or one that did not registerzorphy:zorphy), build_runner exited 0 having written 0 outputs. Nozfacommand createdbuild.yaml, so users who skippedzfa init/zfa setupwere stuck.Secondary: build_runner 2.16.0 removed
--delete-conflicting-outputs, producing theW These options have been removedwarning quoted in the issue.Fix
New
lib/src/commands/build_yaml_guard.dartexposes:BuildYamlStatus check()→missing | missingZorphyBuilder | okscaffold()→ writesDependencyWirer.buildYamlContentmissingZorphyBuilderMessage→ actionable error textBuildCommand.run()now does a pre-flight before invoking build_runner:build.yaml, log, proceed (self-healing, option a)exit(1)without invoking build_runner (fail loudly, option b)Plus:
@Zorphysources exist but no.zorphy.dart/.g.dartwere produced, print a loud warning.--delete-conflicting-outputsfrom_runBuild()args (removed in build_runner 2.16.0).Dry-run reports the pre-flight plan (
Would scaffold…/omits the zorphy builder) without writing.Tests
test/commands/build_yaml_guard_test.dart— 10 unit tests (missing/ok/missingZorphyBuilder/empty/indented/comment-false-positive/scaffold-roundtrip/scaffold-content/default-cwd/actionable-message)test/commands/build_command_test.dart— 5 subprocess integration tests (--help, dry-run missing, dry-run misconfigured, build scaffolds-then-proceeds, build fails-loudly)Verification
dart analyzeon all 4 files: no issuesdart testfull suite: 1162/1162 passzfa build --dry-run→ pre-flightok, build_runner writes 28 outputs, no deprecated-flag warningzfa buildscaffolds build.yaml, proceedszfa buildfails loudly, exit 1, build_runner not invoked, user file untouchedSummary by CodeRabbit
build.yamlbefore building.