Somebody fixes a bug by editing a component the generator ships. It works, it ships, and the next
target add reverts it without a word. Six months later nobody can say which of the forty files
under the customization directory came from the generator, which were edited, and which the team
wrote themselves.
What you get
You will end up with a manifest of what the generator gave you, and an audit that classifies every file in the project against it. The gate then fails on the two classes that are drift. This is for you if your project carries generator-owned files that people edit.
Short answer
Record a checksum for every file the generator ships, at install time, and commit that manifest. On each build, compare the project against the manifest. A shipped file you changed is a fork, a shipped file you deleted is missing, and a file you added is your own. Fail on the first two, report the third.
You will need
Node 22 or later, and a project that carries files a generator shipped. The idea is the one package managers use for a lockfile: record what you were given, so a later comparison has something to be against.1 Subresource integrity makes the same move for a script you did not write.2 sdkgen ships the same check as voxgig-sdkgen doctor.
Voxgig maintains sdkgen. This page compares its own drift check with a manifest you keep, a manual diff, and letting version control track the vendor tree.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A committed checksum manifest | Any generator, because it needs nothing from the tool beyond its output | A manifest to refresh on every update, and a file people must not hand-edit | The generator already ships a drift check you trust |
| A manual diff against the shipped templates | A one-off investigation, when somebody already suspects a fork | Attention, and a result that is stale the next afternoon | The question needs answering on every build |
| Git tracking of the vendor tree | The generator output is a checkout you can update and merge | Merge conflicts in generated files, which is the situation this avoids | The output is written in place by a tool rather than pulled |
| voxgig-sdkgen doctor | Your generator is sdkgen and you want the check it already knows how to do | A check tied to one generator, which says nothing about the rest of the tree | You generate with something else, or with several tools |
The manifest and a tool’s own check answer the same question and differ in coverage. A native check knows what the generator shipped and stops at the edge of what it manages. A manifest covers whatever you point it at, including the files a second generator or a vendored template wrote.
Run the native one if you have it. The manifest earns its place when a project carries output from more than one source, which is most projects past the first year.
Record what you were given
The manifest is written once per install and committed.
const manifest = record('scaffold')
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2) + '\n')
console.log(`recorded ${Object.keys(manifest).length} files`)
Write it at install and at every update, and never by hand. A manifest somebody edits to silence a finding is a manifest that no longer records anything, and the edit looks exactly like the fix.
Store a short hash rather than the file. The comparison needs to know that something changed, not what it was, and the generator still has the original.3
Separate a fork from an addition
Only two of the four verdicts stop a build.
export const blocking = (findings) =>
findings.filter((f) => f.verdict === 'forked' || f.verdict === 'missing')
An additive file is the normal way to customize. A team that gets a build failure for writing their own component learns to switch the gate off, and then the gate catches nothing at all.
A missing file is drift as much as a fork. Deleting something the generator ships means the next regeneration puts it back, and whatever depended on its absence breaks then rather than now.
Check it worked
Audit a project against the manifest of what the generator shipped.
node demo.mjs
manifest: 6 files recorded at install
orphaned cmp/Legacy_ts.ts installed by an older generator, no longer shipped
forked cmp/Main_ts.ts edited since install, 00ed1f3057ca became 4af874f9e882
additive cmp/MeterBadge_ts.ts project-owned, not drift
missing cmp/Readme_ts.ts shipped by the generator and deleted here
forked tm/Makefile edited since install, 0e2383f9bfde became bb684df0b89e
retired tm/Old.mk installed once, no longer shipped, removed here
3 blocking, 3 informational
Three of the six findings will be reverted by the next regeneration, and three will not. That is the whole distinction, and it is what makes the gate usable: the team’s own component is reported and does not fail anything.
The last two are why the manifest belongs in the ownership set rather than only in the comparison.
Both are files an upgrade dropped from the scaffold. Audit the scaffold alone and Legacy_ts.ts
reads as project-owned, because nothing still ships it, while Old.mk disappears from the report
entirely. The output would then give no sign that a generator upgrade had rewritten the history of
every file it stopped shipping.
node --test audit.test.mjs
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 135.022532
What sdkgen’s own check reports, recorded rather than re-run here. A project was generated with
sdkgen 4.17.1, then one component and one template master were edited and one new component added.
voxgig-sdkgen doctor reported a fork on src/cmp/ts/Main_ts.ts, an edited template master on
tm/ts/Makefile, and src/cmp/ts/MeterBadge_ts.ts as additive and project-owned. It summarized as
1 forked, 1 edited, 0 stale, 0 missing and exited 1, drawing the same line between a fork and an
addition. That run is not reproduced by this page’s gates, because it needs the generated project
and its toolchain. The two command blocks are.
When it goes wrong
The audit reports every file as forked. The manifest was recorded from a different version than the one installed. Re-record on update, in the same step that installs.
A fork disappears from the report. Somebody re-recorded the manifest to make the build pass. Make the manifest a reviewed file, and treat a change to it in a pull request as the event it is. A manifest that moves in the same commit as a component is the thing to look at twice.
Nothing is ever reported and people are definitely editing. The audit is pointed at the generated output rather than at the customization tree. Point it where the generator writes files it also owns.
The gate fires on a line ending. A checkout normalized them. Compare normalized text, or set the repository’s attributes so both sides agree.
When not to do this
Do not gate on forks before you have somewhere else to put the change. A fork is usually a fix somebody needed, and failing the build without offering the model or the template hook as the alternative moves the argument rather than settling it. Add the escape hatch first, then the gate.
Do not audit generated output that nobody is meant to edit. That tree is rewritten every run, so a checksum of it changes constantly and says nothing. Audit the customization surface, which is the tree the generator writes once and then leaves to you.
Do not rely on sdkgen’s doctor alone once a second generator is in the project. It reports on the
.sdk tree it manages, which is the right scope for it, and the files another tool wrote fall
outside it, still yours to keep accounted for with a manifest.
Related how-tos
Last verified
Verified 2026-09-14 against Node 22.22.2 and @voxgig/sdkgen 4.17.1. Both output blocks are what the preceding command printed.
Footnotes
-
The lockfile has been recorded three ways. npm’s documentation numbers them: version 1 for npm 5 and 6, version 2 for npm 7 and 8, and version 3 for npm 9 onward. An unnumbered fourth it calls an ancient
shrinkwrapfile, from before npm 5. The same page promises that npm will attempt to get whatever data it can out of a lockfile it was not designed to read. The record of exactly what you were given has changed shape three times, then, and is read by a tool that promises to do its best with whatever it finds. ↩︎ Back to text -
Subresource integrity became a W3C Recommendation in June 2016. It requires every browser conforming to it to support SHA-256, SHA-384 and SHA-512, and names MD5 and SHA-1 as functions it does not recommend. It adds that, at the time of writing, SHA-384 is a good baseline. It then asks user agents to re-evaluate their supported hash functions on a regular basis, which is a standard allowing in advance for its own decay. The check confirms that the bytes fetched are the bytes named. Whether the bytes named were any good is a question the attribute has never claimed to answer. ↩︎ Back to text
-
The twelve hexadecimal characters in the output are 48 bits, and the length has company. The Linux kernel’s patch rules ask for at least the first 12 characters of the SHA-1 of a commit in a
Fixes:tag. Git itself, asked how far to abbreviate, computes a length from the approximate number of packed objects. Its documentation says the result is hopefully enough for abbreviated names to stay unique for some time, and hopefully is the manual’s own word. The manifest has an easier time of it: each hash needs only to differ from the one recorded beside its name. ↩︎ Back to text