Skip to content

feat(ai-atlas): the push sets the map, the cards and every page word - #243

Merged
manager merged 5 commits into
mainfrom
feat/ai-atlas-pushed-cards
Sep 24, 2026
Merged

manager merged 5 commits into
mainfrom
feat/ai-atlas-pushed-cards

Conversation

@manager

@manager manager commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Everything visible on /ai-atlas becomes pushable by the Terminal, so the page changes on prod without a build. Authorized by Wolf on 2026-09-24.

A push to /api/ai-atlas/guide may now carry, beside the guide:

  • cards: card id to paragraphs; replaces the copy in features.ts.
  • stages: all six stages in step order, each with an optional label and 1 to 3 tiles (every tile must be an entry).
  • copy: page text key to text: copy.json strings, ring and node labels, the header label, SEO title and description.
    Anything left out keeps its built-in value. Invalid shapes are refused with 400.

Also three sentences appended to the Engine, Delivered work and Models cards.

Verified on a local production build: a push with stages, copy and cards changed the Result stage tiles and label, a ring label, a card text, the header label and the page title in en, ru and hy; a plain push restored the built-in selection.

🤖 Generated with Claude Code

manager and others added 2 commits September 24, 2026 20:19
…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>
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Claude finished @manager's task in 2m 17s —— View job


Reviewing PR #243

  • Gather context (diff, changed files)
  • Understand request (code review)
  • Review src/lib/aiAtlas/adapter.ts
  • Review src/lib/aiAtlas/features.ts
  • Review src/lib/aiAtlas/stripGuide.ts
  • Post inline comments for findings
  • Post summary

Summary

Small, well-scoped diff (27/-8 across 3 files). No inline comments — nothing rose to a real issue.

What I checked:

  • Fallback precedence in adapter.ts:13-14 — guide.cards?.[id] || features[id] || fallback.filter(Boolean) matches the PR description exactly (pushed card > code copy > guide's own text). describe moved inside adaptGuide to close over guide; correct and matches the doc comment above it.
  • Validation in stripGuide.ts:24-35 — isCards requires a ^[a-z0-9-]+$ key, a non-empty array, and each paragraph a string ≤4000 chars, matching the PR's stated contract. The guide.cards assignment happens before the adaptGuide(guide) render-check at line 73, so a guide with cards that can't render is still refused — consistent with the existing "the page must be able to draw what is stored" guarantee.
  • Auth/security — the push endpoint (src/pages/api/ai-atlas/guide.ts, unchanged) is bearer-key gated with timingSafeEqual. Pushed card text renders through the existing DossierDesc/renderDossierValue path (src/pages/ai-atlas.tsx), which is plain React text plus a restricted [text](https://...|tip:...) inline-link parser — no dangerouslySetInnerHTML, so this doesn't open a new XSS surface even though the text now originates from an external push rather than only features.ts.
  • guide.cards[id] truthiness — arrays are always truthy in JS, so an empty guide.cards[id] array can't silently win over features[id]; isCards already rejects paragraphs.length === 0, so this can't reach the client.
  • Confirmed scripts/ai-atlas/strip-guide.mjs (bundled guide.json fallback, not touched by this PR) intentionally has no cards field — that script strips the Terminal's raw export, and pushed card overrides are a separate pipeline (the API route), so there's no drift with the "same field lists" comment in stripGuide.ts.

Minor, non-blocking observation: isCards (stripGuide.ts:34) allows an empty-string paragraph (p.length <= 4000 doesn't require > 0). In practice this is harmless — DossierDesc splits on blank lines and .filter(Boolean)s the result, so a stray '' paragraph is silently dropped rather than rendered — but if you want the validation to mirror intent exactly, p.length > 0 && p.length <= 4000 would close the gap.

No AGENTS.md convention violations (barrel files, import order, App Router patterns, styling, etc.) — this diff doesn't touch components or styles. any usage is consistent with the pre-existing style of these three files.

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>
@manager manager changed the title feat(ai-atlas): card texts arrive with the pushed guide feat(ai-atlas): the push sets the map, the cards and every page word Sep 24, 2026
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

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


Reviewing PR #243

  • Gather context (diff, changed files)
  • Understand request (code review)
  • Review src/lib/aiAtlas/adapter.ts
  • Review src/lib/aiAtlas/features.ts
  • Review src/lib/aiAtlas/stripGuide.ts
  • Review src/pages/ai-atlas.tsx
  • Post inline comments for findings
  • Post summary

Summary

This PR grew since the earlier automated pass on this branch: a third commit ("the Terminal's push sets the map and every page word") landed on top and now lets a push replace stages (stage labels + tiles) and copy (arbitrary page words: apex/ring/resource labels, SEO title/description, ARIA strings), in addition to the cards change already reviewed. I re-reviewed the full diff against origin/main (172/-43 across 4 files) rather than assume the prior review still covers it.

One inline finding on src/lib/aiAtlas/stripGuide.ts's isCopy (line 48): it caps every PAGE_TEXT_KEYS value at 4000 chars uniformly and has no minimum. Several of those keys — apexLabel, orderLabel, the resource* and ring*Label keys — render inside small, fixed-size SVG foreignObject nodes with overflow: visible (ai-atlas.tsx:234-239), so a long pushed value would overflow the map's circular layout unclipped; stagesError right below already caps stage.label at 40 chars for this exact reason, but the new copy path doesn't get the same treatment. Separately, an empty string passes validation and — unlike card paragraphs, which get .filter(Boolean)-ed downstream — flows straight through text()'s ?? into rendered UI text, and into seoTitle/seoDescription/ogImageAlt (confirmed plain strings in copy.json), so a pushed empty seoDescription would blank the page's meta description/OG tags. Suggested: words.length > 0 plus a shorter cap for the short label keys.

What else I checked, no issues found:

  • Precedence/consistency — text(key) in adapter.ts:48-49 (guide.copy?.[key] ?? PAGE_TEXT_DEFAULTS[key] ?? copy[key]) and the t object's spread order (adapter.ts:190-193) agree; data.copy (used as t in AiAtlasApp, ai-atlas.tsx:1258) is the same merged object, so no drift there.
  • stagesError's value.length !== steps check correctly uses input.steps.length (dynamic) rather than assuming a fixed count — consistent with the pre-existing (unchanged by this PR) hardcoded 6-element labels/angles/chosen/support arrays in adaptGuide, which already assume exactly 6 steps; not a regression introduced here.
  • guide.cards?.[id] || features[id] || fallback truthiness and the card-paragraph validation are unchanged from the prior commit's review and still hold.
  • Minor, non-blocking: AiAtlasPage's top-level words = { ...copy, ...(guide?.copy || {}) } (ai-atlas.tsx:2114) duplicates the merge in adapter.ts but omits the PAGE_TEXT_DEFAULTS layer. Harmless today since the SEO keys it uses all come from copy.json rather than PAGE_TEXT_DEFAULTS, but it's a second place that must be kept in sync by hand.
  • Security: pushed copy/cards/stages text all renders as plain React text/attribute values (no dangerouslySetInnerHTML), so no new XSS surface. The push endpoint's bearer-key auth is unchanged.
  • No AGENTS.md convention violations (barrel files, import order, App Router patterns, styling, any usage matches the pre-existing style of these files).

Could not run tsc/yarn build locally (no node_modules installed in this environment) to independently verify the PR's "tsc clean" claim — the changes are type-narrow enough (Record<string, string>, Set<string>) that I don't expect an issue, but flagging the gap per instructions.

Comment thread src/lib/aiAtlas/stripGuide.ts Outdated
([key, words]) =>
PAGE_TEXT_KEYS.has(key) &&
typeof words === 'string' &&
words.length <= 4000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isCopy applies one blanket <= 4000 cap to every PAGE_TEXT_KEYS entry, but those keys aren't all the same kind of text. Several are short labels rendered in small fixed-size map nodes — apexLabel, orderLabel, resourceAgents/Memory/Tools/Models, the four ring*Label keys — via NodeBody's <foreignObject ... style={{ overflow: 'visible' }}> (src/pages/ai-atlas.tsx:234-239). A pushed value near the 4000-char ceiling for one of these would render as unclipped overflowing text across the circular Atlas map, since overflow: visible doesn't truncate. stagesError right below already caps stage.label at 40 chars for exactly this reason (arc width) — the same discipline isn't applied here.

Separately, words.length <= 4000 doesn't require words.length > 0, so an empty string passes. Unlike card paragraphs (isCards), which get .filter(Boolean)-ed downstream by DossierDesc, these copy values are consumed directly (text(key) in adapter.ts uses ??, so '' isn't replaced by the default) and include seoTitle/seoDescription/ogImageAlt (confirmed plain strings in copy.json). A pushed empty seoDescription would blank the page's meta description/OG tags, not just a UI label.

Consider per-key max lengths (short label keys vs. long-form seoDescription/ringDevEnvDesc/etc.) and words.length > 0 in isCopy.

Fix this →

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

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Claude finished @manager's task in 2m 28s —— View job


Reviewing PR #243

  • Gather context (diff, changed files since last review)
  • Review src/lib/aiAtlas/adapter.ts
  • Review src/lib/aiAtlas/features.ts
  • Review src/lib/aiAtlas/stripGuide.ts
  • Review src/pages/ai-atlas.tsx
  • Post inline comments for new findings
  • Post summary

Summary

The branch grew by one more commit since the last automated pass: 2efc36c — "pushed map labels stay short, empty text is refused." It's a direct fix for both points raised in the prior review's inline comment on stripGuide.ts's isCopy, and it lands cleanly:

  • Empty-string gap closed — isCards and isCopy now both require .length > 0, so a pushed empty seoDescription/card paragraph can no longer blank out meta tags or slip an empty paragraph through.
  • Short-label overflow addressed — new SHORT_LABEL_KEYS set (adapter.ts:45-59) + SHORT_LABEL_MAX = 40 caps the map/ring node labels (apexLabel, orderLabel, the four resource* and ring*Label keys, metaLabel, topicsPlaceholder) at the same 40-char limit stagesError already used for stage.label, for the same reason: these render inside small, fixed/overflow:visible SVG nodes (ai-atlas.tsx:234-239).
  • Merge drift fixed too — AiAtlasPage's words object now includes PAGE_TEXT_DEFAULTS in its spread (ai-atlas.tsx:2114), matching adapter.ts's t merge order exactly. This closes the "second place to keep in sync by hand" note from the earlier pass.

One new finding (posted inline on adapter.ts:45-59): brandTitle was left out of SHORT_LABEL_KEYS. It renders in the same .doc-header .header-left row as metaLabel (ai-atlas.tsx:1546-1559), which is white-space: nowrap with no overflow handling (ai-atlas.css:116-133) — and brandTitle additionally goes through a per-letter-spacing transform (.split('').join(' ')) before render, which amplifies any overflow. A pushed value near the general 4000-char cap would overflow the header the same way the map node labels did before this fix. Since it sits in the identical nowrap row as metaLabel, it should get the same cap.

Re-verified, still hold from earlier passes:

  • guide.cards?.[id] || features[id] || fallback precedence and paragraph validation.
  • stagesError's dynamic steps count check, tile-existence check, and 1–3 tile bound.
  • Push endpoint auth (timingSafeEqual bearer key, unchanged) and no new XSS surface — all pushed text renders as plain React text/attributes, no dangerouslySetInnerHTML.
  • No AGENTS.md convention violations (barrel files, import order, App Router patterns, styling, any usage consistent with these files' pre-existing style).

Could not run tsc/yarn build in this environment (no node_modules) to independently verify type-checking; the new code is type-narrow (Record<string, string>, Set<string>) so I don't expect an issue, but noting the gap.

Comment on lines +45 to +59
export const SHORT_LABEL_KEYS = new Set([
'apexLabel',
'apexSub',
'orderLabel',
'resourceAgents',
'resourceMemory',
'resourceTools',
'resourceModels',
'ringOrderLabel',
'ringDevEnvLabel',
'ringProjectsLabel',
'ringTerritoriesLabel',
'metaLabel',
'topicsPlaceholder',
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good fix for the map-node overflow risk, but brandTitle is missing from this list. It renders in the same .doc-header .header-left row as metaLabel (src/pages/ai-atlas.tsx:1546-1559), and that row is white-space: nowrap with no overflow handling (src/styles/ai-atlas.css:116-133). brandTitle is also run through .split(' ').map(w => w.toUpperCase().split('').join(' ')).join(' · ') before rendering (ai-atlas.tsx:1549-1552), which inserts a space between every letter — so a pushed value near the 4000-char general cap would blow out to several times its length and overflow the header, the same failure mode metaLabel/apexLabel/etc. were just capped to prevent.

Since brandTitle sits in the identical nowrap row as metaLabel, it should get the same SHORT_LABEL_MAX treatment.

[Fix this →](https://claude.ai/code?q=In%20src%2Flib%2FaiAtlas%2Fadapter.ts%2C%20add%20%27brandTitle%27%20to%20the%20SHORT_LABEL_KEYS%20Set%20(around%20line%2045-59).%20It%20renders%20in%20the%20same%20.doc-header%20.header-left%20row%20as%20metaLabel%20(src%2Fpages%2Fai-atlas.tsx%20around%20line%201546-1559)%2C%20which%20is%20white-space%3A%20nowrap%20with%20no%20overflow%20handling%20(src%2Fstyles%2Fai-atlas.css%20line%20116-133)%2C%20and%20brandTitle%20is%20additionally%20expanded%20by%20a%20per-letter-spacing%20transform%20before%20render%2C%20amplifying%20any%20overflow.%20It%20should%20get%20the%20same%20SHORT_LABEL_MAX%20cap%20as%20metaLabel%20since%20a%20long%20pushed%20value%20would%20overflow%20the%20header%20the%20same%20way%20the%20map%20node%20labels%20did%20before%20this%20fix.&repo=keepsimpleio/KeepSimpleOSS

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

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Claude finished @manager's task in 40s —— View job


Reviewing PR #243

  • Gather context (diff since last review)
  • Review latest commit (ca5066d — brandTitle short-label fix)
  • Confirm prior findings are resolved
  • Post summary

Summary

The branch gained one more commit since the last automated pass: ca5066d — "the brand title is a short label too." It's a one-line, surgical fix for the exact gap flagged in the prior inline comment (adapter.ts:45-59): 'brandTitle' is now the first entry in SHORT_LABEL_KEYS.

Verified the fix closes the loop correctly, not just cosmetically:

  • isCopy in stripGuide.ts:53 already reads SHORT_LABEL_KEYS.has(key) ? SHORT_LABEL_MAX : 4000 when validating a pushed copy value — that logic isn't new to this commit, so simply adding brandTitle to the shared SHORT_LABEL_KEYS set (adapter.ts) was sufficient to extend the 40-char cap to the API boundary. No separate change needed in stripGuide.ts, and none was made.
  • Traced the render path to confirm the finding was correctly scoped: text('brandTitle') (adapter.ts:270) feeds data.brand.title, rendered at ai-atlas.tsx:1546-1559 inside .doc-header .header-left, run through the per-letter-spacing transform (.split(' ').map(w => w.toUpperCase().split('').join(' ')).join(' · ')) that amplifies length. That row is white-space: nowrap with no overflow handling, same as metaLabel right below it — so the same 40-char cap is the right fix, not an arbitrary one.
  • No other files changed in this commit (adapter.ts +1/-0 only), so nothing else to re-review.

All findings from the three earlier passes on this branch are now resolved:

  • Empty-string gap in isCards/isCopy — closed (2efc36c).
  • Short-label overflow for map/ring node labels — closed (2efc36c).
  • words merge drift in ai-atlas.tsx omitting PAGE_TEXT_DEFAULTS — closed (2efc36c).
  • brandTitle missing from SHORT_LABEL_KEYS — closed (ca5066d).

No new issues found. guide.cards?.[id] || features[id] || fallback precedence, stagesError's dynamic step-count/tile-bound checks, and the push endpoint's timingSafeEqual bearer-key auth are all unchanged and still hold from earlier passes. No AGENTS.md convention violations (barrel files, import order, App Router patterns, styling, any usage consistent with these files' pre-existing style). No new XSS surface — pushed text still renders as plain React text/attributes.

Could not run tsc/yarn build in this environment (no node_modules) to independently verify type-checking; the change is a single string literal added to a Set<string>, so no type risk expected, but noting the gap per instructions.

@manager
manager merged commit 3b9e5b4 into main Sep 24, 2026
2 checks passed
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