Skip to content

feat: add pre-flight checks for model method execution#697

Merged
stack72 merged 6 commits intomainfrom
feat/pre-flight-checks-619
Mar 12, 2026
Merged

feat: add pre-flight checks for model method execution#697
stack72 merged 6 commits intomainfrom
feat/pre-flight-checks-619

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Mar 12, 2026

Summary

Adds pre-flight checks that run automatically before mutating model methods (create/update/delete/action), catching invalid inputs early with clear error messages instead of letting them fail as raw API errors.

Closes #619

Core Implementation

  • CheckDefinition interface with description, labels, appliesTo, and execute(context) => CheckResult
  • CheckResult with pass: boolean and optional errors: string[]
  • Checks run automatically before mutating methods; read/list methods skip checks
  • isMutatingKind() helper for method kind classification

Extension Checks

  • ModelRegistry.extend() accepts optional third checks parameter
  • Extension checks merge with model-type checks; conflicts throw at registration time
  • Extension checks format matches methods: array of Record<string, CheckDefinition>

Definition-Level Check Selection

  • CheckSelection type: { require?: string[]; skip?: string[] } on definitions
  • skip always wins (even over require)
  • require checks are immune to CLI skip flags (--skip-checks, --skip-check, --skip-check-label)
  • Validation warns on nonexistent check names and require/skip overlap

CLI Flags

  • model method run: --skip-checks, --skip-check <name>, --skip-check-label <label>
  • model validate: --label <label>, --method <method> for check filtering

Validation Service

  • Check results appear alongside schema validations in model validate output
  • Label and method filtering for targeted check execution
  • Definition-level skip lists honored during validation

Test plan

  • 17 new unit tests covering all check behaviors (pass/fail, filtering, skip flags, extension merging, definition-level selection)
  • Integration tests updated for new validation count
  • All 2875 tests pass
  • deno check, deno lint, deno fmt pass
  • End-to-end tested with command/shell model extension checks:
    • Check pass/fail on method execution
    • Label-based filtering (--skip-check-label)
    • Named check skipping (--skip-check)
    • Skip all checks (--skip-checks)
    • Definition-level require (immune to skip flags)
    • Definition-level skip (overrides require)
    • model validate with --label and --method filtering
    • Invalid check selection validation warnings

🤖 Generated with Claude Code

stack72 and others added 3 commits March 12, 2026 21:45
Add a `checks` field to `ModelDefinition` — a `Record<string, CheckDefinition>`
that follows the same pattern as `methods`, `resources`, and `files`. Checks run
automatically before mutating method execution (create/update/delete/action) and
are skipped for read/list operations.

Each check has an `execute(context: MethodContext) => Promise<CheckResult>`
signature, receiving the same context as methods (globalArgs, repos, repoDir,
logger) but without writeResource/createFileWriter — checks are read-only. The
open signature supports value validation, cross-model queries, live API checks,
OPA evaluation, or any future check type without framework changes.

Core changes:
- CheckResult, CheckDefinition interfaces and isMutatingKind() helper in model.ts
- Pre-flight check execution in method_execution_service.ts executeWorkflow()
  with filtering by appliesTo, skipCheckNames, skipCheckLabels, skipAllChecks
- Validation service integration: model validate runs checks when checkContext
  is provided, with --label and --method filtering flags
- User model loader passes through checks from extension model TypeScript files
- CLI flags: --skip-checks, --skip-check <name>, --skip-check-label <label>
  on model method run; --label <label>, --method <method> on model validate

Design decisions:
- All checks run sequentially (may depend on shared state), all failures
  collected before aborting — users see every problem at once
- Labels are free-form strings, not an enum — model authors choose vocabulary
- appliesTo scoping lets checks target specific methods (e.g., create-only)
- CheckValidationContext uses MethodContext["dataRepository"] type to avoid
  adding new domain→infrastructure import violations

This is the foundation layer. Two planned follow-ups:
- Extension checks: allow adding checks to existing model types via extensions
- Definition-level checks: CEL expression-based checks in YAML definition files

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds two capabilities on top of the pre-flight checks foundation:

## Extension Checks

Extension authors can now add pre-flight checks to existing model types
via `modelRegistry.extend()`. The method accepts an optional third
`checks` parameter:

```typescript
modelRegistry.extend("aws/ec2/vpc", {}, {
  "no-cidr-overlap": {
    description: "Ensure CIDR does not overlap",
    labels: ["policy"],
    execute: async (context) => { /* ... */ return { pass: true }; },
  },
});
```

Check name conflicts with existing checks throw an error at registration
time, following the same immutable merge pattern used for methods.

## Definition-Level Check Selection

YAML definition authors can now control which checks run for their
specific definition using `checks.require` and `checks.skip`:

```yaml
checks:
  require:
    - no-dangerous-commands   # immune to --skip-checks CLI flags
  skip:
    - slow-api-check          # always skipped for this definition
```

Semantics:
- `skip` always wins (even over `require` if both list the same check)
- `require` checks are immune to `--skip-checks`, `--skip-check <name>`,
  and `--skip-check-label <label>` CLI flags
- `require` checks still respect `appliesTo` method scoping
- `model validate` honors definition-level `skip` lists
- Validation fails if `require` or `skip` references a check name that
  does not exist on the model type
- Validation warns when the same check appears in both lists

## Domain Changes

- `Definition` entity: added `CheckSelection` type (`require/skip`
  string arrays), `CheckSelectionSchema` Zod schema, `checkSelection`
  accessor, threaded through `create()`, `fromData()`,
  `withUpgradedGlobalArguments()`, and `toData()`
- `ModelRegistry.extend()`: accepts optional `checks` parameter with
  name conflict detection
- `executeWorkflow()`: check filtering respects definition-level
  require/skip before CLI skip flags
- `ValidationService`: added `validateCheckSelection()` private method,
  updated `runCheckValidations()` to honor definition-level skip lists
- `UserModelLoader`: extensions can export `checks` array alongside
  `methods` array

## Tests (17 new)

- model_test.ts (4): extend with checks, check name conflict, preserve
  existing checks when adding methods, merge checks from extension
- method_execution_service_test.ts (5): required check immune to
  skipAllChecks, required immune to skipCheckNames, definition skip
  always wins, skip wins over require, required respects appliesTo
- validation_service_test.ts (8): no selection passes, valid require,
  nonexistent require fails, nonexistent skip fails, require+skip
  overlap warning, valid skip, definition skip excludes from validate
  run, model without checks

## End-to-End Verification

Tested manually with an extension adding 3 checks to `command/shell`:
- Extension checks load and appear in `model validate --json`
- Dangerous command blocked by `no-dangerous-commands` check
- `--skip-checks` bypasses all non-required checks
- `--skip-check <name>` skips specific check
- `--skip-check-label <label>` skips by label
- `checks.require` makes check immune to all CLI skip flags
- `checks.skip` permanently excludes check from execute and validate
- Invalid check selection caught by validation

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- swamp-troubleshooting: add troubleshooting for check selection errors
  (nonexistent checks, require/skip overlap, extension conflicts, required
  check won't skip)
- swamp-extension-model: fix checks format to array-of-records pattern
  matching methods, clarify conflict rules
- swamp-model: clarify check selection precedence rules with explicit
  CLI flag names and override behavior

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@stack72 stack72 marked this pull request as draft March 12, 2026 22:43
github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

@stack72
Copy link
Copy Markdown
Contributor Author

stack72 commented Mar 12, 2026

This allows us to do this:

  export const extension = {
    type: "command/shell",
    methods: [],
    checks: [
      {
        "no-dangerous-commands": {
          description: "Ensures the shell command does not contain dangerous patterns",
          labels: ["offline", "safety"],
          appliesTo: ["execute"],
          async execute(context) {
            const dangerous = ["rm -rf /", "mkfs.", "dd if=", ":(){"];
            const errors = [];
            const globalArgs = context.globalArgs;

            for (const [key, value] of Object.entries(globalArgs)) {
              if (typeof value === "string") {
                for (const pattern of dangerous) {
                  if (value.includes(pattern)) {
                    errors.push(
                      `globalArguments.${key} contains dangerous pattern: "${pattern}"`,
                    );
                  }
                }
              }
            }
            return errors.length > 0 ? { pass: false, errors } : { pass: true };
          },
        },

        "command-not-empty": {
          description: "Ensures the execute method has a non-empty run argument",
          labels: ["offline", "validation"],
          async execute(context) {
            const defRepo = context.definitionRepository;
            const defInfo = context.definition;
            const def = await defRepo.findByName(context.modelType, defInfo.name);
            if (def) {
              const run = def.methodData?.execute?.arguments?.run;
              if (!run || (typeof run === "string" && run.trim() === "")) {
                return { pass: false, errors: ["No 'run' argument set for execute method"] };
              }
            }
            return { pass: true };
          },
        },

        "slow-api-check": {
          description: "Simulates a slow API check (for testing skip functionality)",
          labels: ["api", "slow"],
          appliesTo: ["execute"],
          async execute() {
            return { pass: true };
          },
        },
      },
    ],
  };

CLI usage:

  # Checks run automatically before mutating methods
  swamp model method run my-model execute

  # Skip all checks
  swamp model method run my-model execute --skip-checks

  # Skip by name or label
  swamp model method run my-model execute --skip-check slow-api-check
  swamp model method run my-model execute --skip-check-label api

  # Validate with filtering
  swamp model validate my-model --label offline
  swamp model validate my-model --method execute

If you want to encode the checks in the definition:

  name: my-model
  type: command/shell
  checks:
    require:
      - no-dangerous-commands
    skip:
      - slow-api-check

Move CheckDefinition interface, CheckResult, labels convention, appliesTo
scoping, skip CLI flags, and extension checks documentation from SKILL.md
to references/checks.md to reduce SKILL.md from 611 to 533 lines.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@stack72 stack72 force-pushed the feat/pre-flight-checks-619 branch from 671c89b to 843c5b7 Compare March 12, 2026 23:00
stack72 and others added 2 commits March 12, 2026 23:03
Without this, the new references/checks.md file would not be copied
to repos during `swamp repo init` or `swamp repo upgrade`.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Three fixes from adversarial review:

1. Checks with appliesTo are now skipped during `model validate` when
   no --method is specified, preventing confusing failures from checks
   that only make sense for specific methods.

2. Invalid check return values (undefined, null, missing pass field)
   are now caught defensively instead of crashing, producing a clear
   "invalid result" error message.

3. appliesTo arrays are now validated against existing methods on the
   model — a typo like appliesTo: ["creat"] will surface as a
   validation warning instead of silently never running.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@stack72 stack72 marked this pull request as ready for review March 12, 2026 23:09
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Code Review: Pre-flight Checks for Model Method Execution

Overall Assessment

This is a well-designed and well-implemented feature. The PR adds pre-flight checks that run before mutating model methods, with comprehensive support for extension checks, definition-level check selection, and CLI flags for filtering.

No Blocking Issues Found

Code Quality

  • TypeScript strict mode: All code follows strict TypeScript patterns. The any types in CLI commands are properly documented with lint-ignore comments and represent pragmatic workarounds for Cliffy's type inference limitations.
  • Named exports: All exports are named exports as required.
  • License headers: All files include the AGPLv3 copyright header.

Test Coverage

  • 17 new unit tests covering all check behaviors (pass/fail, filtering, skip flags, extension merging, definition-level selection)
  • Integration tests updated for new validation count (4 → 5)
  • Tests are located next to source files as required

Domain-Driven Design

The implementation follows DDD principles correctly:

  • CheckDefinition and CheckResult are properly modeled as value objects
  • CheckSelection is appropriately stored as part of the Definition entity
  • ValidationService and MethodExecutionService are correctly implemented as domain services

Security

No security vulnerabilities identified. The checks feature is a validation layer that runs before method execution with proper input validation.

Suggestions (Non-Blocking)

  1. Consider future enhancement: The CheckValidationContext creates a mock logger inline (lines 317-325 in validation_service.ts). This works but could be extracted to a test utility in the future for consistency.

  2. Documentation: The skill docs updates are comprehensive and the new references/checks.md is properly included in bundled skills.

Summary of Changes Reviewed

  • model.ts: CheckResult, CheckDefinition interfaces, isMutatingKind() helper, checks field on ModelDefinition
  • method_execution_service.ts: Pre-flight check execution with proper filtering
  • validation_service.ts: Check validation including appliesTo validation against methods
  • definition.ts: CheckSelection type and checkSelection accessor
  • user_model_loader.ts: Support for checks in model and extension exports
  • CLI commands: New flags for skipping checks and filtering
  • Tests: Comprehensive unit test coverage

LGTM! Approving.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Adversarial Review

I've systematically attempted to break this code across logic, error handling, security, concurrency, data integrity, resource management, and API contracts.

Critical / High

None found. The implementation is solid.

Medium

  1. Empty appliesTo array silently disables check (src/domain/models/method_execution_service.ts:296)

    • If a check has appliesTo: [] (empty array), the truthy check passes but [].includes(methodName) always returns false
    • Breaking example: A model author accidentally writes appliesTo: [] instead of omitting the field — the check silently never runs
    • Suggested fix: Add validation in validateCheckSelection() to warn when appliesTo is an empty array, or document this behavior explicitly
    • Mitigating factor: Unlikely to happen accidentally; the validation already checks that appliesTo references existing methods
  2. Check execution order non-deterministic across extension loads (src/domain/models/method_execution_service.ts:291)

    • Object.entries(modelDef.checks) returns checks in insertion order, but when checks come from base model + extension, order depends on when extensions were loaded
    • Breaking example: If extension loading order varies between runs (parallel file discovery, etc.), checks might run in different orders
    • Mitigating factor: Checks should be stateless and independent, so order shouldn't matter. The PR correctly documents "checks run sequentially" without guaranteeing order.

Low

  1. Empty methodName in validation context (src/domain/models/validation_service.ts:315)

    • When --method is not provided, methodName: checkContext.method ?? "" passes empty string to checks
    • Mitigating factor: Checks with appliesTo are already skipped when no method is provided (line 294-300), so only checks without appliesTo run, and they typically don't rely on methodName
  2. No test for definition-level checks round-trip through toData/fromData (src/domain/definitions/definition_test.ts)

    • The existing tests confirm checks: undefined in toData output, but no test exercises a definition with actual check selection through the round-trip
    • Mitigating factor: The schema is validated via Zod, and structuredClone ensures deep copy

Verdict

PASS — The pre-flight checks implementation is well-designed with comprehensive error handling. The defensive check result validation (line 334) properly catches malformed check returns. The filtering logic correctly implements the precedence rules (definition skip > definition require > CLI skip flags). No security vulnerabilities, data loss scenarios, or critical logic errors found.

@stack72 stack72 merged commit 27c95bb into main Mar 12, 2026
12 checks passed
@stack72 stack72 deleted the feat/pre-flight-checks-619 branch March 12, 2026 23:16
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.

Support pre-flight validate method on models before method execution

1 participant