feat(api): let a profile say what its source files are called - #45
Merged
Conversation
The core decided whether a URI was a diagram's source by testing the path against one product's file extension, written out as a literal. Six places did it — both open-diagram commands, the rename command's active-file lookup, the editor provider's save handler and its on-disk change handler — plus a file-system watcher glob pinned to the same extension. For any consumer whose files end in something else, all of that is dead weight that silently refuses. The open commands resolve nothing and show a warning telling the user to open a kind of file the product does not have, naming an extension it does not use, in text it has no way to change. Rename says the same thing in different words. The watcher fires for files the product does not own and never for the ones it does, so an edit made outside the editor — the chat agent through the edit backend, git, a formatter — never reaches the open diagram. A profile declares `sourceExtensions` instead and every site asks it. Messages are built from the list rather than written out, because a message that names an extension is a message the core is not in a position to write. Declaring nothing filters nothing. That is the deliberate half. The core cannot know how a product names its files, and the failure mode of a wrong guess is the worst one available: commands that refuse every file, which looks from the outside like a workspace with no sources in it. An unrecognised file reaches `canOpenSource` instead, where the product can refuse it for a reason it actually knows. The cost is a watcher on every file for a profile that declares neither `sourceExtensions` nor `watch.globs`; the artifact-directory filter that already existed is what keeps that affordable, and either declaration replaces it. `watch.globs` had been on the profile since the v2 contract and nothing read it. It does now, and wins over the derived globs — a product may want to watch more than its own sources, a manifest or a generated index, and it is the more specific statement of intent. The toolkit's profile builder already knew the extension and now hands it over, so the shipped consumer keeps the filtering it had. Gate 5 catches the class. The four existing gates were green for as long as this defect existed, because they look for WORDS and an extension is not a word — it reads as punctuation, which is exactly why it survived. The new gate is narrow on purpose: it bans deciding from a literal extension whether a PATH is one of a product's sources. It does not ban naming an extension inside a directory layout the platform itself defines, which is the platform's to name.
endrix
force-pushed
the
feat/profile-source-extensions
branch
from
September 4, 2026 07:54
976d37f to
9e7df8a
Compare
Three conflicts, all of them two additions landing in one place. The import needed merging rather than concatenating: each side wanted a different symbol from the same statement, so keeping both lines compiled to a duplicate identifier — which the typecheck caught and a textual "keep both sides" would have shipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
The core decided whether a URI was a diagram's source by testing the path against one product's file extension, written out as a literal. Seven sites did it — both open-diagram commands, the rename command's active-file lookup, the editor provider's save handler and its on-disk change handler, and a file-system watcher glob.
For a consumer whose files end in anything else, all of that silently refuses:
The neutrality gate did not catch it. The existing gates look for words, and an extension is not a word — it reads as punctuation, which is exactly why it survived four gates for as long as it existed.
The fix
DiagramProfile.sourceExtensions?: string[]— lower-case, leading dot, e.g.['.foo', '.bar']. Every one of those sites asks the list instead, through a newsource-extensions.tshelper that also builds the noun the user-facing messages use. Messages are derived from the declaration rather than written out, because a message that names an extension is a message the core is not in a position to write.Two things came along with it:
watch.globshad been on the profile since the v2 contract and nothing read it. It does now, and takes precedence over the globs derived fromsourceExtensions— a product may want to watch more than its own sources (a manifest, a generated index), and it is the more specific statement.Gate 5 closes the class: it bans deciding, from a literal extension, whether a path is one of a product's sources. It deliberately does not ban naming an extension inside a directory layout the platform itself defines (its own skills and agents directories) — that layout is the platform's to name. Verified to fail on a reintroduced literal path test and on a reintroduced literal watcher glob.
The trade-off
Declaring nothing filters nothing. That is the deliberate half of the design, and it is a real trade: with no declaration the platform accepts any file where it used to accept one kind, and watches every file where it used to watch one kind.
The alternative — refusing what the core does not recognise — fails in the worse direction. The core cannot know how a product names its files, and a wrong guess makes every command refuse every file, which from the outside is indistinguishable from a workspace that has no sources in it. An unrecognised file instead reaches the profile's own
canOpenSource, which can refuse it for a reason the product actually knows.The watcher cost is the visible half of that trade. It is bounded by the artifact-directory filter that already existed (build trees,
dist,out,node_modules, VCS and virtual-env directories are ignored), and any profile that cares replaces it by declaring either field.Compatibility
Purely additive and optional — no API version bump, following the precedent of the last three profile fields (
entityPaletteItems,paletteIcons,nodeFamilies). Every existing consumer is unaffected; the shipped one keeps its filter through the toolkit builder.Verification
npm run build— succeedednpm test— 874 tests, 152 files, all passingnpm run check:neutrality— 5/5 gatesnpm run typecheck— 5/5 packages