paperclipai/paperclip · error · Error
Expected 99 captures, found
Error message
Expected 99 captures, found ${captureFiles.length} What it means
The ingestion script asserts an exact count of 99 corpus capture markdown files (all *.md except INDEX.md, sorted) before parsing. This is an intentional canary: the capture corpus is expected to be in lockstep with the 99 supported app definitions, so an off-by-one means a capture was added or removed without a corresponding definition change.
Solutions
- Update the hardcoded expected count (99) in scripts/ingest-app-definitions.mjs to the new intended corpus size after intentionally adding/removing an app capture.
- List the corpus directory and diff against expected app slugs to find stray or missing .md files; remove unrelated markdown or restore the missing capture.
- Verify PAPERCLIP_CONTENT_TEMPLATES points at the correct templates directory.
- Use --definitions-only if you only need to regenerate definitions without the capture corpus.
Example fix
// before
if (!definitionsOnly && captureFiles.length !== 99)
throw new Error(`Expected 99 captures, found ${captureFiles.length}`);
// after (intentionally added a 100th app)
if (!definitionsOnly && captureFiles.length !== 100)
throw new Error(`Expected 100 captures, found ${captureFiles.length}`); Defensive patterns
Strategy: validation
Validate before calling
const count = fs.readdirSync(corpus).filter(f => f.endsWith('.md') && f !== 'INDEX.md').length;
if (count !== 99) console.warn(`Corpus count is ${count}; update the expected count or fix the corpus before ingesting.`); Try / catch
try { await ingest(); } catch (e) { if (/^Expected 99 captures/.test(e.message)) { console.error('Capture corpus out of sync: diff corpus dir against app slugs, or update the expected count.'); process.exitCode = 1; } else throw e; } Prevention
- Treat the expected-count constant as a lockstep contract: update it in the same PR that adds/removes an app.
- Keep scratch markdown out of the corpus directory.
- Pin PAPERCLIP_CONTENT_TEMPLATES in CI so the corpus source is deterministic.
When it happens
Trigger: Running scripts/ingest-app-definitions.mjs without --definitions-only when the number of .md files (excluding INDEX.md) in the corpus directory (PAPERCLIP_CONTENT_TEMPLATES or ../../paperclip-content/research/connections/vercel/templates) is not exactly 99.
Common situations: Adding a capture .md for a new app without updating the expected count constant; a stray .md (scratch notes, editor backup like app.md.bak won't match, but notes.md will) landing in the corpus directory; deleting a capture while removing an app; cloning a partial content repo; PAPERCLIP_CONTENT_TEMPLATES pointing at the wrong directory.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- : capture has no states
- ACPX verified runtime descriptor is misplaced
- / / : required field needs placeholder
- / : oauth requires ownershipModes
- / : tool api_key requires keyPlacement
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/426fb726e6cf661c.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/ingest-app-definitions.mjs:1616
...(connectionMethod.extensionFields ?? []),
...(connectionMethod.credentialFields ?? []),
])
if (
connectionField.required &&
connectionField.type !== "checkbox" &&
!connectionField.placeholder
)
throw new Error(
`${app.slug}/${connectionMethod.key}/${connectionField.key}: required field needs placeholder`,
);
}
};
const captureFiles = definitionsOnly ? [] : fs
.readdirSync(corpus)
.filter((fileName) => fileName.endsWith(".md") && fileName !== "INDEX.md")
.sort();
if (!definitionsOnly && captureFiles.length !== 99)
throw new Error(`Expected 99 captures, found ${captureFiles.length}`);
const parsedCaptures = Object.fromEntries(
captureFiles.map((fileName) => [
path.basename(fileName, ".md"),
parseCapture(fileName),
]),
);
const reviewReport = {
schemaVersion: 1,
corpusSize: captureFiles.length,
providers: captureFiles.map((fileName) => {
const slug = path.basename(fileName, ".md");
const states = parsedCaptures[slug].map((state) => inferState(slug, state));
return {
slug,
stateCount: states.length,
states,
ambiguities: states
.filter((state) => !state.auth)View on GitHub (pinned to 3f1d897a7c)