Found by a blind dogfood run against v0.1.90-21-gf56aa72 (agent given only the built binary, README.md and --help; no source, no credential).
Symptom
civitai app validate reports ✓ is valid, exit 0, for a project whose committed package-lock.json is empty. The platform build will still fail.
$ civitai app create dogcheck --dir ./a -y
$ civitai app validate ./a
✗ 1 validation error(s) in ./a:
- package.json is present but no lockfile is committed — buildCommand is "npm run build",
so the platform build will run `npm ci`, which hard-fails without package-lock.json.
Run `npm install` and commit the package-lock.json it writes […]
rc=1
$ touch ./a/package-lock.json # 0 bytes
$ civitai app validate ./a
✓ ./a is valid
rc=0
Why this is a real failure, not a nit
The check is presence-only — it asks whether the file exists, not whether it is a lockfile. Measured on npm 11.17.0, an empty lockfile fails npm ci with the same class of error as a missing one:
$ printf '{"name":"t","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
$ touch package-lock.json && npm ci
npm error code EUSAGE
npm error The `npm ci` command can only install with an existing package-lock.json or
npm error npm-shrinkwrap.json with lockfileVersion >= 1.
AGENTS.md item 3 states the check's entire purpose: the platform build installs strictly from the committed lockfile with no registry re-resolve fallback, and a mismatch "surfaces only as an opaque server-side build failed". That is exactly the outcome an author gets here — after validate told them they were fine.
The failure message makes touch a reachable wrong turn. It names the filename (package-lock.json) as the thing that is missing, so creating an empty one to satisfy it is a natural — and silently wrong — response. The check invites the input that defeats it.
Suggested fix
Validate that the lockfile is plausibly a lockfile, not merely present. Cheapest sufficient check: non-empty and parses as JSON and has a lockfileVersion — which is precisely what npm's own error names as the requirement (lockfileVersion >= 1). That keeps it a syntactic check (no dependency resolution, no network), consistent with the rest of validate.
Note the same question applies to the other package managers packageManagerFor recognises — pnpm-lock.yaml and yarn.lock are not JSON, so the predicate should be per-manager rather than "parse as JSON" globally. An empty file is wrong for all of them.
Test coverage this needs
Both directions, or the guard is satisfiable by a broken fix:
- a 0-byte lockfile must FAIL validation (the regression case);
- a real lockfile must still PASS (positive control — otherwise "reject everything" passes);
- a lockfile with content but no
lockfileVersion — decide deliberately and pin it;
- the missing-lockfile case must keep its existing message and exit code.
Found by a blind dogfood run against
v0.1.90-21-gf56aa72(agent given only the built binary,README.mdand--help; no source, no credential).Symptom
civitai app validatereports✓ is valid, exit 0, for a project whose committedpackage-lock.jsonis empty. The platform build will still fail.Why this is a real failure, not a nit
The check is presence-only — it asks whether the file exists, not whether it is a lockfile. Measured on npm 11.17.0, an empty lockfile fails
npm ciwith the same class of error as a missing one:AGENTS.mditem 3 states the check's entire purpose: the platform build installs strictly from the committed lockfile with no registry re-resolve fallback, and a mismatch "surfaces only as an opaque server-sidebuild failed". That is exactly the outcome an author gets here — aftervalidatetold them they were fine.The failure message makes
toucha reachable wrong turn. It names the filename (package-lock.json) as the thing that is missing, so creating an empty one to satisfy it is a natural — and silently wrong — response. The check invites the input that defeats it.Suggested fix
Validate that the lockfile is plausibly a lockfile, not merely present. Cheapest sufficient check: non-empty and parses as JSON and has a
lockfileVersion— which is precisely what npm's own error names as the requirement (lockfileVersion >= 1). That keeps it a syntactic check (no dependency resolution, no network), consistent with the rest ofvalidate.Note the same question applies to the other package managers
packageManagerForrecognises —pnpm-lock.yamlandyarn.lockare not JSON, so the predicate should be per-manager rather than "parse as JSON" globally. An empty file is wrong for all of them.Test coverage this needs
Both directions, or the guard is satisfiable by a broken fix:
lockfileVersion— decide deliberately and pin it;