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:
parent
e6beef83eb
commit
e1fde25afd
@ -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",
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# ByteLyst Agentic Notes — Master Roadmap
|
||||
# NoteLett — Master Roadmap
|
||||
|
||||
Version: 2.0
|
||||
Date: March 10, 2026
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bytelyst-agentic-notes-mobile",
|
||||
"name": "@notelett/mobile",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bytelyst-agentic-notes-web",
|
||||
"name": "@notelett/web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@ -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" }}>
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user