You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Migrated from PR #175 on 2026-07-18. This issue is the source of truth.
Planned against dev at commit 5158299; refresh live assumptions before implementation.
Priority: P1
Effort: L
Risk: MED
Category: tech-debt
Why this matters
Fork identity currently has no seam. Names, canonical URLs, public contact,
time zone, profile handles, repository identity, and image paths are repeated
across metadata, chrome, social adapters, OG output, refresh scripts, and
verification scripts. A fork must discover and change all of them correctly.
A committed Site Profile module gives those callers leverage through one small
interface and gives maintainers locality: a shared identity change happens
once. Authored prose and personal collections remain content because putting
arbitrary JSX or biography into the interface would make the module shallow.
// lib/seo.ts:3-26functionpublicSiteUrl(){constraw=process.env.PUBLIC_SITE_URL??(process.env.NODE_ENV==='production' ? 'https://cali.so' : 'http://localhost:3199')leturl: URLtry{url=newURL(raw)}catch{thrownewError('PUBLIC_SITE_URL must be a valid URL')}if(url.protocol!=='https:'&&!['localhost','127.0.0.1'].includes(url.hostname)){thrownewError('PUBLIC_SITE_URL must use HTTPS outside local development')}returnurl}exportconstseo={title: publicPageMetadata.home.zh.title,description: publicPageMetadata.home.zh.description,url: publicSiteUrl(),}asconst
components/site-footer.tsx:73,114, components/home-introduction.tsx:156-175, components/footer-clock.tsx:5-18, lib/locale-metadata.ts:57-78, and lib/og-image.tsx:22 repeat more identity. content/social.json and content/github.json duplicate handles/names that are
then consumed as though they were configuration.
The paired Chinese/English route family is a settled v3 contract. Locale
configurability is explicitly out of scope.
Architecture vocabulary for this plan is fixed: the Site Profile module is
the implementation; its exported SiteConfig value/type is the interface;
the import point is the seam. The module is deep when that small
interface hides repeated identity decisions. That depth gives callers leverage and concentrates identity change with locality. Do not use
these terms as decorative prose or create extra adapters; direct imports are
the one real seam.
Target interface
Create client-safe site.config.ts at the repository root. It must contain no
React, process.env, Zod, server-only import, provider client, or secret. Export
one runtime value plus its erased TypeScript interface with this shape:
The interface is the test surface. Keep it limited to shared scalar identity,
public destinations, and public asset locations.
canonicalUrl is the committed default public discovery identity. PUBLIC_SITE_URL remains an environment-specific override for canonical links,
feeds, alternates, and social metadata; a blank value falls back to the committed
origin. SITE_URL remains the operational origin for provider callbacks,
application links, and same-origin mutations, so Staging may intentionally use a
different value. ADMIN_EMAIL remains a durable operator data namespace and is
not the same as owner.publicEmail.
id and keyNamespace are deliberately separate. id preserves existing
durable/provider domain separators beginning cali.so:. keyNamespace
preserves browser events, local-storage keys, and rate-limit scopes beginning cali:. A fork chooses both before creating operator data or shipping clients.
Both values must match ^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$: non-empty, already trimmed,
lowercase ASCII tokens with no colon or trailing delimiter. Callers add their
own single : separator; the configured token never contains one.
repository.fullName must contain exactly one non-empty owner/repository
pair, and repository.url must be exactly https://github.com/${repository.fullName} with no trailing slash, query, or
hash. These fields cannot vary independently; a fork must change both together.
Owner discovery and admin navigation outside components/dock.tsx, including components/preferences.tsx, components/admin-dock.tsx, hooks/use-dock-go-shortcuts.ts, and app/api/admin/session/route.ts; these
remain unchanged until issue Architecture: make the Operator Stack optional per site #181.
Biography JSX in components/home-introduction.tsx, project/experience/book/
record arrays, posts, newsletters, photos, and other authored content. They
stay as the working example and issue Docs: document and verify the supported fork workflow #182 lists them for mandatory replacement.
Arbitrary locale support or removal of the paired zh/en route contract.
Secrets, credentials, ADMIN_EMAIL, provider endpoints, rate limits, or
environment validation.
Changing visible copy, URLs, profile handles, images, metadata, or generated
output for cali.so.
Package, dependency, or lockfile changes.
Steps
Step 1: Record the domain and decision
Create lib/site/CONTEXT.md with two terms:
Site Profile: committed non-secret identity and public destinations that
define one fork of the personal site.
Authored Content: personal prose, collections, and media that remain
repository content rather than fields in the Site Profile.
Add Site Profile to CONTEXT-MAP.md. Add ADR 0014 recording the committed
typed seam, the distinction between canonicalUrl, the PUBLIC_SITE_URL discovery
override, and the operational SITE_URL, the
stable id invariant, the no-secret rule, and the decision not to make locale
or biography arbitrary configuration.
Verify:
test -f lib/site/CONTEXT.md
test -f docs/adr/0014-committed-site-profile.md
fortermin'Site Profile''Authored Content' module interface seam depth leverage locality
do
rg -qF "$term" lib/site/CONTEXT.md docs/adr/0014-committed-site-profile.md \
||exit 1
done
rg -qF '[Site Profile](./lib/site/CONTEXT.md)' CONTEXT-MAP.md
All commands exit 0 with no output. The terms mean exactly what Current state
defines; no extra module, adapter, or locale interface is introduced.
Step 2: Add and validate the Site Profile
Create site.config.ts with the exact interface above and the current cali.so
values. Add lib/site/site-config.test.ts covering:
HTTPS canonical origin with pathname /, no query, and no hash;
valid public /... asset paths whose files exist under public/;
syntactically valid public email and non-empty handles;
valid IANA time zone by constructing Intl.DateTimeFormat;
alternate values satisfying the exported SiteConfig type with each of the
five optional profiles set to null;
both namespace fields match the exact lowercase delimiter-free token rule;
negative fixtures cover empty, whitespace, uppercase, leading/trailing
punctuation, and any colon, including a colon-suffixed keyNamespace;
repository fullName has one non-empty owner/repository pair and its URL is
exactly https://github.com/${fullName} with no trailing slash, query, or
hash; negative fixtures cover missing or multiple segments, stale Cali URLs
after a name change, non-GitHub origins, and URL suffixes;
id === 'cali.so' and keyNamespace === 'cali' only when repository.fullName === 'CaliCastle/cali.so'; forks change both namespace
fields, fullName, and its matching URL together;
no field names containing secret, token, password, or credential, or
conventional secret names such as apiKey, privateKey, secretKey, or encryptionKey; keyNamespace is an explicit non-secret identifier and must
remain allowed.
Do not add a runtime validation dependency. TypeScript plus focused tests own
the interface.
The config tests pass and Node 24 imports the TypeScript config without a
loader. The namespace, repository-pairing, and source-repository compatibility
assertions exit 0.
Step 3: Migrate public metadata and chrome
Replace duplicated shared identity with imports from siteConfig in the public
files listed in Scope:
document title, metadata site name, canonical origin, OG signatures, global
error/not-found titles, newsletter archive title;
home display name and portrait/thumbnail paths;
footer public email, copyright owner, clock time zone/location label;
social profile links, handles, avatar paths, and repository destination;
public page descriptions only where the shared given/display name is
interpolated. Preserve the authored sentence around it.
In lib/seo.ts, trim process.env.PUBLIC_SITE_URL once and treat an empty
or whitespace-only value as absent. Preserve every non-empty discovery override,
keep Local's http://localhost:3199 fallback, and replace only the production
hardcoded default with siteConfig.canonicalUrl. The blank-value fallback is
required so issue #181 can override a populated local env file with PUBLIC_SITE_URL= and prove a public-only production build needs no discovery
origin variable. Do not merge this with SITE_URL; that variable keeps its
separate operational-origin contract.
Do not turn siteConfig into React context. Direct imports are the intended
small interface for server, client, and build-time callers.
Preserve the current public/admin document split in app/_components/site-document.tsx: the owner-admin branch remains a static,
provider-free shell outside public analytics, social reads, public Dock, and
route transitions. In components/dock.tsx, migrate shared avatar identity
without changing the cached owner hint, Preferences-triggered /api/admin/session probe, Admin-row visibility, or G-D ownership added before
this plan. issue #181 owns profile-aware gating for those operator affordances.
All files pass and retain exact cali.so output expectations. Metadata coverage
proves a non-empty PUBLIC_SITE_URL still wins, while blank/whitespace
production values use siteConfig.canonicalUrl and blank Local values use the
existing localhost fallback. Existing SITE_URL operational-origin tests remain
unchanged.
Step 4: Separate social identity from snapshot metrics
Change committed snapshots so configuration is not duplicated:
content/github.json keeps contribution/follower metrics and dates, but not
the GitHub handle.
content/social.json keeps replaceable bios, counts, and asOf values, but
not shared display names or handles.
lib/social-live.ts composes siteConfig.profiles with the committed/live
metrics before returning the existing render-facing shapes.
A null profile is omitted cleanly from chrome rather than producing a broken
link. Adjust SiteFooter/HomeIntroduction props or composition as needed,
but do not add placeholder accounts.
Refresh scripts derive endpoints/handles from siteConfig and update metrics
only.
Node 24 is the repository runtime and can import the erasable TypeScript config
from .mjs; do not add tsx, ts-node, or another config representation.
Add lib/social-live.test.ts to prove snapshot metrics are composed with the
configured GitHub/X/YouTube identities, null profiles are omitted, and refresh
failure keeps current snapshot metrics without reintroducing configured
handles into JSON.
Verify:
pnpm exec vitest run lib/social-live.test.ts components/social-cards.test.tsx \
components/home-introduction.test.tsx components/site-footer.test.tsx
All tests pass with exact current links/labels. Null GitHub, X, Telegram,
YouTube, and Xiaohongshu profiles cause no fetch and no broken public link.
Step 5: Migrate public verification adapters
Update public discovery/link scripts and their tests to derive expected origin,
document title suffix, user agent identity, and profile endpoints from siteConfig. Preserve explicit request URL fixtures when they are testing URL
behavior rather than configuration.
next.config.ts may import siteConfig for current public identity, but the
legacy URL manifest itself remains untouched.
All commands exit 0; syntax checks print no output.
Step 6: Audit remaining identity literals
Add a named test, classifies every upstream identity literal, to lib/site/site-config.test.ts. It scans production .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, and .cts source under app, components, lib, and scripts, plus proxy.ts and next.config.ts; excludes tests/specs,
nested documentation/context files, and site.config.ts; and classifies this
exact marker set:
shared identity to migrate now: Cali Castle, CaliCastle, calicastle, cali_so, hi@cali.so, cali.so, https://cali.so, Asia/Taipei, the
Xiaohongshu profile ID/URL, configured avatar/headshot/portrait paths,
repository identity, and cali:locale-change;
authored identity, prose, and historical/project destinations deferred only
in components/home-introduction.tsx, components/social-cards.tsx, app/_views/ama-page.tsx, app/_views/ama-book-page.tsx, lib/public-page-metadata.ts, lib/personal.ts, and lib/projects.ts for
issue Docs: document and verify the supported fork workflow #182 replacement; this category may contain the generic Cali name and
identity markers embedded in authored destinations, but not shared runtime
chrome/configuration;
operator identity deferred only in the complete production file inventory
in issue Architecture: make the Operator Stack optional per site #181, including app/admin/(protected)/AdminOverview.tsx, app/admin/(protected)/ama/AmaSettings.tsx, and lib/media/storage/contract.ts;
explicit protocol URL fixtures in tests only. Production source may not use
this category.
Repository/deployment identity in README, package metadata, .github/**,
funding, and vercel.json is owned by issue #182 and is outside this runtime
scan. No fifth category exists.
Verify:
pnpm exec vitest run lib/site/site-config.test.ts \
-t "classifies every upstream identity literal"
Exactly one audit test passes with zero unclassified runtime matches. If an
operator path differs from issue #181's inventory, update that plan before
finishing this one.
Step 7: Run the full gates
Run typecheck, deterministic unit suite, localization, public verification, and
build. Compare homepage, footer, social cards, metadata, OG images, feeds, and
global errors against the pre-change output. All current cali.so values must be
identical.
The first nine commands exit 0; every discovered test passes without retries or
raised timeouts. Its file/case count reflects any completed deletion plans plus
the seven new canonical test files and new cases in this plan, rather than the
pre-plan 119-file/1,109-test baseline. Exact-output tests cover the named
surfaces. Status contains only the fully enumerated Create/Modify paths and the
permitted index row; any other path is a STOP condition.
Test plan
Add focused structural tests for the Site Profile interface.
Update current metadata, social, feed, OG, and verification expectations to
derive from config while still asserting exact rendered cali.so output.
Add null-profile cases for all five optional profiles, proving each omission
produces no fetch or broken public link.
Do not replace useful exact-output assertions with snapshots or vague toBeTruthy checks.
Done criteria
site.config.ts is the only committed interface for shared non-secret
identity.
Namespace tokens are non-empty, lowercase, delimiter-free, and compose
with exactly one caller-owned colon; repository full name and URL match.
The module imports no React, environment, provider, or secret code.
Public metadata/chrome/social/scripts use it and render unchanged output.
Social snapshot files no longer duplicate configured names/handles.
Every remaining hardcoded identity match is explicitly classified.
No authored content, locale contract, dependency, or secret handling was
broadened into config.
Focused tests, localization, links, discovery, unit tests, typecheck, and
build all pass.
git diff --check exits 0 and git status --short contains only the
exact Create/Modify inventory.
Only in-scope paths are modified.
STOP conditions
An in-scope consumer has drifted enough that exact current output cannot be
preserved from the plan.
Identity migration would alter the static owner-admin document shell, public
owner-discovery probe, Admin-row visibility, or public/admin shortcut split.
A proposed field would contain a secret, provider credential, or
environment-specific capability.
A config change requires serializing functions, JSX, React elements, or a
second config representation.
Node 24 cannot import the erasable TypeScript config from the .mjs scripts;
stop and report instead of adding a new runtime dependency.
Centralization appears to require locale rearchitecture, a CMS, or moving
authored content into a large shallow interface.
Maintenance notes
Reviewers should reject new shared identity literals outside the Site Profile
unless they are authored content, a protocol fixture, or a documented stable
namespace. siteConfig.id becomes load-bearing for operator data in issue #181; siteConfig.keyNamespace becomes load-bearing for current cali: keys. Forks
must choose both before creating data or shipping clients.
Why this matters
Fork identity currently has no seam. Names, canonical URLs, public contact,
time zone, profile handles, repository identity, and image paths are repeated
across metadata, chrome, social adapters, OG output, refresh scripts, and
verification scripts. A fork must discover and change all of them correctly.
A committed Site Profile module gives those callers leverage through one small
interface and gives maintainers locality: a shared identity change happens
once. Authored prose and personal collections remain content because putting
arbitrary JSX or biography into the interface would make the module shallow.
Current state
Representative duplication at
5158299:components/site-footer.tsx:73,114,components/home-introduction.tsx:156-175,components/footer-clock.tsx:5-18,lib/locale-metadata.ts:57-78, andlib/og-image.tsx:22repeat more identity.content/social.jsonandcontent/github.jsonduplicate handles/names that arethen consumed as though they were configuration.
The paired Chinese/English route family is a settled v3 contract. Locale
configurability is explicitly out of scope.
Architecture vocabulary for this plan is fixed: the Site Profile module is
the implementation; its exported
SiteConfigvalue/type is the interface;the import point is the seam. The module is deep when that small
interface hides repeated identity decisions. That depth gives callers
leverage and concentrates identity change with locality. Do not use
these terms as decorative prose or create extra adapters; direct imports are
the one real seam.
Target interface
Create client-safe
site.config.tsat the repository root. It must contain noReact,
process.env, Zod, server-only import, provider client, or secret. Exportone runtime value plus its erased TypeScript interface with this shape:
The interface is the test surface. Keep it limited to shared scalar identity,
public destinations, and public asset locations.
canonicalUrlis the committed default public discovery identity.PUBLIC_SITE_URLremains an environment-specific override for canonical links,feeds, alternates, and social metadata; a blank value falls back to the committed
origin.
SITE_URLremains the operational origin for provider callbacks,application links, and same-origin mutations, so Staging may intentionally use a
different value.
ADMIN_EMAILremains a durable operator data namespace and isnot the same as
owner.publicEmail.idandkeyNamespaceare deliberately separate.idpreserves existingdurable/provider domain separators beginning
cali.so:.keyNamespacepreserves browser events, local-storage keys, and rate-limit scopes beginning
cali:. A fork chooses both before creating operator data or shipping clients.Both values must match
^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$: non-empty, already trimmed,lowercase ASCII tokens with no colon or trailing delimiter. Callers add their
own single
:separator; the configured token never contains one.repository.fullNamemust contain exactly one non-emptyowner/repositorypair, and
repository.urlmust be exactlyhttps://github.com/${repository.fullName}with no trailing slash, query, orhash. These fields cannot vary independently; a fork must change both together.
Commands you will need
pnpm exec vitest run lib/site/site-config.test.tstest:unitpnpm test:localization && pnpm verify:public-discoverypnpm test:unitpnpm typecheckpnpm buildgit diff --checkScope
Create:
site.config.tslib/site/site-config.test.tsapp/_views/home-page.test.tsxapp/global-metadata.test.tsxcomponents/footer-clock.test.tsxcomponents/home-introduction.test.tsxcomponents/site-footer.test.tsxlib/social-live.test.tslib/site/CONTEXT.mddocs/adr/0014-committed-site-profile.mdModify public identity consumers:
CONTEXT-MAP.mdapp/_components/site-document.tsxapp/global-error.tsxapp/global-not-found.tsxapp/_views/home-page.tsxapp/_views/newsletter-retired-page.tsxapp/feed.xml/route.tsapp/feed.en.xml/route.tscomponents/dock.tsxcomponents/footer-clock.tsxcomponents/home-introduction.tsxcomponents/site-footer.tsxcomponents/social-cards.tsxcontent/github.jsoncontent/social.jsonlib/date.tslib/locale-client.tslib/locale-metadata.tslib/non-public-metadata.tslib/og-image.tsxlib/public-page-metadata.tslib/seo.tslib/social-live.tsnext.config.tsscripts/refresh-github.mjsscripts/refresh-social.mjsscripts/refresh-link-previews.mjsscripts/verify-production-security-boundary.mjsscripts/verify-public-discovery.mjsscripts/verify-public-links.mjsapp/non-public-metadata.test.tsapp/newsletter-retired-metadata.test.tsapp/seo-routes.test.tsapp/site-document.test.tsxcomponents/dock.test.tsxcomponents/social-cards.test.tsxlib/content.test.tslib/date.test.tslib/locale-metadata.test.tslib/og-route-metadata.test.tslib/public-page-metadata.test.tsOut of scope:
operatorStackbehavior and operator route gating; issue Architecture: make the Operator Stack optional per site #181 owns it.components/dock.tsx, includingcomponents/preferences.tsx,components/admin-dock.tsx,hooks/use-dock-go-shortcuts.ts, andapp/api/admin/session/route.ts; theseremain unchanged until issue Architecture: make the Operator Stack optional per site #181.
components/home-introduction.tsx, project/experience/book/record arrays, posts, newsletters, photos, and other authored content. They
stay as the working example and issue Docs: document and verify the supported fork workflow #182 lists them for mandatory replacement.
ADMIN_EMAIL, provider endpoints, rate limits, orenvironment validation.
output for cali.so.
Steps
Step 1: Record the domain and decision
Create
lib/site/CONTEXT.mdwith two terms:define one fork of the personal site.
repository content rather than fields in the Site Profile.
Add Site Profile to
CONTEXT-MAP.md. Add ADR 0014 recording the committedtyped seam, the distinction between
canonicalUrl, thePUBLIC_SITE_URLdiscoveryoverride, and the operational
SITE_URL, thestable
idinvariant, the no-secret rule, and the decision not to make localeor biography arbitrary configuration.
Verify:
All commands exit 0 with no output. The terms mean exactly what Current state
defines; no extra module, adapter, or locale interface is introduced.
Step 2: Add and validate the Site Profile
Create
site.config.tswith the exact interface above and the current cali.sovalues. Add
lib/site/site-config.test.tscovering:/, no query, and no hash;/...asset paths whose files exist underpublic/;Intl.DateTimeFormat;SiteConfigtype with each of thefive optional profiles set to null;
negative fixtures cover empty, whitespace, uppercase, leading/trailing
punctuation, and any colon, including a colon-suffixed
keyNamespace;fullNamehas one non-empty owner/repository pair and its URL isexactly
https://github.com/${fullName}with no trailing slash, query, orhash; negative fixtures cover missing or multiple segments, stale Cali URLs
after a name change, non-GitHub origins, and URL suffixes;
id === 'cali.so'andkeyNamespace === 'cali'only whenrepository.fullName === 'CaliCastle/cali.so'; forks change both namespacefields,
fullName, and its matching URL together;secret,token,password, orcredential, orconventional secret names such as
apiKey,privateKey,secretKey, orencryptionKey;keyNamespaceis an explicit non-secret identifier and mustremain allowed.
Do not add a runtime validation dependency. TypeScript plus focused tests own
the interface.
Verify:
The config tests pass and Node 24 imports the TypeScript config without a
loader. The namespace, repository-pairing, and source-repository compatibility
assertions exit 0.
Step 3: Migrate public metadata and chrome
Replace duplicated shared identity with imports from
siteConfigin the publicfiles listed in Scope:
error/not-found titles, newsletter archive title;
interpolated. Preserve the authored sentence around it.
In
lib/seo.ts, trimprocess.env.PUBLIC_SITE_URLonce and treat an emptyor whitespace-only value as absent. Preserve every non-empty discovery override,
keep Local's
http://localhost:3199fallback, and replace only the productionhardcoded default with
siteConfig.canonicalUrl. The blank-value fallback isrequired so issue #181 can override a populated local env file with
PUBLIC_SITE_URL=and prove a public-only production build needs no discoveryorigin variable. Do not merge this with
SITE_URL; that variable keeps itsseparate operational-origin contract.
Do not turn
siteConfiginto React context. Direct imports are the intendedsmall interface for server, client, and build-time callers.
Preserve the current public/admin document split in
app/_components/site-document.tsx: the owner-admin branch remains a static,provider-free shell outside public analytics, social reads, public Dock, and
route transitions. In
components/dock.tsx, migrate shared avatar identitywithout changing the cached owner hint, Preferences-triggered
/api/admin/sessionprobe, Admin-row visibility, or G-D ownership added beforethis plan. issue #181 owns profile-aware gating for those operator affordances.
Verify:
pnpm exec vitest run lib/site/site-config.test.ts \ app/site-document.test.tsx app/global-metadata.test.tsx \ app/newsletter-retired-metadata.test.ts app/non-public-metadata.test.ts \ app/seo-routes.test.ts app/_views/home-page.test.tsx \ components/dock.test.tsx components/footer-clock.test.tsx \ lib/content.test.ts lib/date.test.ts lib/locale-metadata.test.ts \ lib/og-route-metadata.test.ts lib/public-page-metadata.test.tsAll files pass and retain exact cali.so output expectations. Metadata coverage
proves a non-empty
PUBLIC_SITE_URLstill wins, while blank/whitespaceproduction values use
siteConfig.canonicalUrland blank Local values use theexisting localhost fallback. Existing
SITE_URLoperational-origin tests remainunchanged.
Step 4: Separate social identity from snapshot metrics
Change committed snapshots so configuration is not duplicated:
content/github.jsonkeeps contribution/follower metrics and dates, but notthe GitHub handle.
content/social.jsonkeeps replaceable bios, counts, andasOfvalues, butnot shared display names or handles.
lib/social-live.tscomposessiteConfig.profileswith the committed/livemetrics before returning the existing render-facing shapes.
link. Adjust
SiteFooter/HomeIntroductionprops or composition as needed,but do not add placeholder accounts.
siteConfigand update metricsonly.
Node 24 is the repository runtime and can import the erasable TypeScript config
from
.mjs; do not addtsx,ts-node, or another config representation.Add
lib/social-live.test.tsto prove snapshot metrics are composed with theconfigured GitHub/X/YouTube identities, null profiles are omitted, and refresh
failure keeps current snapshot metrics without reintroducing configured
handles into JSON.
Verify:
pnpm exec vitest run lib/social-live.test.ts components/social-cards.test.tsx \ components/home-introduction.test.tsx components/site-footer.test.tsxAll tests pass with exact current links/labels. Null GitHub, X, Telegram,
YouTube, and Xiaohongshu profiles cause no fetch and no broken public link.
Step 5: Migrate public verification adapters
Update public discovery/link scripts and their tests to derive expected origin,
document title suffix, user agent identity, and profile endpoints from
siteConfig. Preserve explicit request URL fixtures when they are testing URLbehavior rather than configuration.
next.config.tsmay importsiteConfigfor current public identity, but thelegacy URL manifest itself remains untouched.
Verify:
All commands exit 0; syntax checks print no output.
Step 6: Audit remaining identity literals
Add a named test,
classifies every upstream identity literal, tolib/site/site-config.test.ts. It scans production.js,.jsx,.mjs,.cjs,.ts,.tsx,.mts, and.ctssource underapp,components,lib, andscripts, plusproxy.tsandnext.config.ts; excludes tests/specs,nested documentation/context files, and
site.config.ts; and classifies thisexact marker set:
Cali Castle,CaliCastle,calicastle,cali_so,hi@cali.so,cali.so,https://cali.so,Asia/Taipei, theXiaohongshu profile ID/URL, configured avatar/headshot/portrait paths,
repository identity, and
cali:locale-change;in
components/home-introduction.tsx,components/social-cards.tsx,app/_views/ama-page.tsx,app/_views/ama-book-page.tsx,lib/public-page-metadata.ts,lib/personal.ts, andlib/projects.tsforissue Docs: document and verify the supported fork workflow #182 replacement; this category may contain the generic
Caliname andidentity markers embedded in authored destinations, but not shared runtime
chrome/configuration;
in issue Architecture: make the Operator Stack optional per site #181, including
app/admin/(protected)/AdminOverview.tsx,app/admin/(protected)/ama/AmaSettings.tsx, andlib/media/storage/contract.ts;this category.
Repository/deployment identity in README, package metadata,
.github/**,funding, and
vercel.jsonis owned by issue #182 and is outside this runtimescan. No fifth category exists.
Verify:
Exactly one audit test passes with zero unclassified runtime matches. If an
operator path differs from issue #181's inventory, update that plan before
finishing this one.
Step 7: Run the full gates
Run typecheck, deterministic unit suite, localization, public verification, and
build. Compare homepage, footer, social cards, metadata, OG images, feeds, and
global errors against the pre-change output. All current cali.so values must be
identical.
Verify:
The first nine commands exit 0; every discovered test passes without retries or
raised timeouts. Its file/case count reflects any completed deletion plans plus
the seven new canonical test files and new cases in this plan, rather than the
pre-plan 119-file/1,109-test baseline. Exact-output tests cover the named
surfaces. Status contains only the fully enumerated Create/Modify paths and the
permitted index row; any other path is a STOP condition.
Test plan
derive from config while still asserting exact rendered cali.so output.
produces no fetch or broken public link.
toBeTruthychecks.Done criteria
site.config.tsis the only committed interface for shared non-secretidentity.
with exactly one caller-owned colon; repository full name and URL match.
broadened into config.
build all pass.
git diff --checkexits 0 andgit status --shortcontains only theexact Create/Modify inventory.
STOP conditions
preserved from the plan.
owner-discovery probe, Admin-row visibility, or public/admin shortcut split.
environment-specific capability.
second config representation.
.mjsscripts;stop and report instead of adding a new runtime dependency.
authored content into a large shallow interface.
Maintenance notes
Reviewers should reject new shared identity literals outside the Site Profile
unless they are authored content, a protocol fixture, or a documented stable
namespace.
siteConfig.idbecomes load-bearing for operator data in issue #181;siteConfig.keyNamespacebecomes load-bearing for currentcali:keys. Forksmust choose both before creating data or shipping clients.