Skip to content

feat(core): deterministic model patching, new component detection, and remote model repo support - #15

Merged
parse merged 14 commits into
mainfrom
feat/deterministic-model-patching
Mar 3, 2026
Merged

parse merged 14 commits into
mainfrom
feat/deterministic-model-patching

Conversation

@parse

@parse parse commented Mar 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replaces the AI-generated model rewrite pipeline with a hybrid LLM-first + deterministic fallback patcher, adds new component detection from PR diffs, and supports cloning remote model repositories for cross-repo workflows.

What changed

  1. LLM-first patching with deterministic fallback — the patcher tries provider.patchModel() first for intelligent line placement, validated by quickValidatePatch + full DSL validation. Falls back to brace-scanning insertion if the LLM is unavailable or fails validation.

  2. New component detection — Stage 3 can now propose new components (services, apps) found in the PR diff. The patcher deduplicates against the existing componentIndex and renders format-specific DSL blocks (LikeC4 block syntax, Structurizr single-line syntax).

  3. Remote model repo cloning — --model-repo clones to a temp dir via git clone --depth 1 --branch <ref>. New --model-ref flag controls the branch/tag. Credentials injected via GIT_ASKPASS (never visible in ps output). Separate MODEL_REPO_PR_TOKEN / model-repo-token action input for cross-repo auth.

  4. --open-pr auto mode — skips PR creation on regular runs; creates/updates only when a model PR branch already exists. Enables a /erode update-model comment-driven workflow.

  5. Comment deletion — when re-analysis finds no findings (no violations, no model updates, no new components), any previously posted erode comment is deleted instead of leaving a stale comment.

  6. DSL injection prevention — strict Zod patterns (safeDslTextPattern, dslIdentifierPattern, componentIdPattern) on all AI-generated output before it reaches the patcher. Prevents newlines, quotes, braces, backticks, etc. in free-text fields.

  7. Security hardening — execSync replaced with execFileSync, ref character validation, path containment checks, credential scrubbing from error messages.

  8. New GitHub Action inputs — model-ref, model-repo-token, openai-api-key, three-valued open-pr (true/false/auto).

Flag behavior

Flag Default Effect
--patch-model false Enable model patching (Stage 4)
--patch-local — Write patched model to a local path instead of opening a PR
--open-pr true true Always create/update a PR when patching produces changes
--open-pr false — Never open a PR (useful with --patch-local)
--open-pr auto — Only create/update a PR when the model PR branch already exists
--model-repo — Clone a remote model repo instead of using a local path
--model-ref main Branch/tag to clone when using --model-repo

Test plan

  • 689 tests pass (npm run test)
  • Manual: run erode analyze with --patch-model --open-pr auto against a repo with an existing model PR branch and verify it updates the PR
  • Manual: run with --model-repo pointing to a remote repo and verify clone + patch + PR creation
  • Manual: verify comment deletion when re-analysis produces zero findings

parse added 3 commits March 1, 2026 11:24
The old Stage 4 used an expensive advanced-model AI call to generate
raw architecture code, which could invent non-existent components and
required --generate-model to be passed explicitly.

Replace it with a deterministic patcher that derives relationship
changes from the Stage 3 structured analysis output, validates
component IDs against the loaded model, deduplicates against existing
relationships, and patches model files in-place using a cheap
fast-model call. --open-pr now always uses this path.

- Add LikeC4 and Structurizr model patchers with full test suites
- Add StructuredRelationship schema for typed relationship data
- Add PR creation pipeline helper and patch PR body formatter
- Remove --generate-model flag from CLI, CI entry, and entrypoints
- Remove generateArchitectureCode from AIProvider and BaseProvider
- Remove Stage 4 prompt templates, formatters, and prompt variables
- Remove generatedFileExtension from adapter metadata
- Remove allComponents/modelFormat from DriftAnalysisResult
- Update all documentation to reflect the three-stage pipeline
…model targeting

Relocate patchers from patching/ into each adapter directory and add
DSL validation (LikeC4 via workspace parse, Structurizr via CLI export)
so patched output is verified before committing.

Platform writers gain closeChangeRequest() to auto-close model PRs
when a re-analysis finds no violations. New --model-repo flag lets
CI target a different repository for model update PRs. PR body now
links back to the source analysis PR for traceability.
Decouple model patching from PR creation by introducing Stage 4 as an
independent pipeline step, gated by a new --patch flag. Move all
publishing actions (PR creation, commenting, GH Actions outputs) into
a dedicated publishResults() function in publish.ts, reducing analyze.ts
by ~120 lines and allowing local model patching without --open-pr.
@parse parse changed the title feat(core): replace AI model generation with deterministic patching feat(core): deterministic model patching with --patch flag and publish pipeline Mar 1, 2026
Extend VERBOSE=true stderr logging to all remaining blind spots:
Structurizr patcher, base provider, retry utility, quickValidatePatch,
and pipeline stages 2-3.
@parse parse changed the title feat(core): deterministic model patching with --patch flag and publish pipeline feat(core): deterministic model patching, publish pipeline, and verbose debug logging Mar 1, 2026
parse and others added 9 commits March 1, 2026 16:58
…ding

Both patcher implementations shared ~80% identical code. Extract shared
logic into BasePatcher abstract class and withTempWorkspaceCopy utility,
reducing ~250 lines of duplication.

Also fixes a bug in analysisHasFindings where ?? operator ignored
remove-only model updates (false is not nullish), extracts
parseOwnerRepo and buildDiffFromFiles helpers, and removes a redundant
mutation in the analyze pipeline.

Adds ~40 new tests covering quickValidatePatch, analysisHasFindings,
pipeline orchestration (analyze, publish, pr-creation), and provider
patchModel methods.
The openai-api-key GitHub Action input was silently dropped because
entrypoint.sh never exported it. Also adds --model-repo to the
interactive CLI (previously only in ci-entry), documents Stage 4 and
the publish pipeline in web docs, and updates the architecture model
with the new publish component and relationships.
Add --model-ref flag and rework --model-repo to accept repository URLs
(or owner/repo slugs) pointing to the architecture model source. The
analyze pipeline now clones the model repo to a temp directory, runs the
full analysis, and cleans up on exit.

Also improves generated PR titles with source repo and PR title context,
fixes git path resolution in BasePatcher for temp directories, surfaces
generatedChangeRequest in AnalyzeResult, and adds a post-action step to
the interactive wizard.
Add `open-pr: 'auto'` option that creates model PRs only on demand via
`/erode update-model` PR comments, then keeps them updated on subsequent
pushes. Branch naming now includes source repo slug for cross-repo support.
Entrypoint reads PR URL from issue_comment events.

Harden AI-generated content handling: strict Zod schemas for component IDs
and DSL text, quote stripping in patchers, git ref validation, path traversal
checks, credential stripping from error messages, single-quote token escaping
in askpass scripts, and Bitbucket pagination origin validation.
The entrypoint cloned the model repo itself, then passed both the
absolute clone path and --model-repo to the core CLI. The core's
resolveModelSource() cloned again, joining paths into an invalid
nested path like /tmp/erode-model-XXXXX/tmp/model-repo/likec4.

Let the core handle cloning via resolveModelSource() and pass
--model-ref so the branch reaches it.
Move the outbound-call exception directly into the "Internal API Changes"
ignore section so the AI sees it at decision time, not after classifying
handlers as internal. Remove the ineffective standalone IMPORTANT
CLARIFICATION section. Add gateway proxy example. Add test to guard
against accidental removal.

Ref: DRI-21

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the drift analysis pipeline to identify brand-new services and
applications introduced by a PR that are not yet in the architecture
model. The AI now receives the full component index and relationship
list, enabling it to propose newComponents alongside relationships.

Key changes:
- NewComponentSchema with DSL injection guards (id, kind, name, tags)
- Stricter dslIdentifierPattern for relationship/component kind fields
- LikeC4 and Structurizr patchers generate component DSL lines
- Patchers track and surface validationSkipped when tooling unavailable
- PR comment and patch PR body render new components prominently
- Stage 4 handles new components alongside relationship patching
… attribution

- --patch-local was writing to repo-relative path instead of absolute,
  causing writes to fail when CWD differs from model root
- Component DSL line attribution used string matching, which misattributed
  lines when one component ID was a substring of another (e.g. "api" vs
  "api-gateway"); now uses positional indexing
- Replace execSync with execFileSync to avoid shell injection
- Add verbose debug logging in model-source catch blocks
- Document --model-ref flag and improve docs voice
@parse parse changed the title feat(core): deterministic model patching, publish pipeline, and verbose debug logging feat(core): deterministic model patching, new component detection, and remote model repo support Mar 3, 2026
Pass PR file list to the drift analysis prompt so the AI can reference
actual changed paths when detecting new components, instead of relying
solely on the diff content.
@parse
parse merged commit e0bbd9f into main Mar 3, 2026
1 check passed
@parse
parse deleted the feat/deterministic-model-patching branch March 3, 2026 12:55
parse added a commit that referenced this pull request Mar 3, 2026
…d remote model repo support (#15)

* feat(core): replace AI model generation with deterministic patching

The old Stage 4 used an expensive advanced-model AI call to generate
raw architecture code, which could invent non-existent components and
required --generate-model to be passed explicitly.

Replace it with a deterministic patcher that derives relationship
changes from the Stage 3 structured analysis output, validates
component IDs against the loaded model, deduplicates against existing
relationships, and patches model files in-place using a cheap
fast-model call. --open-pr now always uses this path.

- Add LikeC4 and Structurizr model patchers with full test suites
- Add StructuredRelationship schema for typed relationship data
- Add PR creation pipeline helper and patch PR body formatter
- Remove --generate-model flag from CLI, CI entry, and entrypoints
- Remove generateArchitectureCode from AIProvider and BaseProvider
- Remove Stage 4 prompt templates, formatters, and prompt variables
- Remove generatedFileExtension from adapter metadata
- Remove allComponents/modelFormat from DriftAnalysisResult
- Update all documentation to reflect the three-stage pipeline

* feat(core): add DSL validation, auto-close stale PRs, and cross-repo model targeting

Relocate patchers from patching/ into each adapter directory and add
DSL validation (LikeC4 via workspace parse, Structurizr via CLI export)
so patched output is verified before committing.

Platform writers gain closeChangeRequest() to auto-close model PRs
when a re-analysis finds no violations. New --model-repo flag lets
CI target a different repository for model update PRs. PR body now
links back to the source analysis PR for traceability.

* refactor(core): extract Stage 4 and publish pipeline from analyze

Decouple model patching from PR creation by introducing Stage 4 as an
independent pipeline step, gated by a new --patch flag. Move all
publishing actions (PR creation, commenting, GH Actions outputs) into
a dedicated publishResults() function in publish.ts, reducing analyze.ts
by ~120 lines and allowing local model patching without --open-pr.

* feat(core): add verbose debug logging across the pipeline

Extend VERBOSE=true stderr logging to all remaining blind spots:
Structurizr patcher, base provider, retry utility, quickValidatePatch,
and pipeline stages 2-3.

* refactor(core): extract BasePatcher and shared DSL validation scaffolding

Both patcher implementations shared ~80% identical code. Extract shared
logic into BasePatcher abstract class and withTempWorkspaceCopy utility,
reducing ~250 lines of duplication.

Also fixes a bug in analysisHasFindings where ?? operator ignored
remove-only model updates (false is not nullish), extracts
parseOwnerRepo and buildDiffFromFiles helpers, and removes a redundant
mutation in the analyze pipeline.

Adds ~40 new tests covering quickValidatePatch, analysisHasFindings,
pipeline orchestration (analyze, publish, pr-creation), and provider
patchModel methods.

* fix: export OPENAI_API_KEY in entrypoint, document publish pipeline

The openai-api-key GitHub Action input was silently dropped because
entrypoint.sh never exported it. Also adds --model-repo to the
interactive CLI (previously only in ci-entry), documents Stage 4 and
the publish pipeline in web docs, and updates the architecture model
with the new publish component and relationships.

* feat: support cloning remote model repositories for analysis

Add --model-ref flag and rework --model-repo to accept repository URLs
(or owner/repo slugs) pointing to the architecture model source. The
analyze pipeline now clones the model repo to a temp directory, runs the
full analysis, and cleans up on exit.

Also improves generated PR titles with source repo and PR title context,
fixes git path resolution in BasePatcher for temp directories, surfaces
generatedChangeRequest in AnalyzeResult, and adds a post-action step to
the interactive wizard.

* feat: add open-pr auto mode and harden input validation

Add `open-pr: 'auto'` option that creates model PRs only on demand via
`/erode update-model` PR comments, then keeps them updated on subsequent
pushes. Branch naming now includes source repo slug for cross-repo support.
Entrypoint reads PR URL from issue_comment events.

Harden AI-generated content handling: strict Zod schemas for component IDs
and DSL text, quote stripping in patchers, git ref validation, path traversal
checks, credential stripping from error messages, single-quote token escaping
in askpass scripts, and Bitbucket pagination origin validation.

* chore: add secure-coding skill for input validation guidance

* fix: remove double-clone of model repo in entrypoint

The entrypoint cloned the model repo itself, then passed both the
absolute clone path and --model-repo to the core CLI. The core's
resolveModelSource() cloned again, joining paths into an invalid
nested path like /tmp/erode-model-XXXXX/tmp/model-repo/likec4.

Let the core handle cloning via resolveModelSource() and pass
--model-ref so the branch reaches it.

* fix: inline route handler exception in dependency-extraction prompt

Move the outbound-call exception directly into the "Internal API Changes"
ignore section so the AI sees it at decision time, not after classifying
handlers as internal. Remove the ineffective standalone IMPORTANT
CLARIFICATION section. Add gateway proxy example. Add test to guard
against accidental removal.

Ref: DRI-21

* feat(core): detect and patch new components from PR analysis

Extend the drift analysis pipeline to identify brand-new services and
applications introduced by a PR that are not yet in the architecture
model. The AI now receives the full component index and relationship
list, enabling it to propose newComponents alongside relationships.

Key changes:
- NewComponentSchema with DSL injection guards (id, kind, name, tags)
- Stricter dslIdentifierPattern for relationship/component kind fields
- LikeC4 and Structurizr patchers generate component DSL lines
- Patchers track and surface validationSkipped when tooling unavailable
- PR comment and patch PR body render new components prominently
- Stage 4 handles new components alongside relationship patching

* fix(core): use absolute path for --patch-local and fix component line attribution

- --patch-local was writing to repo-relative path instead of absolute,
  causing writes to fail when CWD differs from model root
- Component DSL line attribution used string matching, which misattributed
  lines when one component ID was a substring of another (e.g. "api" vs
  "api-gateway"); now uses positional indexing
- Replace execSync with execFileSync to avoid shell injection
- Add verbose debug logging in model-source catch blocks
- Document --model-ref flag and improve docs voice

---------
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