Thank you for your interest in contributing! This guide explains how to get involved.
If you find a bug, please open an issue with:
- Steps to reproduce the problem
- Expected vs. actual behavior (screenshots or logs help)
- Your environment (OS, and relevant runtime/version)
Have an idea? Open a feature request describing the problem you want to solve and your proposed solution.
- Fork the repository and clone your fork:
git clone https://github.com/<your-username>/archlang.git cd archlang
- Create a branch for your changes:
git checkout -b feature/your-feature-name
- Make your changes and verify them locally (see Development Setup below).
- Commit with a clear message following Conventional Commits:
git commit -m "feat: short description of your change" - Push and open a Pull Request against the
mainbranch.
# Install dependencies (one root install bootstraps EVERY workspace)
npm install
# Rebuild the core on change — `dev` is `tsup --watch`, NOT a web server
npm run dev
# Production build of the core (dist/) — every workspace consumes it
npm run build
# The whole vitest suite: test/, playground/test/, packages/*/test/, editors/vscode/test/
npm test
# Pre-push gate: typecheck + lint + test in one shot
npm run checkThe sites are separate Vite apps: npm run playground:dev and npm run docs:dev (each builds the
core first).
Before you open a PR, run these locally — they are the same checks CI enforces:
| Command | Covers | Run it when |
|---|---|---|
npm run check |
typecheck + Biome + the full test suite | always |
npm run check:drift |
every generator re-run and byte-compared against its artifact | always — npm run check does NOT include it |
npm run typecheck:all |
the four workspaces too (playground, docs-site via vue-tsc, MCP shim, VS Code extension) | you touched anything outside src/ + test/ |
npm run docs:build |
the VitePress site actually builds | any docs/*.md or docs-site/ edit — the core suite never compiles the site |
npm run e2e:playground / npm run e2e:docs |
Playwright (chromium) against the BUILT app | you touched playground/ or docs-site/ |
The E2E suites serve the built output, so build first — e.g.
npm run build && npm run playground:build:only && npm run e2e:playground. Setting
E2E_BASE_URL=<origin> makes either suite drive that origin instead, with no build and no preview
server (that is how the nightly workflow re-runs the read-only @prod subset against production).
What PR CI runs. ci.yml has five gating jobs in parallel — the Node 18/20/22 test matrix
(the 22 leg also collects report-only coverage), a builds job (all four workspaces compile,
typecheck:all, the MCP baked-resource freshness check, the VS Code bundle tests), a Windows
leg (tests + drift at the runner's default line endings), and the two Playwright E2E jobs —
plus an informational benchmark comment that never gates. codeql.yml adds static security
analysis on the same events. A separate nightly.yml runs production smoke, a report-only
dependency audit, a full-history secret scan, a wider OS×Node matrix and the read-only E2E subset
against the live sites.
Full reference: docs/testing.md — the three tiers, every guard (goldens, drift generators, lockstep pins, the docs tripwires, the fuzz suites, the MCP pack gates), the house patterns for adding tests, and what to do when each one goes red. Read it before regenerating a golden or updating a pin.
Several artifacts ship from this repo and are released separately — don't let them drift: the core npm package, the VS Code extension, and (as of v1.13) the optional MCP server.
Since v1.14.0 the npm publish is tokenless and runs in CI (.github/workflows/release.yml,
OIDC trusted publishing with provenance — see the workflow's header comment). There is no npm
token anywhere; do not add one. The full transferable recipe (for this or any other repo) lives
in docs/npm-oidc-publishing-playbook.md.
- Update
CHANGELOG.mdand bumpversionin the rootpackage.json. npm run check(typecheck + lint + test) andnpm run check:driftmust both be green.check:driftis a separate hard CI gate thatnpm run checkdoes not cover — it regenerates every artifact and fails if one drifted (see CI drift gates).npm run build && npm testalso run inside the publish viaprepublishOnly, but they will not catch asrc/manifest.tsedit whosedocs/cli-reference.mdwas never regenerated.- Commit, push, then tag:
git tag vX.Y.Z && git push origin vX.Y.Z. The tag push triggersrelease.yml, which publishes the core, then the MCP shim (if its version moved), then syncs the MCP registry — each step skips versions already on its registry, so re-running a partial failure is safe (gh workflow run release.ymlre-dispatches).
Provenance gotcha: npm rejects the publish (E422) if
package.json'srepository.urlcasing differs from the real repo — it must saygithub.qkg1.top/ChanMeng666/archlang(owner casing byte-exact), notchanmeng666.
One-time npmjs setup (already done for both packages): each published package carries a Trusted Publisher registration on npmjs.com pointing at
ChanMeng666/archlang+release.yml. Creating/changing that registration — like all token/maintainer/account management — is an interactive human-with-2FA operation by npm policy (token-based bypass of these is being retired through 2026–2027); agents cannot and should not automate it.
Pushing to main auto-deploys the playground and docs sites (Cloudflare Workers) — no manual step.
The extension bundles the core at build time (esbuild, --no-dependencies), so a core
release does not reach extension users until the extension is rebuilt and republished.
Whenever a core change touches the language surface — grammar/keywords, completion, hover,
diagnostics, or error/lint codes — republish the extension so its bundled services stay current:
- Bump
versionineditors/vscode/package.jsonand add an entry toeditors/vscode/CHANGELOG.md. - If the core moved, bump the
@chanmeng666/archlangdev-dependency pin there to match. npm run build --prefix editors/vscodethennpm run package --prefix editors/vscode(vsce package --no-dependencies) → a.vsix.- Upload the
.vsixat https://marketplace.visualstudio.com/manage/publishers/ChanMeng (web upload; there is no Azure DevOps org / CI publish for the extension).
Rule of thumb: if you changed
src/grammar/tokens.ts, the language services insrc/lsp.ts, or the error/lint catalogs, the extension is stale until you republish it.
"Did the rebundle actually take?" is automated. esbuild stamps the resolved core version into the bundle as
__CORE_VERSION__, andeditors/vscode/test/stdio.test.tsasserts it matches — runnpm run vscode:build:only && npx vitest run editors/vscode(CI'sbuildsjob does exactly this). That replaced the old by-hand "count new keywords indist/server.js" probe.
Build and package in the PRIMARY checkout only — the build now refuses otherwise. A
git worktreecheckout has nonode_modules, so esbuild resolves the core by walking up and bundles the shared repo's, and the__CORE_VERSION__stamp cannot catch it because both stamp the same version.editors/vscode/resolve-core.mjscompares the resolved core's real path against the repo root of the tree being built and throws naming both paths. Junctioning the worktree'snode_modulesdoes not make it safe (npm links a workspace package by absolute path to the main tree), and the guard fires there too — correctly.
Its
@chanmeng666/archlangrange is pinned by a test, and every core release turns that test red on purpose.editors/vscode/test/lockstep.test.tsasserts the range is a string equal to^+ the root version; re-pin it as step 2, never relax the check. It is distinct from the freshness stamp above — that one says the BUNDLE is current, this one says the MANIFEST is honest, and the range once sat two releases stale while the stamp stayed green throughout.
A repack can also be non-language — the icon, galleryBanner, or other marketplace metadata
(e.g. 0.4.1 was an icon-only repack of 0.4.0). Same steps 1–4 above (skip step 2 when the core
did not move); the .vsix still needs a manual web upload.
Editor syntax colors are generated, not hand-authored. The live-editor highlight palette flows through
scripts/gen-grammars.ts→playground/src/arch-language.jsasvar(--syn-<name>, <fallback>). Since ADR 0014 the--syn-*values are shared: they live in the brand token block (duplicated byte-identically inplayground/src/styles/tokens.cssanddocs-site/.vitepress/theme/style.css) and are also mirrored by thearchlangLightShiki theme indocs-site/.vitepress/config.ts. To recolor, change all four — the two token blocks, the generator's fallback hexes, and the Shiki theme — then runnpm run gen:grammars; never hand-editarch-language.js(CI fails on drift).
The optional stdio shim in packages/mcp/ is a separately versioned package, published
after the core it wraps — and since 0.2.0 the whole chain rides the same release.yml:
- Bump
versioninpackages/mcp/package.jsonandpackages/mcp/server.json(both ofserver.json's version fields — they must match), and bump its@chanmeng666/archlangdependency range to^<the new core version>. Do not hand-edit a version intosrc/server.ts: since 0.2.3 the handshake version is derived frompackage.json(readShimVersion()), and a test pins the two together.packages/mcp/test/lockstep.test.tschecks all of the above, and the dep-range assertion is a string equality on purpose — a core release turns this package red until someone consciously re-pins, rebuilds the baked resources (npm run mcp:build, verified byscripts/check-dist-resources.mjsin CI) and bumps the shim. - The tag-triggered
release.ymlrun publishes it to npm (OIDC + provenance) right after the core, then syncs the MCP registry withmcp-publisher login github-oidc→publish— also tokenless. The registry-sync step is guarded by the registry's own state, so an npm-succeeded/registry-failed partial run is recoverable by re-running the workflow. - Manual fallback (local):
npm run mcp:build,npm publish -w packages/mcp, then frompackages/mcp/:mcp-publisher login github(interactive device flow; CLI lives outside the repo, e.g.D:\mcp-publisher\) →mcp-publisher publish.
Three registry pitfalls (they cost a same-day
0.1.0→0.1.1republish): theio.github.<Owner>/*namespace is case-sensitive and the owner segment must match your GitHub login byte-for-byte (io.github.ChanMeng666/…); the registry exact-matches the npm package'smcpNamefield againstserver.json'sname; and the serverdescriptionis capped at 100 chars. Any mismatch is rejected at publish.
The synthetic repair-trajectory + authoring dataset (roadmap Tranche 5) is generated by the in-repo
dataset/ generator and published to Hugging Face under CC0-1.0. It is repo tooling, not a
package — no version bump of its own; it pins archlang_version.
When to touch it. A language/core release that changes generated output (anything that would
alter a compiled plan, its diagnostics, or describe() facts) means a new archlang_version:
- Regenerate at the pinned seed:
npm run dataset:gen(defaults--repair-rows 1200 --authoring-rows 400 --seed 20260712). - Verify:
npm test— the contamination/determinism guardtest/dataset.test.tsmust stay green. - Bump
archlang_versionindataset/CARD.mdto the new core version. - Re-upload the three artifacts and the card (below).
A card-only edit (prose, links) re-uploads README.md alone.
Upload is the owner's manual step via the hf CLI — logged in as the owner; no tokens in the
repo, no automation. Use the canonical ChanMeng666 namespace casing (the owner segment is
identity-checked, same class of lesson as npm provenance / the MCP registry):
hf upload ChanMeng666/archlang-repair-trajectories dataset/out/repair.jsonl repair.jsonl --repo-type dataset
hf upload ChanMeng666/archlang-repair-trajectories dataset/out/authoring.jsonl authoring.jsonl --repo-type dataset
hf upload ChanMeng666/archlang-repair-trajectories dataset/out/report.json report.json --repo-type dataset
hf upload ChanMeng666/archlang-repair-trajectories dataset/CARD.md README.md --repo-type dataset # card uploads AS README.mdThe card's task_categories must come from HF's official list (text-generation, not
text2text-generation — the upload warns on an off-list value).
Two permanent rules (see dataset/README.md and
ADR 0013): the canary GUID in dataset/canary.ts is
never regenerated (a new value silently splits the corpus and defeats leakage probing), and the
private eval holdout (eval/corpus.json + goldens) is never published — the public corpus is
generated independently and deduplicated against it, enforced permanently by test/dataset.test.ts.
The offline npm run eval:ci (26 golden briefs, no API key) runs in CI. To re-measure against a
real model, run the guarded live harness:
npm run eval:live -- --yes # needs OPENAI_API_KEY; writes eval/results.live.md + a delta vs eval/live-baseline.jsonIt is also wired as the workflow_dispatch workflow .github/workflows/eval-live.yml (uses the
repo secret OPENAI_API_KEY). Two further guarded, paid harnesses live beside it (same --yes
guard and key handling; details in eval/README.md), each with its own workflow_dispatch
workflow: npm run eval:g1 (Gate G1 intent generation — already run, PASSED; kept for
reproducibility) and npm run eval:l2 (the T3 L2 loop-vs-equal-budget-resampling experiment —
not yet run). Harness gotcha: reasoning models spend thinking tokens out of
max_completion_tokens — the cap in eval/run.ts is 16384 (a 4096 cap truncated gpt-5.5 into
bogusly-low scores); suspect the token budget before the language if a new model scores implausibly
low.
Nine generators produce twenty-three artifacts, and CI drift-checks all of them in a single
npm run check:drift step. The run prints its own total (✓ all 23 generated artifacts are in sync with their sources), so read the count there rather than from this page. The authoritative list is
the GENERATORS table in scripts/check-drift.ts — this table mirrors it:
| Generated artifact | Generator | Source of truth |
|---|---|---|
editors/archlang.tmLanguage.json, playground/src/arch-language.js, docs-site/.vitepress/theme/arch-highlight.js |
gen:grammars |
src/grammar/tokens.ts |
docs/error-codes.md |
gen:errors |
src/error-catalog.ts |
docs/cli-reference.md |
gen:cli |
src/manifest.ts |
spec.llm.md |
gen:spec |
src/grammar/tokens.ts + examples/ |
llms-full.txt |
gen:llms |
spec.llm.md + SKILL.md + src/manifest.ts + src/error-catalog.ts |
grammars/archlang.gbnf |
gen:gbnf |
src/grammar/tokens.ts |
schemas/plan.schema.json |
gen:plan-schema |
PLAN_JSON_SCHEMA |
schemas/intent.schema.json |
gen:intent-schema |
INTENT_JSON_SCHEMA |
the thirteen examples/*.svg the README embeds |
gen:example-svgs |
the matching examples/*.arch (list: README_SVGS) |
Whenever a generator's source changes, run npm run gen:all to regenerate every artifact in
dependency order (gen:spec before gen:llms, which consumes it) and commit the output;
generated files must never be hand-edited. npm run check:drift reproduces the CI gate locally.
src/manifest.ts is the single source of truth for the whole CLI — not just its docs. Each
command declares its exact flag set plus at least one worked example, and three things are derived
from that one declaration:
docs/cli-reference.mdis generated from it (npm run gen:cli) and drift-gated in CI.- Help is rendered from it.
src/cli/help.tsbuilds both top-level and per-command help (arch <cmd> --help) out of the manifest — including theexamples[], which a test requires to be non-empty for every command. - The parser is drift-tested against it.
FLAG_KEYSinsrc/cli/io.tsis checked bidirectionally by the "FLAG_KEYS — no drift vs the manifest" suite intest/cli-help.test.ts: every manifest flag (and alias) must have a parse-table entry, every parse-table entry must be declared by some command, and the two must agree on whether the flag takes a value. An undeclared flag is rejected at parse time (exit3, with a did-you-mean), never swallowed as a filename.
So adding or changing a CLI flag or command is a four-part edit, and skipping any part fails a test or the drift gate:
- Declare it in
src/manifest.ts(the flag, plus anexamples[]entry for a new command). - Add its
FLAG_KEYSentry insrc/cli/io.ts(matchingkind— value-taking vsboolean). - Implement it in the command module under
src/cli/. npm run gen:cli(orgen:all) and commit the regenerateddocs/cli-reference.md.
By participating, you agree to abide by our Code of Conduct. For questions or support, see SUPPORT.md. For security issues, see SECURITY.md.