Skip to content

feat: harden NebulaKit and merge CMS v2 - #6

Merged
davis9001 merged 39 commits into
starspacegroup:mainfrom
donaldfilimon:cursor/nebulakit-quality-pass-20260808
Aug 18, 2026
Merged

davis9001 merged 39 commits into
starspacegroup:mainfrom
donaldfilimon:cursor/nebulakit-quality-pass-20260808

Conversation

@donaldfilimon

Copy link
Copy Markdown
Contributor

Summary

  • merge CMS v2 full-page editor, typed embeds, publish locks, and write-path hardening
  • replace client-authored sessions with opaque revocable D1 sessions and one-time OAuth transactions
  • enforce owner/admin, CMS privacy/sanitization, PII, WebMCP, model, Turnstile, and voice boundaries
  • retire template customization workflow and align product docs, PWA assets, CI, migrations, and agent guidance

Verification

  • bun run check: 0 errors/warnings
  • bun run test:coverage: 2,128 passed, 22 skipped; 97.86% statements, 95.13% branches, 97.96% functions, 98.42% lines
  • bun run db:migrate:local: no pending migrations
  • bun run test:e2e: 8/8 passed
  • bun run validate:contrast: passed
  • bun run build:ci: passed
  • touched-file Prettier and git diff --check: passed

External blockers

  • production binding validation/build intentionally fails until project-owned D1/KV IDs replace placeholders
  • HawkScan evidence unavailable because this host has neither Hawk/Docker nor an API key

Direct push to upstream main was denied because the authenticated account has read-only repository permission; this fork PR contains the exact verified local merge.

davis9001 and others added 20 commits July 12, 2026 07:13
…rdening

Complete the davis9001.dev CMS upstreaming into the kit and improve on it.

- Write-path hardening: add cms/sanitize.ts (xss) and run sanitizeRichtextFields
  on POST and PUT before storage.
- Public rendering: add CmsContent.svelte and wire it into the public content
  route so embeds mount on live pages.
- R2 image field: add ImageField.svelte (upload-to-R2) and use it for image
  fields in the admin editor.
- Embed system redesign (roadmap "make it better"):
  - Typed props schema (props-schema.ts) with an auto-generated editor form,
    replacing raw-JSON prop editing (JSON kept as a fallback).
  - One-step registration: manifest/index auto-discover embeds via
    import.meta.glob — drop a folder under embeds/<name>/ (definition.ts + a
    .svelte), no central list.
  - SSR embeds via an eager component registry.
- Ship a brand-neutral Callout reference embed (Dirac physics embeds
  intentionally left downstream). Docs: docs/CMS_EMBEDS.md.

Tests: +71 passing (sanitize, embed codec, upload, typed props, registry,
write-path). New CMS .ts code at 100% coverage. Typecheck and Cloudflare build
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMGQ6kfKSvErz11TUfhKiX
Add server.allowedHosts: ['.starspace.group'] so Cloudflare dev tunnels
(dev-nebulakit-<hash>.starspace.group) aren't blocked by Vite's host check.
Dev-server only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMGQ6kfKSvErz11TUfhKiX
Bring davis9001's dedicated CMS editor into the kit and extend it so both
creating and editing an item happen on their own full page (no modals).

- Add CmsItemEditor.svelte: a shared two-column editor (content fields +
  sticky sidebar for status, command-palette visibility, SEO, save). Handles
  create (POST) and edit (PUT). Stripped of davis9001 app-specifics
  (predictions, field locking, timestamp-proof, tasklist).
- Add routes: /admin/cms/[type]/new (create) and /admin/cms/[type]/[id]
  (edit), each with a clean server loader. `new` beats `[id]` in routing.
- Slim the list page to list-only: New button and row Edit are now links to
  the dedicated pages; removed the create/edit modal and its handlers. Keeps
  filters, pagination, tag manager, and delete confirmation.
- GET /api/cms/[type]/[id] now attaches the item's tags (best-effort) so the
  editor round-trips tag selections.
- Fix the item-count bug: the list read `totalItems` but the API returns
  `total`, so the count always showed 0.

Tests: add loader tests for both new routes and the GET tag-attach paths;
fix the list-loader test mock to use the real `total` key. Full page flow
verified end to end (list → new → create → edit) via a dev tunnel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMGQ6kfKSvErz11TUfhKiX
Decided behavior: logout keeps the user on public pages, goes to
/auth/login with a return-to reference on protected ones, and re-login
returns to the original page. Marked not-yet-implemented (logout
currently always redirects to /auth/login); return-to must be validated
as a same-origin relative path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design doc + ROADMAP wiring for bringing Stripe into the kit with
purchasing-power-parity region pricing on by default. Learned from the
AgapeVerse rewrite: PPP rides on Stripe currency_options of existing
prices (no new price IDs, no migration, entitlement/webhook logic
untouched). ROADMAP's Stripe entry had omitted PPP entirely; now points
at the design doc and lists region-pricing.ts / setup-region-prices.ts /
fx.ts to lift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018yhkXsEDzWYBgfSHwQazu7
… cached

Cloudflare-proxied dev tunnels stamp a browser-cache TTL on query-less dev
module URLs (.svelte-kit/generated/client/nodes/*.js). After routes change
those nodes renumber, so a browser holding stale copies renders the wrong
page. Set Cache-Control: no-store on the dev server to prevent it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018yhkXsEDzWYBgfSHwQazu7
Both branches had independently implemented CMS embeds, so this was a
re-integration rather than a mechanical merge: 11 conflicted files, ~1,900
conflict lines. Donald chose to favor cms-v2 where the two designs collide.
Resolved in an isolated worktree so conflict markers never entered the shared
working tree, which another session is actively using.

Favored cms-v2 (design conflicts):
- admin/cms/[type]/+page.svelte — the full-page create/edit editor replaces
  the item modals (786 conflict lines). Main's modal-side publish-lock and
  contentTypeName changes to that file are superseded.
- CmsContent.svelte, docs/CMS_EMBEDS.md, cms-embed.test.ts, cms-upload.test.ts
  — add/add conflicts, took the v2 implementation.
- The embed registry now ships the `callout` embed. Main asserted the registry
  ships empty "so a new project inherits no embeds it did not ask for"; that
  assertion is replaced. Its real guarantees (unregistered names resolve to
  undefined, and to null rather than undefined for components) are preserved,
  retargeted at a genuinely unregistered name.

Took the union where the sides were complementary, not competing:
- api/cms/[type]/[id]/+server.ts — main's requireAdmin guard and
  session-derived provenance actor, AND v2's write-time richtext
  sanitization. Taking either side wholesale would have dropped real
  hardening. Same for both files' import sets.
- vite.config.ts — main's config is a superset (staleDepsFix, preview port,
  HMR toggle, optimizeDeps); v2's only unique contribution was the
  .starspace.group tunnel host, which is now unioned into allowedHosts. The
  95% coverage thresholds block was outside the conflict and is unchanged.
- [contentType]/[slug]/+page.svelte — same design both sides; kept main's
  `?? ''` null-guards, which stop a literal "undefined" rendering.

One deliberate deviation from "favor cms-v2", called out rather than silent:
PUT with an unknown content type. The v2 branch let that write through with
its richtext UNSANITIZED; the merged route 404s first. Rejecting the write is
strictly safer than storing dirty HTML, and main's timestamp-proof path needs
the content type to exist anyway. The v2 test asserting the tolerant behavior
now asserts the 404.

Also fixed while merging: the resolution initially issued a second, shadowing
getContentTypeBySlug query in PUT — the handler already resolves the content
type above, so it now reuses it. CmsContent's `html` prop is typed
`string | null | undefined` to match its own `html || ''` runtime handling and
the nullable CMS field values callers pass.

Gate: svelte-check 0 errors, 1980 tests pass (0 fail), coverage 98.2% stmts /
95.1% branches / 98.14% funcs / 98.2% lines — above the 95 floor on all four.
NebulaKit is a product, not a starter template, and the repository still
carried the onboarding scaffolding from when it was one. That scaffolding is
now gone: CUSTOMIZE.md, INITIAL_CUSTOMIZATION_STATUS.md, SETUP_COMPLETE.md,
docs/INITIAL_CUSTOMIZATION.md, docs/ZERO_ENV_SETUP.md,
customize.config.example.json and scripts/customize.mjs are deleted, and the
docs that described a rebranding step no longer do. src/lib/site.config.ts
stays the single source of identity, and tests/unit/product-identity.test.ts
reads README, FEATURES, site.config, /documentation, app.html and the
manifest off disk so the surfaces that cannot import it fail loudly when they
drift instead of rotting silently.

Auth and OAuth hardening is the substantive half:

- Secrets are split by purpose. AUTH_SECRET (a leftover from @auth/sveltekit,
  which nothing imported) is replaced by SESSION_SECRET and SETUP_SECRET, so
  session signing and the one-time setup gate no longer share a key.
- Ownership is keyed on GITHUB_OWNER_ID, a numeric user id, instead of
  GITHUB_ADMIN_USERNAME. GitHub usernames are re-assignable; ids are not, so
  the old form let a renamed-and-reclaimed handle inherit owner rights.
- The unused Google provider is dropped rather than left half-wired.
- OAuth flow logic moves out of the route handlers into
  src/lib/utils/oauth-state.ts, src/lib/server/oauth-account.ts and
  oauth-finalization.ts, with src/lib/server/auth-guards.ts holding the
  authorization checks. Migrations 0010 and 0011 add the one-time OAuth
  transaction table and stop persisting provider access tokens we never read.
- src/lib/cms/sanitize.ts sanitizes richtext on the write path.

New suites cover the boundaries this touches rather than only the happy
paths: auth-hook-security, auth-security-routes, oauth-state-security,
oauth-account, oauth-finalization, logout-security, admin-users-pii-security,
cms-sanitization-security, security-boundaries, public-cms-discovery and
usage-agent-hooks.

Also adds LICENSE, the PWA icon set and site.webmanifest, and switches the
Copilot instructions from npm to bun to match the sole lockfile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cms-v2 merge brought in richtext-embed-extension.ts, but only its pure
helper (embedNodeToHtml) was exercised. The extension's stateful surface —
attribute parse/render round-trips, the insertSvelteEmbed command, and the
node view that wires the edit-props and remove buttons back into TipTap's
command chain — was untested, which is the part most likely to break silently
when the editor is refactored.

Adds coverage for that surface, including the fallback paths: an unknown
embed name renders the raw name with no description, and a node view with no
resolvable position must not dispatch a chain at all.

13 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SvelteEmbed node-view test destructured the result of
Array.from(dom.querySelectorAll('button')). `dom` comes back from an untyped
TipTap node view, so both elements widened to `unknown` and svelte-check
failed on the .click() calls. Vitest does not typecheck, so the suite passed
while `bun run check` did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatting only — no assertions, mocks or behavior changed. These three files
landed in the template-retirement commit without having been run through
Prettier; `bunx prettier --check` now passes on them. 8 tests still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Logs what the stash-apply resolution actually verified rather than asserting
it was fine: 34 conflicts resolved with no markers left, check clean,
test:coverage green at 2,149 passing (97.88% statements, 95.18% branches,
98.00% functions, 98.44% lines), no pending local migrations, 8 Chromium e2e
tests passing, contrast passing on both themes, and build:ci completing with
the expected placeholder-binding warning.

Also records the honest gaps and boundaries: HawkScan could not run (no hawk
runtime, no Docker, no API key), so DAST remains missing evidence rather than
a pass; and the note that another process advanced local main to 924a5ac and
9058278 during verification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md carried one factual drift and three gaps:

- Test topology described `poolOptions.threads.singleThread: true`; the
  config actually uses `pool: 'threads'` with `fileParallelism: false`.
  Same serial execution, different key — the `unstubGlobals` leak
  narrative was correct and is unchanged.
- Record that AGENTS.md requires reading tasks/goals.md and tasks/todo.md
  before editing, and that uncommitted changes are user-owned.
- Add a docs/ pointer table. hooks.server.ts cites ADMIN_STATS.md and
  AGENT_READINESS.md by path in source comments; the map was nowhere.
- Name src/lib/server/auth-guards.ts in the auth section, which AGENTS.md
  mandates reusing.

Also make the `§N` convention explicit: it indexes AGENTS.md's Release
Rules bullets, a convention vite.config.ts shares in a comment.

migrations/README.md's inventory omitted 0010_content_item_timestamp_proof
.sql, so it under-reported what is on disk. Two files share the `0010_`
prefix, meaning filename sort — not sequence number — decides which runs
first; CLAUDE.md now records that.

README.md kept one starter-template line ("additions to the kit ...
upstream from downstream projects") that the template retirement missed,
contradicting AGENTS.md §5.

Verified: bun run check 0 errors/0 warnings; product-identity and
agent-readiness suites 53 passing; Prettier clean on all touched files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The remaining ledger item is partially done and partially blocked on access this account does not hold. Recording both precisely, per the ledger's own rule about not treating a blocker as a passing gate.

Done: diff reviewed for publication safety, doc corrections pushed, draft PR #6 head reconciled with local HEAD, remote state verified.

Blocked: the PR's workflow run concluded action_required with zero jobs (fork-PR approval gate), and donaldfilimon holds pull-only permission on starspacegroup/NebulaKit, so neither approving the run nor squash-merging is possible without a maintainer.

Also noted: the squash needs a Co-Authored-By trailer for the cms-v2-embeds lineage, and ROADMAP.md/PAYMENTS_AND_PPP.md carry private paths and third-party project names that are already public via PR #6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The prior entry asserted the content was readable via PR #6 and that scrubbing was 'damage limitation, not prevention'. What was actually verified is that both repos report private: false, so the content is readable at the pushed fork branch. The stronger claim also pre-empted a judgement about third parties' project names that belongs to the owner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Both repos are public. ROADMAP.md and PAYMENTS_AND_PPP.md disclosed absolute local filesystem paths, the names of private and third-party downstream projects, three downstream commit shas, and one third party's subscription pricing floor.

Downstream sources are now identified by what they are rather than who owns them. Nabu keeps its name, being a first-party sibling already cross-linked from AGENTS.md; only its path is gone. The technical substance is unchanged: every upstream candidate keeps its rationale, design constraints, and security notes.

The PPP worked example keeps its figures. With the owner removed, a Premium/price_ABC product at 999 usd with per-currency options is an anonymous illustration, and the arithmetic carries the doc's central point that PPP pricing is a discount rather than an FX conversion. The one genuine third-party business figure, its subscription floor, is gone.

Verified: Prettier clean, bun run check 0 errors/0 warnings, product-identity and agent-readiness 53 passing, and a case-insensitive sweep for the scrubbed identifiers returns nothing in either file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The entry still said the scrub was pending after 620f6e2 had already applied it, and it was the last file in the tree carrying the four project names that commit removed. Both are now fixed: the decision and its outcome are recorded, and the sources are described rather than named.

Also lists what the decision did not cover — attribution comments, a setup-doc row, and test fixtures that pre-exist on public origin/main. The migration comment is called out separately because migrations are immutable, so it needs a deliberate remedy rather than an edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
@donaldfilimon
donaldfilimon marked this pull request as ready for review August 8, 2026 08:56
donaldfilimon and others added 6 commits August 8, 2026 04:56
Extends 620f6e2 to the attribution comments and setup-doc table that the docs scrub did not reach. Two source comments now credit 'a downstream app' rather than naming it, and the shared-database incident table identifies the six affected projects by role. The forensics are unchanged: same six rows, same collision, same conclusion.

First-party names stay — NebulaKit and Guides are already cross-linked from AGENTS.md.

migrations/0006_contact_form_submissions.sql keeps its attribution comment. Migration files are immutable under the Release Rules, and that outranks a cosmetic scrub; it needs a deliberate remedy from the owner instead.

.remember/ was untracked but not ignored, so a git add -A could have committed session transcripts — including the identifiers just removed. vite.config.ts already excluded it from coverage; .gitignore now excludes it from commits.

Verified: bun run check 0 errors/0 warnings; product-identity, agent-readiness and pii-mask suites 68 passing; touched-file Prettier clean. The unrelated type-union reformat Prettier wanted in contact-validation.ts was reverted — that drift pre-exists at HEAD and is not this change's business.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The goal cannot be done: its remaining acceptance criterion is a squash-merge into an org repo where this account holds pull only, and that repo's CI has never run for the PR because the fork approval gate parked it at action_required with zero jobs. Blocked on access, not on work.

Records the full coverage gate (97.86/95.13/97.96/98.42 against a 95 floor) and check across 1,634 files, and names what is still not claimed as passing: DAST, production build, live bindings, and the sibling audit the ledger gates on this merge.

Also records two residuals left deliberately: the immutable migration's attribution comment, and 33 files of pre-existing Prettier drift that a repo-wide sweep would have dumped into a 223-file PR awaiting merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The previous entry listed both as resting on prior evidence. Both were re-run against the current tree: contrast passed both themes, and E2E passed 8/8 Chromium tests in 41.6s with no pending local migrations. Only build:ci still rests on the earlier entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Seven findings, all verified against the source before acting.

CLAUDE.md's migrations section documented the 0010_ collision as a permanent fact. It is not: only 0010_content_item_timestamp_proof.sql is on origin/main, so the other two are branch-only, have never been applied to a remote D1, and can still be renamed before merge. The section now says so, and flags that merging as-is bakes a duplicate number into main.

It also claimed db-migrate.mjs refuses to run against a placeholder, without qualification. Line 63 exempts --local, which is what lets db:migrate:local and therefore test:e2e work on a fresh clone. Documented as load-bearing rather than as a bug to fix.

The section further told the next reader to add a migrations/README.md row that the same commit had already added, and the header said renumbering AGENTS.md's Release Rules desyncs three files when git grep finds 14, four of them shipped source. Both corrected.

ROADMAP.md promised every entry gives paths to lift from, which stopped being true when the credits entry was generalized during the identifier scrub; the intro now matches what the entries deliver. Its first line also still said 'the kit', the template framing retired elsewhere.

The ledger claimed the touched-file Prettier gate still held. It did not: contact-validation.ts is a file this branch edits, so the 'unrelated drift' rationale never covered it. Prettier is applied, which collapses a type union. It also asserted davis9001 was the owner's own handle while ROADMAP.md anonymized it as third-party; both cannot be true, and the ledger should not have picked one without evidence. Recorded as one open decision.

Verified: check 0 errors/0 warnings across 1,634 files; product-identity, agent-readiness, pii-mask 68 passing; contact-validation and contact 19 passing; Prettier clean on every file this session touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The template retirement missed this file. ROADMAP.md called the product 'the kit' 17 times — the framing AGENTS.md §5 forbids and README was already corrected for. The P1 ledger item claiming the conversion was finished across Markdown was therefore not accurate for this file.

Also removes a dangling reference to docs/ZERO_ENV_SETUP.md, which commit 924a5ac deleted when it retired the customization workflow. A code review flagged it as pre-existing context; it is a broken reference in a retired-workflow doc, which is the same class of defect the P1 item covers.

Mechanical note: BSD sed does not support \b, so the first pass silently no-oped on most occurrences and the survivors were replaced with perl. Two artifacts that wrapped across lines escaped a line-based grep and were caught by a whitespace-normalized scan.

Verified: Prettier clean, product-identity and agent-readiness 53 passing, no bare 'kit' and no dangling docs/ reference remain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The independent-product conversion item claimed Markdown was done. README carried a starter-template line and ROADMAP.md called the product 'the kit' 17 times with a dangling reference to a doc the retirement commit deleted. Recording that the item is accurate as of 7cd85e6 rather than when it was first checked off, and noting the seven review findings fixed in 8d14eeb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
@donaldfilimon

Copy link
Copy Markdown
Contributor Author

Code review

Found 1 issue:

  1. Migration numbering collision: this PR adds 0010_oauth_transactions.sql, but 0010_content_item_timestamp_proof.sql is already on main, so two migrations share the 0010_ prefix. Ordering between them falls to the filename string comparison rather than the sequence number. The new pair should be 0011_/0012_. Both new files are branch-only and have never been applied to a remote D1, so this is still safe to fix by renaming before merge; merging as-is makes the duplicate number permanent on main and weakens the rule for the sibling projects that share this file. (migrations/README.md says "Create a new file with the next sequential number: NNNN_description.sql", and AGENTS.md says "Migrations already on main are immutable. Add the next sequential migration")

-- One-time OAuth login/link transactions.
-- The browser receives the raw state while D1 stores only its SHA-256 digest.
CREATE TABLE IF NOT EXISTS oauth_transactions (
id TEXT PRIMARY KEY,

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

donaldfilimon and others added 2 commits August 8, 2026 05:42
A full review of PR #6 scored this the only finding above the reporting threshold. 0010_content_item_timestamp_proof.sql is already on origin/main, so numbering the new pair 0010_/0011_ produced two migrations sharing a prefix, where order falls to the filename string comparison rather than the sequence number.

Renamed via git mv so history follows: 0010_oauth_transactions.sql -> 0011_oauth_transactions.sql, 0011_minimize_oauth_tokens.sql -> 0012_minimize_oauth_tokens.sql. Both were branch-only and had never been applied to a remote D1, so AGENTS.md §2 immutability did not bind them; merging as-is would have made the duplicate permanent on main and weakened the next-sequential rule for the siblings that share this file.

Verified by replaying against a local D1 that had already applied both under their old names: db:migrate:local re-applied them cleanly, as the IF NOT EXISTS guard and the idempotent UPDATE predicted. check 0 errors/0 warnings; coverage 97.86/95.13/97.96/98.42 against a 95 floor; e2e 8/8. Doc references updated in migrations/README.md, CLAUDE.md, and tasks/todo.md.

The CLAUDE.md section that described the collision is replaced with the rule that prevents it: take the next number from origin/main, not from your branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
93bf6aa consolidated src/lib/server/auth-guard.ts into auth-guards.ts and dropped the isSuperAdmin branch, deleting the covering test in the same commit with no rationale recorded. stats-guard.ts still honoured the flag, so the two disagreed: a superadmin passed canViewStats and canManageStatsConnection but was refused by every admin API.

Restores the predicate in requireAdmin and requireOwner, along with the docstring explaining why the two files must agree, and declares isSuperAdmin on App.Locals so the extension point is typed rather than only structural in stats-guard. NebulaKit never sets the flag; the contract exists for downstream apps that add a tier above owner, which is what the deleted docstring said.

The restored test now asserts the two guards agree with the stats guards for the same user, so the files cannot silently diverge again.

Found by a review of PR #6. It scored 75 and so fell below that workflow's reporting threshold — correctly, since in-repo impact is nil — but the inconsistency is real and inherited by the siblings that share this code.

Verified: check 0 errors/0 warnings; coverage 97.86/95.13/97.96/98.42 with the three new branches covered; auth-guard and stats-guard suites 10 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
donaldfilimon and others added 5 commits August 8, 2026 05:47
32 files carried formatting drift, including .prettierrc itself. Earlier sessions deferred this because a repo-wide sweep would add unrelated files to a large PR, and the project's stated gate is touched-file Prettier rather than repo-wide. Sweeping it now on request.

Formatting only: prettier --write across the repo, no hand edits. 12 .svelte files are included, so the change was verified beyond the formatter.

Verified: check 0 errors/0 warnings across 1,634 files; coverage 97.86/95.13/97.96/98.42 against a 95 floor; e2e 8/8; contrast both themes; build:ci compiles with the expected placeholder-binding warning. prettier --check now passes across the whole repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
…duals

The branch is merged into local main and pushed to the fork, which is PR #6's head. Publishing to origin/main is impossible from this account: git push --dry-run origin main returns 'Permission to starspacegroup/NebulaKit.git denied to donaldfilimon', HTTP 403. Recorded as measured evidence rather than inference from the permissions API.

Two residuals closed since the last entry: the repository-wide Prettier drift (106c3ce) and the isSuperAdmin guard regression (e0e509c). build:ci has now been run, so no gate rests on earlier evidence.

No direct push to origin/main was attempted beyond the dry run — it would bypass the PR this ledger specifies and publish 29 commits to a public default branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
ROADMAP.md anonymized davis9001 as a third-party downstream project while the ledger called it the owner's own handle and left it in test fixtures. Both could not be true, and waiting on that answer was the wrong gate: neutral fixtures are correct whichever way it resolves, costing nothing if the handle is the owner's and helping if it is not.

Fixtures now use example-user and example.com across ContentProof.test.ts, branch-coverage-boost.test.ts, branch-final-push.test.ts, and wayback.test.ts. The URL-encoded form in wayback.test.ts was kept consistent with its plain counterpart. Nothing asserted the literal string — all 56 tests in those files pass unchanged.

Verified: prettier --check clean repo-wide; check 0 errors/0 warnings; coverage 97.86/95.13/97.96/98.42.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Runs the read-only half of the deferred sibling audit, which does not depend
on the PR #6 merge gate, across Guides, nabu, and sortalizer for 4 of the 7 P0
items. No sibling-repository file was staged or edited.

- isSuperAdmin does not apply: absent from all three siblings.
- Admin-users PII masking already present in Guides and sortalizer; nabu is
  not exposed, gating all three endpoints on requireOwner instead.
- Destructive reset and auth-key administration are owner-gated everywhere.
- One real gap: Guides alone guards admin/users/[id] PATCH and DELETE with an
  inline isOwner || isAdmin, so any admin can promote or delete users, where
  NebulaKit (924a5ac), nabu, and sortalizer all require owner.

Also re-verifies the 403 rather than carrying the earlier entry forward, and
records that 3 of the 7 P0 items remain unaudited.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWw88WCoHmDnMLdiHUB8pn
…all trap

Three gaps in CLAUDE.md, all verified against enforcement rather than doc
comments before being written down.

The RFC 3161 timestamp-proof subsystem had no mention anywhere despite being
real: src/lib/content-proof/, src/lib/timestamp/, migration 0010_, three API
routes, and seven test files. It has no docs/ note either, so the source and
tests were its only specification. Records that settings.enableTimestampProof
gates it, that dispatch is first-publish-only through waitUntil, and that the
third-party reproducibility of the hash rests entirely on lockedAfterPublish
and lockTitleAndSlugAfterPublish actually freezing the hashed fields — weaken
either and every existing proof stops verifying with nothing failing at write
time.

CI topology was absent. Adds the two jobs, the absent deploy job and why, and
the trap behind it: validate:all runs test, not test:coverage, so the 95%
floor never fires under it, and it skips build:ci and E2E entirely. Framed as
the workflow's intended gates, with the note that it has never executed on
starspacegroup/NebulaKit — reporting it as passing would violate AGENTS.md's
Verification rule.

Also maps the chat/AI files behind the two chat docs already in the table.

AGENTS.md is untouched: its section numbering is referenced from 14 tracked
files, four of them shipped source.

Verified: prettier --check CLAUDE.md clean; product-identity.test.ts 6/6,
which reads CLAUDE.md off disk and asserts on its content. No full gate run —
this is a Markdown-only change and AGENTS.md scopes that gate to source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@davis9001

Copy link
Copy Markdown
Member

Thanks for this — the auth and OAuth work here is genuinely stronger than what's on main, and a couple of the things you found independently (the coverage gate that could never fail, the 0010_ migration collision, the dropped isSuperAdmin) are sharp catches. My concerns are about packaging and three specific defects, not about the security work.

Recommendation: split before merging. Details below.

Blocking — CI cannot install this branch

bun.lock is bumped to lockfileVersion: 2, but ci.yml still pins BUN_VERSION: '1.3.14', which can't parse v2:

$ bun --version
1.3.14
$ bun install --frozen-lockfile
error: Unknown lockfile version   at bun.lock:2:22
UnknownLockfileVersion: failed to parse lockfile: 'bun.lock'
error: lockfile had changes, but lockfile is frozen

This is why the PR shows no checks — nothing has ever run against it, so the verification in the description is local-only. It also breaks downstream: the Cloudflare Pages build image ships bun 1.2.15, older still, and derived projects install with --frozen-lockfile.

Fix is either bumping BUN_VERSION to whatever generated the lockfile, or regenerating it on 1.3.14. Whichever you pick, it'd be good to confirm the Pages image can read the result.

Blocking — the Svelte 5 migration should be its own PR

The description doesn't mention it, but this upgrades the whole toolchain by major versions:

package main here
svelte ^4.2.7 ^5.56.8
vite ^5.0.3 ^8.2.1
vitest ^1.0.4 ^4.1.10
@sveltejs/adapter-cloudflare ^4.7.2 ^7.2.9
@sveltejs/vite-plugin-svelte ^3.0.0 ^7.2.0
happy-dom ^12.10.3 ^20.11.2
wrangler ^3.78.0 ^4.120.0

Credit where it's due: it typechecks clean (0 errors across 1621 files) and I reproduced your test numbers exactly — 2128 passed, 22 skipped. But a framework major belongs in a PR where that's the headline, so it can be reverted independently if something surfaces in a downstream app. Bundled with a security fix, neither can be rolled back without the other.

Needs a product decision, not a review

924a5ac opens with "NebulaKit is a product, not a starter template" and deletes customize.mjs, CUSTOMIZE.md, customize.config.example.json, docs/INITIAL_CUSTOMIZATION.md, and the status/setup docs.

That contradicts the README, POSITIONING.md, and the marketing site now live at nebulakit.starspace.group, whose primary CTA is "Use this template →" pointing at /generate. I used customize yesterday to stand up that site from this template and the flow worked well.

This might be the right direction! But it's a positioning call for the maintainer to make deliberately and record, not something to land inside a security PR. Could it come out of this one?

Two defects

1. Session expiry is up to 24 hours late. expires_at is written with toISOString() (2026-08-08T00:00:00.000Z) and compared against datetime('now') (2026-08-08 07:00:00). SQLite has no date type, so it's a string compare, and 'T' (0x54) sorts after ' ' (0x20):

sqlite> SELECT '2026-08-08T00:00:00.000Z' > '2026-08-08 07:00:00';  -- 1  (expired 7h ago, still valid)
sqlite> SELECT '2026-08-08T06:59:00.000Z' > '2026-08-08 07:00:00';  -- 1  (expired 1m ago, still valid)
sqlite> SELECT '2026-08-01T07:00:00.000Z' > '2026-08-08 07:00:00';  -- 0  (correct once the date rolls)

Any session expiring on the current calendar day survives until midnight UTC. Same pattern in cleanupExpiredSessions, so those rows aren't reaped either.

This is pre-existing on main, not introduced here — but this PR makes D1 sessions the sole production identity path, which promotes it from latent to load-bearing. Storing strftime('%Y-%m-%d %H:%M:%S', ...), or comparing datetime(expires_at) > datetime('now'), fixes it.

2. oauth-state.ts moved out of $lib/server/. SvelteKit enforces that $lib/server/ can never reach a client bundle; $lib/utils/ has no such guarantee, and this module handles signing secrets. I checked every importer — all are +server.ts, hooks.server.ts, or $lib/server/, no .svelte files — so nothing leaks today. But it trades a compile-time guarantee for a convention, and oauth-account.ts/oauth-finalization.ts stayed in $lib/server/, so the split seems incidental rather than intended.

What I verified and liked

  • Authorization strictly tightens — every endpoint 7e820cd hardened keeps its guard, and two get stricter: auth-keys requireAdmin → requireOwner, and /api/setup gains requireOwner where it had none
  • Sessions: 256-bit opaque tokens, SHA-256 at rest, expiry in SQL, revocable, signature checked before the DB is touched
  • OAuth state signed, single-use, with intent/TTL/clock-skew validation; migration 0012 dropping unread access tokens is the right instinct
  • Keying ownership on GITHUB_OWNER_ID instead of a reassignable username — nice catch
  • Inlining the binding guard into build/dev instead of prebuild/predev is a real fix, since bun doesn't run npm lifecycle hooks at all

Suggested split

  1. Security + OAuth + sessions (with the two fixes above) — I'd take this now
  2. Svelte 5 / toolchain migration
  3. CMS v2 merge
  4. Product-vs-template, after the positioning decision is recorded

Happy to re-review any of them individually.

…ner bypass

The session cookie is unsigned base64(JSON(user)) that hooks.server.ts trusts
verbatim, including isOwner/isAdmin, and only ever refreshes isAdmin/canViewStats
from the DB — never isOwner. So anyone can hand-craft

    session=<base64 of {"id":"x","isOwner":true}>

and pass requireOwner on every admin/owner endpoint. No secret, no account. Every
project derived from this template inherits the bypass; verified live against one
deployed downstream (no cookie → 401, forged owner cookie → 200).

The cookie becomes an opaque session id. The trusted payload lives server-side in
sessions.data (migration 0011, additive + nullable) and is read back every
request via getAuthSession; a forged or unknown cookie names no row and resolves
to nobody, so hooks leaves the request unauthenticated — fail closed, including on
a DB error. All session issue points (login, signup, github ×3, discord ×3,
dev-simulate, connections) create a server session instead of encoding one, and a
login that cannot reach the store redirects rather than issuing a cookie the hooks
reject. logout and reset delete the row so a copied cookie cannot be replayed.
encodeSession/decodeSessionCookie are removed with the old scheme.

Also fixes, found alongside:
- getAuthSession/findValidSession/cleanupExpiredSessions compared expires_at with
  datetime('now') against a stored ISO string — a raw string compare where 'T'
  sorts after ' ', so a same-day-expired session read as valid for up to a day.
  Normalized with datetime(expires_at).
- The dev-simulate bypass short-circuited on DEV_AUTH_BYPASS before the localhost
  check, so a stray env var could re-enable the owner simulator on a deployed
  host. Both paths now require a local host; the simulator also mints a real
  server session, so it needs the DB too.

NOTE: this overlaps PR #6, which rewrites sessions to its own opaque-D1 scheme as
part of a much larger change. This is the security fix on its own so main stops
minting vulnerable derived projects now; #6 can rebase its session work on top.
Derived projects need migration 0011 applied before/with the deploy.

Tests: dedicated forgery/round-trip/expiry (auth-session-server), logout/reset
revocation (auth-session-teardown), and every auth suite updated to read the
server-side payload and assert fail-closed. Suite green (1979), svelte-check
clean, coverage 97.69% lines / 95.02% branches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJpGb3i6KG2o941FwEuurW
Resolves the parallel-implementation conflict between origin/security/
opaque-sessions (db30a29, David Monaghan) and main's later HMAC-signed
session scheme — both independently fixed the same forgeable-cookie owner
bypass with incompatible designs. Per Donald's decision the OPAQUE design
wins: the cookie carries an opaque random token, the trusted payload lives
server-side in sessions.data, and a forged cookie names no session and fails
closed. Main's competing signValue/verify cookie scheme is removed.

Resolution record, per surface:
- session.ts: David's, wholesale — every main-side addition was HMAC infra.
- db.ts: union. David's createAuthSession/getAuthSession IMPROVED with two
  hardenings grafted from main's scheme (deliberate, not drift): tokens are
  256-bit createSessionToken() output rather than UUIDs, and rows are keyed
  by hashSessionToken(token) so a leaked sessions table exposes no value a
  cookie could present. findValidSession keeps main's pre-lookup hashing AND
  David's datetime() expiry normalization. New replaceAuthSession revokes a
  previous token in the same D1 batch (link-flow re-auth, no replay).
- hooks.server.ts: David's cookie mechanism + main's per-request privilege
  refresh — identity comes from the stored session, but is_admin /
  can_view_stats / owner status are re-read from the users table on every
  request so revocation needs no re-login. Pretend identities are honored
  only under isDevAuthSimulationEnabled, which keeps David's new
  local-host-only gate (a stray DEV_AUTH_BYPASS on a deployed host can no
  longer mint owner access).
- OAuth callbacks (github/discord): MAIN's, wholesale — its transactions
  rewrite postdates and supersedes the legacy flow David's branch edited
  (his side still parsed the old forgeable base64 cookie for link mode).
  Session issuance swapped to the opaque scheme in oauth-finalization.ts;
  initiation + callback routes validate the raw cookie against the sessions
  table before binding transactions to it.
- login/signup/logout/reset/connections/dev-simulate/dev-auth: David's
  shapes, with main's re-ports (auth-guards import path, reset's DB checks)
  and one behavior kept from main: logout logs a failed server-side
  revocation instead of swallowing it (a failed delete leaves a replayable
  row - worth a log line).
- oauth-state.ts: main's signValue/verifySignedValue helpers RELOCATED here.
  The OAuth state cookie legitimately stays HMAC-signed; only the session
  cookie scheme was removed.
- Migration renumbered 0011 -> 0013 (main had already shipped
  0011_oauth_transactions and 0012_minimize_oauth_tokens; migrations are
  immutable). The ALTER is orthogonal to both.
- Tests: adapted to the merged contract — hash-at-rest assertions
  (stored id == digest of cookie token, raw token never in the DB),
  auth-hook-security rewritten for the opaque hook incl. a NEW case pinning
  the non-local-host pretend rejection, Set-Cookie-reading suites opt into
  the undici Response helper, reset-calling mocks gained cookies.get.

Gate: svelte-check 0 errors / 0 warnings across 1623 files; vitest 2138
passed / 0 failed (22 skipped); coverage 97.69% stmts / 95.02% branches /
97.95% funcs / 98.27% lines — all four above the 95 floor, thresholds
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FBz9gfnaATmjb1C2kBiyc
@donaldfilimon

Copy link
Copy Markdown
Contributor Author

Two things from a diagnostic pass — one is a CI root cause, the other is a fix you haven't seen yet because it landed after your review.

1. CI root cause: the lockfile, not the pin

Both jobs die in 5–7s at Install dependencies:

error: Unknown lockfile version
UnknownLockfileVersion: failed to parse lockfile: 'bun.lock'
error: lockfile had changes, but lockfile is frozen

The cause is not a stale BUN_VERSION:

  • bun.lock on this branch is "lockfileVersion": 2
  • bun.lock on main is "lockfileVersion": 1

It was regenerated by a newer bun during the CMS v2 / Tiptap work (a0ffb27, 3a793c7, 924a5ac). CI pins BUN_VERSION: '1.3.14' (.github/workflows/ci.yml:10), which cannot parse v2.

I deliberately have not pushed a fix, because every version of it is a decision rather than a repair, and it belongs in whichever PR the split lands in:

  • Bumping BUN_VERSION makes CI green but leaves the Pages image (bun 1.2.15) unable to install, so derived projects using --frozen-lockfile stay broken. That trades a red CI for a silent downstream break — worse, not better.
  • Regenerating at v1 isn't possible with a 1.4.x toolchain: bun 1.4.0 exposes no lockfile-version flag at all (--save-text-lockfile, --lockfile-only, --frozen-lockfile, and nothing to pin the format). It would need an older bun installed.
  • Reverting the lockfile alone won't satisfy --frozen-lockfile either, since the dependency set genuinely changed on this branch (svelte ^4.2.7→^5.56.8, vite ^5.0.3→^8.2.1, vitest ^1.0.4→^4.1.10, wrangler ^3.78.0→^4.120.0).

So the bun/lockfile question is the Svelte 5 / toolchain PR you asked to split out — it can't be settled independently of it.

One thing for whoever does fix it: there is a parked stash (strict-review-interim) containing a competing −9 line hunk on ci.yml.

2. Session expiry is already fixed — your review predates it

You flagged expiry being off by up to 24h. That was fixed in the c7040ab merge, which landed ~10 hours after your review, so it wouldn't have been visible to you. src/lib/utils/db.ts:194, :221, and :239 all now use datetime(expires_at), so both the read path and cleanupExpiredSessions are normalized.

Still genuinely open from your review: oauth-state.ts sits at src/lib/utils/oauth-state.ts rather than under $lib/server/, trading SvelteKit's compile-time client-bundle guarantee for a convention on a module that handles signing secrets.

3. On the split

No objection here — #7 being merged first and #6 rebasing its session work on top reads as the right order. Flagging only that this PR's current head has not been reviewed by anyone since c7040ab.

🤖 Generated with Claude Code

donaldfilimon and others added 4 commits August 9, 2026 18:41
CLAUDE.md was last written at 2f3a4b0; db30a29 and c7040ab landed after it and
falsified four statements. Corrected, with two previously undocumented
invariants recorded alongside them.

- Migration inventory was one file behind in three places -- CLAUDE.md, the
  Current Migrations table in migrations/README.md (which omitted the row
  entirely), and the 0010_-collision entry in tasks/todo.md.
  0013_session_payload.sql has landed; the next number is 0014_. The CLAUDE.md
  numbering paragraph now carries 0013_ as its second worked example: authored
  as 0011_ on the opaque-sessions branch, renumbered at merge because main had
  meanwhile shipped 0011_ and 0012_.
- The auth paragraph still described the retired cookie scheme -- "a signed
  opaque session token ... its digest and expiry live in D1". Nothing is
  signed. Rewritten to the actual scheme: an opaque random token, the row keyed
  by its SHA-256, expiry and the trusted payload in sessions.data, and
  getAuthSession failing closed on both a forged cookie and a pre-0013_ row
  with no payload -- plus an explicit instruction not to reintroduce a cookie
  the hooks trust without a server-side lookup.
- Newly documented: the datetime(expires_at) normalization (a raw string
  compare sorts 'T' after ' ' and reads a same-day expiry as valid for up to a
  day), and the two coexisting session APIs in db.ts.

Checked and found still true, so recorded as a verification rather than
changed: authHandler takes identity from the stored payload but re-reads
is_admin, can_view_stats, and owner status from users on every request, so the
AGENTS.md Security Boundaries rule holds and revocation still needs no
re-login. The isPretend bypass, gated on isDevAuthSimulationEnabled, is now
stated explicitly rather than left implicit.

Everything else in CLAUDE.md was re-verified against source and still holds:
the 95 thresholds in vite.config.ts, devPort 4277, the 14-file
`git grep -ln "AGENTS.md §"` count, the CI job topology and BUN_VERSION pin,
the docs/ table, and the absence of a lint/format script.

Docs only, no source touched. prettier --check clean on all four files,
git diff --check clean, and product-identity + agent-readiness pass 53/53 --
those are the suites that read these files off disk. No coverage or e2e claim
is made, because nothing under coverage changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
46a0247 went to PR #6's head branch rather than a new PR, because the
corrections only parse on top of the opaque-sessions work that is not on
origin/main -- a fresh PR would have shown 36 commits instead of four doc
files. PR #6 reports MERGEABLE / CLEAN at that head.

Also records, without acting on it, that PR #7 (security/opaque-sessions)
overlaps PR #6, which already contains the merge of that branch at c7040ab.
Merge order is a maintainer decision.

The org-repo blocker is unchanged and re-verified rather than carried over:
git push --dry-run origin main still returns 403.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lose

fork/dev fast-forwarded c7040ab -> 952438c, so it now contains both open PR
heads. PR #7 needed no separate merge: its head db30a29 is already an ancestor
of PR #6's head, since the opaque-sessions merge landed at c7040ab.

Recorded explicitly because the merge is easy to over-read: both PRs target
starspacegroup:main and remain OPEN, the org repo has no dev branch and one
cannot be created from this account (403), and ci.yml triggers on main/develop
so a dev branch would not run CI regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tems)

The three P0 items deferred in the fourth-session pass are now compared across
Guides, nabu, and sortalizer. Read-only throughout: no sibling-repository file
was staged, edited, or committed, and all three sibling trees are clean.

- Sanitize-before-{@html}: no gap. Verified across every {@html} site in each
  repo rather than sampled. sortalizer has no CMS and no {@html} at all. nabu
  covers both halves of the rule -- write path in cms.ts and again at the
  render boundary in [contentType]/[slug]/+page.server.ts. Guides has no HTML
  sanitizer and no xss dependency, which is not the gap it looks like: its
  three {@html} sites are two escape-first renderMarkdownToHtml calls and one
  hardcoded SVG literal. Guides stores Markdown where NebulaKit stores
  rich-text HTML, so the sanitizer does not apply to it.
- One-time OAuth state: no gap. All four repositories consume the transaction
  with a single atomic UPDATE ... consumed_at IS NULL AND
  datetime(expires_at) > CURRENT_TIMESTAMP RETURNING, then enforce intent match
  and session binding for link mode. Only packaging differs. A first pass
  called sortalizer's expiry check missing; that was a truncated-grep false
  positive and is recorded as one.
- Open finding, nabu only: vite.config.ts excludes src/hooks.server.ts from
  coverage. nabu's handle is sequence(authHandler), so that file is its entire
  authorization boundary. A unit test exists but supplies DB in all four cases,
  so the fail-closed !db branch is exercised by nothing and the 95% floor
  cannot notice, because the file is not measured. Below the Guides escalation
  in severity -- no live privilege gap -- but the gate that would catch one is
  off. Fix is one deleted line and belongs in a nabu branch.

No sibling test suite or coverage run was executed, so the nabu conclusion is a
source-reading result, not a measured percentage. Propagation of both findings
still waits on PR #6, so the ledger item stays [~] rather than [x].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donaldfilimon

Copy link
Copy Markdown
Contributor Author

Superseded by #9 (consolidate/local-main-20260810, head 8443b8b), whose head strictly contains this branch — verified with git merge-base --is-ancestor.

#9 is MERGEABLE/CLEAN with both jobs green on the org's own CI (Test & Coverage SUCCESS, E2E SUCCESS) now that the fork-PR runs are approved. Merging #9 lands this work; this PR can close as superseded.

Note on this PR's red CI: it fails at bun install --frozen-lockfile because this head still carries the canary-written bun.lock (lockfileVersion: 2). The fix is e3b87b9, already on #9's branch — so the failure is evidence for merging #9, not a regression to chase here.

🤖 Generated with Claude Code

@davis9001
davis9001 merged commit 08ab6f9 into starspacegroup:main Aug 18, 2026
1 of 5 checks passed
@davis9001

Copy link
Copy Markdown
Member

Superseded by #9, now merged as 80c2f4f1b.

Closing because every one of these 39 commits is already in #9 — verified by comparing commit sets, not titles. The nesting is complete: #8 (6) ⊂ #6 (39) ⊂ #9 (43), and #7 (1) ⊂ #9.

#9 also carried three commits these branches lacked — fix(ci): regenerate bun.lock with a released Bun, fix: address PR #9 review findings and unblock CI, and test(e2e): make command palette tests survive the hydration race — which is why it passed Test & Coverage and E2E where #6 did not.

Nothing here is lost. It is all on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants