feat(identity): lock NoteLett product identity across all surfaces

- productId: notelett
- displayName: NoteLett
- domain: notelett.app
- iOS bundle: com.bytelyst.notelett
- Android bundle: com.notelett.app
- backend port: 4016
- token namespace: --nl-* (CSS), NoteLettTheme (native)

Rippled through:
- shared/product.json (canonical source)
- backend package.json, config, cosmos-init, all 10 test files
- web package.json, landing page, notes-client test
- mobile app.json, package.json, auth screen
- docs: PRD, ROADMAP, architecture review, foundations, web/mobile roadmaps
- registered in learning_ai_common_plat/products/notelett/

Verification: backend typecheck + 18 tests, web typecheck + 6 tests, mobile typecheck — all pass.
This commit is contained in:
saravanakumardb1 2026-03-10 18:47:01 -07:00
parent e6beef83eb
commit e1fde25afd
25 changed files with 63 additions and 55 deletions

View File

@ -1,8 +1,8 @@
{ {
"name": "@bytelyst-notes/backend", "name": "@notelett/backend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"description": "ByteLyst Agentic Notes product-specific backend — notes, workspaces, relationships, tasks, artifacts, agent actions", "description": "NoteLett product backend — notes, workspaces, relationships, tasks, artifacts, agent actions",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/server.ts", "dev": "tsx watch src/server.ts",

View File

@ -6,7 +6,7 @@ const envSchema = z.object({
HOST: z.string().default('0.0.0.0'), HOST: z.string().default('0.0.0.0'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
CORS_ORIGIN: z.string().optional(), CORS_ORIGIN: z.string().optional(),
SERVICE_NAME: z.string().default('bytelyst-notes-backend'), SERVICE_NAME: z.string().default('notelett-backend'),
COSMOS_ENDPOINT: z.string().min(1, 'COSMOS_ENDPOINT is required').optional(), COSMOS_ENDPOINT: z.string().min(1, 'COSMOS_ENDPOINT is required').optional(),
COSMOS_KEY: z.string().min(1, 'COSMOS_KEY is required').optional(), COSMOS_KEY: z.string().min(1, 'COSMOS_KEY is required').optional(),
COSMOS_DATABASE: z.string().default('bytelyst'), COSMOS_DATABASE: z.string().default('bytelyst'),

View File

@ -21,9 +21,9 @@ export async function initCosmosIfNeeded(): Promise<void> {
try { try {
await initializeAllContainers(); await initializeAllContainers();
process.stdout.write('[bytelyst-notes-backend] Cosmos containers ensured\n'); process.stdout.write('[notelett-backend] Cosmos containers ensured\n');
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`[bytelyst-notes-backend] Cosmos init failed: ${msg}\n`); process.stderr.write(`[notelett-backend] Cosmos init failed: ${msg}\n`);
} }
} }

View File

@ -8,7 +8,7 @@ const { listNotesMock, getNoteMock, createNoteMock, createNoteAgentActionMock }
createNoteAgentActionMock: vi.fn(), createNoteAgentActionMock: vi.fn(),
})); }));
vi.mock('../lib/product-config.js', () => ({ PRODUCT_ID: 'bytelyst-notes' })); vi.mock('../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('../modules/notes/repository.js', () => ({ vi.mock('../modules/notes/repository.js', () => ({
listNotes: listNotesMock, listNotes: listNotesMock,
getNote: getNoteMock, getNote: getNoteMock,
@ -23,7 +23,7 @@ import { NotesExecutableMcpTools, getNotesExecutableMcpTool } from './note-tools
const req = { const req = {
id: 'req_1', id: 'req_1',
headers: {}, headers: {},
jwtPayload: { sub: 'user_1', role: 'admin', productId: 'bytelyst-notes' }, jwtPayload: { sub: 'user_1', role: 'admin', productId: 'notelett' },
log: { log: {
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
@ -51,7 +51,7 @@ describe('note executable MCP tools', () => {
items: [ items: [
{ {
id: 'note_1', id: 'note_1',
productId: 'bytelyst-notes', productId: 'notelett',
workspaceId: 'ws_1', workspaceId: 'ws_1',
userId: 'user_1', userId: 'user_1',
title: 'Draft', title: 'Draft',
@ -74,7 +74,7 @@ describe('note executable MCP tools', () => {
req req
); );
expect(listNotesMock).toHaveBeenCalledWith('user_1', 'bytelyst-notes', { expect(listNotesMock).toHaveBeenCalledWith('user_1', 'notelett', {
workspaceId: 'ws_1', workspaceId: 'ws_1',
limit: 10, limit: 10,
offset: 0, offset: 0,
@ -87,7 +87,7 @@ describe('note executable MCP tools', () => {
items: [ items: [
{ {
id: 'note_1', id: 'note_1',
productId: 'bytelyst-notes', productId: 'notelett',
workspaceId: 'ws_1', workspaceId: 'ws_1',
userId: 'user_1', userId: 'user_1',
title: 'Retention policy', title: 'Retention policy',
@ -119,7 +119,7 @@ describe('note executable MCP tools', () => {
it('gets a scoped note', async () => { it('gets a scoped note', async () => {
getNoteMock.mockResolvedValue({ getNoteMock.mockResolvedValue({
id: 'note_1', id: 'note_1',
productId: 'bytelyst-notes', productId: 'notelett',
workspaceId: 'ws_1', workspaceId: 'ws_1',
userId: 'user_1', userId: 'user_1',
title: 'Note', title: 'Note',
@ -180,7 +180,7 @@ describe('note executable MCP tools', () => {
expect(createNoteAgentActionMock).toHaveBeenCalledTimes(1); expect(createNoteAgentActionMock).toHaveBeenCalledTimes(1);
expect(createNoteAgentActionMock).toHaveBeenCalledWith( expect(createNoteAgentActionMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
productId: 'bytelyst-notes', productId: 'notelett',
workspaceId: 'ws_1', workspaceId: 'ws_1',
userId: 'user_1', userId: 'user_1',
actorId: 'agent_1', actorId: 'agent_1',

View File

@ -6,7 +6,7 @@ const { extractAuthMock } = vi.hoisted(() => ({
})); }));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock })); vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'bytelyst-notes' })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('./repository.js', () => ({ vi.mock('./repository.js', () => ({
listNoteAgentActions: vi.fn(async () => ({ items: [], total: 0 })), listNoteAgentActions: vi.fn(async () => ({ items: [], total: 0 })),
getNoteAgentAction: vi.fn(async () => null), getNoteAgentAction: vi.fn(async () => null),

View File

@ -6,7 +6,7 @@ const { extractAuthMock } = vi.hoisted(() => ({
})); }));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock })); vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'bytelyst-notes' })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('./repository.js', () => ({ vi.mock('./repository.js', () => ({
listNoteArtifacts: vi.fn(async () => ({ items: [], total: 0 })), listNoteArtifacts: vi.fn(async () => ({ items: [], total: 0 })),
getNoteArtifact: vi.fn(async () => null), getNoteArtifact: vi.fn(async () => null),

View File

@ -6,7 +6,7 @@ const { extractAuthMock } = vi.hoisted(() => ({
})); }));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock })); vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'bytelyst-notes' })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('./repository.js', () => ({ vi.mock('./repository.js', () => ({
listRelationships: vi.fn(async () => ({ items: [], total: 0 })), listRelationships: vi.fn(async () => ({ items: [], total: 0 })),
createRelationship: vi.fn(async (doc: unknown) => doc), createRelationship: vi.fn(async (doc: unknown) => doc),

View File

@ -6,7 +6,7 @@ const { extractAuthMock } = vi.hoisted(() => ({
})); }));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock })); vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'bytelyst-notes' })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('./repository.js', () => ({ vi.mock('./repository.js', () => ({
listNoteTasks: vi.fn(async () => ({ items: [], total: 0 })), listNoteTasks: vi.fn(async () => ({ items: [], total: 0 })),
getNoteTask: vi.fn(async () => null), getNoteTask: vi.fn(async () => null),

View File

@ -16,7 +16,7 @@ const {
})); }));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock })); vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'bytelyst-notes' })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('./repository.js', () => ({ vi.mock('./repository.js', () => ({
listNotes: listNotesMock, listNotes: listNotesMock,
getNote: getNoteMock, getNote: getNoteMock,

View File

@ -6,7 +6,7 @@ const { extractAuthMock } = vi.hoisted(() => ({
})); }));
vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock })); vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'bytelyst-notes' })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('./repository.js', () => ({ vi.mock('./repository.js', () => ({
listWorkspaces: vi.fn(async () => ({ items: [], total: 0 })), listWorkspaces: vi.fn(async () => ({ items: [], total: 0 })),
getWorkspace: vi.fn(async () => null), getWorkspace: vi.fn(async () => null),

View File

@ -17,7 +17,7 @@ vi.mock('@bytelyst/fastify-core', () => ({
})); }));
vi.mock('jose', () => ({ vi.mock('jose', () => ({
jwtVerify: vi.fn(async () => ({ payload: { sub: 'user_1', productId: 'bytelyst-notes' } })), jwtVerify: vi.fn(async () => ({ payload: { sub: 'user_1', productId: 'notelett' } })),
})); }));
vi.mock('./modules/note-agent-actions/routes.js', () => ({ noteAgentActionRoutes: vi.fn() })); vi.mock('./modules/note-agent-actions/routes.js', () => ({ noteAgentActionRoutes: vi.fn() }));
@ -30,14 +30,14 @@ vi.mock('./lib/cosmos-init.js', () => ({ initCosmosIfNeeded: initCosmosIfNeededM
vi.mock('./lib/datastore.js', () => ({ initDatastore: initDatastoreMock })); vi.mock('./lib/datastore.js', () => ({ initDatastore: initDatastoreMock }));
vi.mock('./lib/config.js', () => ({ vi.mock('./lib/config.js', () => ({
config: { config: {
SERVICE_NAME: 'bytelyst-notes-backend', SERVICE_NAME: 'notelett-backend',
CORS_ORIGIN: '*', CORS_ORIGIN: '*',
PORT: 4016, PORT: 4016,
HOST: '0.0.0.0', HOST: '0.0.0.0',
JWT_SECRET: 'test-secret', JWT_SECRET: 'test-secret',
}, },
})); }));
vi.mock('./lib/product-config.js', () => ({ DISPLAY_NAME: 'ByteLyst Agentic Notes' })); vi.mock('./lib/product-config.js', () => ({ DISPLAY_NAME: 'NoteLett' }));
describe('server bootstrap', () => { describe('server bootstrap', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -1,4 +1,4 @@
# ByteLyst Agentic Notes — Architecture Review, Gap Analysis, and Reuse-First Action Plan # NoteLett — Architecture Review, Gap Analysis, and Reuse-First Action Plan
Version: 1.0 Version: 1.0
Date: March 10, 2026 Date: March 10, 2026

View File

@ -1,4 +1,4 @@
Product Requirements Document: ByteLyst Agentic Notes Product Requirements Document: NoteLett
Version: 2.0 Version: 2.0
Date: March 10, 2026 Date: March 10, 2026
@ -7,7 +7,7 @@ Status: Draft
# 1. Product Vision # 1. Product Vision
ByteLyst Agentic Notes is a knowledge product for humans and AI agents to capture, structure, retrieve, and operationalize notes in a shared system of record. NoteLett is a knowledge product for humans and AI agents to capture, structure, retrieve, and operationalize notes in a shared system of record.
This is not just a notes UI with an AI chat panel bolted on. It is a product-specific knowledge application that fits the existing ByteLyst ecosystem: This is not just a notes UI with an AI chat panel bolted on. It is a product-specific knowledge application that fits the existing ByteLyst ecosystem:

View File

@ -1,4 +1,4 @@
# ByteLyst Agentic Notes — Master Roadmap # NoteLett — Master Roadmap
Version: 2.0 Version: 2.0
Date: March 10, 2026 Date: March 10, 2026

View File

@ -60,9 +60,9 @@ Make the product implementation-ready and eliminate ambiguity before parallel co
- 2026-03-10 — Phase 0 decisions were partially locked to unblock implementation. - 2026-03-10 — Phase 0 decisions were partially locked to unblock implementation.
- The repo now has active `web/`, `backend/`, and `shared/` surfaces instead of a docs-only structure. - The repo now has active `web/`, `backend/`, and `shared/` surfaces instead of a docs-only structure.
- Provisional bootstrap values are in use for implementation: - Provisional bootstrap values are in use for implementation:
- product display name: `ByteLyst Agentic Notes` - product display name: `NoteLett`
- provisional `productId`: `bytelyst-notes` - `productId`: `notelett`
- provisional domain: `notes.bytelyst.app` - domain: `notelett.app`
- backend port: `4016` - backend port: `4016`
- Backend scaffolding now uses the established ByteLyst product-backend pattern: - Backend scaffolding now uses the established ByteLyst product-backend pattern:
- Fastify 5 - Fastify 5
@ -72,8 +72,8 @@ Make the product implementation-ready and eliminate ambiguity before parallel co
# Open Questions # Open Questions
- Should the final canonical `productId` remain `bytelyst-notes` or use a shorter ecosystem-standard form? - ~~Should the final canonical `productId` remain `bytelyst-notes` or use a shorter ecosystem-standard form?~~ **Resolved:** `notelett`
- Should the final public domain be `notes.bytelyst.app` or another product-specific domain? - ~~Should the final public domain be `notes.bytelyst.app` or another product-specific domain?~~ **Resolved:** `notelett.app`
- What is the final operator vs shared-admin boundary for approval and audit review flows? - What is the final operator vs shared-admin boundary for approval and audit review flows?
- Does v1 mobile parity include approvals and lightweight editing, or capture/retrieval only? - Does v1 mobile parity include approvals and lightweight editing, or capture/retrieval only?
- What token namespace should be requested for shared design-system generation? - What token namespace should be requested for shared design-system generation?

View File

@ -189,10 +189,7 @@ Stack: Next.js 16 + React 19 + TypeScript
# Blockers # Blockers
- Product identity is still draft-level in the planning docs, so the scaffold currently uses provisional values: - ~~Product identity is still draft-level in the planning docs~~ **Resolved:** product identity locked as NoteLett (`notelett`, port 4016, `notelett.app`)
- `ByteLyst Agentic Notes`
- `agentic-notes`
- `4016` as a placeholder notes API port in `.env.example`
- Backend integration contracts are only partially aligned with the web shell, so some routes still rely on demo auth fallback and client-derived operator/saved-view summaries. - Backend integration contracts are only partially aligned with the web shell, so some routes still rely on demo auth fallback and client-derived operator/saved-view summaries.
# Deferred # Deferred

View File

@ -96,8 +96,8 @@ Stack: React Native + Expo + TypeScript
# Open Questions # Open Questions
- Should the current provisional bootstrap values (`productId: bytelyst-notes`, backend port `4016`) now be treated as final? - ~~Should the current provisional bootstrap values be treated as final?~~ **Resolved:** `productId: notelett`, port 4016, domain `notelett.app`
- What are the final iOS bundle identifier, Android package name, and URL scheme/domain values? - ~~What are the final iOS bundle identifier, Android package name, and URL scheme/domain values?~~ **Resolved:** iOS `com.bytelyst.notelett`, Android `com.notelett.app`, scheme `notelett`
- Should mobile notes access go directly to the product backend for note CRUD while auth remains on `platform-service`? - Should mobile notes access go directly to the product backend for note CRUD while auth remains on `platform-service`?
- Should the current local approval/activity model map directly to backend `note-agent-actions`, or stay as a mobile-specific condensed surface? - Should the current local approval/activity model map directly to backend `note-agent-actions`, or stay as a mobile-specific condensed surface?

View File

@ -1,18 +1,18 @@
{ {
"expo": { "expo": {
"name": "ByteLyst Agentic Notes", "name": "NoteLett",
"slug": "bytelyst-agentic-notes", "slug": "notelett",
"version": "0.1.0", "version": "0.1.0",
"orientation": "portrait", "orientation": "portrait",
"userInterfaceStyle": "dark", "userInterfaceStyle": "dark",
"scheme": "bytelyst-notes", "scheme": "notelett",
"plugins": ["expo-router"], "plugins": ["expo-router"],
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"bundleIdentifier": "com.bytelyst.agenticnotes" "bundleIdentifier": "com.bytelyst.notelett"
}, },
"android": { "android": {
"package": "com.bytelyst.agenticnotes" "package": "com.notelett.app"
}, },
"web": { "web": {
"bundler": "metro" "bundler": "metro"

View File

@ -1,5 +1,5 @@
{ {
"name": "bytelyst-agentic-notes-mobile", "name": "@notelett/mobile",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"main": "index.ts", "main": "index.ts",

View File

@ -12,6 +12,8 @@ export type MobileApprovalItem = {
export type MobileActivityItem = { export type MobileActivityItem = {
id: string; id: string;
workspaceId: string;
noteId: string;
title: string; title: string;
summary: string; summary: string;
kind: 'note' | 'task' | 'agent'; kind: 'note' | 'task' | 'agent';
@ -71,6 +73,8 @@ function toActivityKind(actionType: NoteAgentActionDoc['actionType']): MobileAct
function toActivityItem(action: NoteAgentActionDoc): MobileActivityItem { function toActivityItem(action: NoteAgentActionDoc): MobileActivityItem {
return { return {
id: action.id, id: action.id,
workspaceId: action.workspaceId,
noteId: action.noteId,
title: action.afterSummary ?? `${action.actionType.replaceAll('_', ' ')} update`, title: action.afterSummary ?? `${action.actionType.replaceAll('_', ' ')} update`,
summary: action.reason ?? action.afterSummary ?? `State: ${action.state}`, summary: action.reason ?? action.afterSummary ?? `State: ${action.state}`,
kind: toActivityKind(action.actionType), kind: toActivityKind(action.actionType),

View File

@ -13,7 +13,7 @@ export default function AuthScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>ByteLyst Agentic Notes</Text> <Text style={styles.title}>NoteLett</Text>
<Text style={styles.subtitle}>Mobile MVP auth shell</Text> <Text style={styles.subtitle}>Mobile MVP auth shell</Text>
<TextInput <TextInput
autoCapitalize="none" autoCapitalize="none"

View File

@ -1,9 +1,16 @@
{ {
"productId": "bytelyst-notes", "productId": "notelett",
"displayName": "ByteLyst Agentic Notes", "displayName": "NoteLett",
"licensePrefix": "NOTES", "licensePrefix": "NOTELETT",
"configDirName": ".ByteLystNotes", "configDirName": ".NoteLett",
"envVarPrefix": "NOTES", "envVarPrefix": "NOTELETT",
"bundleIdSuffix": "Notes", "bundleIdSuffix": "notelett",
"packageName": "bytelyst-notes" "packageName": "notelett",
"domain": "notelett.app",
"bundleId": {
"ios": "com.bytelyst.notelett",
"android": "com.notelett.app"
},
"appGroup": "group.com.bytelyst.notelett",
"backendPort": 4016
} }

View File

@ -1,5 +1,5 @@
{ {
"name": "bytelyst-agentic-notes-web", "name": "@notelett/web",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@ -7,10 +7,10 @@ export default function HomePage() {
<div className="badge">Backend-backed web surface</div> <div className="badge">Backend-backed web surface</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}> <div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
<h1 style={{ margin: 0, fontFamily: "var(--ml-font-display)", fontSize: "var(--ml-fs-3xl)" }}> <h1 style={{ margin: 0, fontFamily: "var(--ml-font-display)", fontSize: "var(--ml-fs-3xl)" }}>
ByteLyst Agentic Notes web surface NoteLett
</h1> </h1>
<p style={{ margin: 0, color: "var(--ml-text-secondary)", lineHeight: 1.6 }}> <p style={{ margin: 0, color: "var(--ml-text-secondary)", lineHeight: 1.6 }}>
The web app now exposes backend-backed dashboard, workspace, search, review, and note detail flows with authenticated ByteLyst client wiring. Structured notes workspace for humans and agents with search, review, and operational context.
</p> </p>
</div> </div>
<div style={{ display: "flex", gap: "var(--ml-space-3)", flexWrap: "wrap" }}> <div style={{ display: "flex", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>

View File

@ -68,7 +68,7 @@ describe("getNoteDetail", () => {
artifactType: "file", artifactType: "file",
title: "Launch brief.pdf", title: "Launch brief.pdf",
description: "Ready for review", description: "Ready for review",
blobPath: "bytelyst-notes/user-1/launch-brief.pdf", blobPath: "notelett/user-1/launch-brief.pdf",
contentType: "application/pdf", contentType: "application/pdf",
sizeBytes: 2048, sizeBytes: 2048,
}, },
@ -141,7 +141,7 @@ describe("getNoteDetail", () => {
name: "Launch brief.pdf", name: "Launch brief.pdf",
type: "file", type: "file",
status: "ready", status: "ready",
blobPath: "bytelyst-notes/user-1/launch-brief.pdf", blobPath: "notelett/user-1/launch-brief.pdf",
contentType: "application/pdf", contentType: "application/pdf",
sizeBytes: 2048, sizeBytes: 2048,
}, },