Skip to content

fix(partials): keep substituted files faithful and never emit a live tag - #46

Merged
steve-calvert-glean merged 5 commits into
mainfrom
fix/partials-fidelity
Oct 21, 2026
Merged

steve-calvert-glean merged 5 commits into
mainfrom
fix/partials-fidelity

Conversation

@steve-calvert-glean

Copy link
Copy Markdown
Contributor

Problem

Partial substitution could corrupt files and ship live tags on a green build:

  • A .md file with a partial tag and one non-UTF-8 byte had that byte rewritten to U+FFFD. Without a tag, the byte survived. NUL-containing text files behaved the same way.
  • A partial ending in { placed before {> auth}} produced a live {{> auth}} in the output. The comments claimed the escape was the only way a tag could reach output.
  • NOTES.MD with a tag failed the build, because extension matching was case-sensitive.
  • Every correct use of the documented \{{> escape produced a validate warning.
  • With a plugin emitted at the output root (path: "."), validate walked the whole repository, including node_modules/ and user sources, and failed on correct output.

Change

  • Substitution checks that a file and each partial decode as UTF-8. If not, the build fails and names the file.
  • Escaped tags use their own placeholder. After rendering, any remaining tag fails the build, so the comments are now true.
  • Extensions are matched case-insensitively.
  • validate no longer scans substituted file types for tags. Other file types are still checked.
  • Partials lose a leading BOM and take the including file's line endings. The README documents this, along with the escape and its limits.
  • For a root plugin, validate scans the target's native manifest, MCP and hooks files, plus the managed files when .pluginpack state exists. Without state, it falls back to the component directories.

Breaking changes

  • A file or partial that is not valid UTF-8 now fails the build instead of shipping U+FFFD.
  • Output changes for partials with a BOM or with line endings that differ from the including file.
  • {{{> name}}} and other assembled tags now fail the build.

Testing

npm run check passes. On unfixed source, the new tests fail for the intended reasons. Root-plugin validation is tested for cursor, claude, copilot, codex and agent-plugins, with and without managed state.

Partial substitution decoded a file as UTF-8 and re-encoded it, so a file
with a partial tag and any invalid byte (a Latin-1 "é", or a UTF-16
byte-order mark) came out with U+FFFD in place of those bytes, on a green
build. The same file without a tag kept its bytes. A file that uses
partials, and every partial, now has to survive a UTF-8 round trip, or the
build fails and names the file.

Extension matching was case-sensitive, so NOTES.MD or REF.TXT was treated
as a file type substitution skips, and the build failed with an error
saying substitution only runs on .md and .txt files. The extension is now
compared in lower case, as the README already implied.

Rendering could assemble a tag that no single piece contained: a partial
ending in "{" before "{> name}}", a partial ending in "{{" before
"> name}}", or "{{{> name}}}". The build guard skipped substituted file
types on the reasoning that only the documented escape could leave a tag
there, so these shipped as live tags. Escaped tags now render through
their own placeholder, and substitute() fails on any tag left in its
output. That makes the reasoning true, so validate no longer warns on
every documented \{{> name}} escape, which was permanent noise on the one
sanctioned way to write partial syntax.

A partial's byte-order mark was inlined as U+FEFF mid-file, and a CRLF
partial left mixed line endings in an LF file. Partials now drop a leading
byte-order mark and take the line ending of the file that includes them.
The README documents these rules and the backslash edge case.
…he root

Each target's validator reports the plugin directories it owns, and
validate scans those for surviving partial tags. A plugin configured with
path "." lives at the output root, so with outDir "." its directory is the
repository itself. Validate then walked node_modules, authored sources and
unrelated files, and failed on tags in pluginpack's own dependency cache.

For a plugin at the output root, validate now scans only its component
directories and static files. The managed manifest still covers the rest
of the files the build wrote. Plugins in their own directory are scanned
as before.
The previous change stopped validate from walking the whole repository for
a plugin emitted at the output root, but it went too far in one direction
and not far enough in the other.

It dropped the target's native files from the scan. Without a managed
manifest, a partial tag in the plugin manifest or MCP config of a root
plugin passed validation, although the app reads those files and the
previous release reported the tag. Each target now names the files its app
reads from a plugin root (manifest, MCP config, hooks file, and for Agent
Plugins each client's extension directory), and validate always scans them.

It still scanned every recognized component directory when a managed
manifest was present, so an unrelated repository script under scripts/
failed validation for a plugin that only ships skills. Validate now tells
each target whether the output has managed state. With it, the managed
file list covers the plugin's output and the component directories are not
scanned. Without it, they remain the fallback.
File values may be Uint8Array rather than Buffer, so decode through Buffer.from instead of calling Buffer methods on the value.
@steve-calvert-glean steve-calvert-glean added the breaking Considered a breaking change label Oct 21, 2026
@steve-calvert-glean
steve-calvert-glean marked this pull request as ready for review October 21, 2026 23:57
@steve-calvert-glean
steve-calvert-glean merged commit 9fb2e40 into main Oct 21, 2026
@steve-calvert-glean
steve-calvert-glean deleted the fix/partials-fidelity branch October 21, 2026 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Considered a breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant