Author SHA1 Message Date
Senrokai 83375f1ae4 Merge pull request #10 from avalon-vanguard/develop
Link the live docs site
2026-08-05 17:35:32 +02:00
Claude fd42675d3d 🔗 docs: link the live site, now that Pages is confirmed
Pages serves docs/ from main, so the page rebuilt in #6 is live at
avalon-vanguard.github.io/cereale. The README pointed at the local file
because the URL could not be verified from here; it now links the site and
keeps the local instructions as the fallback. package.json homepage moves
there too — npm renders it as the package's headline link, and a live
playground is a better landing spot than an anchor inside the README.

Adds canonical and Open Graph tags. No og:image: a preview card with a
broken image is worse than one without, and there is no artwork yet.

check-docs.mjs flagged the canonical link as a remote subresource, which it
is not — the browser never fetches it. Rather than exempt the URL, the
check now looks at rel and only flags the relations that actually fetch or
connect. Verified it still catches a CDN stylesheet, a preconnect and a
script src; a check that cannot tell a declaration from a request is one
that gets switched off the first time it is wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 15:33:55 +00:00
Senrokai e3873dcdd3 Merge pull request #9 from avalon-vanguard/develop
Serve the docs folder verbatim on GitHub Pages
2026-08-05 17:16:14 +02:00
Claude e626a1003a 🔧 fix: generate .nojekyll instead of hand-editing generated files
The previous commit appended the .nojekyll rationale to
docs/vendor/README.md — a file whose own first line reads "Generated by
npm run build:docs. Do not edit by hand." CI's "Landing page bundle is in
sync with src/" step regenerated it, the text vanished, git diff was
non-empty and all three Node jobs failed.

The guard did its job; I was the one who put a hand-written paragraph in a
generated file. The note now lives in the generator, and build:docs writes
docs/.nojekyll itself so it is part of the generated set rather than a
loose file that a docs/ rewrite could drop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 15:14:20 +00:00
Claude 364f44f4bf 📄 chore: serve the docs folder verbatim on GitHub Pages
Pages runs Jekyll by default. Jekyll ignores paths beginning with an
underscore and carries default `vendor/` exclusions, neither of which suits
a hand-built page — and the failure mode is an asset that silently does not
publish, which for this page means the playground's compiler 404s and the
Run button dies exactly the way the old CDN-based one did.

`.nojekyll` opts out, so what is in docs/ is what gets served.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 15:11:48 +00:00
Senrokai 2dcc8d74d0 Merge pull request #8 from avalon-vanguard/develop
Fix tag fetching in the release workflow
2026-08-05 17:06:48 +02:00
Claude 8bd770e343 🔧 ci: fetch tags in the release workflow
The version gate resolves `v$VERSION` with git, but actions/checkout does
not fetch tags at its default depth — so the guard would have failed every
release with "No tag v0.3.0" whether or not the tag existed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 15:05:36 +00:00
Senrokai b5b5576439 Merge pull request #7 from avalon-vanguard/develop
Release 0.3.0
2026-08-05 17:04:52 +02:00
Claude 9796d599dc 📦 chore: make the published package actually complete
`files` was ["dist"], but dist carries 256KB of .js.map and .d.ts.map
whose `sources` point at ../../src/*.ts — which was not published. Every
shipped sourcemap resolved to nothing: 44% of the tarball, dead weight.

The source is 116KB and its comments are the most detailed explanation of
why the engine does what it does, so it now ships (tests and the demo
excluded) and the maps resolve. Verified from a real `npm pack` install:
both utils.js.map and utils.d.ts.map now resolve to a file that exists, so
stepping into cereale in a debugger and "go to definition" from a decorator
both land in the real TypeScript.

Also: CHANGELOG.md ships; the repository/homepage/bugs URLs said
Avalon-Vanguard and only worked via GitHub's redirect, now lowercase to
match the org; publishConfig.access is explicit so a later move to a scoped
name cannot quietly attempt a private publish.

Adds a Publish to npm workflow, deliberately manual — pushing a tag does
not publish, because a tag is a decision to cut a release and publishing is
a decision to make it public and immutable. It asserts the tag exists and
points at the commit being published, refuses a version already on the
registry, runs the full verify gate, prints the file list, and defaults to
a dry run.

Checked end to end against the tarball: a strict consumer (no skipLibCheck,
no DOM lib) compiles and runs against both `cereale` and `cereale/vite`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-05 15:03:28 +00:00
Senrokai 7aaef4d388 Merge pull request #6 from avalon-vanguard/claude/library-development-i46yqm
0.3.0 — make the silent failures loud, and rebuild the landing page
2026-08-05 16:48:03 +02:00
Senrokai 551d53912f Merge pull request #5 from avalon-vanguard/develop
Release 0.2.0 to main

Brings main up to date. It had been sitting at the initial commit, so anything
installed from main was the pre-repair code: a test suite that never executed
and every defect fixed in #1 still present.

The project stays on 0.x because nothing has been published to npm. Under semver
that says what is true - the API may still move - where a 1.0.0/2.0.0 split
would have claimed a stability and a history that do not exist. A breaking change
is therefore a minor bump, which is exactly the relationship between the lines:

  develop / main   0.2.0   TC39 standard decorators, rules type-checked
  0.1.x            0.1.0   legacy experimentalDecorators, for oxc toolchains

What lands, across four merged PRs:

- The test suite had never run. Vitest 4 transpiles with oxc, which does not read
  experimentalDecorators from a tsconfig excluding the files it transforms, so
  every suite failed to parse and was reported as "0 test". Reviving it exposed
  eight engine defects, each now pinned by a regression test - among them
  inheritance silently discarding base-class rules, and a circular reference
  exhausting an 8 GB heap.
- Field-name mapping, access control, transform options, error flattening, and
  30 validation decorators.
- Performance: profiling showed roughly half of validation time re-deriving
  answers that cannot change while the predicates themselves were under 1%.
  Per-class plans are memoized and recursion is bounded.
- A synchronous API, built by making the engines sync internally rather than
  duplicating the traversal, which sped up the async path as well.
- Standard decorators, so a rule that does not fit its field is a compile error.

  Tests actually executing      0 -> 193
  validate (50 orders)     221.6us -> 17.8us
  toInstance (50 orders)   255.1us -> 31.7us

Clean fast-forward, no conflicts. Nothing is tagged or published.
2026-08-05 10:14:11 +02:00
9 changed files with 170 additions and 9 deletions
+93
View File
@@ -0,0 +1,93 @@
name: Publish to npm
# Deliberately manual. Pushing a tag does NOT publish — a tag is a decision to cut a release,
# not a decision to make it public and immutable, and npm's 72-hour unpublish window makes the
# second one hard to take back. Run this workflow from the Actions tab when you mean it.
#
# Before the first real run:
# 1. Create an npm automation token and add it as the NPM_TOKEN repository secret.
# 2. Run once with dry_run left as `true` and read the file list it prints.
# 3. Run again with dry_run set to `false`.
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Resolve and pack everything, but do not publish'
type: boolean
default: true
permissions:
contents: read
id-token: write # required for npm provenance
jobs:
publish:
name: ${{ inputs.dry_run && 'Dry run' || 'Publish' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Tags are not fetched at the default depth, and the version gate below
# resolves one — without this it fails on every release, tag or no tag.
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
# The tag and the manifest disagreeing is the classic way to publish 0.3.0 as 0.2.0.
- name: Tag and package.json version must agree
run: |
VERSION=$(node -p "require('./package.json').version")
echo "package.json version: $VERSION"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "tag v$VERSION exists"
else
echo "::error::No tag v$VERSION. Tag the release commit before publishing."
exit 1
fi
if [ "$(git rev-parse HEAD)" != "$(git rev-parse "v$VERSION^{commit}")" ]; then
echo "::error::v$VERSION does not point at the commit being published."
exit 1
fi
- name: Refuse to republish a version already on the registry
run: |
VERSION=$(node -p "require('./package.json').version")
NAME=$(node -p "require('./package.json').name")
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
echo "::error::$NAME@$VERSION is already published. Bump the version."
exit 1
fi
echo "$NAME@$VERSION is not on the registry yet."
# The same gate that guards every push: type-check, lint, 259 tests, build, and the
# checks that the published types stand alone and the landing page has no CDN deps.
- name: Verify
run: npm run verify
- name: Show exactly what would ship
run: npm publish --dry-run
- name: Publish
if: ${{ inputs.dry_run == false }}
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Summary
run: |
VERSION=$(node -p "require('./package.json').version")
{
echo "### cereale@$VERSION"
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "Dry run — nothing was published."
else
echo "Published to https://www.npmjs.com/package/cereale/v/$VERSION"
fi
} >> "$GITHUB_STEP_SUMMARY"
+19
View File
@@ -166,6 +166,25 @@ Serialization is a few percent slower for the representability check. Primitives
inline, and arrays and dates skip it, so it costs one `Symbol.toStringTag` read per object.
Validation is unchanged.
### Packaging
`files` was `["dist"]`, but `dist` carries 256 KB of `.js.map` and `.d.ts.map` files whose
`sources` point at `../../src/*.ts` — which was not published. Every shipped sourcemap
resolved to nothing: 44% of the tarball, dead. The source is only 116 KB and its comments are
the most detailed explanation of why the engine does what it does, so it is now published
(tests and the demo excluded) and the maps resolve. Stepping into cereale in a debugger, and
"go to definition" from a decorator, both land in the real TypeScript.
`CHANGELOG.md` ships too. The `repository`, `homepage` and `bugs` URLs said `Avalon-Vanguard`
and only worked through GitHub's redirect; they now use the org's actual lowercase name.
`publishConfig.access` is set explicitly so a future move to a scoped name cannot quietly
attempt a private publish.
A `Publish to npm` workflow is in place but deliberately manual — pushing a tag does not
publish. It checks that the tag exists and points at the commit being published, refuses a
version already on the registry, runs the full `verify` gate, prints the file list, and
defaults to a dry run. Publishing needs an `NPM_TOKEN` secret and someone choosing to run it.
## [0.2.0] - 2026-08-04
> The project stays on 0.x while nothing has been published: under semver that signals the
+4 -3
View File
@@ -25,9 +25,10 @@ const user = fromJsonSync(User, body); // a real User
user.greet(); // your methods are still there
```
`docs/index.html` is a self-contained page with an interactive playground that runs this
library in the browser. Build its assets with `npm run build:docs` and open the file — it
loads nothing from the network.
**[avalon-vanguard.github.io/cereale](https://avalon-vanguard.github.io/cereale/)** — an
interactive playground that runs this library in your browser, the full decorator reference,
and the toolchain matrix. The page is self-contained and loads nothing from the network; it is
served from `docs/` on `main`, and `npm run build:docs` rebuilds its assets to open locally.
## Where it fits
View File
+12
View File
@@ -6,8 +6,20 @@
<title>cereale — validated domain objects, not validated data</title>
<meta name="description" content="Zero-dependency JSON mapping and validation for TypeScript classes. Maps JSON onto your own classes and checks the validation rules against the fields they are attached to, at compile time.">
<meta name="color-scheme" content="light dark">
<link rel="canonical" href="https://avalon-vanguard.github.io/cereale/">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌾</text></svg>">
<!-- Link previews. No og:image: a preview card with a broken image is worse than one
without, and there is no artwork to point at yet. -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://avalon-vanguard.github.io/cereale/">
<meta property="og:site_name" content="cereale">
<meta property="og:title" content="cereale — validated domain objects, not validated data">
<meta property="og:description" content="Zero-dependency JSON mapping and validation for TypeScript classes. Maps JSON onto your own classes and checks every rule against the field it is attached to, at compile time.">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="cereale — validated domain objects, not validated data">
<meta name="twitter:description" content="Zero-dependency JSON mapping and validation for TypeScript classes. Rules are checked against fields at compile time.">
<style>
/* ---------------------------------------------------------------- tokens */
:root {
+2
View File
@@ -3,3 +3,5 @@
Generated by `npm run build:docs`. Do not edit by hand.
- `babel.min.js` — @babel/standalone 8.0.4, used by the playground to compile TypeScript with standard decorators in the browser. Vendored rather than loaded from a CDN: the previous page referenced an unpinned unpkg URL, which silently began serving Babel 8 and broke the playground.
- `../.nojekyll` — opts the directory out of Jekyll, so GitHub Pages serves it verbatim. Jekyll ignores paths beginning with an underscore and carries default `vendor/` exclusions, and the failure mode is an asset that silently does not publish — for this page, the playground's compiler 404ing while everything else looks fine.
+11 -4
View File
@@ -23,7 +23,11 @@
"./dist/cjs/metadata.js"
],
"files": [
"dist"
"dist",
"src",
"CHANGELOG.md",
"!src/**/*.test.ts",
"!src/example.ts"
],
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
@@ -45,7 +49,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/Avalon-Vanguard/cereale.git"
"url": "git+https://github.com/avalon-vanguard/cereale.git"
},
"keywords": [
"json",
@@ -62,9 +66,9 @@
"author": "Avalon Vanguard",
"license": "MIT",
"bugs": {
"url": "https://github.com/Avalon-Vanguard/cereale/issues"
"url": "https://github.com/avalon-vanguard/cereale/issues"
},
"homepage": "https://github.com/Avalon-Vanguard/cereale#readme",
"homepage": "https://avalon-vanguard.github.io/cereale/",
"devDependencies": {
"@babel/standalone": "^8.0.4",
"@eslint/js": "^10.0.1",
@@ -78,5 +82,8 @@
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.2",
"vitest": "^4.1.4"
},
"publishConfig": {
"access": "public"
}
}
+8 -1
View File
@@ -73,9 +73,16 @@ await writeFile(
`- \`babel.min.js\` — @babel/standalone ${JSON.parse(await readFile(path.join(root, 'node_modules/@babel/standalone/package.json'), 'utf8')).version}, ` +
`used by the playground to compile TypeScript with standard decorators in the browser. ` +
`Vendored rather than loaded from a CDN: the previous page referenced an unpinned unpkg URL, ` +
`which silently began serving Babel 8 and broke the playground.\n`
`which silently began serving Babel 8 and broke the playground.\n\n` +
`- \`../.nojekyll\` — opts the directory out of Jekyll, so GitHub Pages serves it verbatim. ` +
`Jekyll ignores paths beginning with an underscore and carries default \`vendor/\` exclusions, ` +
`and the failure mode is an asset that silently does not publish — for this page, the ` +
`playground's compiler 404ing while everything else looks fine.\n`
);
// 5. Written rather than committed by hand so it cannot be lost in a docs/ rewrite.
await writeFile(path.join(docs, '.nojekyll'), '');
console.log(`docs/cereale.js ${await size(path.join(docs, 'cereale.js'))}`);
console.log(`docs/meta.js ${await size(path.join(docs, 'meta.js'))} (v${pkg.version})`);
console.log(`docs/vendor/babel.min.js ${await size(path.join(vendor, 'babel.min.js'))}`);
+21 -1
View File
@@ -22,12 +22,23 @@ const failures = [];
/** Subresource references — the things a browser fetches without being clicked. */
const SUBRESOURCES = [
[/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi, 'script src'],
[/<link\b[^>]*\bhref\s*=\s*["']([^"']+)["']/gi, 'link href'],
[/<(?:img|iframe|video|audio|source|embed)\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi, 'media src'],
[/@import\s+(?:url\()?["']([^"']+)["']/gi, 'css @import'],
[/url\(\s*["']?(https?:\/\/[^)"']+)/gi, 'css url()'],
];
/**
* `<link>` relations the browser actually fetches or connects to.
*
* Checked against `rel` rather than flagging every `<link href>`, because the metadata
* relations — `canonical` above all — are declarations about the document, not requests. A
* check that cannot tell the difference gets switched off the first time it is wrong.
*/
const FETCHING_REL = new Set([
'stylesheet', 'icon', 'shortcut icon', 'apple-touch-icon', 'apple-touch-icon-precomposed',
'manifest', 'preload', 'modulepreload', 'prefetch', 'prerender', 'preconnect', 'dns-prefetch',
]);
const isRemote = (url) => /^(?:https?:)?\/\//i.test(url);
const html = (await readdir(docs)).filter((name) => name.endsWith('.html'));
@@ -40,6 +51,15 @@ for (const name of html) {
if (isRemote(match[1])) failures.push(`docs/${name}: remote ${kind} — ${match[1]}`);
}
}
for (const match of source.matchAll(/<link\b([^>]*)>/gi)) {
const attrs = match[1];
const rel = (/\brel\s*=\s*["']([^"']+)["']/i.exec(attrs)?.[1] ?? '').trim().toLowerCase();
const href = /\bhref\s*=\s*["']([^"']+)["']/i.exec(attrs)?.[1];
if (href && isRemote(href) && FETCHING_REL.has(rel)) {
failures.push(`docs/${name}: remote link rel="${rel}" — ${href}`);
}
}
// A fetch to a CDN would not be caught by the markup scan.
for (const match of source.matchAll(/\b(?:fetch|importScripts)\(\s*["'`](https?:\/\/[^"'`]+)/gi)) {
failures.push(`docs/${name}: remote fetch — ${match[1]}`);