Operations — what bites, and what it costs
This page is about working in this repository, not about what the platform guarantees. The contract is under
openspec/specs/; this page names the guards that run here and the traps they catch.
Working knowledge that decays between sessions. Traps, not run numbers: every entry below fails in a way that looks like something else, which is what makes it expensive.
Running things locally
- A leftover development server on port 4200 will be adopted by the end-to-end suite. Playwright
reuses an existing server, so a whole suite can silently run against a stale build and its verdict
is about code it never loaded. Either free the port or point the run elsewhere with
BASE_URL. The demo is the exception: its own suite is pinned to port 4210 andBASE_URLmoves only the assertion URL, so pointing it elsewhere makes it wait for a server that never starts. - A
cdpersists across shell invocations, and a parallel invocation does not see it. Use absolute paths, because “the build is green” and “I was not in the workspace” look alike from outside. - A pipe after a test command reports the pipe’s exit status, not the test’s.
… | tailreports success for a failed run, and… | headaborts the run mid-way, after which the abort reads as a result. Write full runs to a file and grep the summary line explicitly. - Never match on the absence of the word “error”. Match on the success line the tool actually prints. An empty output is not a pass.
- Node scripts resolve their modules relative to the script’s own path. A helper written to
/tmpcannot import the workspace’s packages. Put it in the workspace. - zsh has a readonly
statusvariable, and does not word-split an unquoted variable. Both fail silently.
Scaffolding from inside this repository
The generators are published, so a product repository reaches them through @loomweaver/cli or the
MCP server. Working in this repository is the one case that does not: the collection is used through
the workspace path alias, and the placement is passed explicitly.
cd platformnx g @loomweaver/devkit:weaver --id notes --directory libs/weavers/notes-weaver \ --import-path @loomweaver/notes-weaver --app loom-testbedTo point an MCP client at a local build rather than the published package, bundle it first and name the bundle:
cd platform && nx bundle mcp{ "mcpServers": { "loomweaver": { "command": "node", "args": ["platform/libs/tooling/mcp/dist/main.mjs"] } }}Editing
- Never run Prettier over the Angular template files. Its HTML parser does not know the control flow blocks and flattens their indentation, and the damage then has to be taken back by hand.
prettier --writeon a source file also reformats the lines you did not touch, andstructure-baseline.jsoncounts lines. A three-line fix that lets the formatter rewrap one unrelated import can carry the file past its own baseline entry, andstructure-checkthen fails in a way that reads like the fix rather than like the formatter. Nothing underplatform/is prettier-clean anyway, some 225 files, and no pipeline step checks it, so format the lines you wrote and leave the rest alone. Markdown is the exception:docs/, the README and CONTRIBUTING are formatted in full,npm run docs-format-checkholds it, and the root.prettierrckeeps the formatter out of the code blocks so a sample’s density stays the author’s decision.- Take a calibration change back line by line, never with
git checkout <file>. The file also holds the work you meant to keep, and the checkout throws it away with the calibration. - A checker you wrote yourself is not evidence until it has been calibrated in both directions. Break something on purpose, confirm it is reported, take the break back, confirm the file is unchanged. Link checkers written here are the standing example: each one strips code spans its own way, and each flaw in that reports failures that are not there.
- Vitest does not type-check. Packaging is the type check; a green test run can sit on code that
does not compile. Moving a constant and leaving a bare
export { X } from './elsewhere'behind keeps the whole suite green whilenx package shellfails outright, because a re-export does not bring the name into local scope. - A comment inside a template literal is only invisible to the scanner while the template has no
${}. At a substitution the token ends and a plain scan loop carries on in ordinary code mode, so template text after the closing brace gets tokenised as code. Take template spans from the AST. import typeis not an import at run time. TypeScript erases it, so a type-only edge cannot cause a circular-initialisation bug and does not count as a file cycle. It still counts towards a library split, which is whycheck-import-cycles.mjsbuilds two graphs rather than one.nx migratewrites TypeScript compatibility pins into everytsconfig:esModuleInterop: false,types: ["*"],noUncheckedSideEffectImports: falseandignoreDeprecations: "6.0", all to preserve pre-TS6 behaviour. That assumes a workspace coming from TypeScript 5. This one is already on 6, so the pins change behaviour instead of preserving it:esModuleInterop: falsebroke the CommonJS default import ofdompurifyand turned 13 tests red. Discard them after--run-migrationswithgit checkout HEAD -- '*tsconfig*'; the files were already correct.migrations.jsonandtools/ai-migrations/are throwaway artefacts.@types/nodefollows the Node pin in.nvmrc, neverlatest. Its major mirrors the runtime; a higher one types APIs that are not there at run time. It moves when the pin moves, not before.- Upgrade through
npx nx migrate <version>, notnpm update.scripts/update-all-dependencies.shdeliberately stays inside the semver ranges and only reports the majors that are waiting. - A page under
docs/needs an opening paragraph of prose, or the site build fails.website/tools/sync-docs.mjsderives the page’s meta description from it, the way it derives the title from the# Titleheading. Blockquotes, the derived-from-specs header, tables and code blocks are skipped, so the paragraph has to be prose. Without one the page would ship the site-wide fallback description and say nothing about itself in a search result, which is what the failure prevents. - A new page under
docs/has to be staged before the site sees it. The sync lists the pages withgit ls-files, so an untracked file is invisible to it. Every link to the new page then fails the build as unresolvable while the file sits right there.git addthe page and the build passes; nothing else is wrong. - The site dates every page from
git log, so a shallow checkout dates the whole site to today. Those dates are the sitemap’slastmodand the structured data’sdateModified. Both workflows that build the site check out withfetch-depth: 0for exactly this reason; a local shallow clone has the same effect and there is no error to notice, only wrong dates. - The tour on the README and the landing page is recorded, not hand-made. Serve the testbed, then
node platform/tools/record-tour.mjswritesassets/media/tour-{light,dark}as webm, mp4, gif and a poster still, which is exactly the setwebsite/tools/sync-docs.mjsrefuses to build without. It refuses the same way for the social card and the icon set inassets/brand/. It needsffmpegon PATH and nothing in CI runs it. Re-record when you change the workbench chrome the tour shows, and follow a written route while recording: an outdated tour leaves a broken layout on the front page, where it stays until somebody notices. - The tutorial’s picture is taken by the quick-start check, not by hand. Package first
(
nx run-many -t package,nx run shell:styles,nx bundle frame-kit,nx bundle cli). Then runLOOM_QUICK_START_STILLS=$PWD/../assets/media node tools/check-quick-start.mjsinplatform/. It writesquick-start-{light,dark}.pngof the product at the point the page shows, before the agent and the recipes ride along. The status bar in the picture shows the version the working tree is stamped with, so take it after a release, when that is the version a reader gets. - The stills of the dialogs and menus are captured, not hand-made. They are of the demo.
Serve it (
npm run start -- --port 4210indemo/), thennode platform/tools/capture-screenshots.mjswritesassets/media/<motif>-{light,dark}.pngfor every motif it lists. The motifs are the plugin store and its consent prompt, the settings dialog, the workspace dialog, the rail’s curation dialog, the tab and rail context menus, quick open, and split panes. The guide The workbench your users get and the landing page embed them, and the website sync fails on a missing one. It needs no ffmpeg, and nothing in CI runs it. Re-run it when the chrome of a dialog or menu changes, or when the demo’s content behind one changes. Look at every picture before committing: the version in the status bar dates each one, and a motif the demo can no longer reach fails loudly rather than shooting the wrong thing.
Verifying
-
Test names in this repository read as scenarios. They are the most reliable description of what the system does, and they are why most requirements rest on tests rather than on prose.
-
The published contract is the packed type declarations, not the source barrels. The barrels export more than ships. Check
platform/dist/**after packaging. -
A concordance hit is not a coverage hit. That a source is listed as dissolved proves only that someone opened it.
-
Status lines lie in both directions. A document can call work pending that shipped weeks earlier, and call work done that never landed. Check against the code.
-
A bundle budget that always warns is not a budget. The shell built at 898 kB against a 500 kB limit, the testbed at 1.03 MB against 1 MB. Both warned on every production build, so nobody read the line. Each budget here sits on the next quarter-megabyte above what that build measures, and the error a quarter above the warning. Raise one only after building the thing and reading the number.
-
The scaffold’s budget is not derived that way, on purpose.
DISTRIBUTION_INITIAL_BUDGETin the devkit is what a consumer’s product gets, and we cannot measure a product we have not seen. It stays generous, and it is the single source for both scaffold paths.
Guards this repository runs
| Command | Where | What it fails on |
|---|---|---|
npm run api-docs-check |
platform/ |
a published export mentioned in no documentation, or a declaration the packed rollup carries without exporting, so that a consumer sees a type in a public field and cannot name it. The known ones are listed in the checker with the change that decides them |
npm run comments-check |
platform/ |
a comment that is neither a functional directive nor JSDoc on something a consumer can reach in the packed declarations, a private member being out of reach. Needs the three packages that ship declarations packed first. The residue in tools/comment-residue.json names the exceptions with their reason; it is a ratchet, and the checker fails on a stale entry too |
npm run import-cycles-check |
platform/ |
a new import cycle between files in @loomweaver/shell, or a new mutually dependent pair of feature slices. Baselines in tools/cycle-baseline.json, both ratchets: file cycles stand at zero, slice pairs at 21, and the check fails on an entry that is no longer true so the list gets trimmed |
npm run package-exports-check |
platform/ |
a package manifest promising a file the package does not ship |
npm run region-ids-check |
platform/ |
a shell default aimed at a region no scaffold emits, or a scaffolded weaver docked into a region that is not a panel |
npm run docs-style-check |
platform/ |
a documentation page with more sentences over 40 words than tools/docs-style-baseline.json records (a ratchet: the number may only go down, and a stale entry fails too), a page under docs/ without the derived-from-specs header (the index, the glossary and this page are exempt), or a spelling the glossary does not use. --list prints every long sentence per page |
npm run structure-check |
platform/ |
a folder over 12 concepts or a source file over 400 lines, where a concept is one non-spec .ts file. Baseline in tools/structure-baseline.json, a ratchet: five folders and ten files are recorded today, and the check fails on a new entry, a worse one, an improved one still over the threshold, and one that no longer applies |
npm run bundle-size-check |
platform/, demo/, examples/ |
an application’s initial bundle growing past the ceiling recorded in tools/bundle-size-baseline.json, or falling a whole 5 kB step below it, which fails as stale. It is told which applications to check and fails when a named one has no build output, because a guard that silently measures nothing when a path moves is worse than none. It reads dist/, so the build has to have run. The Angular budget beside it catches a jump; this catches the creep that stays under one |
npx nx run-many --target=lint --all |
platform/ |
module-boundary violations, member ordering, inline templates, unknown Tailwind utilities |
npm run lint |
demo/, website/ |
the same conventions in the two products that live outside the Nx workspace |
npm run icon-docs-check |
platform/ |
the icon catalogue in docs/reference/icons.md no longer matching the shell’s icon map; --write regenerates it |
npm run agent-versions-check |
platform/ |
the two agent packages the weaver generator emits as literals, or the frame kit the distribution generator records, drifting from the versions the workspace installs |
npm run command-names-check |
platform/ |
two shipped commands presenting the same name to a user, or one command labelled two ways |
npm run pwa-check |
demo/ |
a promise the product makes to the browser and never to itself: the manifest, the icons and the service worker it advertises |
npm run sync |
website/ |
a page under docs/ missing from the site’s sidebar or from llms.txt, a link that resolves to nothing, or a page that does not open with one title heading. It runs before every build of the site |
npm run check-contrast |
website/ |
a colour pair the landing page or the site chrome uses falling below WCAG AA in either theme, at 4.5 for text and 3.0 for a control’s boundary. It also fails when its own copy of the palette stops matching brand.css, because the ratios are then arithmetic about a site that no longer exists |
npm run check-head |
website/ |
a built page carrying no description of its own, no social card, no icon set, or structured data whose @id references do not resolve. Also a sitemap entry without a lastmod and a robots.txt that lost its AI-crawler section. It reads dist/, so the build has to have run |
npm run licence-check |
platform/, demo/, website/ |
a production dependency under a licence outside the allow-list |
shellcheck scripts/*.sh |
repo root | a shell script warning |
npm run quick-start-check |
platform/, nightly |
the Getting started commands no longer producing a running, tested product against the published packages: two weavers composed in, built, and the generated tests run |
openspec validate --all --strict is run by hand before a change is handed over; no pipeline runs it.
The end-to-end suite, including the accessibility audit, runs in the nightly rather than in the merge gate, which is a standing decision to keep the gate fast.
Two gates in the merge run against the pull request rather than against the tree, so there is no
local command for them. Sign-off fails when a commit in the branch carries no Signed-off-by
line naming its own author; the DCO section of CONTRIBUTING.md says what that line states and how
to add it to commits already written. Copilot reviews every pull request automatically, which blocks
nothing by itself, but main requires conversation resolution, so a thread it opens holds the merge
until someone answers it.