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",
"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",
"scripts": {
"dev": "tsx watch src/server.ts",

View File

@ -6,7 +6,7 @@ const envSchema = z.object({
HOST: z.string().default('0.0.0.0'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
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_KEY: z.string().min(1, 'COSMOS_KEY is required').optional(),
COSMOS_DATABASE: z.string().default('bytelyst'),

View File

@ -21,9 +21,9 @@ export async function initCosmosIfNeeded(): Promise<void> {
try {
await initializeAllContainers();
process.stdout.write('[bytelyst-notes-backend] Cosmos containers ensured\n');
process.stdout.write('[notelett-backend] Cosmos containers ensured\n');
} catch (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(),
}));
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', () => ({
listNotes: listNotesMock,
getNote: getNoteMock,
@ -23,7 +23,7 @@ import { NotesExecutableMcpTools, getNotesExecutableMcpTool } from './note-tools
const req = {
id: 'req_1',
headers: {},
jwtPayload: { sub: 'user_1', role: 'admin', productId: 'bytelyst-notes' },
jwtPayload: { sub: 'user_1', role: 'admin', productId: 'notelett' },
log: {
info: vi.fn(),
warn: vi.fn(),
@ -51,7 +51,7 @@ describe('note executable MCP tools', () => {
items: [
{
id: 'note_1',
productId: 'bytelyst-notes',
productId: 'notelett',
workspaceId: 'ws_1',
userId: 'user_1',
title: 'Draft',
@ -74,7 +74,7 @@ describe('note executable MCP tools', () => {
req
);
expect(listNotesMock).toHaveBeenCalledWith('user_1', 'bytelyst-notes', {
expect(listNotesMock).toHaveBeenCalledWith('user_1', 'notelett', {
workspaceId: 'ws_1',
limit: 10,
offset: 0,
@ -87,7 +87,7 @@ describe('note executable MCP tools', () => {
items: [
{
id: 'note_1',
productId: 'bytelyst-notes',
productId: 'notelett',
workspaceId: 'ws_1',
userId: 'user_1',
title: 'Retention policy',
@ -119,7 +119,7 @@ describe('note executable MCP tools', () => {
it('gets a scoped note', async () => {
getNoteMock.mockResolvedValue({
id: 'note_1',
productId: 'bytelyst-notes',
productId: 'notelett',
workspaceId: 'ws_1',
userId: 'user_1',
title: 'Note',
@ -180,7 +180,7 @@ describe('note executable MCP tools', () => {
expect(createNoteAgentActionMock).toHaveBeenCalledTimes(1);
expect(createNoteAgentActionMock).toHaveBeenCalledWith(
expect.objectContaining({
productId: 'bytelyst-notes',
productId: 'notelett',
workspaceId: 'ws_1',
userId: 'user_1',
actorId: 'agent_1',

View File

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

View File

@ -16,7 +16,7 @@ const {
}));
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', () => ({
listNotes: listNotesMock,
getNote: getNoteMock,

View File

@ -6,7 +6,7 @@ const { extractAuthMock } = vi.hoisted(() => ({
}));
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', () => ({
listWorkspaces: vi.fn(async () => ({ items: [], total: 0 })),
getWorkspace: vi.fn(async () => null),

View File

@ -17,7 +17,7 @@ vi.mock('@bytelyst/fastify-core', () => ({
}));
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() }));
@ -30,14 +30,14 @@ vi.mock('./lib/cosmos-init.js', () => ({ initCosmosIfNeeded: initCosmosIfNeededM
vi.mock('./lib/datastore.js', () => ({ initDatastore: initDatastoreMock }));
vi.mock('./lib/config.js', () => ({
config: {
SERVICE_NAME: 'bytelyst-notes-backend',
SERVICE_NAME: 'notelett-backend',
CORS_ORIGIN: '*',
PORT: 4016,
HOST: '0.0.0.0',
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', () => {
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
Date: March 10, 2026

View File

@ -1,4 +1,4 @@
Product Requirements Document: ByteLyst Agentic Notes
Product Requirements Document: NoteLett
Version: 2.0
Date: March 10, 2026
@ -7,7 +7,7 @@ Status: Draft
# 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:

View File

@ -1,4 +1,4 @@
# ByteLyst Agentic Notes — Master Roadmap
# NoteLett — Master Roadmap
Version: 2.0
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.
- The repo now has active `web/`, `backend/`, and `shared/` surfaces instead of a docs-only structure.
- Provisional bootstrap values are in use for implementation:
- product display name: `ByteLyst Agentic Notes`
- provisional `productId`: `bytelyst-notes`
- provisional domain: `notes.bytelyst.app`
- product display name: `NoteLett`
- `productId`: `notelett`
- domain: `notelett.app`
- backend port: `4016`
- Backend scaffolding now uses the established ByteLyst product-backend pattern:
- Fastify 5
@ -72,8 +72,8 @@ Make the product implementation-ready and eliminate ambiguity before parallel co
# Open Questions
- Should the final canonical `productId` remain `bytelyst-notes` or use a shorter ecosystem-standard form?
- Should the final public domain be `notes.bytelyst.app` or another product-specific domain?
- ~~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?~~ **Resolved:** `notelett.app`
- 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?
- 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
- Product identity is still draft-level in the planning docs, so the scaffold currently uses provisional values:
- `ByteLyst Agentic Notes`
- `agentic-notes`
- `4016` as a placeholder notes API port in `.env.example`
- ~~Product identity is still draft-level in the planning docs~~ **Resolved:** product identity locked as NoteLett (`notelett`, port 4016, `notelett.app`)
- 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

View File

@ -96,8 +96,8 @@ Stack: React Native + Expo + TypeScript
# Open Questions
- Should the current provisional bootstrap values (`productId: bytelyst-notes`, backend port `4016`) now be treated as final?
- What are the final iOS bundle identifier, Android package name, and URL scheme/domain values?
- ~~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?~~ **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 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": {
"name": "ByteLyst Agentic Notes",
"slug": "bytelyst-agentic-notes",
"name": "NoteLett",
"slug": "notelett",
"version": "0.1.0",
"orientation": "portrait",
"userInterfaceStyle": "dark",
"scheme": "bytelyst-notes",
"scheme": "notelett",
"plugins": ["expo-router"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.bytelyst.agenticnotes"
"bundleIdentifier": "com.bytelyst.notelett"
},
"android": {
"package": "com.bytelyst.agenticnotes"
"package": "com.notelett.app"
},
"web": {
"bundler": "metro"

View File

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

View File

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

View File

@ -13,7 +13,7 @@ export default function AuthScreen() {
return (
<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>
<TextInput
autoCapitalize="none"

View File

@ -1,9 +1,16 @@
{
"productId": "bytelyst-notes",
"displayName": "ByteLyst Agentic Notes",
"licensePrefix": "NOTES",
"configDirName": ".ByteLystNotes",
"envVarPrefix": "NOTES",
"bundleIdSuffix": "Notes",
"packageName": "bytelyst-notes"
"productId": "notelett",
"displayName": "NoteLett",
"licensePrefix": "NOTELETT",
"configDirName": ".NoteLett",
"envVarPrefix": "NOTELETT",
"bundleIdSuffix": "notelett",
"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",
"private": true,
"scripts": {

View File

@ -7,10 +7,10 @@ export default function HomePage() {
<div className="badge">Backend-backed web surface</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
<h1 style={{ margin: 0, fontFamily: "var(--ml-font-display)", fontSize: "var(--ml-fs-3xl)" }}>
ByteLyst Agentic Notes web surface
NoteLett
</h1>
<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>
</div>
<div style={{ display: "flex", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>

View File

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