Skip to content

Create all type: model files in models/ instead of sources/#8811

Merged
lovincyrus merged 11 commits intomainfrom
cyrus/models-dir-files
Feb 19, 2026
Merged

Create all type: model files in models/ instead of sources/#8811
lovincyrus merged 11 commits intomainfrom
cyrus/models-dir-files

Conversation

@lovincyrus
Copy link
Copy Markdown
Contributor

@lovincyrus lovincyrus commented Feb 10, 2026

This PR creates all type: model files in models/ instead of sources/

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

@lovincyrus lovincyrus self-assigned this Feb 10, 2026
@lovincyrus lovincyrus marked this pull request as ready for review February 10, 2026 22:32
Copy link
Copy Markdown
Contributor

@royendo royendo left a comment

Choose a reason for hiding this comment

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

barring fail e2e, looks good to me

@lovincyrus lovincyrus force-pushed the cyrus/models-dir-files branch 2 times, most recently from 60a5975 to abe9669 Compare February 12, 2026 17:01
Copy link
Copy Markdown
Contributor

@ericpgreen2 ericpgreen2 left a comment

Choose a reason for hiding this comment

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

isSourceModel heuristic can false-positive on YAML-only models

In file-and-resource-watcher.ts:368-369, the source detection changed from filePaths[0]?.startsWith("/sources/") to:

const isSourceModel =
  filePath?.startsWith("/models/") && filePath?.endsWith(".yaml");

This matches any YAML file in /models/, but not all of those are sources. A model defined entirely in YAML (like the incremental model in the test suite) would also match. Combined with the other conditions in isNewSourcespecVersion === "1", stateVersion === "2", and isLeafResource — a YAML-only leaf model would incorrectly trigger the "Source imported successfully" modal on first reconciliation.

The runtime already tags sources with defined_as_source: true in parse_source.go:35. If that flag is available on the model spec in the frontend, checking it here would be more precise than relying on the file extension.


Developed in collaboration with Claude Code

lovincyrus and others added 9 commits February 13, 2026 15:10
When submitting the explorer step in multi-step connectors, model files are now created in models/ instead of sources/, ensuring consistency with the file type designation (type: model in YAML).
Consolidate model file creation across all code paths:
- submitAddSourceForm (multi-step connector source step)
- createSource (local file uploads and drag-and-drop)
- Local file picker and drag-and-drop navigation

This ensures consistency: all YAML files with type: model are created in
models/ instead of sources/.
Centralize EntityType.Table mapping to models/ in getFilePathFromNameAndType
and getFileAPIPathFromNameAndType. Restore all call sites to use the mapper
instead of hardcoded paths. Single source of truth for file paths.
Update all test file paths from /sources/ to /models/ for Table/Source
entity types. Fix file-and-resource-watcher.ts to check /models/ instead
of /sources/ when detecting newly imported sources, which enables the
"View this source" modal to appear after file upload. Also fix
getFilePathFromPagePath() in entity-mappers.ts for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check that filePath ends with .yaml to avoid matching directory paths when detecting imported sources in the models/ directory.
Source files now live in /models/ instead of /sources/, but should retain
manual save behavior for safety (they contain credentials and connections).
Add isFileWithoutAutosave() function to handle the new location while
preserving auto-save for SQL model files.
The incremental model test now requires a manual save since YAML files
in /models/ have auto-save disabled. This ensures the model is persisted
before waitForProfiling checks for API responses.
Replace file-path-based isSourceModel check with the runtime's
definedAsSource flag, which precisely identifies sources without
false-positiving on YAML-only models in /models/.
@lovincyrus lovincyrus force-pushed the cyrus/models-dir-files branch from 2de0a57 to ebade47 Compare February 13, 2026 23:10
@lovincyrus
Copy link
Copy Markdown
Contributor Author

lovincyrus commented Feb 13, 2026

Question: How should the watcher detect newly-imported sources?

The e2e test breadcrumbs.spec.ts is failing because the "Source imported successfully" modal never appears after a file upload. Here's why:

Background

The FileAndResourceWatcher shows the modal by setting sourceImportedPath when it detects a new source has finished ingesting. Previously it used a path-based check:

const isSourceModel = res.resource.meta?.filePaths?.[0]?.startsWith("/sources/");

Since this branch moves source files to models/, that check was replaced with:

res.resource.model?.spec?.definedAsSource === true

The problem

The YAML generated by sourceModelFileTop() uses type: model (intentionally — that's the goal of this branch). But definedAsSource is only set automatically by the Go parser when it processes type: source files (via parseSource). For type: model files, it reads defined_as_source from the YAML itself — and it's not there.

So definedAsSource is always false for UI-created sources, and the modal never shows.

Options

  1. Add defined_as_source: true to the generated YAML — it's a valid field in the model YAML schema (parse_model.go line 48). The generated YAML would be:

    type: model
    materialize: true
    defined_as_source: true
  2. Use type: source in generated YAML — the parser converts it to a model internally and sets defined_as_source: true automatically. But this goes against the branch goal of creating type: model files.

  3. Use a different watcher heuristic — e.g. check materialize + isLeafResource instead of definedAsSource. But this is fragile and could false-positive on regular materialized models.

Thoughts on which approach is best? @ericpgreen2


Update: resolved offline.

Replace the definedAsSource check with a pendingSourceImports set
that the UI registers before writing source files. This avoids false
positives on YAML-only models and eliminates the need for fragile
heuristics. The UI now explicitly tells the watcher which files are
source imports via three entry points: submitAddSourceForm, createSource
(used by LocalSourceUpload and FileDrop), and the watcher clears the
path from the set when the modal is triggered.
Copy link
Copy Markdown
Contributor

@ericpgreen2 ericpgreen2 left a comment

Choose a reason for hiding this comment

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

The path migration from /sources/ to /models/ looks complete — no stale references remain. The pendingSourceImports approach is a better signal than the old startsWith("/sources/") heuristic. Two things worth addressing:


1. Missing cleanup in createSource.ts

createSource adds to pendingSourceImports before calling runtimeServicePutFile but never cleans up on failure. If the PUT throws, the entry leaks in the Set. Compare with submitAddDataForm.ts which correctly cleans up in both error paths.

The callers (FileDrop.svelte, LocalSourceUpload.svelte) catch errors but don't clean up either.


2. pendingSourceImports and sourceImportedPath are implicitly coupled

This PR introduces pendingSourceImports as new state that's tightly coupled to the existing sourceImportedPath — the watcher has to remember to call delete on one and set on the other in lockstep, and error paths have to delete without setting. That coupling is invisible and easy to get wrong (see issue 1).

Since you're introducing new state here anyway, consider synthesizing both into a single class that owns the full ingestion lifecycle:

class SourceIngestionTracker {
  private pending = new Set<string>();
  public ingestedPath = writable<string | null>(null);

  trackPending(filePath: string) {
    this.pending.add(filePath);
  }

  isPending(filePath: string) {
    return this.pending.has(filePath);
  }

  /** Ingestion finished — remove from pending and notify the UI */
  trackIngested(filePath: string) {
    this.pending.delete(filePath);
    this.ingestedPath.set(filePath);
  }

  /** Source creation was rolled back */
  trackCancelled(filePath: string) {
    this.pending.delete(filePath);
  }

  /** User dismissed the success modal */
  dismiss() {
    this.ingestedPath.set(null);
  }
}

This makes the transitions explicit — trackIngested vs trackCancelled — so it's impossible to forget half the pair. The watcher calls tracker.trackIngested(filePath) instead of two separate operations, and error paths call tracker.trackCancelled(filePath) with clear intent.

The naming also aligns with the domain language the UI already uses ("has been ingested") and replaces the more ambiguous "imported" terminology.

…estionTracker

Replace the implicitly coupled `pendingSourceImports` Set and
`sourceImportedPath` writable with a single `SourceIngestionTracker`
class that owns the full ingestion lifecycle. This makes state
transitions explicit (trackPending → trackIngested vs trackCancelled)
and fixes a leak where `createSource` never cleaned up pending entries
on failure.
@lovincyrus lovincyrus merged commit 467dd69 into main Feb 19, 2026
10 checks passed
@lovincyrus lovincyrus deleted the cyrus/models-dir-files branch February 19, 2026 19:36
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.

3 participants