The human-facing companions to this file, which state the same rules with their reasons rather than as instructions: CONTRIBUTING.md (what is editable, what is derived, what to run) and RELEASING.md (publishing the npm package). When a rule here changes, check whether one of those states it too.
src/ is authored; everything else is produced from it. That was not true
until 2026-08-26, and the shape it replaced is why this section exists: the
components were in docs/authored/, the tokens in docs/, the SVG masters in a
root src/ that held nothing else, the rendered logos in three places at once,
and the build scripts inside the directory that publishes the website.
| Directory | Holds | Edit? |
|---|---|---|
src/logos/ |
the three SVG masters — every raster is rendered from them | yes |
src/styles/ |
colors_and_type.css (the deliverable) and theme.css |
yes |
src/tokens/ |
the same tokens as typed TS modules, hand-mirrored | yes |
src/styles/fonts/ |
the ten Satoshi cuts, the Manrope Cyrillic subset with its licence, and fonts.css |
the CSS only |
src/components/ |
every component, helper and icon the system ships | yes |
src/captured/ |
the eight files copied from codecave.pro | never |
docs/ |
the website, and the published brand kit | pages, yes |
tools/ |
every build and check script | yes |
packages/brand/ |
the npm package — a pure derivative | never |
dist/ |
the built website, gitignored | never |
DESIGN.md |
the rules document, at the repository root | yes |
Three of the paths a consumer links — colors_and_type.css, tokens/ and
fonts/ — are authored under src/ and published into dist/ at the URLs
they have always had. DESIGN.md is the fourth entry in the same map and the
only one reaching upward: it moved out of docs/ on 2026-08-27 to conform
to the DESIGN.md format spec, which places it at the project root, and the
passthrough copies it back down to /DESIGN.md so the URL never moved. tools/astro-passthrough.mjs holds that map in
PUBLISHED and asserts the copy arrived; check:links and check:examples
read the same map rather than restating it, because both broke the day the
files moved and both were wrong about correct deliverables.
Nothing here needs a codecave.pro checkout. build-storybook.mjs borrowed
that repo's node_modules for its toolchain until 2026-08-26, on the argument
that a specimen must be built with the versions the website builds with — an
argument that died with the inversion, since the site is the consumer now. Every
module is declared here and pinned to match, and the switch was byte-neutral:
all twelve bundles and tw-bridge.css came out identical. Two optional reads
remain, both of which say when they are skipped — check:collisions sweeping
the site's SFCs, and the sanitizer port's dompurify pin. CI no longer checks that
repo out, and CODECAVE_PRO_TOKEN is unused.
npm run build:assets needs Inkscape and ImageMagick and will not run on a
stock Windows checkout. That is why the rendered ramps are tracked: they are the
record of the last run.
Jira is the only list. This repo used to keep a TODO.md; it was deleted on
2026-08-20 once its contents were filed, so the absence of a to-do file in this
repository does not mean the work is done.
Open items for the brand package, all in
CCWEB2 under the brand-kit
label (live query):
- CCWEB2-340 — build tooling sits in
dependencies, so every advisory against it reads as production. This repo is already clean — everything is a devDependency andpackages/branddeclares no dependencies at all — so it is live only against codecave.pro, whereastro, the@astrojs/*integrations,@tailwindcss/viteandsharpare all runtime deps. Labelledbrand-kitbut now purely site-side.
CCWEB2-318, the epic that inverted the direction of truth, closed on 2026-08-25 with the 2.2.0 release. What it established is architecture now and is described below rather than tracked as work.
Open bugs in what codecave.pro ships, found while resyncing the captures or
while working on the site itself. These are the site's to fix, not the
package's — do not "correct" them in docs/. A bug leaves this list the
moment it is fixed; Jira keeps the history, so nothing here is a record:
- CCWEB2-320 —
TextField.vuedraws its inner focus halo at alpha 0.8 whereInputText.vueand--shadow-input-focususe 0.6. Same halo, two alphas, visible only on a focused textarea. The ticket's other half is fixed: production movederror-300off#B42318on 2026-08-27, so the 2.91:1 error text is gone. Assigned to Maria Shaban.
The fonts are settled. The package declares eleven @font-face rules: ten for Satoshi — 300/400/500/700/900, upright and italic, every weight Satoshi has — and one for Manrope, which --font-sans names second because Satoshi has no Cyrillic. codecave.pro declares the same eleven. The binaries are still not in the tarball: that is a redistribution question, recorded in build.mjs, and no release changes it. The vendor's own stylesheet.css is unusable and both stylesheets say why next to the declarations.
A consumer that installs the components has to put the package back in Tailwind's scan. Automatic content detection skips node_modules and this package ships Vue source, so a utility used only inside an installed component is never emitted — with no error anywhere: the build succeeds, the typecheck passes, and the elements render unstyled. One @source line fixes it, the package README.md says so, and codecave.pro's check:classes fails the build when it is missing (CCWEB2-360).
The version mechanic stays true and is the thing to remember — CI installs with pnpm install --frozen-lockfile, so the site runs whatever its lockfile pins and nothing moves it on its own. A token change here reaches the site only when someone raises the range and regenerates the lockfile. pnpm's minimumReleaseAge guard used to add a day on top of that, and no longer does: codecave.pro's pnpm-workspace.yaml exempts this package as a whole, with an entry naming no version at all. Every wildcard spelling is still rejected — @codecavepro/brand@*, @x, @>=0.0.0 — which is why both repos said for a while that a whole-package exemption was unavailable. It is the wildcards that are unavailable; an entry with no version predicate is a different thing and pnpm accepts it.
There is one way around all of that, and it is for development only.
codecave.pro's astro.config.mjs aliases @codecavepro/brand at
../brand/packages/brand/dist whenever that path exists — no flag to remember,
and CI cannot satisfy it, because both of its workflows check out that repo
alone. So a component edited here appears on that site with no publish, no
version bump and no lockfile. npm run watch:package is the other half: it
rebuilds dist/ on every save under src/, and astro dev reloads the page
about two seconds later.
That watcher is why the package build stopped being rm -rf dist and a full
rewrite. The two rules that replaced it are load-bearing, and both exist for
the same reason: a consumer's dev server is reading these files, and our write
is what woke it up.
dist/is updated, never rebuilt. Every output is compared before it is written and skipped when the bytes already match;dist/is then swept for anything the run did not produce. Editing one component moves exactly one mtime. Rewriting all 58 would invalidate the consumer's whole module graph on every save, and clearingdist/first would 404 whatever it asked for meanwhile.- A write is a rename wherever Windows allows one.
writeFileSynctruncates before it writes, and polling six shipped outputs through six full rewrites caught them at zero bytes 15 times — which a dev server reports asCannot find module '@codecavepro/brand/components/…', naming a different component every time. Renaming a temp file over the target closes that window and cannot replace a file another process holds open: on its own it failed five times in eight rebuilds, every one onBrandNav.vue, which every page of that site imports. So the build renames, waits out a reader for half a second, and only then writes in place.
And only one build may run at a time. .tmp is a fixed path — tsconfig.json
names it in both include and rootDir — so two builds share a scratch
directory, and the second one's rm -rf .tmp lands inside the first one's window
between running tsc and reading what tsc emitted. The old shape raced in the
same place and was worse about it: it cleared dist/ up front, so the loser
shipped a dist/ missing whatever the winner had not yet rewritten, and exited
0. A watcher makes that collision ordinary rather than exotic, because
npm run build:storybook begins with a package build and its whole point is to
run while you are editing. So the second build waits, and a crashed build's
lock is stolen rather than waited on. npm run check is unaffected: --check
returns before either .tmp or dist/ is touched.
check-captures.mjs was deleted on 2026-08-25, along with its npm script and
its release gate. The reason is the version mechanic above: the site installs
this package and pins it, so it lags by design. Components are developed
here, tried in the storybook here, published, and only then does the site
bump — which means a check demanding the two be equal was red for exactly the
changes it existed to protect, and green only when there was nothing to
release. It had already narrowed itself: 37 of the then-51 files under
src/captured/ had no upstream left, and it reported them as "frozen by
definition".
A component that reaches for one company's data does not belong here, and
that is a different test from what it is written in. A component ships once
its reach becomes props — items, serviceLinks, ctaHref, basePath,
logo, resolveImage, an ICrmFormClient. A footer carrying an EIN, a street
address and a schema.org LocalBusiness graph, or a section calling a CMS for six
known record ids, has nothing left underneath the data; those are assemblies OF
shipped parts, not parts.
Finished under CCWEB2-317 on
2026-08-21, so it is architecture now rather than open work. Sources stay in
docs/, output goes to gitignored dist/, and Pages deploys dist/. Every
page is an .astro under docs/pages/, and everything else in docs/ is
payload that passes through — which tools/astro-passthrough.mjs asserts
every build, both halves being silent when they fail.
There are three browsable surfaces, and that is the shape to hold in mind.
guides/ asks what the rules are — the four prose files, rendered; the other
two ask whether the work obeys them. kitchen-sink/ asks whether a part is
right, 25 specimens from tokens through live components on one hub; examples/
asks whether the parts compose, as six deliverables behind a gallery. MAIN is
in that reading order: prose, part, composition.
It used to be five surfaces, then two. A brand page, preview/, storybook/,
artifacts/ and two indexes over them collapsed to kitchen-sink/ and
examples/ with the brand page folded into the front door as anchors — 30 routes
became 34, the surfaces collapsed and the content did not. guides/ then took it
to 39, and that one went the other way: the content already existed and had no
surface at all. docs/storybook/ still exists and serves no pages — it is
the compiled bundles, ports and placeholders the specimens mount, and it keeps
its name because every specimen identity derives from it.
The six files under docs/examples/raw/ are never rendered by Astro and never
will be. Each has a wrapper page that documents it and embeds it in an
<iframe>, which is what lets both rules hold at once: every ROUTE carries the
menu, and every ARTIFACT stays a standalone document someone can be handed. That
split has a cost worth knowing — an <iframe> reports nothing about what
happens inside it, so nothing on the wrapper page can see a broken deliverable.
Moving artifacts/ one directory deeper broke the wordmark in four of the six
and the Satoshi @font-face rules in two, and every check in the repo stayed
green. npm run check:examples is what asks now.
Every route carries the same main menu, by construction. DocPage renders
DsNav with no prop to override it, and the items come from
docs/components/menu.ts; a section's second bar comes from SUB in the same
file and is the only half that varies. So the one real failure mode is a page
getting no menu by skipping the layout — which builds, renders and passes
everything while being absent from the navigation. npm run check:links asserts
every page under pages/ imports DocPage, and separately that every
documentation route the prose cites still resolves, anchors included: collapsing
the surfaces moved most of this site's URLs and broke 49 citations across nine
files, which nothing else here would ever have noticed.
It also asks the plainest question about a page, which nothing used to ask:
when this runs, does the file it reaches for exist? Every relative src,
href, url() and module specifier below the frontmatter fence, resolved
against the page's OUTPUT location and matched against the union of what the
build renders and what it copies -- 123 of them. Frontmatter is excluded
deliberately: imports above the fence are Astro's, resolved at build time against
the source tree, so judging both by one rule reports every layout import as a
dead file. Separately, a page importing a bare specifier must pass importmap to
DocPage, and a page passing it must import one -- the layout emits the map only
on request, so a specimen without it fails on import ... from 'vue' in the
browser and nowhere else. This is the check that matters when a page MOVES: a
relative path that was right at the old depth is wrong at the new one and still
looks perfectly reasonable.
Four of the 39 routes are Starlight's, and everything about that arrangement
is deliberate. STARLIGHT.md has the measurements behind every
line of it, and is worth reading before touching docs/starlight.css, the
loader or the overrides: every fault this integration hit was silent, and
four of them had to be found by measuring the rendered page rather than by
reading a build log. That is the standing cost of running a shell this repo does
not own. docs/content.config.ts renders DESIGN.md, README.md,
SKILL.md and guide.md where they already sit — which since 2026-08-27
means one of the four sits above docs/, so the loader and check:links
both resolve file against the repository root rather than against docs/ — they are payload, cited by
path from eight files, and two of them carry frontmatter belonging to other
systems (SKILL.md's is Claude Skill metadata, DESIGN.md's is a token
manifest). So the loader synthesizes the title and never touches a file. Two
things there look optional and are not: the data must go through parseData(),
or the entries store and render nothing at exit 0; and the titles are a map,
because three of the four open with the same # heading.
guides/index.html is a hand-built DocPage, not a Starlight page. Starlight
renders no section index, and autogenerating one would mean authoring prose inside
the loader. A static route beats Starlight's injected [...slug], which the
passthrough check asserts every build.
The four guides carry this site's chrome, both tiers, from the same
menu.ts. docs/starlight-overrides/Header.astro renders DsNav and
SubNav; Sidebar.astro renders nothing, because an autogenerated sidebar comes
out empty here and would duplicate the sub-nav. docs/starlight.css is the
whole reconciliation and every rule in it carries the measurement that forced it —
--sl-nav-height derived from --ds-bar rather than left at one bar's worth, and
the header dropped out of position: fixed below 768px so the bars scroll away as
they do everywhere else.
DocPage cannot run for a Starlight route, so check:links asserts the
mechanism that replaces it — the config names the override, the override renders
both tiers — and derives the four collection routes from content.config.ts so
/guides/* is inside the dead-link check rather than exempt from it. It also
asserts menu.ts and the collection agree in both directions: a slug the bar
names and nothing renders, and a guide that renders and is in no bar. The second
is the orphan case, which is what the four pages were before the surface existed.
One rule survives the migration and is in CONTRIBUTING.md
with its reason, and it bites when adding a page: a .html at the same path
wins over the .astro, so writing one page in both forms leaves the .astro
dead — Astro's own behaviour is a WARN and exit 0, and the passthrough check
fails the build instead.
The rule that used to sit beside it said the storybook specimens are not Astro
islands and must not become them. It is reversed, and the reversal is the point
of the current arrangement. A specimen mounted storybook/compiled/*.js in the
browser through an import map, so that a reader saw what codecave.pro shipped
rather than a rebuild of it; is:inline on both tags was what kept that true.
That argument rested on this repository being downstream, and it has not been
since 2026-08-25. The components are authored in src/ and the site installs the
package built from them, so compiling a specimen from source is showing what
ships — and mounting a prebuilt bundle instead shows whatever the last package
build produced. The practical half is the reason it changed: a specimen that
imports the .vue hot-reloads while you edit the component, and a prebuilt
bundle never can.
So a specimen page is now an ordinary <script type="module"> importing
../../../src/components/…, @astrojs/vue compiles them, and DocPage no
longer has an importmap prop. build:storybook still runs and still writes
compiled/*.js — ds-bundle/ consumes them for a Design project that cannot
run a bundler. It is simply no longer what this site mounts.
That leaves three resolvers on a page, and check:links judges a reference by
whichever owns it. Above the fence is Astro's, at build time, against the
source tree. A <script> without is:inline is Vite's — also build time, also
the source tree. Everything else, is:inline scripts included, is the browser's
at request time against the output tree. Those are different depths, so one
path is right for one and quietly wrong for the other: the conversion moved four
placeholders.js imports from correct to broken without changing a character of
them, and this is the check that said so. The old pairing it replaces — "a page
importing a bare specifier must pass importmap" — is now the opposite
assertion, that no is:inline script may import a bare specifier, because
nothing resolves one any more.
The vendored runtime map is checked against the bundles, both ways, and against
the disk. vue and the gsap entry points are left external by esbuild, so
something has to resolve them for whatever mounts a bundle — and the mapping is
a judgement rather than arithmetic (vue is one of half a dozen builds in the
package; gsap is a directory). A capture moving to a different entry point of
the same package silently kills every specimen: nothing errors, the page renders,
the canvas is simply empty. npm run check:importmap reads the bare specifiers
out of storybook/compiled/*.js and the keys out of tools/storybook-vendor.mjs
and fails on either mismatch, and on a mapped file that is not in
docs/vendor/ — the DocPage version compared two lists of names, so a map
entry pointing at a runtime nobody committed passed cleanly and 404'd in the
browser. Both inputs are committed, so it needs no site checkout and runs in CI.
A layout may not derive a page's depth from Astro.url.pathname. With
build.format: 'preserve', a directory index is requested at /storybook in
dev and emitted at /storybook/ by the build, so the same page counts a
different number of levels in each mode — and nothing else in the request
separates an index from a leaf. DocPage.astro asks import.meta.glob which
pages are index.astro instead; Vite resolves that at transform time, so it is
the same static list in both modes.
Edit under src/components/ or src/captured/, and commit. That is the
whole process. Nothing has to be regenerated by hand, because
docs/storybook/compiled/*.js and docs/storybook/tw-bridge.css are
gitignored — produced by npm run build:storybook on the same rule as
packages/brand/dist/ and dist/: src/ is authored, everything else is
generated.
To see the change, npm run dev. The kitchen-sink specimens import the
.vue sources through Vite and hot-reload, so the storybook output is not in
that loop at all.
What the two outputs are for. compiled/*.js are the SFCs compiled to plain
ES modules; tw-bridge.css is the site's Tailwind theme compiled against
exactly the utility classes the component templates use, scoped to
.sb-canvas, .sb-mount. Their one consumer is ds-bundle/, whose Design-project
cards cannot run a bundler, and they are read where they are written —
tools/design-sync-map.mjs pairs each Design-project path with the authored
file that answers it, so nothing is staged or copied first.
build:storybook builds the package first, because it compiles each
specimen out of packages/brand/dist/src and cannot run without it, and it
writes the Design-project component cards last, because those mount the bundles
it just produced. Both are chained prerequisites in the script rather than
errors telling you to go run something yourself.
ds-bundle/ stopped being a staging directory on 2026-08-27, and what it
stopped doing is the point. It held 46 gitignored byte-for-byte copies of
files that already existed under src/ and docs/, materialized before every
push by tools/build-ds-bundle.sh and then verified against their originals by
40 lines of that same script. All of it was work to keep a copy honest, and the
dishonesty was real: on 2026-08-20 the mirrored colors_and_type.css sat two
shape fixes behind its source for weeks, because a stale copy uploads exactly
as cleanly as a fresh one.
None of it was ever required. DesignSync's write_files takes the project
path and the local path as independent arguments, so an upload is a list of
source-to-destination pairs rather than a directory to mirror.
tools/design-sync-map.mjs is that list. ds-bundle/ is now 76K of seven
tracked authored files with no upstream anywhere — README.md, styles.css,
guidelines/brand.md and the four Foundations cards — plus the seven generated
Components cards. The project-side layout did not change; only the local
staging disappeared.
check:design-sync asks the question the copy step never could, and the
answer was no. A card's references are the browser's, resolved at request time
against the project tree — so the check reads the map's destination paths,
never its local ones. A compiled bundle injects its scoped CSS as a <style>
element, where a relative url() resolves against the document; the cards
sit three levels deep, so Checkbox.js's url(../assets/images/checked-icon.svg)
asks for components/Components/assets/…. The old copy put it at
components/assets/…, one level up, where no card could ever reach it. The
checkbox tick has been 404 on the remote ever since, and every check in both
repositories stayed green — the same shape as CCWEB2-370, one system over. The
map carries the correct path and STALE names the dead one for deletion; both
land on the next push.
One check reads the output rather than the tree: check:importmap resolves
the bare specifiers in compiled/*.js against tools/storybook-vendor.mjs, so
it needs a build first. npm run check already required one — it reads
packages/brand/dist/ — so this is a second reason for a precondition that was
always there, and static.yml runs it after the rebuild rather than in the
dependency-free block.
This replaced a tracked-artifact arrangement on 2026-08-27, and the deletion
is the point. The thirteen files were committed and guarded by
check-tw-bridge.mjs, which compared a sha256 of both component roots against a
digest recorded in the tw-bridge.css header. That only ever made sense while
the generator needed a codecave.pro checkout and so could not run everywhere — a
node-only checker standing in for a build nobody could perform. The decoupling
ended that, and left the check doing something worse than nothing: CI already
rebuilt the storybook and published the fresh output, so a red check failed
the build over files the same run then discarded. It cost the 2.4.1 release and
a red development before it went, and its line-ending sensitivity had cost 36
consecutive Pages deploys in August. tools/source-digest.mjs went with it.
The rule it leaves behind is the general one, stated once: a derived file is
not committed. If something under docs/ starts being generated, gitignore it
rather than writing a checker to notice when it rots.
Where a captured component depends on something the docs build cannot carry (a
Strapi client, an HTML sanitiser), do not stub it. docs/storybook/ports/
holds the interface and a docs-build adapter, wired through the PORTS table in
build-storybook.mjs, and npm run check:ports typechecks each adapter against
its interface in CI. A stub fails as an undefined in a reader's browser; an
adapter fails the build. The bar is in ports.d.ts: if swapping the
implementation would change what the specimen looks like, it is not a port.
An adapter substitutes the environment, never the behaviour. SanitizerPort
is real dompurify — pinned to the version codecave.pro/pnpm-lock.yaml
resolves for isomorphic-dompurify, and build-storybook.mjs fails the build
if the two drift — not a pass-through; the wrapper's jsdom half is what gets
dropped, because a docs page is only ever a browser. It shipped as an identity
function for a day and that was the wrong call: it made the one page people
visit to check sanitising the one page not doing it. Where an adapter truly
cannot reach production, the specimen must say so on its face, not in a
comment. No port is in that position today.
A port exists because a captured component needs it, and every build says
which ones a specimen actually reached for. check:ports typechecks every
adapter whether or not anything imports it, so on its own a green check reads
as coverage of something that may be running for nothing — which is exactly
what StrapiPort became once the CMS-shaped components took an injected
resolveImage(). So build-storybook.mjs names the specimens each port stood
in for, and names any port nothing reached for at all.
Related rule: nothing under src/captured/ is authored — which,
since the components moved to src/components/, is a claim about eight files
rather than a rule spanning the whole component tree.
packages/brand/ is a pure derivative of src/ — it copies
src/styles/colors_and_type.css and src/styles/fonts/fonts.css byte-for-byte, copies
the root LICENSE, copies the components out of src/components/ and
src/captured/, extracts tokens.css from colors_and_type.css and
theme.css from src/styles/theme.css, and compiles src/tokens/*.ts. No
design content under packages/ is authored; the only tracked files are its
manifest, build script and README.md, and npm run check asserts the
byte-identity of every copy. src/ is the single origin; edit there, never in
packages/ and never in docs/.
The package ships no brand marks, and that is now a decision rather than an
accident. logo.svg was only ever in the tarball because a menu imported it,
and it left with the menus in 2.0.1. BrandNav.vue takes the wordmark as a
logo prop — a URL the consumer resolves through its own asset pipeline — so
the bar can be shared without the package distributing the mark. Same shape as
the font binaries, which stay out for a different reason (redistributing
third-party type), and the two answers agree about what "the brand kit ships":
the system, not the assets.
src/components/ is the root anyone writes into, and as of 2026-08-25 it holds
every component. It began as a home for BrandNav.vue alone, which has no
upstream to be captured from — it is the site bar and the docs bar reconciled, so
neither repo owns it. Everything else joined it once the same became true of them:
37 files with no upstream left, sitting in a directory whose name said they were
copies of something. src/captured/ keeps the eight that really are.
Two directories rather than a flag, because the rule that nothing under
src/captured/ is authored stops being checkable the moment the two sit side
by side. The directory name is the claim, and a path present in both roots
fails the build rather than letting whichever walked last win.
Two things a shipped component may reach for are checked at build time, and
both were added after one got out. assertDistResolves() asks whether every
import and url() in dist/ resolves inside dist/ — that is CCWEB2-370, the
checkbox tick. assertTokensSuffice() asks the same question of CSS: every
var(--x) a shipped component reads must be declared by tokens.css,
theme.css or the component itself. --duration-control was the one that was
not — codecave.pro declared it privately in its own :root, so Checkbox and
Radio transitioned correctly here and on the site and instantly for everybody
else. A property the site happens to declare is not a property the package
has, and the only way to tell the difference is to ask the tarball.
Specifiers are rewritten on the way in, and they are all the same problem. A
capture names a file the package already ships, but spells it in a form that
only resolves from outside the package — so copied verbatim it produces a
tarball that looks complete and breaks in a consumer's build. The whole rule is
the table in tools/import-aliases.mjs, which is also read by
build-storybook.mjs when it compares the package's copy of a component against
the capture it came from: an identical-bytes test there would mean the package
was not built from that capture. Rewritten captures "match their origin once
@helpers is resolved" rather than byte-for-byte, and check says which count
is which.
- codecave.pro's path aliases —
@assets/,@components/,@helpers/,@lib/. tsconfigpathsentries in that repo, one per top-level directory undersrc/; this package ships no tsconfig and a consumer has never heard of them.@helperswas silently both un-followed and mistaken for an npm package, which would have dropped every helper out of the tarball while the build stayed green (CCWEB2-355). @layouts/and@styles/— in the same table with a null target, which is not an omission. The package deliberately ships neither, so a shipped file reaching one is a bug rather than a missing rewrite; the entry is what makes the build say so. Without it@styles/global.cssfell through as an unrecognised bare specifier and was reported as an undeclared npm peer named@styles— a real fault under a name that sends the reader topackage.jsonto declare a package that does not exist.@codecavepro/brand/...— the package's own name. The site installs this package, so any component that renders a Button imports it that way. Node would resolve a self-reference through theexportsmap, which is what makes it worse than a plain error: it would work here, work in a consumer that installed a matching version, and demand@codecavepro/brandas its own peerDependency. Only the four subpaths that map intodist/src/are rewritten; anything else is left alone soassertNoSelfImport()catches it by name rather than a wrong path resolving to nothing.
Adding one is not routine, even though the table makes it look it. The bar
every rewritable entry clears: the alias must name a directory the package
ships, so the rewrite is pure path arithmetic with no judgement in it.
assertDistResolves() is the backstop and asks the built files, not the
captures — every import in dist/ must resolve inside dist/. Three details
were each a silent failure first, and are worth knowing before touching this:
assertNoSelfImport()andassertPeersDeclared()readshippedText(), the post-rewrite bytes, not the capture. Judging a specifier the rewrite removes is how both fired on a spelling that never reaches the tarball.- Both skip anything
isAlias()recognises — not merely anything with a target — which is the@stylescase above. usesAlias()is a specifier-position regex, not a substring test. A comment mentioning@helpers/service-links.tsin prose was once enough to fail the build for carrying an unrewritten alias: it failed on its own documentation, the same shape as a Tailwind utility named in prose.
A url() in a <style> block is never aliased, on either side. It already
resolves in the shipped layout, and unalias() only understands quoted
specifiers — so aliasing one would break the tarball rather than the site build.
codecave.pro's tsconfig.json says so where it declares the aliases;
Checkbox.vue's tick is the reason (CCWEB2-370).
theme.css is the half that makes the components visible, and its import order
is load-bearing. tokens.css carries the token values; only an @theme entry
makes .bg-surface-primary exist, so a consumer needs both. Several @theme
entries are deliberate self-references (--x: var(--x)) — never "fix" one on
sight. The declaration is what makes Tailwind emit the utility; the value comes
from tokens.css, whose unlayered :root beats @layer theme. Behind a layered
or absent tokens.css the same line is a cycle and resolves to nothing. Where the
two files both give a name a literal value they must agree, and the build fails
if they do not — otherwise one is dead and nothing shows it. --font-sans is the
one excused difference, and the exception carries its reason in build.mjs.
--breakpoint-sm must stay a literal: Tailwind bakes it into @media (width >= 457px) at build time, where a var() would be invalid CSS.
The component list is computed, never written down. build.mjs takes every
non-excluded .vue under either root as a root and follows everything it
reaches transitively — in the layout the package ships, and that layout is
now a mirror of the source: dist/src/common/Button.vue for
src/components/common/Button.vue, one file to one file. A reference that lands
outside the package fails the build and names itself.
It ships as a mirror because it did not, and that cost a day. dist/src/
used to re-insert codecave.pro's src/components/ level, so a component under
src/components/common/ imported ../../assets/… — a path correct only after
the build re-rooted it. The sources were therefore unimportable by anything
without that resolver: Vite could not resolve them, and astro dev reported
them as missing npm packages. Mirroring makes ../assets/… right in both
places, and exports absorbs the change — ./components/* maps to ./dist/src/*,
so @codecavepro/brand/components/common/Button.vue still resolves for a
consumer. components is a name in the export map now, not a directory. So a component is added by capturing it, and the only
hand-written thing is NOT_SHIPPED, where each exclusion carries its reason as
a string.
"Reaches" means imports and url() targets in a <style> block, and it did
not always. Following imports alone shipped logo.svg, because a menu happens
to import it, while Checkbox.vue's tick — a background-image — was never
an edge at all: the icon stayed out of the tarball, and every check agreed,
because assertDistResolves() read the built files with the same two import
patterns and so was blind in exactly the same place. It surfaced as a 404 and a
checkbox that would not tick. Assets are not a category the build knows; a file
ships because something reaches it, so a new spelling of "reaches" has to be
taught to referencesOf(), which both the walk and the backstop now call.
What a component imports from npm is checked against the manifest. The
relative-import walk proves every ../ resolves inside the package; it proves
nothing about import { Carousel } from 'vue3-carousel'. An undeclared one
resolves here anyway — this repo declares every one of them as a devDependency
so the storybook can compile — and fails in the consumer's build, the one place
nobody would look. So build.mjs
collects every bare specifier the shipped files import and fails unless
peerDependencies names exactly that set, in both directions: an import
nothing declares, and a declared peer nothing imports any more.
The storybook compiles from the package where the package has the component and
from the captures where it does not, and says which on every build. A
specimen quietly reverting to the captures is exactly the drift this
arrangement prevents, and the build log is the only place anyone would see it.
A component's identity — its scoped-style id, its __file — is its capture
path whichever root supplied the bytes, so promoting one (or removing it again)
does not rewrite every data-v- attribute in its bundle.
The LICENSE copy is not housekeeping: npm picks a licence up only from the
package root and files: ["dist"] neither includes nor excludes it, so the
tarball shipped no licence text at all while package.json claimed
"Unlicense". Copying it beats authoring a second one for the same reason as
the palette. Publishing the result is RELEASING.md.
Publishing is a pushed tag, and nothing else. git push origin 2.2.0 runs
.github/workflows/release.yml, which verifies
and then publishes as an npm trusted publisher — OIDC, so there is no token
in this repository, no NPM_TOKEN secret and no OTP. Two consequences worth
holding: the workflow's FILENAME is part of the credential (npmjs.com matches
org + repo + release.yml, so renaming the file revokes publishing and npm
reports it as a 404 on the PUT, which reads like a missing package), and the
tag IS the version.
packages/brand/package.json is committed as 0.0.0 and stays there.
tools/stamp-version.mjs writes the tag into it during the release — as its own
workflow step after npm ci, and again in release:package — so there is no
second place to bump and nothing to forget. It is not a prepack hook,
which is where it belongs by shape and is wrong by behaviour: npm resolves the
tarball's FILENAME from the manifest before prepack runs and packs the manifest
as prepack left it, so a prepack stamp yields codecavepro-brand-0.0.0.tgz
whose package.json says 2.3.0. Which of the two the registry would believe
is not a thing to learn by burning a version number. The script carries the
measurement.
This replaced an assertion that the tag equalled the manifest, which a human
kept true with a hand-run npm version. It could only ever be a tripwire on a
step people forget, and 2.3.0 is where it caught one — tag 2.3.0, manifest
2.2.0, release refused. A manifest at 0.0.0 also means a local build is
unpublishable by construction, which is the right default for the one command
that cannot be undone.
The workflow reruns every check plus one that only makes sense against the
artifact: tools/smoke-tarball.mjs installs the packed tarball and asks it
eighteen questions — the last being that the stamped version reached the
artifact. That is not duplication of npm run check — files,
exports and npm's by-name pickup of LICENSE/README.md are all invisible to a
check that walks dist/ in place, so a package can be correct on disk and broken
for everyone who installs it. Its reference walk mirrors referencesOf() in
build.mjs by copy; keep the two in step.
The release has no capture gate, deliberately. It briefly did; see the note
where that step used to be in release.yml. A release is the moment this
repository is furthest ahead of the site, so a check requiring the two to match
would fail every real release.
The README is authored because npm renders it as the package page and there is
nowhere else for that page to come from — it is manifest-adjacent, the same
category as package.json. It explains how to install and consume, and links
rather than restates: the rules live in DESIGN.md.
Its usage example does quote real token values, which is a second home for
them, so they are asserted, not trusted. Any README line shaped
group.key; // 'value' is checked against the freshly compiled module by
build.mjs; a palette change that leaves the README behind fails the build, and
prepack runs it, so a stale value cannot be published. Prose comments are
ignored — verified by corrupting a value and watching it exit 1. Add values to
the example freely in that shape; do not write one in a form the check
cannot see. The same treatment covers the two counts the README quotes as
prose — the :root property count and the components-and-icons count — because
those are facts about what the build produced, not claims worth trusting. The registry is npm public — this repo is already a public repo,
so nothing private was being protected.
CCWEB2 is the website project and these are brand-package items — they sit there
because no brand project exists yet. The brand-kit label is what makes them
movable in bulk when one does. Print colour space / CMYK is deliberately not
filed anywhere: it is Maria Shaban's decision and gets its own project later.
WEBSITE-REVIEW.md is the component and token review,
mirrored as CCWEB2-270…310. Its findings are mostly OURS now — it was
written when the components were the site's, and they now live in
src/components/ and ship in the package, so a component remark is this
repository's to fix and reaches the site at its next bump. Only remarks about
files the site still owns are the site's.
Its section numbers must not be renumbered. They are cited by number from
the shipped src/styles/colors_and_type.css and from DESIGN.md; the list
starting at section 2 is deliberate, because section 1's only finding was
fixed and deleted.
These are the foundational rules and assets that dictate how every other template is built. [1]
- Logo System: Primary logo, secondary logo, submarks, favicon, and monochrome versions.
- Color Palette: Exact HEX and RGB codes divided into primary, secondary, and accent colors. (Print colour space — CMYK, and whether a spot colour is used — is deferred to Maria Shaban, and is tracked outside this file.)
- Typography Hierarchy: Specific brand fonts, scale sizes, and line-height instructions for headers, subheaders, and body text.
- Brand Style Guide: A PDF manual outlining do's and don'ts, spacing rules, and usage examples. [1, 2, 3, 4]
These assets turn your daily digital touchpoints into brand-building opportunities. [1]
- Email Templates: Newsletter layouts, transactional email designs, and automated welcome sequence graphics.
- Email Signatures: Standardized layouts for all employees featuring consistent fonts, logos, and social links.
- Presentation Slides: Keynote, PowerPoint, or Google Slides templates containing master slides for data, quotes, and title pages.
- Landing Page Blueprints: Wireframes or built-out page templates featuring pre-styled hero sections, buttons, and testimonials. [1, 2, 3, 4]
These tools keep your backend operations looking as professional as your frontend marketing. [1]
- Forms & Surveys: Branded Google Forms, Typeform layouts, or PDF applications with custom headers and field styling.
- Invoices & Receipts: Clean templates for billing that include your logo, brand colors, and official typography.
- Contracts & Proposals: Standardized word-processing templates (Word or Google Docs) with branded cover pages, headers, and footers. [1, 2, 3]
These ready-to-use graphics help teams create content rapidly without losing visual cohesion. [1]
- Profile Kits: Coordinated profile pictures and banner graphics for LinkedIn, X, YouTube, and Meta platforms.
- Post Templates: Editable Canva or Adobe files for standard static posts, Instagram Stories, and carousel layouts.
- Video Assets: Branded intro/outro animations, lower-third graphic overlays for names, and watermarks. [1, 2, 3]