Framework Copy Manifest
Every file Sondely takes from TourCockpit (Ideaplaces/ideaplaces-tour-platform), what happens to it, which of its imports are cut, in which phase it is copied and where it lands in Sondely. After the copy the two products diverge; a TourCockpit fix reaches Sondely only when someone carries it across on purpose, by the procedure below.
As built on 7 November 2026. The copy lives in its own repository, CatalyzeUp/sondely-platform, whose root is the Next.js app, rather than a server/ folder in CatalyzeUp/sondely; read every server/ path on this page as the repository root. The base is TourCockpit commit 74e9a2a53 (74e9a2a53956731c46f707790e9ffae96fb9fafa, 7 November 2026, the head of develop that day), 230 commits after the f952d0eea this page was read at. framework-base.json at the repository root records the base and the copied files (on 8 November 2026: 84 copied and 19 written for Sondely, while about 120 files carry a copy header, the assistant and MCP files among them); it, not these tables, is meant to be the record of what was copied.
The imports in every table were read from the files at f952d0eea (git show f952d0eea:<path>), not inferred from names. The verdicts stand; line counts and imports may differ at 74e9a2a53. The plan this manifest serves is the migration overview; how the assistant pieces are rewritten for surveys is in Agent Layer.
How to read the tables
| Verdict | Meaning |
|---|---|
| Copy as is | Copied byte for byte apart from the header and the global renames below. It has no TourCockpit domain imports, or only imports that are themselves copied |
| Copy and strip | The mechanism is copied, the tour content is removed or rewritten. The "Cut" column names the imports and the content that go |
| Later phase | Useful to Sondely, but not until a capability exists that it serves. Not copied now; the phase or condition is named |
| Leave behind | Tour domain, or a TourCockpit feature Sondely has no counterpart for. Never copied. Any import of it from a copied file is cut |
Target: same means the same path at the root of sondely-platform (src/lib/api/idempotency.ts lands at src/lib/api/idempotency.ts). The tables write it with the server/ prefix of the first plan. A different name is a rename, recorded in the file's header so the source is never lost. Keeping paths identical is deliberate: it is what makes a later port a plain diff.
Phases are the ones in the plan:
| Code | Phase | What it needs from this manifest |
|---|---|---|
| P2 | Scaffold and infrastructure | The API plumbing, tenant layer, auth core, logger, test harness and CI. Serves no traffic |
| P3 | Parity | Sign-in doors, sends, the write handler list as route groups are ported |
| P4 | Cutover | End to end and deploy workflows, the migration rehearsal |
| P4b | Screens into Next | Component and usability harness, UI parity scans, cookie session |
| P5 | API an agent can read | Asked marks, batch, read-back, refusal classes and their tests |
| P6 | MCP | MCP server, OAuth, the write path (propose, dry run, commit) |
| P7 | In-app chat | The assistant turn, drawer events, nightly matrix |
| P8a | Respondents by WhatsApp text (first half of the plan's phase 8) | The WhatsApp door, voice note transcription |
| P8b | Respondents by voice (second half of phase 8) | Call transport, recording, call simulation |
Global renames applied to every copied file
| TourCockpit | Sondely | Why |
|---|---|---|
Company, companyId, company in identifiers |
Organization, organizationId, organization |
Organizations are Sondely's tenant (Data Model) |
UserCompany |
OrganizationMember |
Users are global and belong to organizations through memberships |
UserRole ADMIN, MANAGER, OPERATOR |
MemberRole: Sondely's roles (owner, admin, respondent) |
Roles are decided in Auth |
| Faro (persona name) | Sona, held in src/lib/assistant/config.ts (ASSISTANT_NAME) |
TourCockpit already keeps the persona in one file (Digby's rename rule, in the file's own comment). Named from Chip's goal of 7 November 2026 |
Postgres GUC app.tenant_id |
Unchanged | It is a name inside migrations and db.ts, with no product meaning; keeping it keeps the RLS policy text (98 uses of current_setting('app.tenant_id') at the base) portable |
Cookie tp_session |
sondely_session, set in auth/config.ts |
SESSION_COOKIE is the one place it is defined |
The renames are mechanical and applied in the strip commit, never in the copy commit (see the procedure), so git diff <copy commit> -- <file> always shows exactly what Sondely changed.
The copy procedure
1. Record the base once
A single file, framework-base.json at the repository root, is the record of where the framework came from. As built (abridged):
{
"source": "https://github.com/Ideaplaces/ideaplaces-tour-platform",
"commit": "74e9a2a53956731c46f707790e9ffae96fb9fafa",
"copiedOn": "2026-11-07",
"renames": { "Company": "Organization", "companyId": "organizationId", "UserCompany": "OrganizationMember", "UserRole": "MemberRole", "tp_session": "sondely_session" },
"files": {
"src/lib/api/idempotency.ts": { "from": "src/lib/api/idempotency.ts", "verdict": "copy-and-strip", "ported": [] },
"src/lib/api/dispatch.ts": { "written": "sondely" }
}
}
files holds one entry per copied file, written by the copy step, and marks the files written for Sondely with "written": "sondely". It is the machine-readable twin of this page.
2. Copy from the commit object, never from a working tree
TP=/path/to/ideaplaces-tour-platform
BASE=74e9a2a53956731c46f707790e9ffae96fb9fafa
git -C "$TP" show "$BASE:src/lib/api/idempotency.ts" > src/lib/api/idempotency.ts
git show <commit>:<path> reads the file as it was at the base, whatever branch or local change the TourCockpit checkout carries. A copy from a working tree would silently take someone's uncommitted edit.
3. Header on every copied file
The first lines of every copied file, before any import:
/**
* @framework-source ideaplaces-tour-platform src/lib/api/errors.ts @ 74e9a2a53956731c46f707790e9ffae96fb9fafa
* @framework-verdict copy-and-strip: the tour error codes and their messages removed; the envelope and the classing kept
* @framework-ported (none)
*/
| Tag | Content |
|---|---|
@framework-source |
Repository, source path at the base, commit (the full sha, as built) |
@framework-verdict |
copy-as-is or copy-and-strip: followed by one sentence naming what was cut |
@framework-ported |
(none), or one line per fix carried across later: TourCockpit sha, date, one sentence |
JSON files cannot hold a comment: their header lives in framework-base.json only. A YAML workflow or a Markdown agent file takes the same three tags as # or HTML comment lines.
A unit test, tests/unit/framework-headers.test.ts, reads framework-base.json and fails when a listed file lacks the header, when the header's source path disagrees with the JSON, or when a file under src/lib/{api,tenant,auth,mcp,assistant} carries no header and is not marked "written": "sondely" in the JSON. It does not need TourCockpit at test time. On 8 November 2026 the test walks src/lib/{api,tenant,auth} only, and the files copied for Sona (src/lib/assistant, src/lib/mcp) carry their headers but have no entry in framework-base.json: a gap between this rule and the repository, to close.
4. Two commits per group: copy, then strip
- Copy commit: the files exactly as at the base, plus the header. It does not compile, and that is expected; it never lands alone on
develop(the two commits travel in one pull request). - Strip commit: the global renames, the cuts named in the tables, the tour content removed. This commit is the Sondely divergence and the one a reviewer reads.
Groups follow the tables below (API, tenant, auth, logger, then the harness, then CI), so each pull request stays one concern.
5. Carrying a TourCockpit fix across later
The two products diverge, and that is the design: nothing syncs automatically. A fix is ported when someone decides it matters for Sondely.
- Find what changed upstream for a copied file:
git -C "$TP" log --oneline 74e9a2a53..origin/develop -- src/lib/api/idempotency.ts - Read the fix as a diff against the base, not against Sondely:
git -C "$TP" show <fix sha> -- <path>. - Reproduce it first: a failing test in Sondely that shows the defect, per the engineering standard. If the defect does not exist in Sondely (the strip removed the code it lived in), stop: nothing to port.
- Apply by hand or with
git applyon the Sondely file. Because the path and the copy commit are kept,git diff <copy commit> -- <file>shows what Sondely changed, and the upstream hunk usually lands cleanly next to it. - Append to the header and the JSON:
@framework-ported <sha> 2026-12-01 idempotency: a replay answers the stored content type, and the same line infiles[...].ported. - Never move the base.
commitinframework-base.jsonstays74e9a2a53for ever; a port is recorded per file. Moving the base would claim every file matches a newer TourCockpit, which is false.
A drift report (a script listing, for every entry in framework-base.json, the upstream commits since the base and whether each is already in ported) is a reasonable later convenience. It is inferred, not built.
src/lib/api (32 files)
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
asked.ts |
114 | Copy as is | P5 | None (imports zod only) |
same |
auth.ts |
259 | Copy and strip | P2 | tourOperatorId operator scope on the bound context; role names via @/lib/auth/roles; @/lib/tenant/read-only member check reduced to the hook |
same |
batch-match.ts |
73 | Copy as is | P2 | None. Needed early: strict-request.ts and read-back.ts import it |
same |
batch-refs.ts |
73 | Copy as is | P5 | None | same |
batch.ts |
85 | Copy as is | P5 | @/lib/validation batch schema, rewritten in Sondely's validation module |
same |
came-by.ts |
8 | Leave behind | @/lib/reference/came-by: how a booking reached the office |
||
commission.ts |
15 | Leave behind | @/lib/auth/commission-visibility |
||
delivery.ts |
258 | Copy and strip | P3 | @/lib/board/send-doors (DoorRefusal type, redefined locally), @/lib/board/send-intent (SENDING_STALE_MS, copied as a constant). Kept: a dry run sends nothing, the Idempotency-Key is the send's record of intent. Serves survey invitations and reminders |
same |
door.ts |
42 | Leave behind | @/lib/reference/came-by. Its import from request-transaction.ts is cut |
||
errors.ts |
189 | Copy and strip | P2 | @/lib/services/errors (tour ServiceErrorCode list) and @/lib/i18n/service-errors (their messages). Kept: { "error": { code, message, details } } and the Zod flattening |
same |
examples.ts |
989 | Copy and strip | P5 | No imports; every example is a tour route. The types and readers stay, the content is written for survey routes | same |
guide-pay-door.ts |
19 | Leave behind | @/lib/auth/guide-pay-visibility, guide pay private feature |
||
idempotency.ts |
243 | Copy as is | P2 | None (imports ./errors, @/lib/db) |
same |
invitation-read-back.ts |
16 | Copy and strip | P5 | @/lib/people/invitation-answer; catalog keys rewritten for organization invitations |
same |
mail-api.ts |
44 | Leave behind | @/lib/services/mail, @/lib/private-features (operator mail) |
||
names.ts |
62 | Copy and strip | P5 | @/lib/reference/came-by; one reader per tour kind replaced by survey, question group, question, member |
same |
no-route.ts |
32 | Copy and strip | P2 | @/lib/seo/public-routes (the public site list) |
same |
openapi.ts |
1605 | Copy and strip | P2 | @/lib/private-features; every tour registry entry and its @/lib/validation schemas. Kept: RouteDef, the registry mechanics, RBAC per route, the document builder |
same |
pagination.ts |
38 | Copy as is | P2 | None (the page schema it reads from @/lib/validation is copied with it) |
same |
read-back.ts |
468 | Copy and strip | P5 | @/lib/assistant/endpoint-map (read through an interface until P6), @/lib/i18n/recipient, the per kind wording for tours |
same |
refs.ts |
176 | Copy and strip | P2 | RefKind (tour, departure, booking, hotel, zone, vehicle, driver, operator, language, childSeat, rep, cameBy) and REF_KINDS rewritten for survey kinds |
same |
request-transaction.ts |
328 | Copy and strip | P2 | @/lib/outbox/drain (after-commit sends; returns if Sondely adopts the outbox, see Business Logic), ./door. Kept: one serializable transaction per write, the tenant GUC, the audit binding, ?dryRun=true |
same |
rest-description.ts |
31 | Copy as is | P2 | None | same |
standalone-writes.ts |
44 | Copy and strip | P3 | The board send routes in the set; Sondely's send and export routes take their place | same |
strict-request.ts |
62 | Copy as is | P2 | None | same |
strict-schema.ts |
223 | Copy as is | P6 | None. Only the model's write tools read it | same |
tower-door.ts |
20 | Leave behind | @/lib/services/whatsapp-tower (office WhatsApp inbox) |
||
tower-json.ts |
25 | Leave behind | @/lib/whatsapp/tower-write |
||
ui-parity.ts |
298 | Copy and strip | P4b | No imports; the tour screen to route table is rewritten as screens move | same |
ui-read-parity.ts |
243 | Copy and strip | P4b | @/lib/assistant/screens; tour screen list |
same |
unknown-fields.ts |
303 | Copy as is | P2 | None | same |
write-handlers.ts |
199 | Copy and strip | P5 | All 90 @/app/api/v1/... tour route imports. The file becomes the list of Sondely's write routes |
same |
Summary: 10 copy as is, 15 copy and strip, 7 leave behind.
src/lib/tenant (7 files)
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
audit-extension.ts |
168 | Copy as is | P2 | None | same |
audit-payload.ts |
184 | Copy and strip | P2 | The captured model list. @/lib/audit/diff is copied with it (93 lines, no imports) |
same |
context.ts |
66 | Copy and strip | P2 | Rename only: companyId to organizationId, the role type |
same |
extension.ts |
183 | Copy and strip | P2 | TENANT_MODELS: the closed list of tenant models, rewritten for Sondely's tables. User is tenant owned in TourCockpit and global in Sondely, so it is not on the list |
same |
read-only-send.ts |
87 | Leave behind | Phone number matching for read-only members' sends | ||
read-only.ts |
182 | Copy and strip | P2 | Read-only memberships have no Sondely counterpart. The memberMayWrite hook the extension calls is kept and answers yes; inferred use: the cutover write freeze (Cutover Runbook) |
same |
request-scope.ts |
91 | Copy as is | P2 | None | same |
src/lib/logger.ts
| File | Lines | Verdict | Phase | Cut | Target |
|---|---|---|---|---|---|
logger.ts |
53 | Copy as is | P2 | None (no imports). One JSON object per line on stdout, which Azure Container Apps hands to Log Analytics and the C3 log monitor reads by level |
same |
src/lib/auth (34 files)
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
brand-host.ts |
133 | Later phase | P4b | White label by <slug>.tourcockpit.com. Becomes custom domains per organization once screens are in Next (an open item in the plan) |
same |
card-token.ts |
44 | Leave behind | Invitation card render tokens | ||
commission-visibility.ts |
34 | Leave behind | Commission gate | ||
company.ts |
149 | Copy and strip | P2 | @/lib/company/kind; the session's active company becomes the active organization |
server/src/lib/auth/organization.ts |
config.ts |
65 | Copy and strip | P2 | tp_session and TourCockpit's window constants; the secrets' names |
same |
email.tsx |
754 | Leave behind | 15 @/emails/* templates, @/lib/activation/*, @/lib/follow-through/registry, @/lib/help/*, @/lib/onboarding/*, @/lib/services/activation-seats. Sondely's emails are ported from Python (Business Logic) |
||
google-invite.ts |
33 | Copy and strip | P3 | Invitation lookup through magic-link.ts kinds |
same |
google-return.ts |
17 | Copy as is | P3 | None | same |
google.ts |
142 | Copy as is | P3 | None | same |
guide-pay-visibility.ts |
32 | Leave behind | @/lib/private-features, guide pay |
||
history-visibility.ts |
24 | Leave behind | @/lib/audit/visibility, commission and guide pay gates |
||
index.ts |
666 | Copy and strip | P2 | @/lib/cross-company/fan-out, @/lib/mail/pilot, @/lib/services/onboarding, @/lib/services/view-as, @/lib/staff, @/lib/whatsapp/tower-pilot, ./view-as. Kept: getCurrentUser, runAsUser, AuthUser (without tourOperatorId) |
same |
invite-reminders.ts |
53 | Leave behind | TourCockpit's reminder cadence. Sondely keeps its own cadence until a product decision changes it | ||
joined.ts |
33 | Leave behind | First-join stamp with no Sondely counterpart | ||
magic-link.ts |
320 | Copy and strip | P3 | ./email, @/lib/email/send, @/lib/people/contact, @/lib/people/greeting-name, @/lib/i18n/recipient. TokenKind gains the one-time sign-in code; the storage and single use stay |
same |
next-path.ts |
66 | Copy and strip | P4b | The operator-request path; Sondely's landing paths | same |
operator-invite-limit.ts |
55 | Leave behind | Operator invites | ||
operator-request-token.ts |
115 | Leave behind | Operator request links | ||
password-policy.ts |
205 | Copy as is | P2 | None. Applies to passwords set after cutover only | same |
password.ts |
116 | Copy as is | P2 | None. A bcrypt verify for hashes migrated from Python is written new beside it and replaced by argon2 on first success | same |
profile-locale.ts |
56 | Copy and strip | P4b | @/lib/i18n/latest-choice; locale cookie names |
same |
rate-limit.ts |
30 | Copy as is | P2 | None. In memory, per replica: Sondely runs more than one replica (the plan's reminders note), so a shared store replaces it before public endpoints rely on it (inferred) | same |
read-only-switch.ts |
38 | Leave behind | Read-only members | ||
report-token.ts |
46 | Leave behind | Weekly WhatsApp report links | ||
reserved-domains.ts |
30 | Copy as is | P3 | None | same |
roles.ts |
91 | Copy and strip | P2 | ADMIN, MANAGER, OPERATOR predicates rewritten for Sondely's roles; the "one place" rule kept |
same |
session-cookie.ts |
107 | Copy and strip | P4b | ./brand-host until custom domains. Cutover keeps the bearer contract; the cookie arrives with the screens (Auth) |
same |
session.ts |
265 | Copy and strip | P2 | @/lib/people/contact, tourOperatorId on the membership. activeCompanyId becomes activeOrganizationId |
same |
share-token.ts |
45 | Leave behind | Board share tokens | ||
sign-in.ts |
138 | Copy and strip | P3 | ./company renamed; the LOGIN audit row and the one-function-for-every-door rule kept |
same |
signed-payload.ts |
94 | Copy as is | P2 | None | same |
switch-company.ts |
50 | Copy and strip | P4b | ./brand-host; switches organization |
server/src/lib/auth/switch-organization.ts |
tokens.ts |
11 | Copy as is | P2 | None | same |
view-as.ts |
22 | Leave behind | Manager view as operator |
Summary: 8 copy as is, 12 copy and strip, 1 later phase, 13 leave behind.
src/lib/mcp (12 files) and the routes that serve it
All MCP files are copied in P6. What each one becomes is in Agent Layer.
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
deps.ts |
427 | Copy and strip | P6 | @/lib/board/board-screenshot, @/lib/changelog/reader, @/lib/help/registry, @/lib/kpi, @/lib/assistant/changelog-search, @/lib/assistant/help-corpus, @/lib/assistant/transcribe (until P8a). Kept: the API over loopback as the caller |
same |
identity.ts |
152 | Copy and strip | P6 | @/lib/assistant/open-features (private features) |
same |
instructions.ts |
147 | Copy and strip | P6 | @/lib/kpi/definitions, @/lib/assistant/capabilities; rules come from the rewritten assistant/rules.ts |
same |
oauth/clients.ts |
158 | Copy as is | P6 | None | same |
oauth/metadata.ts |
58 | Copy as is | P6 | None | same |
oauth/request.ts |
85 | Copy as is | P6 | None | same |
oauth/seal.ts |
64 | Copy as is | P6 | None | same |
server.ts |
720 | Copy and strip | P6 | @/lib/services/daily-board, @/lib/kpi/dimensions, @/lib/kpi/window, ./ui/board-view, the propose_change replacement notice |
same |
tools.ts |
510 | Copy and strip | P6 | @/lib/board/file-ask, @/lib/help/registry, @/lib/kpi/question, @/lib/assistant/changelog-search, @/lib/assistant/deliver-file, @/lib/assistant/help-* |
same |
ui/board-view.ts and ui/board.generated.html |
3037 | Leave behind | The daily board as an MCP Apps component | ||
ui/views.ts |
93 | Later phase | after P7 | No imports. An interactive results view in Claude or ChatGPT is a later product choice | same |
Route files outside src/lib that come with P6: src/app/mcp/route.ts (cut @/lib/assistant/asker-on-crew), src/app/oauth/{authorize,register,revoke,token}/route.ts, src/app/oauth/consent/{page.tsx,actions.ts}, src/app/.well-known/oauth-authorization-server/[[...issuer]]/route.ts, src/app/.well-known/oauth-protected-resource/[[...resource]]/route.ts. Same target paths under server/.
src/lib/assistant (115 files)
Phases P6 to P8b; the survey rewrite of each piece is described in Agent Layer. Files renamed because they carry the persona or the word company are marked in Target.
The write path and its tools
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
actions.ts |
1208 | Copy and strip | P6 | @/components/profile/notification-copy, @/lib/board/booking-link, @/lib/board/language-coverage, @/lib/board/recipients, @/lib/private-features, @/lib/whatsapp/tower-access, @/lib/whatsapp/tower-visibility. Kept: validateWrite |
same |
call-api.ts |
651 | Copy and strip | P6 | @/lib/company/clock (the organization's time zone instead). Kept: GET only, registered routes only, RBAC per route |
same |
capabilities/types.ts |
68 | Copy as is | P6 | None (write-tools.ts imports it) |
same |
capabilities/index.ts |
21 | Copy and strip | P7 | The tour catalogue; survey capabilities are written new | same |
capabilities/find-departures.ts |
289 | Leave behind | @/lib/board/guest-fit, @/lib/names/tour-words |
||
capabilities/pickup-place.ts |
127 | Leave behind | @/lib/pickup/answer |
||
capabilities/price-for.ts |
158 | Leave behind | @/lib/services/prices |
||
capabilities/resolve.ts |
105 | Leave behind | Tour, hotel and zone resolution | ||
capabilities/who-changed.ts |
145 | Later phase | after P7 | @/lib/audit/relations, @/lib/board/booking-name. "Who changed this survey" over the Audit table |
same |
endpoint-map.ts |
373 | Copy and strip | P6 | @/lib/private-features; reads @/lib/api/examples content |
same |
look-ahead.ts |
572 | Copy and strip | P6 | @/lib/names/language-words, @/lib/names/tour-words, @/lib/pickup/answer, @/lib/rules. Kept: names matched to ids for ref fields. @/lib/names/match (350 lines, no imports) is copied as is with it |
same |
named-ids.ts |
149 | Copy and strip | P6 | Tour id fields (driverId, tourOperatorId) |
same |
plan-readings.ts |
96 | Leave behind | @/lib/money, @/lib/company/opening-hours, @/lib/pay/rate-words. Its import from proposal-actions.ts is cut |
||
proposal-actions.ts |
1158 | Copy and strip | P6 | ./change-confirmation, ./plan-readings, ./rep-row, @/lib/money, @/lib/i18n/service-errors (tour messages). Kept: propose, dry run, commit once with an idempotency key |
same |
proposal-card.ts |
147 | Copy and strip | P6 | @/lib/board/rep-choice, @/lib/reference/child-seat, @/lib/services/daily-board. The card shape is rewritten for a survey draft |
same |
proposal-form.ts |
90 | Later phase | after P4b | @/lib/board/booking-form, @/lib/services/{child-seats,pickup-time-options,reference,reps,schedule}. Edit on the card opens the builder once the builder is in Next |
same |
refused-before.ts |
78 | Copy as is | P6 | None | same |
rep-row.ts |
24 | Leave behind | @/lib/rules/rep |
||
write-tool-names.ts |
99 | Copy and strip | P6 | The tool name list (create_booking, add_departure and the rest) |
same |
write-tools.ts |
634 | Copy and strip | P6 | Tour wording in tool descriptions; the generator from the registry is kept unchanged | same |
writes-together.ts |
144 | Copy as is | P7 | None | same |
written.ts |
57 | Copy as is | P6 | None | same |
Refusals
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
refusal-legibility.ts |
268 | Copy and strip | P5 | @/lib/whatsapp/note-cap-words; every row of LEGIBILITY_BY_CODE, LEGIBILITY_BY_INVALID_REASON, LEGIBILITY_BY_DOOR_CODE rewritten for Sondely codes. Kept: the four classes and the typing that refuses an unclassed code |
same |
refusal-fixes.ts |
151 | Copy and strip | P5 | Tour fixes (the smallest free vehicle and the rest) | same |
write-refusal.ts |
37 | Copy and strip | P7 | @/lib/i18n/common-words, @/lib/validation |
same |
answer-guards.ts |
252 | Copy and strip | P7 | The tour assertions it checks (guides, departures); the guard mechanism kept | same |
promise-guard.ts |
54 | Copy as is | P7 | None | same |
The turn, the prompt and the drawer
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
access.ts |
47 | Copy as is | P7 | None | same |
agent.ts |
2022 | Copy and strip | P7 | @/lib/board/file-ask, @/lib/help/registry, @/lib/kpi/{dimensions,question,window}, @/lib/services/company, ./changelog-search, ./find-change, ./help-*, ./tool-set-store, ./capabilities tour entries, ./asker-on-crew. The largest strip in the copy |
same |
audience.ts |
16 | Copy and strip | P6 | @/lib/help/registry. Office or desk becomes builder or respondent |
same |
azure.ts |
76 | Copy as is | P7 | None | same |
changes-made.ts |
123 | Copy as is | P7 | None | same |
config.ts |
135 | Copy and strip | P6 | @/lib/company-features, @/lib/staff; the persona name |
same |
conversation-kind.ts |
115 | Copy as is | P7 | None | same |
conversation-since.ts |
21 | Copy as is | P7 | None | same |
delivered.ts |
187 | Copy as is | P7 | None. Who a send reached, line per person: fits invitation sends | same |
door.ts |
88 | Copy as is | P7 | None | same |
earlier-reads.ts |
183 | Copy as is | P7 | None | same |
events.ts |
105 | Copy as is | P7 | None | same |
faro-rules.ts |
473 | Copy and strip | P6 | @/lib/help/registry; every rule text rewritten (the structure, one export per principle, is kept) |
server/src/lib/assistant/rules.ts |
faro-view.ts |
12 | Copy and strip | P7 | View names | server/src/lib/assistant/history-view.ts |
language.ts |
34 | Copy as is | P7 | None | same |
memory.ts |
92 | Copy as is | P7 | None | same |
model-calls.ts |
181 | Copy as is | P7 | None | same |
names-in-query.ts |
70 | Copy and strip | P7 | Tour filters (vehicleId, tourOperatorId) |
same |
profile.ts |
178 | Copy as is | P7 | None | same |
prompt.ts |
299 | Copy and strip | P7 | Tour sections of the prompt | same |
rate-limit.ts |
28 | Copy as is | P7 | None | same |
retold.ts |
78 | Copy as is | P7 | None (turn.ts and the assistant service import it) |
same |
role-scope.ts |
67 | Copy and strip | P7 | ./tool-set-store (until it is needed), ./capabilities |
same |
screens.ts |
82 | Copy and strip | P7 | @/lib/i18n/nav-label; the screen list grows as screens move |
same |
service-tier.ts |
18 | Copy as is | P7 | None | same |
starters.ts |
146 | Copy and strip | P7 | Tour starter questions | same |
step-watch.ts |
54 | Copy as is | P7 | None | same |
stub.ts |
176 | Copy and strip | P7 | Tour fixtures. The no-model mode every CI test runs on | same |
tool-bytes.ts |
40 | Copy as is | P7 | None | same |
transcript.ts |
303 | Copy as is | P7 | None | same |
turn-context.ts |
77 | Copy as is | P7 | None | same |
turn-trace.ts |
206 | Copy as is | P7 | None | same |
turn.ts |
597 | Copy and strip | P7 | @/lib/board/board-screenshot, @/lib/changelog/reader, @/lib/company/clock, ./call-company, ./change-confirmation, ./changelog-search, ./deliver-file, ./open-features |
same |
where-now.ts |
143 | Copy as is | P7 | None | same |
widgets.ts |
121 | Copy and strip | P7 | Board widgets; results widgets take their place | same |
working-notes.ts |
119 | Copy as is | P7 | None | same |
Organizations (TourCockpit's companies)
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
company-binding.ts |
207 | Copy and strip | P6 | Sandbox naming. One connection, every organization, one at a time | server/src/lib/assistant/org-binding.ts |
company-list.ts |
104 | Copy and strip | P6 | Rename only | server/src/lib/assistant/org-list.ts |
company-names.ts |
202 | Copy and strip | P7 | The tour names fed to the voice model's hearing | server/src/lib/assistant/org-names.ts |
company-switch.ts |
175 | Copy and strip | P7 | @/lib/auth/switch-company (renamed) |
server/src/lib/assistant/org-switch.ts |
asker-on-crew.ts |
41 | Leave behind | @/lib/whatsapp/number-key, the crew roster |
||
open-features.ts |
32 | Leave behind | @/lib/private-features, @/lib/whatsapp/tower-*, @/lib/auth/guide-pay-visibility. Cut from mcp/identity.ts and turn.ts |
Help, changelog, tool index, mail
| File | Lines | Verdict | Phase | Reason | Target |
|---|---|---|---|---|---|
help-chunks.ts, help-corpus.ts, help-index.ts, help-index.generated.json |
327 | Later phase | when Sondely has a help center | @/lib/help/* content |
same |
changelog-search.ts |
128 | Leave behind | @/lib/changelog/*; Sondely has no What's new |
||
find-change.ts, tool-index.ts, tool-index.generated.json, tool-set-store.ts |
682 | Later phase | when the write tools outgrow the prompt budget | TourCockpit has 87 write tools and picks a subset per turn; Sondely's set is small enough to send whole (inferred from the route count in API Routes) | same |
deliver-file.ts |
106 | Later phase | with exports | POST /api/daily-board/send-to-me; becomes the CSV or PDF export to the person |
same |
mail/act.ts, mail/agent.ts, mail/prompt.ts, mail/reader.ts, mail/record.ts |
756 | Leave behind | Operator booking mail. The pattern (a model with no tools extracts, code acts) may suit importing a survey from a document; inferred | ||
change-confirmation.ts |
101 | Leave behind | @/lib/company/recipient-guard, @/lib/services/whatsapp, @/lib/whatsapp/senders: the office's written confirmation of a change made on a call |
||
whatsapp-pass.ts |
100 | Leave behind | @/lib/whatsapp/needs-office, @/lib/whatsapp/office-promise: a desk handing over to the office |
WhatsApp and voice
| File | Lines | Verdict | Phase | TourCockpit imports and content to cut | Target |
|---|---|---|---|---|---|
whatsapp.ts |
846 | Copy and strip | P8a | ./whatsapp-pass, @/lib/whatsapp/{door-closed,faro-intro}, @/lib/company/recipient-guard, @/lib/company-features, @/lib/services/whatsapp-notes, @/lib/board/board-screenshot. Kept: number as credential, one conversation per channel, unknown number gets a log line and no reply. @/lib/whatsapp/{wire,senders,number-key} and @/lib/services/{whatsapp,whatsapp-media} come with it, stripped of the office inbox |
same |
call-limits.ts |
78 | Copy as is | P8a | None | same |
transcribe.ts |
192 | Copy as is | P8a | None. Respondents answer by voice note | same |
whatsapp-call.ts |
670 | Copy and strip | P8b | @/lib/kpi, @/lib/board/board-screenshot, ./company-switch, @/lib/auth/brand-host |
same |
voice.ts |
410 | Copy and strip | P8b | The call instructions; ask_faro becomes the survey ask tool |
same |
voice-live.ts |
232 | Copy as is | P8b | None | same |
voice-ask-client.ts |
51 | Copy and strip | P8b | @/lib/board/board-screenshot |
same |
voice-character.ts |
34 | Copy and strip | P8b | Faro's character text | same |
call-controller.ts |
776 | Copy and strip | P8b | The greeting and nudge wording it imports; the transport-free rules kept | same |
call-delivery.ts |
197 | Copy and strip | P8b | Board list reading rules; question reading rules instead | same |
call-nudge.ts |
62 | Copy and strip | P8b | Nudge words (catalog keys) | same |
opening.ts |
128 | Copy and strip | P8b | The office greeting | same |
call-audio.ts |
90 | Copy as is | P8b | None | same |
call-audio-response.ts |
67 | Copy as is | P8b | None (@/lib/http/range copied with it) |
same |
call-id.ts |
14 | Copy as is | P8b | None | same |
call-mix.ts |
205 | Copy as is | P8b | None | same |
call-recorder.ts |
108 | Copy as is | P8b | None | same |
call-relay.ts |
453 | Copy as is | P8b | None | same |
live-calls.ts |
86 | Copy as is | P8b | None | same |
recording-check.ts |
162 | Copy as is | P8b | None | same |
recording-sound.ts |
73 | Copy as is | P8b | None | same |
replay.ts |
215 | Copy as is | P8b | None | same |
reply-recovery.ts |
248 | Copy as is | P8b | None | same |
said-since-ask.ts |
83 | Copy as is | P8b | None | same |
until-quiet.ts |
112 | Copy as is | P8b | None | same |
web-call.ts |
201 | Later phase | after P8b | A call from the survey page; WhatsApp calls come first | same |
call-audio-split.ts |
83 | Leave behind | A call that changes company mid-call | ||
call-company.ts |
109 | Leave behind | Same | ||
call-part-intervals.ts |
93 | Leave behind | Same |
Summary for the assistant directory, counted by file including the two generated JSON indexes: 44 copy as is, 40 copy and strip, 12 later phase, 19 leave behind. By lines, 19,100 of its 22,900 lines are copied in some form (5,300 as is, 13,800 copied and stripped), 2,200 are left behind and 1,550 wait for a later phase. The plan's estimate that about a third of the assistant code is reusable is about what survives: the copy and strip files keep their mechanism and lose their tour wording, and how many of their 13,800 lines remain is known only after the strip.
Services and support files outside the listed directories
The copied directories import these. Each comes with the phase of its first importer.
| File | Lines | Verdict | Phase | Cut | Target |
|---|---|---|---|---|---|
src/lib/db.ts |
446 | Copy and strip | P2 | None beyond renames. Two clients: db on the constrained app role with the tenant extension and RLS, dbAdmin on the owner role for pre-auth reads and seeding. Holds the write_gate lock |
same |
src/lib/prisma.ts |
3 | Copy as is | P2 | None | same |
src/lib/perf/prisma-timing.ts, request-timing.ts |
160 | Copy as is | P2 | None | same |
src/lib/audit/diff.ts |
93 | Copy as is | P2 | None | same |
src/lib/services/errors.ts |
514 | Copy and strip | P2 | @/lib/board/slots; the 65 tour codes of ServiceErrorParamMap. The err() helper, ServiceError and ServiceErrorParamMap typing kept |
same |
src/lib/i18n/service-errors.ts |
511 | Copy and strip | P2 | Tour messages; see Translation | same |
src/lib/validation/index.ts |
2014 | Leave behind, except the pieces copied | P2 | Tour schemas. The page schema, the batch schema and the shared helpers are written into Sondely's validation module | |
src/lib/names/match.ts |
350 | Copy as is | P6 | None | same |
src/lib/http/range.ts |
24 | Copy as is | P8b | None | same |
src/lib/services/assistant.ts, assistant-proposals.ts, assistant-memory.ts, assistant-profile.ts |
1269 | Copy and strip | P6 and P7 | ./company in the profile service |
same |
src/lib/services/assistant-recordings.ts |
253 | Copy as is | P8b | None | same |
src/lib/services/mcp-connections.ts |
606 | Copy and strip | P6 | @/lib/company-features, @/lib/private-features, @/lib/people/contact |
same |
src/lib/errorReporter.ts, src/lib/deployment.ts |
409 | Copy as is | P4b | None. The browser error forwarder, with the screens | same |
Prisma models that come along
From prisma/schema.prisma at the base. Every model with a company becomes organization scoped and joins TENANT_MODELS, so the tenant isolation and coverage tests cover it. Sondely's own models are in Data Model.
| TourCockpit model | Sondely model | Phase | Change |
|---|---|---|---|
IdempotencyKey |
IdempotencyKey |
P2 | companyId to organizationId; unique on (organization, key) kept |
Audit and enum AuditSource |
Audit |
P2 | role takes Sondely's role enum; actingAsOperatorId cut; actorKind, apiKeyId, requestId, revertsRequestId kept |
WriteGate |
WriteGate |
P2 | Unchanged: an empty table the serializable writes lock |
Session |
Session |
P2 | activeCompanyId to activeOrganizationId. Replaces the Redis sessions |
MagicLinkToken and enum TokenKind |
VerificationToken |
P3 | Kinds: set password, reset, plus the one-time sign-in code and email verification. Hash only, single use, expiry per kind kept |
ApiKey |
ApiKey |
P2 | Bound to one organization; role becomes a scope (read, draft, full); tourOperatorId cut |
UserCompany |
OrganizationMember |
P2 | tourOperatorId, boardExportPrefs, readOnly, sellsDirect cut |
OutboundSend |
Decided in Business Logic | P3 | Only if invitations and reminders move to an outbox |
McpGrant, McpConnection |
Same names | P6 | Company to organization |
AssistantConversation, AssistantMessage, AssistantProposal |
Same names | P6 | channel on the conversation gains the respondent channels; costUsd on the message is filled from the first day |
AssistantCallRecording, AssistantLiveCall |
Same names | P8b | Company to organization |
Row-level security comes with the models: 45 TourCockpit migrations enable RLS, with policies on current_setting('app.tenant_id'). Sondely's first migration writes the same policy for every organization-scoped table rather than copying the 45 migrations, whose tables are tour tables.
Test harness
| File | Lines | Verdict | Phase | Cut | Target |
|---|---|---|---|---|---|
vitest.config.ts |
32 | Copy as is | P2 | None | server/vitest.config.ts |
vitest.integration.config.ts |
102 | Copy and strip | P2 | The real-time-first sequencer for call files stays dormant until P8b; test database names | server/ same |
vitest.component.config.ts |
35 | Copy as is | P4b | None | server/ same |
vitest.coverage.ts |
42 | Copy and strip | P2 | The help and page content excludes | server/ same |
coverage-floors.json, scripts/coverage-merge.ts, scripts/coverage-changed.ts |
440 | Copy and strip | P2 | Layer paths; the floors are Sondely's call (TourCockpit: 85 for the repository) | server/ same |
playwright.config.ts |
242 | Copy and strip | P4 | _fake-google, Faro desk people, port 7961 (Sondely takes its own from the port registry) |
server/ same |
playwright.call-sim.config.ts |
56 | Copy and strip | P8b | Ports 7975 and 7976 | server/ same |
.eslintrc.json |
107 | Copy and strip | P2 | The react/jsx-no-literals allowed strings (GUIA, CHOFER, PAX and the rest) |
server/ same |
tsconfig.json, tsconfig.scripts.json |
38 | Copy as is | P2 | None | server/ same |
tests/stubs/server-only.ts |
3 | Copy as is | P2 | None | same |
tests/component/setup.ts, tests/component/server-only-stub.ts |
102 | Copy as is | P4b | None | same |
tests/integration/global-setup.ts |
127 | Copy and strip | P2 | tourplatform_test template name |
same |
tests/integration/per-worker-db.ts |
61 | Copy and strip | P2 | Database names | same |
tests/integration/setup.ts |
41 | Copy and strip | P2 | The truncate list | same |
tests/integration/_parts.ts |
24 | Copy and strip | P2 | The calls part until P8b | same |
tests/integration/helpers.ts |
364 | Copy and strip | P2 | @/lib/reference/kalitravel, @/lib/notifications/register; TestTenant becomes an organization with an owner, an admin and a respondent |
same |
tests/integration/api-helpers.ts |
84 | Copy and strip | P2 | Operator keys; mints org-bound scoped keys | same |
tests/integration/audit-helpers.ts |
31 | Copy as is | P2 | None | same |
tests/integration/tenant-isolation.test.ts |
91 | Copy and strip | P2 | @/lib/services/{bookings,daily-board,users}. Rewritten: a member of organization A cannot read, edit, publish or delete a known survey, session or response id of B, and gets not found |
same |
tests/integration/tenant-coverage.test.ts |
130 | Copy as is | P2 | None. Fails when a model with organizationId is missing from TENANT_MODELS |
same |
tests/integration/audit-isolation.test.ts |
133 | Copy and strip | P2 | @/lib/services/bookings |
same |
tests/integration/dual-membership-isolation.test.ts |
240 | Copy and strip | P2 | One user in two organizations, which is Sondely's normal case | same |
tests/integration/global-email-uniqueness.test.ts |
118 | Copy and strip | P3 | @/lib/auth/email mock |
same |
tests/unit/no-raw-tenant-sql.test.ts |
55 | Copy as is | P2 | The two reviewed exceptions are re-listed | same |
tests/unit/refusal-legibility.test.ts |
198 | Copy and strip | P5 | Tour codes in fixtures | same |
tests/unit/refusal-fixes.test.ts |
115 | Copy and strip | P5 | Tour fixes | same |
tests/unit/refusal-sites.test.ts, refusal-sites.json |
170 | Copy and strip | P5 | The site list starts empty and is regenerated | same |
tests/fixtures/refusal-probes.ts, service-error-samples.ts |
179 | Copy and strip | P5 | Tour samples; typed against the new code list, so they do not compile until every code is sampled | same |
tests/integration/refusal-probes.test.ts |
48 | Copy and strip | P5 | @/app/api/v1/{bookings,reference/drivers,tours}/route |
same |
tests/integration/rule-matrix.test.ts |
256 | Copy and strip | P5 | Booking rules; becomes the survey validation matrix | server/tests/integration/survey-matrix.test.ts |
tests/unit/faro-rules-parity.test.ts |
260 | Copy and strip | P7 | @/lib/kpi, @/lib/kpi/definitions |
server/tests/unit/rules-parity.test.ts |
tests/unit/mcp-parity.test.ts |
139 | Copy as is | P7 | None | same |
tests/unit/ui-api-parity.test.ts, ui-api-parity.scan.ts, ui-read-parity.test.ts, ui-read-parity.scan.ts |
515 | Copy and strip | P4b | Tour screen expectations | same |
tests/e2e/global-setup.ts, prepare-db.ts |
110 | Copy and strip | P4 | Operational table list | same |
tests/e2e/seed-e2e-users.ts |
617 | Copy and strip | P4 | src/lib/reference/kalitravel, src/lib/help/onboarding |
same |
tests/e2e/faro-matrix.ts, assistant-matrix.spec.ts, faro-personas.ts, faro-jobs.ts, faro-legibility.ts, legibility-rubric.ts, assistant-legibility.spec.ts, tests/fixtures/legibility-calibration.ts, tests/unit/faro-matrix-grade.test.ts, tests/unit/legibility-rubric.test.ts |
2791 | Copy and strip | P7 | Booking cells, personas and jobs; the grader and the judge calibration are kept | faro- becomes survey- in names |
tests/call-sim/meta.ts |
51 | Copy as is | P8a | None. Signs a webhook the way Meta does, so WhatsApp text tests go through the real route | same |
tests/call-sim/caller.ts, fake-voice-live.ts, opus.ts, caller-wav.ts, heard-tone.ts |
798 | Copy as is | P8b | None | same |
tests/integration/_whatsapp-call-simulated.ts |
1827 | Copy and strip | P8b | @/app/api/v1/batch/route, @/lib/services/bookings, @/lib/whatsapp/classify, ./_tower-gate, booking scenarios |
same |
tests/call-sim-e2e/web-call.spec.ts |
214 | Later phase | with web-call.ts |
same | |
tests/visual/* (usability capture, contrast, rubric) |
1449 | Copy and strip | P4b | Tour routes and rubric wording | same |
CI workflows and scripts
Workflows live at the repository root (.github/workflows/), where GitHub reads them, and replace Sondely's current ci.yml, deploy.yml and deploy-prod.yml as each phase lands. TourCockpit runs on the IdeaPlaces nous runner pool; Sondely's jobs move to CatalyzeUp's own self-hosted runner (labels self-hosted, catalyzeup-linux, as used by catalyzeup-docs; that it can carry Postgres and Playwright jobs is inferred). Ideaplaces/discord-ci-status is used from another organization's repository, which needs the action to be readable from CatalyzeUp (inferred, check before relying on it).
| File | Lines | Verdict | Phase | Cut | Target |
|---|---|---|---|---|---|
.github/workflows/ci.yml |
928 | Copy and strip | P2, growing | Jobs scope and films (help films), e2e-webkit until P4b, integration-calls until P8b. Kept from P2: static (typecheck, lint), unit, integration, integration-migrations, verify (coverage merge, changed lines, Discord). e2e from P4, component and usability from P4b. migration-check and migration-rehearsal from P4, once there is a production database to copy |
.github/workflows/ci.yml |
.github/workflows/deploy-dev.yml, deploy.yml |
469 | Copy and strip | P4 | The red-night gate until P7; the drain job until P8b; image and app names |
same |
.github/workflows/drain-revisions.yml |
50 | Copy as is | P8b | None. Keeps a deploy from cutting a live call | same |
.github/workflows/faro-matrix.yml |
340 | Copy and strip | P7 | Booking matrix; skipped on nights with no change, as TourCockpit does | .github/workflows/survey-matrix.yml |
.github/workflows/voice-scenarios.yml |
132 | Copy and strip | P8b | Faro scenarios | same |
.github/workflows/webkit-nightly.yml |
110 | Copy as is | P4b | None. Respondents are on phones, Safari included | same |
.github/workflows/mail-eval.yml |
100 | Leave behind | Operator mail golden set | ||
scripts/ci/no-ai-attribution.mjs, parallel-checks.sh, npm-install.sh |
Copy as is | P2 | None | server/scripts/ci/ |
|
scripts/ci/migration-rehearsal.sh, e2e-build.ts, wait-for-e2e-build.mjs |
Copy and strip | P4 | Database and app names | server/scripts/ci/ |
|
scripts/ci/faro-matrix-changed.sh, nightly-gate.sh |
Copy and strip | P7 | The paths that count as an assistant change | server/scripts/ci/ |
|
scripts/ci/drain-revisions.mjs |
Copy as is | P8b | None | server/scripts/ci/ |
|
scripts/ci/clip-bump-only.sh |
Leave behind | Help clips |
Repository agents
TourCockpit's agents live in .claude/agents/. Each copy is renamed, its paths and spec links pointed at Sondely and these docs, and its incident history kept only where the rule still applies.
| TourCockpit agent | Verdict | Phase | Sondely agent |
|---|---|---|---|
tour-platform-legibility.md |
Copy and strip | P5 | sondely-legibility.md: reads every API contract change the way an outside agent meets it |
tour-platform-ui.md |
Copy and strip | P4b | sondely-ui.md: desktop and phone render parity |
tour-platform-components.md |
Copy and strip | P4b | sondely-components.md: each UI element built once |
tour-platform-translator.md, tour-platform-translate.md |
Copy and strip | P4b | sondely-translator.md, sondely-translate.md (Translation) |
tour-platform-calls.md, tour-platform-voice-architect.md |
Copy and strip | P8b | sondely-calls.md, sondely-voice-architect.md |
tour-platform-help.md, tour-platform-help-judge.md, tour-platform-week.md |
Leave behind | Sondely has no help center or What's new yet | |
tour-platform-insights.md, tour-platform-insights-toolsmith.md |
Later phase | after P7 | Once Sondely has insight tools over results |
What this manifest does not cover
- The translation catalogs and scripts (
i18n/,scripts/i18n/): Translation. - The route handlers under
src/app/api/v1/: none is copied. Sondely's routes are written new on the copied plumbing (API Routes). - UI components under
src/components/: chosen screen by screen in Frontend Screens. - Infrastructure (Postgres server, Key Vault, container apps): Infrastructure.