feat: add Definition entity with CEL expression support (#117)#133
feat: add Definition entity with CEL expression support (#117)#133github-actions[bot] merged 1 commit intomainfrom
Conversation
Fixes: #117 Implements the Definition entity that provides a declarative way to define model configurations with support for CEL expressions and parameterized inputs. This closes #117. - Add `Definition` entity with branded ID, Zod validation, and JSON Schema support for inputs - Add YAML-based repositories for raw and evaluated definitions - Integrate with existing expression evaluation service for CEL expression support - Add domain events for definition lifecycle (created, updated, deleted) - Add comprehensive unit tests (28) and integration tests (10) ## Plan Comparison | Planned Item | Status | Notes | |--------------|--------|-------| | **Files to Create** | | | | `src/domain/definitions/definition.ts` | ✅ Created | Includes entity, schema, and branded ID (combined vs separate files) | | `src/domain/definitions/definition_schema.ts` | ✅ Merged | Combined into `definition.ts` for cohesion | | `src/domain/definitions/definition_id.ts` | ✅ Merged | Combined into `definition.ts` for cohesion | | `src/infrastructure/persistence/yaml_definition_repository.ts` | ✅ Created | Storage at `.swamp/definitions/{type}/{id}.yaml` | | `src/infrastructure/persistence/yaml_evaluated_definition_repository.ts` | ✅ Created | Storage at `.swamp/definitions-evaluated/{type}/{id}.yaml` | | `src/domain/definitions/definition_test.ts` | ✅ Created | 28 unit tests | | **Files to Update** | | | | `src/domain/expressions/expression_evaluation_service.ts` | ✅ Updated | Added `evaluateDefinition()`, `evaluateAllDefinitions()`, `hasDefinitionExpressions()` | | **Additional Files** | | | | `src/domain/definitions/repositories.ts` | ✅ Created | Repository interface (DDD pattern) | | `integration/definition_test.ts` | ✅ Created | 10 integration tests for CEL evaluation | | `src/domain/expressions/model_resolver.ts` | ✅ Updated | Added definition support to context | | `src/domain/events/types.ts` | ✅ Updated | Added definition domain events | | `.gitignore` | ✅ Updated | Added `.swamp/definitions-evaluated/` | ### Acceptance Criteria - [x] Definition entity supports all design doc fields (`id`, `name`, `tags`, `attributes`, `version`, `inputs`) - [x] Repository saves/loads definitions as YAML - [x] CEL expressions preserved in raw definitions - [x] Evaluated definitions cached in `.swamp/definitions-evaluated/` - [x] Integration with existing expression evaluation service - [x] Unit tests for Definition entity (28 tests) - [x] Integration test: create definition with CEL expressions, evaluate expressions (10 tests) ### Expression Context Support | Context Path | Status | Notes | |--------------|--------|-------| | `inputs.{param}` | ✅ Supported | Access definition input values | | `self.{field}` | ✅ Supported | Self-reference within definition | | `model.{name}.definition.attributes` | ✅ Supported | Reference other definitions | | `model.{name}.input.attributes` | ✅ Supported | Reference model inputs | | `model.{name}.resource.attributes` | ✅ Supported | Reference model resources | | `vault.get(vault_name, key)` | ✅ Supported | Already existed | | `model.{name}.data.{data-name}.attributes` | 📋 Future | Documented as architectural enhancement | ## Test plan - [x] Run `deno check` - Type checking passes - [x] Run `deno lint` - Linting passes - [x] Run `deno fmt` - Formatting applied - [x] Run `deno run test` - All 1378 tests pass - [x] Run `deno run compile` - Binary compiles successfully 🤖 Generated with [Claude Code](https://claude.ai/code)
e22c5ee to
210f596
Compare
There was a problem hiding this comment.
Review Summary
This PR implements the Definition entity with CEL expression support. The implementation is well-structured and follows established patterns in the codebase.
✅ Strengths
Domain-Driven Design Compliance:
Definitionentity follows established DDD patterns (branded ID, private constructor with factory methods, Zod validation)- Repository interface (
DefinitionRepository) properly abstracts persistence for the aggregate root - Domain events (
DefinitionCreated,DefinitionUpdated,DefinitionDeleted) capture lifecycle changes for cross-aggregate communication
Code Quality:
- Uses named exports throughout (no default exports)
- No
anytypes - TypeScript strict mode requirements followed - Proper encapsulation with getters returning copies of mutable state (
structuredClonefor nested objects) - Consistent patterns with existing entities like
ModelInput
Test Coverage:
- 28 unit tests for the Definition entity
- 10 integration tests covering CEL expression evaluation flows
- Tests cover edge cases (empty attributes, self-references, cross-model references)
Integration:
- Clean integration with existing
ExpressionEvaluationService - Model resolver properly updated to support definition context
.gitignorecorrectly updated for evaluated definitions cache
📋 Suggestions (Non-blocking)
-
Consider adding repository unit tests: The project has a pattern of having unit tests next to source files (e.g.,
yaml_input_repository_test.ts). While the integration tests cover the repositories well, addingyaml_definition_repository_test.tsandyaml_evaluated_definition_repository_test.tswould align with the codebase convention and provide faster feedback during development. -
Interface consistency:
YamlEvaluatedDefinitionRepositoryhas a similar API toYamlDefinitionRepositorybut doesn't implementDefinitionRepository. This is likely intentional since evaluated repos serve a different purpose (caching), but consider documenting this design decision or creating a separate interface to prevent API drift. -
Attribute copy consistency:
Definition.attributesusesstructuredClone(this._attributes)whileModelInput.attributesuses{ ...this._attributes }. The Definition approach is more robust for deeply nested structures. Consider updatingModelInputto match this pattern in a future PR for consistency.
Overall, this is a well-implemented feature that adheres to the project's DDD principles and coding standards. The test coverage is comprehensive and the integration with existing systems is clean.
🤖 Reviewed by Claude Code
…lve (swamp-club#133) (systeminit#1197) Fixes [swamp-club#133](https://swamp.club/lab/issues/133). ## Summary Auto-resolve treated any `pulled-extensions/<name>/` directory that existed on disk as "installed" — regardless of whether its contents were complete. A tree with `manifest.yaml` missing or a declared kind subdirectory missing still passed the `isInstalled` check, so the resolver emitted `reason: already_installed` and the downstream loader produced a confusing `Unknown datastore type "@swamp/s3-datastore"` / `Unknown model type: @swamp/issue-lifecycle` error with no pointer at what was actually wrong. This is a regression from [PR systeminit#1187](systeminit#1187) (swamp-club#121). Before that change, the adapter called `installExtension` with `force: true` on any load failure — a truncated tree self-healed on the next command. After systeminit#1187 the strict "never overwrite" guard caught the truncated case alongside the user-edit case it was meant for, and the truncation recovery path was lost. ## Approach The fix extends the "never overwrite" guarantee to truncation while giving users an actionable error. It is **read-only** — no filesystem mutation in the auto-resolve path. `--force` remains the only way auto-install state can overwrite a pulled extension. - Introduces a tri-state `InstallationInspection` result (`missing` | `intact` | `truncated`) on `ExtensionInstallerPort`, replacing `isInstalled` + `installedPath`. Both the path and the missing-files list ride back on the result. - The adapter drives the check from the lockfile's file list (authoritative file-level granularity, so partial truncation inside a kind subdir is caught too — not just whole-subdir removal). - The domain service branches on the three states: `missing` → install, `intact` → `alreadyInstalledButFailed` (unchanged systeminit#121 path for user WIP), `truncated` → new `alreadyInstalledTruncated` event naming every missing file plus the `swamp extension pull <name> --force` recovery command. Auto-repair was considered and rejected. The user-journey analysis surfaced that mutating the filesystem in response to a read-shaped command has too many quiet side effects for shared-repo / multi-worktree setups: silent version bumps (install is always `version: null` → latest), concurrent-worktree-rm reversal, masking of deliberate user state changes, and latency surprise. A loud, actionable error with an explicit recovery command is safer and keeps the systeminit#121 invariant intact. ## What changed - `src/domain/extensions/extension_auto_resolver.ts` — new `InstallationInspection` type, port method `inspectInstallation` (replaces `isInstalled` + `installedPath`), new `alreadyInstalledTruncated` event on `AutoResolveOutputPort`, three-way branch in `installAndLoad`. - `src/cli/auto_resolver_adapters.ts` — implements `inspectInstallation` against the lockfile + per-extension directory + per-file stats; wires the new renderer through `createAutoResolveOutputAdapter`. - `src/presentation/renderers/extension_auto_resolve.ts` — new `renderAutoResolveTruncated` for both `log` (three-line actionable error) and `json` (`{"event":"auto_resolve","status":"failed","reason":"truncated","missing":[...]}`). - `src/cli/auto_resolver_adapters_test.ts` — 7 new `inspectInstallation` tests replacing the 5 prior `isInstalled`/`installedPath` cases; covers missing / intact / truncated / partial-truncation / all-files-missing / empty-files edge cases. - `src/domain/extensions/extension_auto_resolver_test.ts` — updated mock port; new regression guards for the three-way branch (`intact → alreadyInstalledButFailed` preserved from systeminit#121, `truncated → alreadyInstalledTruncated` is new). - `integration/auto_resolver_truncated_test.ts` — new hermetic integration test: hand-written scratch fixture, deletes one declared file, asserts the truncated event fires with the missing file AND the on-disk tree is byte-identical after the resolve attempt. - `integration/auto_resolver_no_clobber_test.ts` — ported to the new port method (no behavior change; same systeminit#121 invariant). - `design/extension.md` — "Safety: never overwrite on-disk extensions" section extended with the tri-state taxonomy, truncation predicate, and JSON event shape for scripting consumers. ## Coverage Works across all kinds that go through auto-resolve (models, vaults, datastores) — any file missing anywhere in the extension tree is detected when auto-resolve fires for a type from that extension, because `inspectInstallation` walks the whole lockfile file list. Driver-only or report-only extensions (no auto-resolve trigger) are not covered by this PR; extending to those would require new `resolveDriverType` / `resolveReportType` plumbing and is scope creep for systeminit#133. Worth a follow-up if belt-and-braces coverage is desired. ## Test plan - [x] `deno fmt` clean - [x] `deno check` clean - [x] `deno lint` clean - [x] `deno run test` — 4445 pass / 0 fail (6 new adapter tests, 2 new domain-service tests, 1 new integration test; rewrote 5 systeminit#121 regression cases against the new port shape) - [x] `deno run compile` — binary built - [x] Manual reproduction against `/tmp/swamp-repro-issue-133` with the compiled binary: - **Before truncation:** `swamp extension pull @swamp/digitalocean` then `swamp model create @swamp/digitalocean/droplet test-droplet` succeeds. - **Truncate:** `rm -rf .swamp/pulled-extensions/@swamp/digitalocean/models`. - **Re-run `model create`:** json mode emits `{"event":"auto_resolve","status":"failed","reason":"truncated","missing":[68 files]}`; log mode emits three ERR lines naming the missing files and the `swamp extension pull @swamp/digitalocean --force` recovery. - **Follow the recovery command:** `swamp extension pull @swamp/digitalocean --force` restores the tree. - **Re-run `model create`:** succeeds. - [x] Tree on disk is byte-identical before and after the resolve attempt — no auto-repair, no silent overwrite. ## Follow-up - Driver-only / report-only extensions remain uncovered by auto-resolve (see "Coverage" above). Worth a separate issue if exhaustive cross-kind coverage is desired. - Root cause of truncation itself is still unknown (partial extract, interrupted install, concurrent worktree op). This PR makes the symptom actionable so the cause is easier to investigate when it next occurs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
…tion check (systeminit#1199) ## Summary The tri-state `inspectInstallation` introduced in systeminit#1197 stats every path in the lockfile's file list to detect a truncated install. That list includes compiled bundles under `.swamp/bundles/**`, `.swamp/vault-bundles/**`, `.swamp/driver-bundles/**`, `.swamp/datastore-bundles/**`, and `.swamp/report-bundles/**` — regenerable build artifacts, not source. When a user clears any of those caches (normal hygiene, stale rebuild, fresh worktree), auto-resolve flipped from `intact` → `truncated` and emitted: ``` incomplete — missing 1 file(s): ".swamp/bundles/91139983/system_usage.js" ``` …instead of the `alreadyInstalledButFailed` ("already installed at … run `--force` to recover") path that protects user WIP per systeminit#121. The motivating swamp-club#133 symptoms are all source-tree artifacts (manifest, kind subdirs, declared source), so the fix is to scope the truncation predicate to source files only. This filters bundle prefixes out of the stat loop in `inspectInstallation`. Prefixes are derived from `SWAMP_SUBDIRS.{bundles, vaultBundles, driverBundles, datastoreBundles, reportBundles}` so a future bundle-dir addition stays in sync. No filesystem mutation, no API/event/wire-format changes. ## Test Plan - [x] `deno fmt --check`, `deno lint`, `deno check` clean - [x] `src/cli/auto_resolver_adapters_test.ts` — 18/18 pass, including 2 new regression cases: - `inspectInstallation ignores absent bundle artifacts and stays intact` (covers all 5 bundle subdirs) - `inspectInstallation reports truncated only for missing source, not missing bundles` - [x] `integration/auto_resolver_truncated_test.ts` — passes (systeminit#1197/systeminit#133 source-truncation behaviour preserved) - [x] `integration/auto_resolver_no_clobber_test.ts` — passes (systeminit#121 no-clobber invariant preserved) - [x] `deno run compile` succeeded - [x] **End-to-end repro against the rebuilt binary:** `swamp extension pull @stack72/[email protected]` → edit pulled `.ts` with WIP marker + syntax error → `rm -rf .swamp/bundles` → `swamp model create @stack72/system-usage instance1` now produces: ``` Extension "@stack72/system-extensions" is already installed at … but failed to load. Local edits may be preventing it from registering — inspect the source and fix errors. To reset to the registry version and discard local changes, run: swamp extension pull "@stack72/system-extensions" --force ``` …instead of the buggy `incomplete — missing 1 file(s): ".swamp/bundles/91139983/system_usage.js"`. - [ ] UAT `tests/cli/extension/pull_wip_preservation_test.ts:156` in swamp-uat (CI gate — local run blocked by membership auto-trust collision unrelated to this fix) ## Cross-references - Introducing PR: systeminit#1197 - Fixes regression to swamp-club#121 / systeminit#1187 user-WIP preservation - swamp-uat coverage: swamp-uat#149 Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Fixes: #117
Implements the Definition entity that provides a declarative way to define model configurations with support for CEL expressions and parameterized inputs. This closes #117.
Definitionentity with branded ID, Zod validation, and JSON Schema support for inputsPlan Comparison
src/domain/definitions/definition.tssrc/domain/definitions/definition_schema.tsdefinition.tsfor cohesionsrc/domain/definitions/definition_id.tsdefinition.tsfor cohesionsrc/infrastructure/persistence/yaml_definition_repository.ts.swamp/definitions/{type}/{id}.yamlsrc/infrastructure/persistence/yaml_evaluated_definition_repository.ts.swamp/definitions-evaluated/{type}/{id}.yamlsrc/domain/definitions/definition_test.tssrc/domain/expressions/expression_evaluation_service.tsevaluateDefinition(),evaluateAllDefinitions(),hasDefinitionExpressions()src/domain/definitions/repositories.tsintegration/definition_test.tssrc/domain/expressions/model_resolver.tssrc/domain/events/types.ts.gitignore.swamp/definitions-evaluated/Acceptance Criteria
id,name,tags,attributes,version,inputs).swamp/definitions-evaluated/Expression Context Support
inputs.{param}self.{field}model.{name}.definition.attributesmodel.{name}.input.attributesmodel.{name}.resource.attributesvault.get(vault_name, key)model.{name}.data.{data-name}.attributesTest plan
deno check- Type checking passesdeno lint- Linting passesdeno fmt- Formatting applieddeno run test- All 1378 tests passdeno run compile- Binary compiles successfully🤖 Generated with Claude Code