Thanks for helping improve construct.
pnpm install
pnpm run typecheck # tsc --noEmit (strict)
pnpm test # vitest
pnpm run build # bundle src/ → scripts/construct.mjs
pnpm run check:build # rebuild + assert the committed bundle is unchanged
pnpm run demo # offline: render + check the sample brief
Requires Node ≥ 20 for the dev toolchain (vitest); the published bundle runs on Node ≥ 18 — the CI floor job proves it.
Thin deterministic engine + thick agent playbook. See
DOCUMENTATION.md. The two places to extend:
- A new research angle → add
src/research/<angle>.tsreturningSourceResult[], register it insrc/research/registry.ts. Keep all network I/O behindresearch/fetch.tsand subprocesses behindutil.ts::shso it stays testable offline. - SRD content/shape →
src/srd.ts(the model) andsrc/templates.ts(the Markdown). Both are pure — add a golden test intests/.
The agent-facing guidance lives in SKILL.md + references/*.md — markdown, no
code. Prefer teaching the agent there over hard-coding behaviour in the engine.
Two engines are vendored under src/vendor/, each pinned by tag and SHA-256 and
inlined by tsup so the skill still ships as one file with no install:
- codeindex — the code you have locally.
- webindex — the web: HTTP, extraction, the PDF and office ladders, ranking, forges and package registries, the container stack, and the whole MCP protocol.
Everything in src/ reaches webindex through src/engine.ts — never
src/vendor/* directly. That module calls configure() once, so you cannot
obtain an engine function without first importing the module that configured it.
Three scripts keep it honest, all wired into check:build:
| Script | What it refuses |
|---|---|
sync-engine.mjs --check |
A tampered vendor (bytes differ from the pin) or a stale one (pinned below minRef). The second matters because tsup inlines the bundle: a stale pin ships old behaviour with every test green. |
verify-engine-usage.mjs |
A local declaration that shadows an engine export — exported or not — unless it is argued for in engine-forks.json. Also a drop below the usage floor. |
verify-skill-bundle.mjs |
A skill directory that would not install, including a stale docker/ copy the engine now writes out on demand. |
Two rules that are easy to get wrong:
- Adopting an engine export and bumping
minRefhappen in the SAME commit. Deleting a local copy while pinned to a release that lacks its replacement builds green and ships broken. engine-forks.jsonis a ratchet. Entries may leave, never arrive — each one carries the argument for why that fork still exists. A fork that no longer matches anything also fails, so the list cannot rot.
To pull a new engine release: node scripts/sync-engine.mjs --ref vX.Y.Z, then
pnpm run check:build.
Write a failing test first. The deterministic core (brief, srd, render,
check) is fully offline; the research layer mocks fetch/sh. Fixtures live in
tests/fixtures/ (sample-brief.json, sample-evidence.json) — keep timestamps
out of golden comparisons (the body is deterministic; generatedAt is injected).
Rebuild the bundle (pnpm run build) before committing any src/ change — the
committed scripts/construct.mjs is checked for reproducibility in CI.
Conventional Commits: feat: → minor,
fix: → patch, feat!:/BREAKING CHANGE: → major. semantic-release owns the
version, changelog, tag and GitHub release — never bump by hand.
feat(oss): mine related PRs for pitfalls
fix(check): treat empty entity refs as valid closure
docs(readme): clarify grounding-vs-completeness