Skip to content

Staging batch: Library AI by book count, Opus 5.5 default, Copilot on the relay - #245

Merged
manager merged 25 commits into
devfrom
feat/uxcore-cybersecurity
Sep 25, 2026
Merged

manager merged 25 commits into
devfrom
feat/uxcore-cybersecurity

Conversation

@manager

@manager manager commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Staging batch. Everything on this branch goes to staging; OffSec stays behind its dev-only gate there as it does now. Authorized by Wolf on 2026-09-25.

New on staging with this merge:

Checks run on the merged tree: node scripts/release/library-batch-check.cjs PASS, tsc --noEmit clean.

🤖 Generated with Claude Code

manager and others added 24 commits September 22, 2026 20:12
Thinking is disabled explicitly on every Claude call, since Sonnet 5
runs adaptive thinking when the field is omitted, and the reply caps
grow from 600 to 800 and 360 to 480 tokens for its larger tokenizer.
Authorized by Wolf on 2026-09-22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The concierge and landing routes no longer hold a paid Anthropic key.
They ask claude-sonnet-5 at low effort through the relay, which moves a
call from track t1 to t2 to t3 on a rate limit, and read the reply JSON
out of the text since the relay runs one turn without tools. A
streaming caller now gets the whole reply at once. Authorized by Wolf
on 2026-09-22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude through the subscription relay is the only model the concierge
and landing routes call. When every track is spent the reply is empty
and the widget stays quiet instead of switching to a paid key.
Authorized by Wolf on 2026-09-22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cover's buildings were bound to usernames. Two owners renamed
themselves on production (Mary13 is now Mary, alinamarg is now Alina), so
their buildings matched no library and the hover card showed only a name
with no About or object counts. Hotspots now carry the owner's account id,
which survives a rename and is the same on staging, whose database is a
copy of production's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hout a rebuild

POST /api/ai-atlas/guide takes the Terminal's full guide behind a bearer
key (AI_ATLAS_PUSH_KEY), cuts it to the fields the page renders, refuses
one the adapter cannot draw, stores it on the container's persistent mount
and revalidates /ai-atlas in every locale. One journal line per push.

The page reads the stored guide in getStaticProps and falls back to the
bundled one, so the guide leaves the client chunk and is still never a
standalone public file. A tile the Terminal removes is left off the map
instead of crashing the render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hout a rebuild

POST /api/ai-atlas/guide takes the Terminal's full guide behind a bearer
key (AI_ATLAS_PUSH_KEY), cuts it to the fields the page renders, refuses
one the adapter cannot draw, stores it on the container's persistent mount
and revalidates /ai-atlas in every locale. One journal line per push.

The page reads the stored guide in getStaticProps and falls back to the
bundled one, so the guide leaves the client chunk and is still never a
standalone public file. A tile the Terminal removes is left off the map
instead of crashing the render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed store write is journaled and answered with a 500. Requests
without the key leave no journal line, so strangers cannot grow it. The
page re-checks the stored guide against the adapter it ships and falls
back to the bundled guide. An empty system.nodes gets a clear refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed store write is journaled and answered with a 500. Requests
without the key leave no journal line, so strangers cannot grow it. The
page re-checks the stored guide against the adapter it ships and falls
back to the bundled guide. An empty system.nodes gets a clear refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(ai-atlas): Terminal pushes the Atlas guide, no rebuild needed
…s the cross-vendor review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A push may carry cards (card id to paragraphs); a card listed there
replaces its copy in features.ts, so text edits reach /ai-atlas without
a build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the cross-vendor review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A push may carry cards (card id to paragraphs); a card listed there
replaces its copy in features.ts, so text edits reach /ai-atlas without
a build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A push may now carry, beside cards:
- stages: per stage, in step order, a label and up to three tiles;
- copy: any page text key (copy.json strings, ring and node labels,
  SEO title and description) mapped to new text.
Each is validated before storing; the map keeps its built-in selection
and words for anything the push leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A push may now carry, beside cards:
- stages: per stage, in step order, a label and up to three tiles;
- copy: any page text key (copy.json strings, ring and node labels,
  SEO title and description) mapped to new text.
Each is validated before storing; the map keeps its built-in selection
and words for anything the push leaves out.

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

feat(ai-atlas): the push sets the map, the cards and every page word
The AI shelf and the magic books now open to an owner who holds the
library-ai flag or whose library holds more than 15 books (type book,
every shelf). One check, opensLibraryAi, decides for the page and both
routes; the count is read from the library on each check, nothing is
stored. The AI shelf keeps its own 30-book lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Claude finished @manager's task in 8m 1s —— View job


Review of PR #245

  • Gather context (PR description, changed files list)
  • Read diff (git diff origin/dev...HEAD — origin/dev's local copy is a shallow, unrelated-history ref, so I diffed each changed file directly against origin/dev instead)
  • Review Library AI flag / cover hotspot changes
  • Review AI Atlas adapter/store/stripGuide changes
  • Review Copilot relay (llmClient, concierge, concierge-landing)
  • Post inline comments for issues found
  • Post final summary

One inline comment posted on src/lib/aiAtlas/stripGuide.ts (lines 116-126): the entries/steps/system.nodes validation only checks that id is a string — title/text/detail/children pass through unchecked, and adaptGuide()'s try/catch (the stated safety net) doesn't actually throw for most bad values in these fields, so a malformed Terminal push can be accepted and stored, then crash the page client-side later for a real visitor instead of being refused at push time. Details and a fix link are in the inline comment.

Everything else looked solid

  • opensLibraryAi/countBooks (src/lib/library/flags.ts) are correct: > LIBRARY_AI_BOOKS_OVER (15) matches "more than 15 books," countBooks safely no-ops on a null library via optional chaining, and the server-side gate in ownerOfLibrary (src/lib/library/owner.ts) fetches the library with the caller's own token, so the private-shelves book count is always accurate for the real owner regardless of what the client UI has hydrated.
  • /api/ai-atlas/guide.ts: bearer-token check hashes both sides with SHA-256 before timingSafeEqual, correctly avoiding the classic length-mismatch throw/timing leak; refuses cleanly when the key isn't configured; journals refused/failed/stored/live outcomes without leaking to unauthenticated callers.
  • InteractiveCover/coverHotspots.ts: switching hotspot matching from username to a stable ownerId is a real correctness fix (usernames can change and silently break the binding); no hooks or SSR issues introduced.
  • llmClient.ts / concierge.ts / concierge-landing.ts: removing the direct Anthropic/OpenAI calls in favor of the relay is a clean refactor, askClaudeJson's parameter order is consistent at both call sites, and the streaming→single-shot change matches what the PR description says was authorized. One thing worth keeping an eye on (not blocking, and explicitly called out as intended): the relay's job-polling path can in theory take much longer than the old direct/streaming call for a live chat widget — worth confirming the widget's own timeout/UX tolerates that.
  • No App Router patterns, no Tailwind/CSS-in-JS, no new state libraries, no <img> for SVGs, no global CSS imports outside _app.tsx, no empty/named-export barrels touched. tsc --noEmit and library-batch-check.cjs are reported passing per the PR body.

I was not able to run eslint/tsc myself in this sandbox (network/tool restrictions on this run), so import-order/lint conformance is based on manual inspection, not a tool run — flagging that gap per instructions.

Comment on lines +116 to +126
steps: input.steps.map((s: any) =>
pick(s, ['id', 'title', 'location', 'text', 'children']),
),
entries: input.entries.map((e: any) =>
pick(e, ['id', 'title', 'text', 'detail', 'children']),
),
system: {
nodes: input.system.nodes.map((n: any) =>
pick(n, ['id', 'title', 'role', 'detail', 'basis']),
),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: title/text/detail/children on entries, steps and system.nodes aren't type-checked, so a malformed push can slip past this "guide it cannot draw is refused" gate.

isRecordList (line 21-26) only checks that id is a string; everything else is copied through pick() unchanged. The comment at line 132 says "the page must be able to draw what is stored; a guide it cannot draw is refused," relying on adaptGuide(guide) throwing to catch bad data — but adaptGuide mostly assigns these fields raw (e.g. title: entry.title at adapter.ts:84) and only throws for a handful of hardcoded ids that happen to call a string method (node(id).title.toLowerCase() for layerIds, adapter.ts:217). A title that's an object on an arbitrary entry sails through stripGuide → gets stored → gets served, and only breaks when a visitor opens that specific dossier, since ai-atlas.tsx renders {data.title} directly as a JSX child (e.g. line 745) with no error boundary around the page — a full client-side crash for that visitor rather than a push-time refusal.

Worth adding an explicit string+length check for title/text/detail (and that children is an array of strings) on entries/steps/system.nodes, mirroring the rigor already applied to isCards/isCopy/stagesError just below.

Fix this →

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20428ab: title, location, text, role and basis must be text, detail and children lists of texts, each up to 4000 characters, on steps, entries and system nodes. A guide that fails is refused at push time and the page keeps the last stored one. Checked against the bundled guide (accepted), an object title and a number in a detail list (both refused).

Titles, texts and the detail and children lists on steps, entries and
system nodes are now checked as text before the guide is stored. Before,
an object in a title passed adaptGuide and broke the page only when a
visitor opened that dossier. Raised by the review on #245.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Claude finished @manager's task in 1m 15s —— View job


Review of PR #245 (updated push)

  • Gather context (diff against origin/dev on the new HEAD — same shallow/unrelated-history caveat as before, diffed the changed commit directly)
  • Check whether the previously flagged stripGuide.ts validation gap was addressed
  • Review any other changes introduced by this push
  • Post inline comments for issues found (none needed this round)
  • Post final summary

This push (commit 20428ab) fixes the issue flagged in the previous review

src/lib/aiAtlas/stripGuide.ts now type-checks every kept field before a Terminal-pushed guide is stored:

  • TEXT_FIELDS = ['title', 'location', 'text', 'role', 'basis'] must each be a string ≤ 4000 chars.
  • LIST_FIELDS = ['detail', 'children'] must each be an array of such strings.
  • fieldsError() runs over steps, entries, and system.nodes and is checked before the guide is assembled/stored.

I checked this against how each field is actually consumed in adapter.ts to make sure the shape matches reality, not just the strip logic:

  • detail is read as an array in both places it's used — entry.detail || [entry.text] (line 85) and describe(systemRef(id), node(id).detail) (line 198) — so treating it as a list-of-strings is correct for both entries and system nodes.
  • role/basis are read directly as scalars (badge: node(id).role, why: node(id).basis) — correctly TEXT_FIELDS.
  • children is consumed as an array of entry ids (entries.get(child)) — correctly a LIST_FIELDS of strings.
  • Every field in the pick() lists for steps/entries/system.nodes is now covered by either TEXT_FIELDS or LIST_FIELDS; nothing passes through unchecked anymore. null values are also caught (null !== undefined but typeof null !== 'string', so isText correctly rejects it).

This closes the gap cleanly: a malformed push with e.g. an object title is now refused at push time with a clear error instead of getting stored and crashing the page for a real visitor later.

One non-blocking note, not introduced by this PR: scripts/ai-atlas/strip-guide.mjs (the script that refreshes the bundled fallback, called out in the stripGuide.ts file header as keeping "the same field lists") doesn't have the equivalent type check — it still just does the raw pick(). That script isn't touched by this PR and its input is presumably admin-controlled rather than an arbitrary Terminal push, so it's lower risk, but worth a follow-up if that source ever becomes less trusted.

No other files changed in this push — everything else from the earlier full review (Library AI-flag gate, cover hotspot owner-id fix, Copilot relay refactor, AI Atlas guide endpoint auth) stands as previously assessed.

@manager
manager merged commit 37ba613 into dev Sep 25, 2026
4 checks passed
manager added a commit that referenced this pull request Sep 25, 2026
* feat(library): more than 15 books opens the AI without the flag

The AI shelf and the magic books now open to an owner who holds the
library-ai flag or whose library holds more than 15 books (type book,
every shelf). One check, opensLibraryAi, decides for the page and both
routes; the count is read from the library on each check, nothing is
stored. The AI shelf keeps its own 30-book lock.

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

* fix(widget): quieter Ask-UX-Core pill

The idle pill drops to 34px, smaller label, 0.7 opacity, and loses the
orange pulse glow, so it stops burying the host page's bottom-corner
buttons and text. Full opacity returns on hover, while the panel is
open, and in the relevance-nudge state, whose pulse is also toned down.
Opacity stops at 0.7 because the 12px label must keep the 4.5:1
contrast floor over light pages. Mobile pill shrinks in step. Bundle
rebuilt, so the change is live on every locale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit d957e9b)

* feat(uxcore): Arc of Self nav item, navbar single-row fit, Our Projects modal pass

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 7fcd5ec)

* fix(auth): cover the login hand-off with a branded loader, stop the grey banding

The /auth, magic-link and email-change pages hold almost no markup while the
provider handshake runs. Two defects came out of that:

- The body gradient is propagated to the canvas but sized against the root
  box, so on those near-empty documents it tiled down the viewport as grey
  bands. The root now holds viewport height and the gradient no longer repeats.
- /auth rendered the shared Spinner without its visible prop, so it drew
  nothing at all and left the bare document on screen. All three pages now
  render AuthLoader: a full-viewport paper surface with the house brain loader,
  dark-theme aware, honouring prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 55af48d)

* fix(library): restore the branch's own fixes on top of prod's Library

The merge took prod's side on every conflicted file, which dropped three
fixes this branch had made against the August structure. Rebuilt on the
new one:

- The info panel again sits in a host that is transparent to layout on
  desktop and a fixed clipping layer at the drawer breakpoints, so the
  panel parked off the right edge stops counting towards the page's
  scrollable width. Without it Safari measures a library page about two
  screens wide on a phone. The CSS for it had survived the merge with no
  element to apply to.
- The Add controls pulse again while the library holds no objects. Prod
  rewrote the empty-shelf area out of Shelf, so only the header control
  carries it now, and the round-control animation goes with the control
  it belonged to.
- The avatar boxes declare their own widths again (48px panel, 152px
  edit dialog) instead of paying for the home card's headroom.

The fourth, the single-request library load, is not rebuilt: prod paints
the library on the server, so the two round trips it removed no longer
happen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 45a2db7)

* library: AI shelf and Magic default to Claude Opus 5.5 (Wolf, 2026-09-22)

(cherry picked from commit 9db1dc7)

* widget: Copilot defaults to Claude Sonnet 5

Thinking is disabled explicitly on every Claude call, since Sonnet 5
runs adaptive thinking when the field is omitted, and the reply caps
grow from 600 to 800 and 360 to 480 tokens for its larger tokenizer.
Authorized by Wolf on 2026-09-22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit cd740e3)

* widget: Copilot's Claude calls go through the subscription relay

The concierge and landing routes no longer hold a paid Anthropic key.
They ask claude-sonnet-5 at low effort through the relay, which moves a
call from track t1 to t2 to t3 on a rate limit, and read the reply JSON
out of the text since the relay runs one turn without tools. A
streaming caller now gets the whole reply at once. Authorized by Wolf
on 2026-09-22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d6c6b74)

* widget: drop the paid OpenAI fallback from Copilot

Claude through the subscription relay is the only model the concierge
and landing routes call. When every track is spent the reply is empty
and the widget stays quiet instead of switching to a paid key.
Authorized by Wolf on 2026-09-22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit bac80e9)

* fix(library): cover hotspots find their library by owner id

The cover's buildings were bound to usernames. Two owners renamed
themselves on production (Mary13 is now Mary, alinamarg is now Alina), so
their buildings matched no library and the hover card showed only a name
with no About or object counts. Hotspots now carry the owner's account id,
which survives a rename and is the same on staging, whose database is a
copy of production's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c1aaa5f)

* fix(library): phone width and avatar sizing from the cybersecurity branch

* fix(ai-atlas): a pushed guide with a field of the wrong shape is refused

Titles, texts and the detail and children lists on steps, entries and
system nodes are now checked as text before the guide is stored. Before,
an object in a title passed adaptGuide and broke the page only when a
visitor opened that dossier. Raised by the review on #245.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 20428ab)

* fix(release): Arc of Self keeps its name below 1160px, drop a dead doc link

The Arc of Self link carries aria-label, so it keeps an accessible name
when its label is hidden on narrow screens, and its tooltip also shows on
keyboard focus. CLAUDE.md no longer links a voice guide that is not in the
tree. Raised by the review on #246.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant