Requirements and their checks¶
Lens's requirements are the lists in the README: what is built, what is in progress (an open pull request) and what is planned. Each built item there links its section here, and each row of a section names the checks that fail when that requirement stops being true: a test, a clicks journey and the number it is held to, or the CI workflow that builds or tests it. A requirement nothing checks yet says so and links the issue that adds one.
How the checks are kept¶
make requirements runs
ci/check_requirements.py, and pre-commit
runs it on every pull request, so drift shows up in CI. It fails when:
- a built item in the README links no section here, or a section here is linked by no item;
- a check is gone: its file, a test function or class in a Python test, a quoted name (a test's title, a journey's
id) in a script, a job or step
name:in a workflow, or a journey in a clicks baseline; - a measured number is no longer met:
journey≤ N clicks (or keys) inci/clicks/baseline.json(a computer) orbaseline-phone.json(a phone), or the journey fails there; - a requirement with no check yet links no issue;
- an In progress item links no pull request, or one the changelog already lists as released;
- a Planned item links nowhere.
The script checks that a check exists, not that it passes: CI runs the tests, the Clicks workflow the journeys. A check named here runs in CI rather than skipping there (a test that needs a program the runners lack is not a check yet). When a pull request builds something, it adds a line to the README's built list and a row here, and takes its own line out of In progress. Renaming or removing a test that a row names means changing the row in the same pull request, to another check that proves the same thing; loosening a requirement or its number waits for the owner (AGENTS.md).
Install¶
| Requirement | Checks |
|---|---|
One line (install.sh) opens a web installer that checks the computer |
install.yml Install from the web installer, Start the installer, as install.sh does on a first install; test_lens_installer.py test_plan_lists_docker_when_it_is_missing, test_only_the_link_opens_it |
| It installs with the defaults, using a model server it finds on the computer | journeys.mjs install; baseline.json install ≤ 3 clicks, ≤ 2 keys |
| It installs what's missing, Docker included, starts Lens and opens its setup page | install.yml A new computer, without Docker, Answer it and install, Lens answers, waits for its first admin, and has the installer's settings; test_lens_installer.py test_install_runs_install_sh_with_the_answers |
| Lens can run in a microsandbox microVM instead of Docker | install.yml One-line install in a microsandbox; test_lens_installer.py test_lens_app_offers_microsandbox_first_where_it_runs |
| Running the line again updates Lens in place and keeps its secrets | install.yml One-line install, Running it again updates in place and keeps the secrets |
Windows installs with install.ps1 |
install.yml install.ps1 parses in Windows PowerShell 5.1, what Windows has; test_install_windows.py test_installs_and_hands_over_the_setup_address, test_a_failed_build_says_so |
| Double-click launchers for each system | launchers.yml Build the launchers, Check them |
| Lens.app on a Mac with Apple silicon installs Lens and runs it without Docker | desktop.yml Build Lens.app, The bundled msb runs, The app carries its helper and microsandbox |
| Packages for Synology, QNAP and Cloudron | synology.yml Build; qnap.yml Build the package; cloudron.yml build |
| Each release publishes Lens's images for amd64 and arm64 to GHCR | images.yml Build, One tag for both architectures, Both architectures are there |
LENS_IMAGES=pull installs a release from its prebuilt images, without building |
install.yml Install a release from its images, Lens runs the downloaded images and keeps them named for later runs |
| A Proxmox container script | No check yet (#486) |
| The first admin signs up with a passkey from the setup link, which fills in the setup code | journeys.mjs first-admin; baseline.json first-admin ≤ 1 click; test_passkeys.py test_first_admin_with_a_passkey_and_no_password; setupForm.test.tsx fills in the code from the setup link and starts at the email |
Connect your agents¶
| Requirement | Checks |
|---|---|
An MCP server at /mcp: agents search, read assets' text and notes, and cite exact moments |
test_mcp.py test_search_and_cite, test_reading_recordings, test_note_tools |
| Agents find assets by face, voice, entity, topic, type, content type and date in one call, each with the moments that match | test_find.py test_by_face_voice_entity_and_date, test_by_topic_type_content_type_and_text, test_find_for_agents |
| Agents classify an asset or a text against labels, content types or topics with the decision model, acting only on a sure answer | test_classify.py test_labels_given_and_what_the_model_reads, test_vocabularies_and_applying_a_sure_answer, test_agents_classify |
| Agents find what was said about a day or a range, each date with its line and a link to that moment | test_dates.py test_agents_find_what_was_said_about_a_day |
| Agents find similar content | test_mcp.py test_search_and_cite; test_similar.py test_like_a_text_by_its_key_words, test_like_a_resource_by_words_topics_and_entities |
| Agents explore the graph with Cypher or SPARQL, or browse it as folders and files | test_mcp.py test_graph_tools, test_sparql; test_file_tree.py test_agents_browse_it_over_mcp |
| Agents suggest topics and graph changes, and edit and merge entities and topics | test_mcp.py test_topic_tools, test_editing_topics_and_entities |
| Agents write notes and make podcasts | test_mcp.py test_note_tools; test_podcasts.py test_agents_make_episodes |
| Agents sign in with OAuth and see only what their person may read | test_mcp.py test_clients_are_sent_to_sign_in, test_collection_roles; test_oauth.py test_an_app_gets_access_and_acts_with_the_persons_roles |
| The account menu copies the MCP address in one click; connecting an assistant takes a handful | connect-assistant.test.tsx copies the address from the account menu in one click; journeys.mjs agent-connect; baseline.json agent-connect ≤ 5 clicks |
A namespace's own MCP server at /mcp/<namespace>, off until its owner turns it on |
test_mcp_namespace.py test_off_until_an_owner_turns_it_on, test_it_sees_one_namespace |
| OpenAI's chat completions API per namespace, cited from that namespace alone, off until turned on | test_completions.py test_off_until_an_owner_turns_it_on, test_answers_from_the_namespace_alone_with_citations, test_streams_as_openai_does |
| API keys that open one namespace and nothing else | test_namespace_keys.py test_a_key_for_one_namespace_opens_its_servers_alone, test_it_has_the_persons_role_there_and_no_more |
| The graph's folders and files mounted read-only over S3, WebDAV, NFS and SMB | test_s3.py test_people_read_their_namespaces_over_s3, test_what_s3_refuses; test_webdav.py test_mount_and_read, test_what_webdav_refuses; test_nfs_share.py test_the_runner_follows_the_setting; test_smb_share.py test_reads_the_tree_as_its_key_sees_it, test_nothing_can_be_written |
| S3 and WebDAV at the web app's address, through remote access, once an admin allows it | test_s3.py test_at_the_web_apps_address_once_an_admin_allows_it; test_webdav.py test_at_the_web_apps_address_once_an_admin_allows_it, test_through_the_web_app_each_visitor_has_its_own_throttle |
| A file copied in over S3, WebDAV, SMB or NFS is imported where it was put, with an upload's checks and permissions | test_s3_writes.py test_a_file_put_into_a_collection_is_imported_there, test_folders_become_collections, test_multipart_uploads, test_what_may_not_be_added_is_refused, test_files_added_at_the_web_apps_address; test_webdav_writes.py test_finder_copies_a_file, test_windows_copies_a_file, test_a_folder_dragged_in_becomes_collections, test_what_may_not_be_added_is_refused; test_smb_writes.py test_a_file_copied_into_a_collection_is_imported_there, test_new_folders_under_collections_are_new_collections, test_what_may_not_be_added_is_refused, test_the_share_is_read_only_while_add_files_is_off; test_s3_writes.py test_the_nfs_shares_key_follows_nfs_add_files (NFS adds through rclone's S3 client) |
| The web app runs on the same REST API, through a client generated from its OpenAPI schema | ci.yml OpenAPI schema is up to date |
Ask, and get cited answers¶
| Requirement | Checks |
|---|---|
| Questions about Lens itself answered from Lens's docs and this server's facts, both cited, for people and agents over MCP | test_lens_help.py test_the_docs_are_found_by_words, test_the_assistant_answers_about_lens_itself, test_agents_ask_lens_over_mcp, test_without_a_question_lens_help_still_gives_the_server |
| A chat assistant on every page | test_chats.py test_asking_from_a_page; page-chat.test.tsx; ask-place.test.tsx |
| By voice | assistant-mode.test.tsx starts a voice chat from the mic; test_voice.py test_what_is_said_comes_back_as_text |
| A search result opens the asset with Find showing the words searched for, at the moment it linked to | search-find.test.ts carries them on the links from the results, finds the words together, else the longest word there, on the match at the linked moment |
| It searches by words, and by meaning once search by meaning is set up | test_chats.py test_tools_approvals_and_checking; test_semantic.py test_chat_retrieval_by_meaning, test_off_or_unconfigured |
| Word search takes "phrases", OR, and NOT or -word to leave lines out | test_search_index.py test_not_and_minus_leave_lines_out, test_the_queries_parts |
| Answers cite their sources, and only what the person may read | test_chats.py test_chat_cites_only_what_you_can_read; chat.test.tsx splits [n], [n, m] and [n][m] |
| It acts with tools, and changes wait for approval | test_chats.py test_tools_approvals_and_checking; test_approvals.py test_the_assistant_lists_and_declines_but_never_approves |
| Questions about Lens itself answered from Lens's docs and this server's facts, both cited, for people and agents over MCP | test_lens_help.py test_the_docs_are_found_by_words, test_the_assistant_answers_about_lens_itself, test_agents_ask_lens_over_mcp, test_without_a_question_lens_help_still_gives_the_server |
| A conversation, or one question, picks the tools an answer may use, a limit on its tool calls and the namespaces it reads | test_chat_tools.py test_an_answer_is_offered_only_what_was_picked, test_at_most_so_many_tool_calls, test_a_scope_for_one_answer; chat-tools-picker.test.tsx picks groups, keeps single tools, and goes back to Auto, limits the tool calls, from 1 to the server's most |
| Web search and reading pages, cited like the archive, off until an admin turns it on | test_web_search.py test_an_answer_cites_the_web_and_the_conversation_keeps_it, test_reading_pages_and_pdfs; chat-web-citations.test.tsx |
| An admin's assistant searches and reads connected mail, calendars and files, cites messages and events, and brings them in only once approved | test_connection_tools.py test_only_admins_get_them_and_only_for_what_is_connected, test_searching_and_reading_gmail, test_calendar_events_between_two_times, test_finding_and_reading_files_in_storage, test_bringing_things_in_waits_for_the_persons_yes; chat-web-citations.test.tsx a message from a connected mailbox |
| Where an admin allows it, the assistant sends mail, adds events and writes files, each only after the person reads all of it and says yes, signed in or in the chat; an API key, an app or an agent only declines | test_connection_tools.py test_sending_mail_waits_for_the_persons_yes, test_adding_an_event_waits_for_the_persons_yes, test_writing_a_file_waits_for_the_persons_yes; test_approvals.py test_only_the_person_approves_what_the_assistant_asks; approvals-page.test.tsx shows the assistant's whole message before it is sent; add-connection.test.tsx connects Gmail the same way, and asks a service account whose mail it reads |
| Python and bash in a sandbox of each conversation's own, off until an admin turns it on | test_sandbox.py test_python_and_bash_share_one_sandbox_per_conversation, test_the_sandbox_is_off_until_an_admin_turns_it_on_for_the_people_it_names |
| Each run opens under the answer with its code and output, and a file the sandbox makes downloads from there | test_sandbox.py test_a_run_shows_its_output_and_a_file_it_shares_downloads_from_the_answer; sandbox-steps.test.tsx shows the code collapsed, then the code and its output, downloads a file the sandbox shared from the answer |
| On a Docker install, Settings starts and stops the sandbox's gateway next to Lens, and says why a machine can't run it | test_sandbox.py test_the_gateway_is_started_and_stopped_from_settings, test_a_machine_that_cant_run_the_gateway_says_why; sandbox-gateway.test.ts says what the machine is doing, and whether Lens reaches the gateway |
| Deep research: a goal gets a plan to approve, runs in the background and comes back as a cited report, saved as a note or a podcast episode | test_research.py test_a_goal_gets_a_plan_that_waits_for_approval_and_can_be_revised, test_an_approved_plan_runs_and_its_report_comes_back_checked, test_a_report_becomes_a_note_and_a_podcast; chat-research.test.tsx approves the plan with Enter, sending the edits, saves it as a note, asking where when it could go in several namespaces, turns the next message into a research goal, and says when it can't; journeys.mjs chat-research; baseline.json chat-research ≤ 4 clicks |
| Regenerating an answer or editing a question makes a new version beside the old one | test_chat_versions.py test_regenerate_edit_switch_and_go_on; chat-versions.test.tsx regenerating an answer, the version switch; journeys.mjs chat-regenerate; baseline.json chat-regenerate ≤ 4 clicks |
| A new conversation with nothing in its scope offers Import, and Change scope when namespaces are picked, instead of questions nothing can answer | chat-empty.test.tsx with nothing imported, offers Import instead of questions nothing can answer, with an empty namespace in scope, offers Import and a wider scope, someone who can't import is told who can |
| A thumbs up or down on an answer is saved, shown when the conversation opens, carried in its JSON export and given by agents over MCP | test_chat_ratings.py test_rating_an_answer_is_saved_shown_and_exported, test_someone_elses_answer_cant_be_rated, test_agents_rate_answers_over_mcp; chat-rating.test.tsx thumbs up saves the rating, and pressing it again takes it back |
| Each answer keeps a record of what produced it: the model, its tools, how long it took and what it cost | test_chat_records.py test_an_answer_records_what_produced_it, test_a_regenerated_answer_has_its_own_record; chat-details.test.tsx says what produced it: model and host, scope, tools, use, tokens, cost, time and how it ended |
| A conversation exported as Markdown, PDF or JSON, its citations as footnotes linking back to Lens, by people and by agents over MCP | test_chat_export.py test_markdown, test_json, test_pdf_is_printed_by_what_the_server_has, test_agents_read_and_export_over_mcp; chat-copy-export.test.tsx downloads what the server makes, named as the server names it, prints in the browser when the server can't make the PDF |
| Answers, questions and code blocks copied from the web app, an answer as Markdown or plain text | chat-copy-export.test.tsx gives Markdown with citations as footnotes linking back to Lens, or plain text, copies a question, copies a code block and says Copied |
| An answer is finished, saved and shown even when the person leaves before it's done; only Stop ends it early | test_chats.py test_an_answer_is_saved_when_the_client_leaves, test_stopping_an_answer; chat-fresh-answer.test.tsx shows the answer even when the conversation's first read left before it was saved |
Answers read aloud by a speech provider, macOS say or StyleTTS2 (opt-in) |
test_voice.py test_answers_are_read_aloud_by_a_speech_model; test_speech.py test_macos_say_reads_aloud_on_a_mac, test_styletts2_reads_aloud_from_its_own_service; styletts2.yml Read a sentence aloud |
| Answers and podcasts read aloud by Kokoro v1.0 on this machine, in 54 voices, fetched once it's picked and checked against its SHA-256 (opt-in) | test_kokoro.py test_kokoro_reads_answers_aloud_on_this_machine, test_the_model_size_is_a_setting, test_kokoro_is_fetched_once_it_is_picked, test_every_voice_is_listed_before_it_is_fetched, test_a_podcast_is_read_by_kokoro; settings-kokoro.test.tsx reads on this machine, with the model size to pick; kokoro.yml Lens fetches Kokoro and reads aloud with each model |
| Any message read aloud in one click, one at a time, its audio kept so a replay costs nothing | chat-read-aloud.test.tsx plays an answer by the speech model in one click, stops on the next, and keeps its audio, reads one message at a time: starting another stops the first, reads a long answer piece by piece, asking for the next while one plays |
| Conversations title themselves after their first answer, and a name you give is kept | test_chat_titles.py test_a_first_answer_titles_the_conversation, test_a_title_you_give_is_kept, test_agents_rename_and_retitle_over_mcp |
Long conversations summarised before they fill the model's context, and /clear and /compact |
test_compaction.py test_older_messages_are_folded_into_a_summary, test_the_context_window_is_the_model_servers, test_a_long_conversation_is_folded_in_runs_that_fit_the_summary_model, test_a_conversation_that_doesnt_fit_is_folded_in_and_asked_again, test_what_tools_returned_is_cut_to_fit_the_model, test_clear_starts_afresh_and_compact_folds_everything_now, test_commands_in_the_app; chat-summary-mark.test.tsx says how many messages the summary stands for, and shows it on request |
| Questions about Lens itself answered from its docs and this server, citing both, for people and agents over MCP | test_lens_help.py test_the_assistant_answers_about_lens_itself, test_admins_see_where_each_setting_is, test_agents_ask_lens_over_mcp |
| A namespace's own assistant, with its own name, instructions and memory | test_ns_assistant.py test_off_by_default_and_owners_turn_it_on, test_remembers_across_conversations_with_citations; ns-assistant.test.tsx saving a name and forgetting a memory |
| Telegram and Slack built in, with voice notes and files | test_telegram.py test_it_answers_private_chats_and_what_is_said_to_it_in_groups, test_voice_notes_are_heard, test_files_are_held_for_the_assistant_to_import; test_slack.py test_it_answers_direct_messages_mentions_and_the_slash_command, test_voice_clips_are_heard_and_files_held |
| Other chat rooms through Matterbridge | test_bridge.py test_it_answers_what_is_said_to_it_and_nothing_else; test_matterbridge_run.py test_chat_networks_are_set_in_the_app_and_become_matterbridges_config, test_the_bridge_talks_to_the_matterbridge_lens_runs; matterbridge.yml Lens writes a config, and Matterbridge starts with it |
| By email, at an address of its own | test_mailbox.py test_mail_is_answered_by_email_in_its_thread, test_the_mailbox_is_set_up_and_checked_in_the_app, test_answers_list_the_excerpts_they_cite |
Answer your email¶
| Requirement | Checks |
|---|---|
| A reply drafted to an imported email from its own namespace, each fact cited, what the archive doesn't say marked to confirm, a citation to an excerpt it wasn't given taken out and a link of the model's making broken up, addressed and threaded as a reply; nothing sent | test_replies.py test_a_cited_threaded_draft |
| Drafts edited, set aside and brought back, the text before kept; the mail that waits for an answer listed, not mailing lists' | test_replies.py test_editing_setting_aside_and_listing |
| A pipeline's reply step drafts each new email, skipping mailing lists, no-reply senders and the person's own mail without a model call | test_replies.py test_a_pipeline_drafts_only_mail_to_answer |
| Agents list what waits, draft with instructions, read and edit replies through MCP | test_replies.py test_agents_draft_read_and_edit_replies |
| The Replies page drafts the first email's reply with Enter from its focused field, shows a draft with its sources and what's left to confirm, and opens it in your mail app without citations | replies-page.test.tsx drafts a reply from the first field with Enter, saying what it should say; replies-page.test.tsx shows a draft with its sources and what's left to confirm, and opens it in your mail app |
| Drafts edited on the page (Ctrl+Enter saves, addresses checked) and set aside at once with Undo instead of a question | replies-page.test.tsx saves an edit with Ctrl+Enter, and won't save an address that isn't one; replies-page.test.tsx sets a draft aside at once, with Undo instead of a question |
| A reply sent, or kept in Gmail's drafts, through the Gmail connection its email came in on, in its thread and without citations: only an admin signed in, only from a connection let send, never with something left to confirm, and never twice | test_replies.py test_sending_after_the_persons_yes |
| Agents ask to send a reply through MCP; the ask waits in Approvals showing all of it, an agent can only decline it, and approved it sends the text it showed, never a later edit | test_replies.py test_agents_ask_to_send_and_the_person_approves |
| The Replies page sends after a few seconds with Undo instead of a question, saving an edit first, and keeps a draft in Gmail at once; no Send without a connection that sends | replies-page.test.tsx sends through Gmail after a few seconds unless undone, once nothing is left to confirm; replies-page.test.tsx keeps a draft in Gmail at once, and offers no Send without a connection that sends or to members |
Work toward a goal in projects¶
| Requirement | Checks |
|---|---|
| A project with a goal, instructions and namespaces, a coordinator conversation and threads scoped to its namespaces, marked done, brought in or taken out; deleting it keeps its conversations | test_projects.py test_a_project_with_a_goal_namespaces_and_threads, test_changing_its_namespaces_moves_its_conversations |
| A project is its owner's alone and draws only on namespaces they can read | test_projects.py test_projects_are_their_owners_and_draw_on_readable_namespaces |
| Every answer in a project reads its goal and instructions; the coordinator is told about the threads, as data | test_projects.py test_every_answer_in_a_project_reads_what_it_is_for |
| The coordinator starts threads with a brief, reads them and marks them done; a thread can't start threads; it asks first after reading outside text, and starts a few per answer at most | test_projects.py test_the_coordinator_starts_threads_with_a_brief, test_starting_threads_after_reading_outside_text_asks_first |
| A brief from the person, an agent or the coordinator is answered in the background as the person; one cut off by a stopped process is marked, never left waiting | test_projects.py test_a_brief_from_the_api_or_an_agent_is_answered_in_the_background, test_a_brief_cut_off_by_a_stop_is_marked_by_the_sweep |
| The Projects page starts a project with just a name, starts threads with a brief on Enter, shows which are working and marks them done in one click; projects sit under Chat | projects.test.tsx starts one in the current namespace with just a name, then opens it, starts a thread with a brief on Enter and keeps the page, marks a thread done with one click, are under Chat in the navigation |
| A schedule asks its prompt as a brief in a new thread each time or always the same one, and leaves a thread still answering to finish | test_projects.py test_a_schedule_asks_its_prompt_in_a_new_thread_or_the_same_one |
| Schedules wait while switched off or the project is archived, are their owner's alone, end with the project, and agents set them over MCP | test_projects.py test_schedules_wait_while_off_or_archived_and_agents_set_them |
| The project page adds a schedule from a list in the person's time zone, switches one off in a click and removes one with Undo | projects.test.tsx asks on a schedule picked from a list, in the person's time zone, shows when each runs, switches one off in a click and removes one with Undo |
| Agents list, read, start and change projects over MCP | test_projects.py test_agents_use_projects_over_mcp |
Extend the assistant¶
| Requirement | Checks |
|---|---|
| Tools: a prompt, a web request, a graph drawn on the canvas, or sandboxed Python | test_extensions.py test_tools_and_skills_in_a_conversation, test_tools_drawn_on_the_canvas, test_python_tools_run_apart_and_only_admins_write_them |
| Skills, hooks and MCP servers, by code, on the canvas or by asking | test_extensions.py test_written_as_code_shared_and_versioned, test_change_tools_ask_first_and_hooks, test_made_in_chat_behind_an_approval; journeys.mjs extension-add; baseline.json extension-add ≤ 3 clicks |
| Remote MCP servers connect by address | test_mcp_servers.py test_an_admin_adds_a_server_and_the_assistant_uses_it |
MCP servers that run as a program (npx, uvx) run in a sandboxed container of their own |
test_mcp_servers.py test_a_server_that_runs_as_a_program_runs_in_the_runner; mcp-runner.yml Runs an npm server and a PyPI server |
| Ready-made servers: the browser (a headless Chromium in a sandbox of its own, one session for everyone) and GitHub added in a click; the MCP runner, the browser and monitoring started and stopped from the web app through the upgrade agent, or the line to run shown without one | test_services.py test_each_profile_is_turned_on_and_off_on_its_own, test_no_agent_shows_the_line_to_run, test_only_admins; test_mcp_servers.py test_the_browser_preset_keeps_one_session; mcp-server-ui.test.tsx adds the browser with one click once its container runs, turns the browser's container on first, when it isn't running, sets GitHub up with the token typed in its place |
| Settings › AI assistant › MCP servers lists every server with its tools and the containers' switches; agents list and add servers over MCP | test_mcp_servers.py test_agents_list_and_add_servers; mcp-server-ui.test.tsx lists each server with its tools, and the containers' switches |
| An MCP server added by form or a pasted config, its tokens sealed and each tool switched on or off | test_mcp_servers.py test_a_pasted_config_reads_as_servers_to_add, test_its_secrets_are_sealed_and_never_shown, test_each_tool_is_switched_and_says_whether_it_asks; mcp-server-ui.test.tsx adding an MCP server |
Extensions kept as files: lens ext lists, pulls, checks and pushes manifests, here or over the API, losing neither side's changes |
test_cli_ext.py test_pull_edit_and_push_on_this_install, test_over_the_api_with_a_token |
| Skills and plugins from marketplaces in the open plugin format, a repository or an archive | test_marketplaces.py test_a_marketplace_an_admin_adds, test_imported_from_a_link_or_a_file |
| Plugins found across marketplaces, updates shown, and installed by agents over MCP, the API or chat | test_marketplaces.py test_found_updated_and_installed_by_agents; marketplace-ui.test.tsx an installed plugin with another version listed offers the update |
Bring in everything¶
| Requirement | Checks |
|---|---|
| Watched folders on storage connections (S3, Dropbox, Google Drive, OneDrive, SFTP, SMB, WebDAV, local disks) | test_sources.py test_sources_and_watches_without_rclone, test_importing_chosen_files_of_a_source |
| A local folder in four clicks | journeys.mjs source-folder; baseline.json source-folder ≤ 4 clicks; watch-folder.test.tsx |
| A watched folder on each kind of storage connection, run against rclone in CI | No check yet (#486) |
| IMAP mail, Gmail and Google Workspace | test_mail_calendar_sources.py test_an_imap_account_as_a_source, test_an_imap_message_is_an_email_document_with_its_attachments; test_gmail_source.py test_a_workspace_mailbox_through_the_gmail_api, test_a_connected_google_account |
| A shared inbox's mail routed to namespaces, or skipped, by rules on a watch and shared rules on the email account that every watch on it tries after its own; agents read and change them over MCP | test_mail_routes.py test_a_shared_inbox_routed_to_namespaces, test_an_accounts_shared_rules_apply_to_every_watch, test_agents_read_and_change_routing_rules |
| iCal calendars and Google Calendar | test_mail_calendar_sources.py test_a_calendar_feed_as_a_source; test_google_calendar_source.py test_a_shared_calendar_read_as_a_service_account, test_a_connected_google_account |
| RSS and Atom news feeds: a feed or a site's page as a source, each post imported as text with its link, author and date; private networks only when allowed | test_news_feed_source.py test_a_news_feed_as_a_source, test_feed_addresses, test_what_a_feed_holds_stays_data, test_a_feed_file_imported_as_text |
Agents follow news feeds in one step: MCP subscribe_feed checks a feed and watches it into a namespace, news_feeds lists them with addresses sealed, unsubscribe_feed stops one; the assistant's tools ask first |
test_news_feed_source.py test_subscribing_in_one_step; test_setup_assistant.py test_the_assistant_follows_news_feeds |
| A built-in MQTT broker, syslog and webhook streams | test_sensors.py test_hub_takes_devices_in_and_organises_them, test_syslog_senders_become_sensors, test_webhooks_and_one_list_of_sensors; journeys.mjs sensor-webhook; baseline.json sensor-webhook ≤ 3 clicks |
| Import webhooks for scanners and phone shortcuts | test_import_hooks.py test_pushing_files_urls_and_text, test_owners_manage_hooks; import-hooks.test.tsx |
| Import webhooks hand back the PDFs of documents a light server can't convert | test_import_hooks.py test_a_hook_hands_back_the_pdfs_of_documents_this_server_cant_convert |
| Connect with Google; Dropbox, OneDrive and Google Drive sign in through rclone on the server | test_gmail_source.py test_connecting_the_account_in_the_browser; test_drive_google.py test_drive_connects_with_google_for_rclone; test_rclone_signin.py test_drive_signs_in_through_rclone_with_no_google_app, test_a_browser_on_the_server_reaches_rclone_itself, test_onedrive_finds_the_accounts_own_drive |
| Uploads of audio, PDFs, Office files, photos, JSON and archives | test_uploads.py test_uploading_audio_in_chunks, test_json_uploaded_is_read_as_records; test_documents.py test_a_pdf_becomes_a_document_with_its_pages_and_text, test_scans_and_images_are_read_by_ocr; test_conversion.py test_office_and_text_files_become_documents; test_anytopdf.py test_an_uploaded_zip_keeps_its_files; test_archives.py test_reading_an_archive; journeys.mjs import-file, import-paste |
Transcripts made elsewhere (text, Markdown, Word, PDF, SRT, WebVTT, JSON) imported with each line credited to the speaker the file names, WebVTT voice spans (<v Name>, <v.loud Name>) included |
test_importing.py test_formats_and_speakers_by_name, test_webvtt_voice_spans_name_each_line |
| A transcript imported without times says its times are estimated, and is lined up with its audio only when asked (API, MCP and one button): its words matched to the words the speech engine hears, each line taking their times and the rest placed between their neighbours, the text and speakers unchanged | test_align.py test_lines_take_the_times_of_their_words_and_the_rest_fill_in_between, test_a_long_transcript_is_matched_as_well_as_word_by_word_and_quickly, test_imports_say_their_times_are_estimated_and_older_ones_are_recognised, test_lining_up_a_transcript_with_its_audio, test_agents_line_up_a_transcript_through_mcp; align.test.tsx is offered only for estimated times with audio, and not while a job is at it, lines it up in one click, then gets out of the way |
| Media browsers can't play (an AVI, HEVC, AMR) gets a copy they can, for the player only, off by default; files they play get none | test_playable.py test_a_camera_clip_gets_a_copy_every_browser_plays, test_a_file_browsers_play_gets_no_copy |
Photos and videos dated by when they were taken, from the file itself (a photo's EXIF date and GPS fix as place, a video's creation time) when no name or source dates them; a date already set is kept and none is invented |
test_file_dates.py test_a_photo_is_dated_and_placed_by_its_exif, test_a_date_already_set_is_kept, test_a_video_is_dated_by_its_creation_time, test_a_scanned_video_is_dated_by_its_creation_time |
Assets placed by their file's GPS fix (a photo's EXIF, a video's ISO 6709 location) on the Library's map, listed by API (located, bbox) and MCP to members only, on tiles from a setting |
test_photo_map.py test_located_assets_are_listed_inside_a_box, test_an_agent_finds_what_was_taken_in_a_place, test_a_video_is_placed_by_its_location, test_the_map_tiles_are_a_setting; library-map.test.tsx placed assets are dots on the tiles from Settings, framed to show them all |
| An uploaded video, end to end | No check yet (#486) |
| Apache Tika (opt-in) reads documents nothing else here can | test_tika.py test_off_by_default_nothing_changes, test_what_nothing_here_can_convert_tika_reads_as_text |
| Web pages captured one at a time, from a link list or a bookmarks file, by API or by asking; private networks only when allowed | test_webcapture.py test_a_web_page_becomes_a_document, test_an_admin_allows_a_private_network_in_settings, test_private_addresses_are_refused; test_web_links.py test_one_or_several_addresses_as_a_list, test_a_bookmarks_file_becomes_pages_to_capture; test_setup_assistant.py test_the_assistant_captures_web_pages |
| A content type chosen as a file or a page comes in, in Import, by API or to the assistant, runs that content type's pipeline from the start | import-content-type.test.tsx offers the content types of the file's base type, and none chosen means recognised from the file; import-web.test.tsx captures pages as the content type chosen for them; test_uploads.py test_a_content_type_chosen_at_upload, test_a_held_file_is_placed_as_a_content_type; test_imports.py test_choosing_the_content_type_at_import; test_web_links.py test_pages_captured_as_a_content_type; test_setup_assistant.py test_the_assistant_imports_files_as_the_content_type_said |
| A file, page or video the namespace already has links to the one there instead of being imported again: a copy in a watched folder on this machine, a page or video address imported twice | test_uploads.py test_a_copy_in_a_folder_on_this_machine_links_to_the_one_there; test_video_urls.py test_an_address_imported_again_links_to_what_the_namespace_has |
| A video's own captions, written by people, become its transcript instead of transcribing it, off by default | test_video_urls.py test_a_videos_own_captions_become_its_transcript |
| Videos and podcasts imported by their address, off by default | test_video_urls.py test_off_by_default, test_a_video_by_its_address_is_downloaded_then_transcribed; import-web.test.tsx imports a video or podcast by its address, when asked to |
| A bulk import (a folder scan, an archive's files, a link list, a source's files) is one run, listed and undone whole by its owners, by API, the Import screen or the assistant | test_import_runs.py test_a_link_import_is_one_run_undone_whole, test_an_uploaded_archive_and_its_files_are_one_run, test_a_folder_scan_is_one_run, test_chosen_files_of_a_source_are_one_run, test_the_assistant_lists_and_undoes_an_import_with_approval; import-runs.test.tsx |
Clip from the browser¶
| Requirement | Checks |
|---|---|
| A page, a selection or a part you pick becomes Markdown, looked over and sent to a namespace | clip.test.js a full page becomes its article, with front matter, the selection, a picked element, with the selection template; clipper-edge.yml Smoke test in Edge |
| The clipper signs in to Lens, or takes your API token or an import webhook's, and says why a token is refused | lens.test.js with the person's own token, the clip names its namespace, errors say what to do, by the kind of token, the popup offers each namespace a signed-in Lens can take, the picked one chosen; test_import_hooks.py test_people_push_with_their_own_token, test_the_clipper_signed_in_as_the_person |
| Built for Chrome, Edge, Firefox and Safari | browser-extension.yml Test and build the web clipper, Build every target; browser-extension-firefox.yml Lint and package the Firefox clipper; safari-clipper.yml Build the Safari clipper; clipper-edge.yml Clip a page in Edge |
| New releases reach installed copies through a store or a self-hosted update file | release.test.js update manifests |
| In Chrome and Edge, the side panel is a chat with the assistant | assistant.test.js; clipper-edge.yml Smoke test in Edge |
| The assistant uses that browser on allowed sites, asking before anything that sends, deletes, buys or posts | act.test.js; test_browser.py test_a_connected_side_panel_gives_the_assistant_the_browser, test_after_reading_a_page_the_browser_asks_before_what_the_person_didnt_write |
Understand it¶
| Requirement | Checks |
|---|---|
| Transcription | test_speech.py test_an_openai_compatible_server_transcribes, test_deepgram_transcribes_with_speakers_and_sentiment |
| Transcription that expects the names the namespace knows (custom vocabulary, named people, defined entities, names people fixed), off by default | test_speech.py test_the_namespaces_own_names_go_to_the_engine |
| Transcription by a local engine (SenseVoice, Whisper), run in CI | No check yet (#486) |
| One asset's voice split off from the person it was matched to, with Undo, and a voice never matched or suggested again as someone it was said not to be | test_speakers.py test_detach_one_assets_voice; test_voices.py test_a_voice_never_goes_to_someone_it_was_said_not_to_be |
How sure transcription was of each word kept (Whisper, Deepgram, AssemblyAI, ElevenLabs), words from before still loading, the unsure ones marked in the transcript with how sure, a threshold in Settings (0 for none), and unsure words per line over MCP |
test_speech.py test_providers_keep_how_sure_they_were_of_each_word, test_whisper_keeps_how_sure_it_was_of_each_word; test_split_merge.py test_words_keep_how_sure_transcription_was, test_words_from_before_how_sure_was_kept_still_load; test_mcp.py test_get_transcript_says_which_words_transcription_was_unsure_of; recording-split-words.test.ts keeps how sure transcription was of each word, and words from before it was kept, marks the words below the threshold, and none when it's off; recording-unsure-words.test.tsx are underlined with how sure, and a click still plays the line |
| The language transcription hears kept on the asset, and a person sets or corrects it as a BCP 47 tag with a region (en-GB, pt-BR), checked and written the usual way, kept over the detected one when it is transcribed again; through the Details tab, the API and MCP | test_language.py test_tags_are_written_the_usual_way, test_what_isnt_a_tag_is_refused, test_editors_set_and_correct_an_assets_language, test_transcribing_again_keeps_the_language_a_person_set, test_a_pipeline_condition_on_en_takes_its_regions, test_agents_set_it_over_mcp; test_speech.py test_whisper_keeps_the_language_it_heard; recording-language.test.tsx sends a tidy tag, null to forget it, and nothing when it's the same, lets an editor correct it with Enter, and refuses what isn't a tag |
| People recognised by voice; long recordings sent to a speech provider in pieces that fit its limits | test_voices.py test_match_suggest_merge_undo_link, test_merging_keeps_who_they_are_not, test_upgrade_gives_pairs_lost_to_merges_to_who_they_were_merged_into; test_speech.py test_a_long_recording_goes_in_pieces_and_comes_back_whole, test_a_piece_too_large_to_send_is_halved |
| An owner turns voice recognition off for a namespace: voices told apart in each recording only, its voiceprints deleted | test_voice_mode.py test_voices_can_be_told_apart_without_being_recognised |
| Text on screen and in scans | test_video.py test_shots_text_search_chat_and_iiif; test_documents.py test_scans_and_images_are_read_by_ocr; test_ocr.py test_a_video_read_by_doctr_and_without_it |
| Faces, off by default | test_video.py test_people_on_screen, test_shots_text_search_chat_and_iiif; test_video.py test_face_modes |
| The faces nobody has named yet, the largest group first with crops, the assets they're in and a name to offer, named one after another (Enter for the next), a name someone has joining that person, with Undo; through the API, MCP and the People page | test_unnamed_faces.py test_unnamed_faces_largest_first_named_in_a_row_with_undo, test_only_where_faces_are_recognised, test_agents_list_and_name_faces; jest name-faces.test.tsx focuses the largest group first; Enter names it and moves on to the next, with Undo, joins a face to the person who has the name, and Undo takes the merge back |
| NSFW pictures flagged by a model on your own server, off by default, per namespace | test_nsfw.py test_off_by_default_nothing_is_fetched_or_classified, test_a_namespace_classifies_flags_blurs_and_hides; nsfw.test.tsx |
| Low quality images enhanced before they're read, off by default | test_enhance.py test_off_by_default_the_original_is_read, test_a_low_quality_image_is_read_from_its_enhanced_copy, test_a_good_image_is_left_as_it_is, test_agents_and_automations_enhance_too, test_the_copy_is_encrypted_where_the_files_are; enhance.test.tsx |
| Entities and topics in a knowledge graph to explore and query with Cypher or SPARQL | test_graph.py test_query; test_rdf.py test_sparql; journeys.mjs graph-find |
| The graph as it was at any version | test_graph_history.py test_the_explorer_and_cypher_as_of_a_version, test_the_canvas_as_of_a_version; test_graph_history.py test_every_curation_is_an_event_and_every_version_replays |
| Sources, sensors, people and agents as actors in the graph, with ADDED links to what they brought in, on the canvas, in the explorer and in Cypher | test_graph.py test_sources_are_actors_in_the_graph; jest graph-explorer.test.ts shows a source only while something it brought in is visible |
| A For you order of the timeline, learned from each person's own reactions, comments, marks and opens, each item saying why, with Latest the default; through the API, MCP, the assistant and the page | test_timeline_for_you.py test_for_you_learns_from_reactions_comments_and_marks_and_says_why, test_opening_an_item_counts_for_you_a_little_each_time_up_to_a_few, test_a_persons_affinity_is_reused_for_a_minute_until_a_signal_changes, test_agents_read_for_you_too; jest timeline-model.test.ts keep the For you order as a view, not a filter, timeline-ui.test.tsx For you: the toggle goes in the address, each placed line says why, and Latest stays the default |
| Muting a source, a person or a kind on the timeline, for good or until a time, for the person alone, with nothing deleted; through the API, MCP, the assistant (asking first after outside text) and the page | test_timeline_mutes.py test_a_mute_hides_an_actor_or_a_kind_for_the_person_alone, test_agents_mute_too_and_the_assistant_asks_first_after_outside_text; jest timeline-ui.test.tsx muting a source takes everything from it out, lists it as muted, and Undo or the chip brings it back, the menu beside Mute snoozes a source for an hour, a day or a week, or mutes the item's kind, a snooze can end at a time you pick, nine tomorrow to start with, and Enter mutes, timeline-model.test.ts a snooze ends an hour, a day or a week from now, a snooze to a time you pick starts at nine tomorrow, in the browser's own time |
An asset moved to another namespace is asset.moved, a Move seen where it is now, and a sensor a person pauses, ignores, resumes or sets back to new is sensor.changed, an Update with the status it had before, seen in its namespace or by admins; a change that changes nothing logs nothing |
test_timeline_verbs.py test_moving_an_asset_is_a_move_seen_where_it_is_now, test_switching_a_sensor_is_an_update_with_what_it_was |
A file a watched folder finds changed in its source and reads again is file.changed, an Update linking the asset it was before, not a second file.added; it is still a child of asset.added, so triggers on new assets fire on it as before |
test_events_content.py test_a_file_changed_in_its_source_is_file_changed_linked_to_the_asset_it_was_before |
An opened timeline item shows what it caused, the runs it started of the automations the person manages and the approvals those asked for, with Approve and Decline on waiting ones; GET /activities/{id}/caused, approvals?activity=, MCP activity_caused and the assistant |
test_timeline_caused.py test_an_item_lists_what_it_caused_and_the_waiting_approval_is_decided_there, test_agents_read_what_an_item_caused_too; jest timeline-ui.test.tsx an opened item shows what it caused, and a waiting approval is approved there |
One timeline item by its id in the address (/timeline?item=<id>), opened above the timeline with its actions and a way back, which is where a comment on an item, a reaction and an approval's "because" link |
jest timeline-ui.test.tsx the address can name one item, opened above the timeline, with a way back; test_timeline_social.py test_comments_on_a_logged_activity_follow_its_audience |
| Saved views of the timeline: its filters and order under a name, each person's own, picked from the page's Views menu and through the API and MCP | test_timeline_views.py test_timeline_views_are_saved_renamed_changed_and_deleted_by_their_maker_alone, test_agents_list_save_and_delete_timeline_views; jest timeline-model.test.ts a saved view keeps the filters and order as they are, and offers a name from them, timeline-ui.test.tsx Views: one click shows a saved view, and the filters shown now are saved under a guessed name, Views: the view the filters match is named on the button, and deleting it can be undone |
Every entity's own page at /wiki/<namespace>/<Name> |
test_entities.py test_wiki_addresses; wiki-page.test.tsx; journeys.mjs entity-find |
| Entities, topics and people edited and merged from their lists, the graph or an asset, with Undo instead of a confirm | test_entities.py test_suggestions_and_merge_undo; entities-curate.test.tsx; topics-curate.test.tsx; recording-entities-edit.test.tsx; journeys.mjs entity-merge, graph-merge, speaker-rename |
| Names get their real types (people, places, organisations, events, works) by rules, without a model; older Terms are typed on upgrade | test_name_types.py test_names_get_the_type_their_words_give, test_analysis_types_people_places_and_events, test_the_upgrade_types_the_old_terms_and_keeps_what_people_chose |
| A decision model (Jev or Laya) types the names the rules leave as Terms, in one call, keeping only types it is sure of; Settings › Analysis can leave them Terms | test_name_types.py test_the_decision_model_types_what_the_rules_cant, test_a_type_the_model_isnt_sure_of_stays_a_term, test_by_default_the_language_model_isnt_asked, test_with_typing_rules_nothing_is_asked |
| What is most like a passage, an asset, a topic or an entity | test_similar.py test_like_a_text_by_its_key_words, test_like_a_resource_by_words_topics_and_entities, test_like_a_topic_and_an_entity; similar.test.tsx |
| Search finds and filters moments by the sound heard in them (laughter, applause, music), typed any way, and counts each | test_search.py test_sound_events_filter_and_count; test_semantic.py test_search_by_meaning_keeps_to_a_sound; search.test.ts reads sound events typed any way |
| Search keeps to the assets recorded between two days, with a tag, from a source or in a language, by the API, MCP and the search page (empty until picked, kept in the link and in saved searches) | test_search.py test_dates_tags_origins_and_languages_narrow_search; search-asset-filters.test.tsx starts with none set, so the search is as it was, and picking one puts it in the address, asks with the ones in the address, shows each as a chip, and says which to clear when nothing matches; search.test.ts reads dates, tags, sources and languages, and keeps them in the address |
| With anytopdf, photos found by what they show | test_photos.py test_photos_found_by_what_they_show, test_off_by_default |
| Pages and shots described by the provider's vision model or, off by default, by Florence-2 on this server, fetched pinned and checked when picked | test_florence.py test_florence_describes_pictures_without_a_model_provider, test_florence_is_fetched_pinned_and_checked, test_admins_and_agents_choose_what_describes_pictures, test_a_picture_is_read_as_the_model_was_trained |
| Or by Moondream on this server, off by default, fetched at a pinned revision only where the chosen model can run, and saying why where it can't | test_moondream.py test_moondream_describes_pictures_without_a_model_provider, test_where_each_moondream_can_run, test_on_a_cpu_moondream_3_is_never_fetched_and_the_step_says_why, test_moondream_is_fetched_pinned, test_admins_and_agents_choose_moondream |
| A recording's sound cleaned up before transcription in a pipeline that has the step (noise, gain, echo), with silence left out of what's transcribed and the transcript's times kept on the recording's timeline, optionally only for low quality recordings | test_audio_cleanup.py test_speech_is_found_and_silence_left_out_of_transcription, test_echo_of_the_far_side_is_taken_out, test_without_the_step_or_with_every_option_off_it_is_transcribed_as_it_is, test_an_agent_reads_what_the_step_did, test_quality_gate_leaves_a_clean_recording_as_it_is, test_quality_gate_still_cleans_a_low_quality_recording |
| Transcripts cleaned up before analysis (stutters, "um"s, spacing), off by default, each change in the edit history with Revert and people's corrections left alone | test_cleanup.py test_off_by_default_changes_nothing, test_clean_step_tidies_lines_and_keeps_them_in_the_history, test_people_s_corrections_outrank_the_rules |
| Punctuation put back in transcripts that lack it by a local model, fetched only when picked | test_cleanup.py test_punctuation_put_back_where_it_is_missing, test_without_the_model_the_rules_run_alone, test_the_model_is_fetched_only_when_it_is_asked_for |
| Grammar fixed by a LanguageTool server you run, when its address is set: grammar fixes only, never spelling or a word with a capital | test_cleanup.py test_grammar_fixes_from_languagetool_leave_names_and_spelling, test_without_its_server_the_rules_run_alone, test_the_grammar_server_is_an_address_without_a_password |
| Entities found by spaCy, fetched the first time it's picked, with the defined entities first and the rules until it's there | test_spacy_entities.py test_defined_entities_come_first_then_spacy_s_names, test_the_analyze_step_and_workflows_use_it, test_without_spacy_the_rules_find_them, test_spacy_is_fetched_only_when_it_is_picked |
| Dates said in an asset read as days ("next Tuesday", "last week"), off by default, with dates that read two ways left out | test_dates.py test_dates_are_read_as_days, test_what_could_be_two_dates_is_left_out, test_off_by_default_reads_none |
| A transcript translated into another language only when someone asks, the estimate first: in batches of lines that keep their times and speaker, the transcript marked as data, every call in the activity ledger and within budgets, kept as a translation file like an uploaded one; through the web app's Files tab, the API, MCP and the assistant (asking first) | test_translations.py test_translating_writes_a_translation_file, test_the_transcript_reaches_the_model_as_data, test_lines_go_in_batches_and_keep_their_place, test_a_spent_budget_refuses, test_the_estimate_comes_first, test_what_stops_a_translation, test_agents_translate, test_the_assistant_asks_first; recording-translate.test.tsx is the first the browser reads that the transcript isn't in, translates into the browser's language with Enter, after showing the estimate |
| A date said in an asset is proposed as a calendar event in one click, prefilled from its line (the day, a time said, the line as its title, who it names), and added only once the person approves it, signed in; declined, nothing is added; offered only to admins with a calendar connection that may add events, and agents propose over MCP but can't approve | test_event_proposals.py test_a_date_said_is_proposed_and_added_only_once_approved, test_a_declined_proposal_adds_nothing, test_offered_only_to_admins_with_a_calendar_that_adds_events, test_agents_propose_over_mcp_but_cannot_approve, test_the_time_of_day_is_read_from_the_line; said-dates.test.tsx proposes an event from a date in one click, and says it waits in Approvals, offers no Propose event without a calendar that adds events, and shows nothing without dates |
| Videos play their transcript and timed supplementary captions and translations as WebVTT tracks with signed links, in the workspace (drawn by the browser only where the overlay can't be seen) and on the public page (off until a button turns them on), with only what the viewer may read | test_caption_tracks.py test_the_player_offers_the_transcript_and_timed_files_as_tracks, test_the_public_page_offers_what_the_visitor_may_read_as_tracks; jest caption-tracks.test.tsx stay undrawn while the page's own overlay shows the captions, and are drawn full screen, plays its text as captions the browser draws once the visitor turns them on |
Learn from it¶
| Requirement | Checks |
|---|---|
| A two-host podcast episode, every claim cited and checked against its source, in two voices | test_podcasts.py test_episode_is_made_by_a_job, test_fact_check_rewrites_or_drops, test_episode_gets_two_voices; journeys.mjs podcast-create; baseline.json podcast-create ≤ 5 clicks |
| A technical spec from a transcript, in a note linked to it: decisions, nos and constraints in the speaker's words linked to when they were said, gaps left to decide flagged, quotes checked against the transcript; through the web app's More actions menu, the API, MCP, the assistant (asking first) and pipelines | test_specs.py test_writing_a_spec, test_quotes_are_found_word_for_word, test_writing_it_again_keeps_one_note, test_long_transcripts_say_where_they_were_cut, test_agents_write_specs, test_the_assistant_asks_first, test_the_template_is_there_to_edit; recording-spec.test.ts offers to write a spec until there is one, opens the spec and writes it again once it's written |
| Notes beside every asset, entity and topic, written by you or the assistant | test_notebook.py test_pages_of_things, test_links_and_backlinks, test_assistant_keeps_notes; journeys.mjs note-create |
| A tree of notes renamed, moved and pinned (pins are your own), today's note one click away, and a search | test_notebook.py test_tree, test_pins, test_search; notes-tree.test.tsx; notes-today.test.tsx is one click from the Notes page |
| A note, or all of a namespace's notes, downloaded as Markdown in folders that follow the tree | test_notebook.py test_markdown_export; notes-tree.test.tsx downloads a note as Markdown; notes-find.test.tsx downloads every note as Markdown from its menu |
| Templates a note starts from, and a copy of any note | test_notebook.py test_templates_and_copies; notes-templates.test.tsx; notes-tree.test.tsx makes a copy beside the note and opens it |
| A note shows the notes it's inside and the notes inside it, to people and agents | test_notebook.py test_path_and_children, test_assistant_keeps_notes; test_mcp.py test_note_tools; note-page.test.tsx |
| Undo and Recently deleted instead of a confirm | test_notebook.py test_recently_deleted; notes-delete.test.tsx |
| A namespace publishes a private RSS feed or a podcast feed (Apple Podcasts' tags, audio with byte ranges) of resources, episodes, timeline activities and text, holding only what its readers can already read; through the API and MCP | test_feeds.py test_making_filling_and_reading_a_feed, test_a_podcast_feed_lists_what_has_audio, test_timeline_activities_in_a_feed, test_vaults_and_settings, test_agents_use_feeds |
| A saved collection is a private podcast feed of its assets with audio or video: one address per person who may read it, listing only what they can read, from the collection's page in one click with Copy, rotated or stopped like a namespace's feed; through the API and MCP | test_feeds.py test_a_collection_feed_lists_its_assets_with_media, test_a_collection_feed_token_rotates_and_stops, test_agents_get_a_collection_feed; collection-feed.test.ts |
| A routine fills a feed on a schedule, or makes a podcast episode about what's new: graph workflow nodes pick the resources or timeline activities since the last run, add them to a feed, or make an episode about them; Try says what they would do | test_feed_nodes.py test_a_routine_publishes_new_resources_to_a_feed, test_a_routine_publishes_the_timeline_and_text, test_a_routine_makes_an_episode_and_the_feed_lists_it_once_made, test_try_says_what_it_would_do_and_bad_settings_are_refused |
| Feeds in the web app: a Feeds page, reached from the Podcasts page and under Explore once there is a feed; New feed with the title focused and Enter making it in the current namespace; a feed's page copies the address in one click, gets a new one, edits what readers show, and takes items out or deletes the feed with Undo instead of a confirm; the publishing nodes on the canvas, checked before Publish | feeds-ui.test.tsx the Feeds page lists each feed with its kind, namespace, items and whether an app reads it, New feed opens on the title, defaults to the current namespace, and Enter makes it and opens its page, a feed's page copies the address, gets a new one, and takes an item out with Undo, deleting a feed is undone by making it again with its items, at a new address, editors change what readers show, and Enter in a field saves; workflow-flow.test.ts a Select node into Publish to feed or Podcast episode is a whole graph workflow, says what to fix: a feed to choose, the limits, and a graph that publishes nothing, names the selection and the feed on the canvas; nav-groups.test.tsx feeds are under Explore, after Podcasts, for everyone, Feeds is in the nav only once there is a feed, so a phone's nav stays as short as it was |
| A moment of any resource kept as a clip (a title, a note) in one click from the transcript or a highlight, in the playlist used last; playlists play clips back to back, reorder and remove with Undo, and each reader gets only the clips of resources they can read; through the API and MCP | test_playlists.py test_playlist_crud_and_clips, test_order_reorder_remove_and_undo, test_last_used_is_the_default, test_readers_get_only_clips_of_resources_they_can_read, test_clips_of_highlights, test_clips_go_with_their_resource_and_private_playlists_with_their_maker, test_agents_list_playlists_and_add_moments; playlists-model.test.ts; playlist-page.test.tsx reorders with Up and Down and saves the new order, removes a clip at once, and Undo puts it back in its place; recording-comments.test.tsx keeps a highlight as a clip in the latest playlist in one click, with Undo, makes a first playlist called Clips when there is none; nav-groups.test.tsx Playlists is in the nav only once there is one, under Explore after Feeds |
| An asset or a collection exported as an evidence PDF, whole or the parts you pick | test_evidence.py test_a_collection_as_one_evidence_pdf, test_everything_lens_found_goes_in, test_picking_the_parts; evidence-parts.test.tsx |
Automate it¶
| Requirement | Checks |
|---|---|
| Pipelines and workflows on a canvas | test_workflows.py test_workflows_run_in_pipelines_drawn_as_graphs; workflow-canvas.test.ts; journeys.mjs pipeline-create, workflow-create |
| What a pipeline's model step or a workflow's model node writes about an asset is found by search, by the API and MCP, under the step's name; running it again replaces those words, a step can opt out, deleting the asset removes them, and outputs written before are made searchable on upgrade | test_generated_search.py test_a_pipelines_model_step_is_found_by_search, test_searchable_is_an_llm_steps_setting, test_a_workflows_model_output_is_found_by_search, test_outputs_a_model_wrote_before_become_searchable; search.test.ts says where what wasn't said was found: on screen, on a page, seen, what a shot or page shows, or a model wrote; pipelines-templates.test.ts keeps an llm step's words in search unless it opts out |
| Convert steps make new assets: a video's soundtrack as audio, a document's pages as images, each run through its own pipeline | test_conversions.py test_a_videos_soundtrack_becomes_an_audio_asset, test_a_documents_pages_become_image_assets, test_settings_are_checked; pipeline-convert.test.ts is added from the library ready to run: to audio unless images are picked |
| A pipeline's Read step picks its own converter (auto, anytopdf or Lens's own) in place of the server's, checked as it's saved | test_anytopdf.py test_a_read_step_picks_its_own_converter; pipeline-convert.test.ts is kept only on a read step, and said in its summary |
| A Classify node in workflows chooses from labels, content types or topics, and acting on a sure answer keeps something | test_classify.py test_a_workflow_classifies; workflow-flow.test.ts chooses from labels, content types or topics, and acting on a sure answer keeps something |
| An Entity filter node keeps a list's entities by type, stop list, length, pattern or how often they're said | test_flow.py test_entity_filter_keeps_the_entities_asked_for; workflow-flow.test.ts an entity filter says what it keeps and refuses lowercase types |
| An HTTP call node calls a web address through the web tools' guard, with its host written out, its credentials sealed to their host, and a try that only reads | test_http_nodes.py test_it_calls_the_address_and_passes_the_answer_on, test_where_it_goes_is_written_out_and_guarded, test_a_try_only_reads; workflow-flow.test.ts an HTTP call node's host is written out |
| Routines on a schedule | test_routines.py test_routines_are_checked_and_run_when_due, test_schedules; journeys.mjs routine-create; baseline.json routine-create ≤ 2 clicks |
| Graph workflows find look-alikes to merge or link, wrong types, noise and topics to group, make the sure changes and propose the rest, each undoable and not made again once taken back | test_routines.py test_graph_workflows_organise_entities, test_rules_read_types_and_noise, test_graph_workflows_retype_and_hide, test_graph_workflows_group_topics; graph-changes.test.tsx say a type change in words and make it, show a group of topics under a new broader one |
| Everything that runs on a clock (routines, automations' schedule and at triggers, the backup) in one Schedules list, switched, re-timed and run from it, by people and by agents over the API and MCP | test_schedules.py test_one_list_holds_every_routine_automation_schedule_and_the_backup, test_schedules_are_changed_switched_and_run_from_the_list, test_agents_list_change_and_run_schedules_over_mcp; schedules-page.test.tsx lists every routine, automation schedule and the backup, each linking to its own page, switches one off, runs one now and changes when one runs, from the list |
| Batch runs over a collection | test_batches.py test_collections_and_batches |
| Notifications to webhooks, Slack, Discord and Matterbridge | test_notifications.py test_owners_manage_targets_and_a_test_is_signed, test_runs_that_end_are_sent_once_to_matterbridge |
| A routine's run that fails or leaves graph changes to review is told through Matterbridge and webhooks, to each namespace it runs over | test_notifications.py test_routine_runs_that_fail_or_leave_changes_to_review_are_told |
| Everything that happens is kept as W3C Activity Streams activities, read by agents through the API, MCP and the assistant | test_events.py test_activities_have_one_shape; test_activities.py test_the_timeline_lists_new_assets_newest_first, test_agents_read_the_timeline_too; test_news_feed_source.py test_a_news_feed_as_a_source |
Ask the assistant about a timeline item from the item (its button, or a on its line), in the chat beside the page, with the item's id, line and fields as what the question is about |
timeline-ui.test.tsx asking about an item hands the page chat its id, line and fields, by button or the a key; page-chat.test.tsx opens with what a page's own Ask about this hands it, and sends it as the highlighted part |
| The Timeline page and Latest on Home, marking what you've seen | test_timeline_marks.py test_marks_are_set_cleared_and_filter_the_timeline, test_a_tidy_drops_marks_and_reactions_left_on_activities_that_are_gone; timeline-ui.test.tsx Home's Latest card lists what you haven't reviewed |
| Reactions and comments on timeline activities, shared by everyone who sees them, each reaction saying who gave it, comments edited and threads resolved in place, through the API, MCP and the page | test_timeline_social.py test_reactions_are_shared_counted_and_taken_back, test_comments_on_an_activity_are_a_thread_everyone_who_sees_it_joins, test_comments_on_a_logged_activity_follow_its_audience, test_agents_react_and_comment_too, test_a_reaction_names_who_else_gave_it_earliest_first_up_to_five; timeline-social-model.test.ts a reaction's hover says who gave it: you first, then up to five names, then how many more; timeline-ui.test.tsx reactions and comments: a line sums its items', a 👍 goes to the server, and Enter sends a comment, your comment is edited in place, and a thread you can resolve is resolved and reopened from the timeline |
Tagging @lens in a comment on the timeline or an asset gets the agent's answer in the thread, as the person who tagged it, with every change waiting for their approval, off with ai.answer_comments |
test_comment_agent.py test_the_agent_answers_a_tagged_comment_in_the_thread, test_without_a_model_or_within_a_used_up_budget_the_reply_says_so; timeline-social-model.test.ts the agent's reply links its addresses |
| Automations act on the stream, on a schedule, at a time or when their own webhook is called | test_automation_runs.py test_each_new_activity_runs_each_automation_it_matches_once, test_a_schedule_runs_its_automation_when_its_timer_fires, test_a_call_to_a_webhook_runs_its_automation; test_timer_triggers.py test_an_at_trigger_fires_once_then_reads_done |
| They test conditions, then tell you, ask the model or the agent, use a tool, call a web address with sealed credentials, run Lens work, or change assets and notes | test_automation_runs.py test_the_if_limits_and_dry_runs; test_actions_messages.py test_notify_tells_me_in_my_chat_app; test_actions_ai.py test_ask_model_answers_in_text_or_as_the_fields_a_schema_names, test_ask_agent_works_on_what_happened_and_saves_the_conversation, test_call_tool_reads_as_the_person_and_changes_only_by_asking_them; test_actions_web.py test_credentials_in_headers_are_sealed_to_the_automation_and_the_host_and_never_shown; test_actions_run.py test_run_workflow_queues_it_on_the_asset_as_the_automation, test_run_pipeline_processes_the_asset_again; test_actions_lens.py test_set_field_is_kept_in_the_assets_history_by_the_automation, test_write_note_writes_a_page_adds_to_a_things_page_or_notes_an_asset |
| What reaches outside Lens, or changes more than what set it off, asks first: on the Approvals page or with a yes in chat; agents decline but never approve | test_automation_spec.py test_risk_follows_the_effect; test_approvals.py test_approve_carries_the_action_out_and_the_run_records_it, test_agents_over_mcp_list_and_decline_but_never_approve; test_approvals_chat.py test_a_yes_in_my_chat_decides_an_action_inside_lens; approvals-page.test.tsx groups what waits, puts the first in focus, and Enter approves it |
| Conditions test text, patterns, numbers, lists, times, days and whether a value changed, checked as they're saved and read in words | test_match_ops.py test_each_test_passes_what_it_should, test_checks_refuse_values_a_test_cant_use, test_a_pattern_reads_a_long_value_only_so_far_and_gives_up_when_slow, test_changed_compares_with_the_last_value_seen, test_the_added_tests_read_in_words |
| Automations fire when an entity is mentioned: any entity of a type, a name with * and ? wildcards, or an id, among up to fifty entities a processed asset is tagged with; one that names neither doesn't fire, and conditions saved before pass as they did | test_automation_mentions.py test_a_wildcard_a_type_and_an_id_find_a_mention, test_a_glob_is_linear_and_checks_refuse_what_it_cant_use, test_trees_saved_before_still_pass_as_they_did, test_an_automation_fires_on_any_entity_of_a_type_or_a_name_pattern_and_not_on_others, test_a_processed_asset_is_tagged_with_up_to_fifty_entities |
| Conditions look back over what happened in the automation's namespaces, as its person may see it, and compare each activity with the last one; a dry run keeps nothing | test_match_seen.py test_seen_finds_what_happened_in_the_window, test_an_if_looks_back_and_reads_the_context, test_seen_is_only_for_an_automations_if; test_automation_conditions.py test_the_if_looks_back_in_the_automations_namespaces, test_changed_in_a_where_compares_with_the_last_activity, test_the_if_and_a_condition_node_agree_and_dry_runs_keep_nothing |
| Joins wait for an automation's triggers together: all of them, a number of times, one that doesn't come in time, or a batch, on timers that outlast a restart | test_match_joins.py test_all_waits_for_every_trigger_within_the_window_on_the_same_key, test_count_fires_once_per_crossing_then_counts_again, test_absent_waits_with_a_timer_and_the_missing_one_cancels_it, test_batch_collects_then_runs_once, test_two_writers_at_once_lose_nothing; test_automation_conditions.py test_an_absent_join_waits_on_a_timer_and_switching_off_drops_it |
| Each automation has limits and budgets | test_automation_runs.py test_the_if_limits_and_dry_runs, test_a_run_over_budget_is_held_for_the_persons_pick |
| Agents make, try and run automations over MCP and in the assistant, or draft one from a sentence | test_automation_tools.py test_agents_save_try_and_run_automations_over_mcp, test_the_assistant_saves_at_once_and_asks_to_switch_on_or_run; test_automations.py test_an_automation_is_drafted_from_a_sentence |
| Starter automations come switched off, each one saving as it is, and changing nothing until someone turns it on; in the builder one fills it in to save and turn on | test_automation_starters.py test_every_starter_saves_as_it_is_and_stays_off, test_the_school_starter_tells_me_at_once_and_asks_before_posting_to_the_family; automations-ui.test.tsx a starter fills the builder, and Save and turn on saves it switched on; journeys.mjs automation-starter; baseline.json automation-starter ≤ 4 clicks, 0 keys |
| From an email to a yes: an automation tells you at once, asks before posting to the family, carries the post out on approval, and its run shows on the timeline | test_automation_journey.py test_an_email_runs_an_automation_that_tells_me_asks_before_posting_and_shows_on_the_timeline |
| The automations guide reads from a five-minute walkthrough to the reference, and asked "how do I make Lens tell me when the school emails?", the assistant and agents find that walkthrough | test_lens_help.py test_the_automations_guide_answers_how_to_be_told_when_the_school_emails |
| An automation's workflow is drawn and tried on the canvas: its actions in the palette, their settings drawn from each action's params, Decide's ports and the leaf rule | workflow-canvas.test.ts give a decide node a port per option, then unsure, keep the leaf rule: nothing after an action that reaches outside Lens or waits, say what the backend would reject in their settings; automation-nodes-ui.test.tsx sets an action up from the catalog, its params drawn from its schema; schema-form.test.tsx opens the variable picker on {{ and puts the pick in with Enter |
| In the web app, automations are listed with what sets each off in words, and built in When, If and Then with a test on a recent activity, problems next to their fields | automations-ui.test.tsx the list reads each automation, and Delete is undone from its toast, the builder adds a trigger with a condition and an action, then saves it switched off, the test panel runs the draft on a recent activity and says what it would do, API problems go next to what they are about; test_automations.py test_lists_read_the_triggers_in_words_and_say_when_it_runs_next |
| An automation's page shows its runs a page at a time with each one's log, what's coming up, its variables to edit and its costs | automation-detail.test.tsx Runs pages back with before, and a run opens to what it did and its log, a run held over a budget can run once or be skipped, Coming up lists its times, and says why when there are none, Variables set from the keyboard, and Clear is undone from its toast |
Share with the people close to you¶
| Requirement | Checks |
|---|---|
| Circles of people on the same Lens are each person's own, and hold only people they may add | test_circles.py test_circles_are_your_own |
| Sharing an activity or a resource with a circle and a note shows it in each member's own timeline and the sharer's, with the item or without, undone with its permissions, never from a vault | test_circles.py test_sharing_with_a_circle |
| Automations follow what was shared with the person; removing someone takes their circles and what was shared with them | test_circles.py test_automations_follow_shares |
| Agents make circles and share over MCP; the assistant shares only after the person's yes | test_circles.py test_agents_share_too |
| Circles live under Profile, a suggested circle takes one click and deleting one is undone, not confirmed | circles-ui.test.tsx makes a suggested circle in one click; circles-ui.test.tsx deletes a circle with Undo instead of a confirm |
| Share on a timeline item or a resource picks the only circle, takes a note and is undone from its toast | circles-ui.test.tsx picks the only circle, shares with a note, and undoes from the toast |
| A rule shares what comes in with a circle and a note, asking first until the person lets it run on its own | test_circles.py test_rules_share_on_their_own_only_when_let |
| A circle sees when you're free only once you let it, only when and never what, each ask in your timeline; agents and the assistant find a time | test_circles.py test_when_people_are_free; circles-ui.test.tsx lets a circle see when you're free in one click |
| Circles reach people on other servers only once an admin and the person turn it on: shares go to them signed, and theirs are taken only signed, recent and from someone in a circle | test_federation.py test_off_until_turned_on_and_findable, test_people_elsewhere_get_shares_and_send_them, test_notes_from_people_in_a_circle_only; circles-ui.test.tsx makes you findable, and adds someone elsewhere by handle with Enter |
| Someone's agent answers a question only from the namespaces they let that circle ask about, citing them, and they see it | test_circles.py test_asking_their_agent; circles-ui.test.tsx lets a circle ask your agent about the namespaces you pick |
See what it costs¶
| Requirement | Checks |
|---|---|
| An activity ledger of every model call, with reports | test_activity.py test_a_routines_calls_count_for_it_its_run_and_the_jobs_it_queued, test_chats_and_people_see_only_their_own |
| Models priced by tokens or by the hour, and speech providers by the minute heard or the character read | test_activity.py test_models_are_costed_by_tokens_by_time_or_not_at_all; test_speech.py test_speech_calls_are_costed_per_minute_heard_and_per_character_read; settings-prices.test.ts round-trips every unit |
| One overall budget, asked after a cloud model, with a soft limit that warns and a hard limit that stops paid calls | test_budgets.py test_one_budget_for_everything_with_a_soft_and_a_hard_limit; test_setup.py test_the_wizard_asks_only_what_has_no_answer |
| A meter in the top bar; the Costs page by day, model and what cost most | budget-meter.test.tsx the top bar's budget meter; test_budgets.py test_the_meter_follows_the_namespace_and_its_budget |
The assistant and agents over MCP answer where budgets stand (budgets), each person seeing only their namespaces |
test_budgets.py test_agents_ask_where_budgets_stand |
| The Costs page's charts | No check yet (#486) |
| Routine choices go to small decision models, the LLM only when it adds value | test_decisions.py test_the_decision_model_answers_when_it_has_a_key, test_the_language_model_decides_without_one_and_when_it_fails |
Scale out¶
| Requirement | Checks |
|---|---|
| Off until an admin turns it on; off, every unit is let go and the scaler writes nothing | test_scaling.py test_scaling_is_off_and_lists_its_drivers, test_the_settings_section, test_this_machine_runs_more_workers_at_no_cost |
| Providers keep their credentials sealed, shown only as set, and prove them with a read-only call | test_scaling.py test_a_providers_credentials_are_sealed_and_checked |
| A group launches nothing until a person signed in to the web app approves its terms; agents never approve, and widening the terms takes the approval away | test_scaling.py test_only_a_person_in_the_web_app_approves_a_groups_terms; test_scaling_rules.py test_what_widens_an_approval |
| Target, step and schedule rules pick the number of units, within min and max | test_scaling_rules.py test_a_target_rule_keeps_its_metric_at_the_target, test_a_step_rule_fires_once_its_condition_has_held, test_a_schedule_rule_sets_the_bounds_when_its_time_comes, test_the_largest_proposal_wins; test_scaling.py test_rules_scale_on_waiting_jobs_and_on_a_schedule |
| The scaler launches units, sees them join, drains busy workers before letting them go, and fails and pauses on units that don't come up | test_scaling.py test_the_scaler_launches_joins_drains_and_terminates, test_a_unit_that_never_joins_fails_and_failures_pause_the_group, test_protected_and_terminated_units |
| Each unit's cost goes into the activity ledger as an estimate, and a group stops at its budget's cap | test_scaling.py test_the_scaler_launches_joins_drains_and_terminates, test_at_its_budgets_cap_a_group_stops |
| Settings › Auto scaling shows each group's terms in words and approves them in one click | settings-scaling.test.tsx shows each group's terms and approves them in one click, puts the terms and the state in words |
| Settings › Auto scaling adds a provider from its driver's fields (credentials sent apart, never shown), shows its check in words, removes it with Undo, and makes a group, with a budget when the provider charges | settings-scaling.test.tsx adds a provider from its driver's fields, with the secret sent apart, shows a failed check in words, after saving and in the list, makes a group with a budget on a priced provider, asks no budget on a free provider and ticks a list's options, removes a provider with Undo instead of a question |
| Units a provider runs with Lens's tag that Lens has no unit for show on the provider and in the activity ledger once; Lens lets none go itself, a person signed in to the web app does (with Undo), and the assistant only asks | test_scaling.py test_units_lens_lost_track_of_show_on_their_provider, test_a_person_lets_a_stray_go_and_an_agent_cant; test_scaling_agents.py test_the_assistant_asks_to_let_a_stray_go_and_the_person_says_yes; settings-scaling.test.tsx shows the units a provider runs that Lens lost track of, and why it couldn't ask, lets a stray go with Undo instead of a question |
| A unit that goes (let go, failed, or never came up) leaves the mesh: its unspent join token is withdrawn and the node that joined with it is removed, its worker stopped and its database user gone | test_scaling.py test_a_unit_let_go_or_failed_leaves_the_mesh; test_mesh_workers.py test_a_scaling_unit_let_go_leaves_the_mesh_with_its_token |
| This machine runs more workers at no cost | test_scaling.py test_this_machine_runs_more_workers_at_no_cost |
| DigitalOcean and Hetzner Cloud: credentials checked, list prices, machines launched with a one-use join token, watched, found by tag and deleted; a group waits for the mesh's install line before launching | test_scaling_clouds.py test_a_digitalocean_group_waits_for_the_install_line_then_launches_droplets, test_hetzner_prices_in_euros_and_its_errors_read_as_hetzners, test_the_cloud_drivers_are_listed_and_need_the_mesh |
| Lambda: GPU instances launched with the account's SSH key and a one-use join token in their user-data, tagged with Lens's tag and the unit, priced from Lambda's list, watched (an instance Lambda takes back counts as gone), found by tag across pages and terminated; every request and answer held to Lambda's published API document | test_scaling_clouds.py test_a_lambda_group_launches_gpu_instances_that_join_over_the_mesh, test_lambda_needs_an_ssh_key_and_pages_through_instances, test_lens_reads_only_fields_the_clouds_answer_with |
| A group of machines elsewhere counts only the jobs a worker node takes, and never launches one for others | test_scaling_clouds.py test_a_group_elsewhere_counts_only_the_jobs_a_node_takes |
| Proxmox VE: clones of a VM template, started and booted through the guest agent, found by tag, stopped and deleted; a clone whose agent never answers says so; every request and answer held to Proxmox's own API schema | test_scaling_proxmox.py test_proxmox_clones_boots_through_the_guest_agent_and_deletes, test_a_clone_whose_agent_never_answers_says_so, test_a_clone_still_booting_is_pending, test_lens_reads_only_fields_proxmox_answers_with |
| Linux hosts over SSH: host keys trusted on first use and held to, workers started per host with a script that removes itself, found, counted and stopped | test_scaling_ssh.py test_host_specs, test_ssh_trusts_host_keys_on_first_use_and_holds_to_them, test_ssh_starts_counts_and_stops_workers |
| AWS EC2 units launch as spot or on-demand instances, hardened (IMDSv2, encrypted disk, terminate on shutdown), with only the mesh's one-time join line in their user-data | test_scaling_aws.py test_a_spot_unit_launches_hardened_and_priced_in_its_zone, test_the_driver_is_offered_and_its_settings_are_checked |
| A spot unit falls back to on demand when AWS has none; without a price or the mesh, nothing launches | test_scaling_aws.py test_without_spot_capacity_it_falls_back_to_on_demand_unless_told_not_to, test_a_unit_without_a_price_or_the_mesh_launches_nothing |
| When AWS takes a spot instance back, the scaler says why and launches another | test_scaling_aws.py test_describe_says_when_aws_took_a_spot_instance_back, test_the_scaler_replaces_a_spot_unit_aws_took_back, test_terminate_check_and_listing |
| RunPod: GPU pods serving a model with vLLM, each with an API key of its own sealed on the unit, running once vLLM answers, deleted when stopped; no mesh needed | test_scaling_models.py test_a_runpod_group_serves_a_model_on_gpus_it_rents, test_runpod_says_why_it_made_no_pod |
| Hugging Face: Inference Endpoints serving a model with vLLM on hardware picked from Hugging Face's list (price, cloud, GPUs), each with a key of its own as an endpoint secret, never the Hugging Face token; requests and answers held to Hugging Face's published API document; units that never come up pause the group after three | test_scaling_models.py test_a_hugging_face_group_serves_a_model_on_an_inference_endpoint, test_endpoints_that_never_come_up_pause_the_group, test_lens_reads_only_fields_hugging_face_answers_with |
| Amazon SageMaker: real-time endpoints serving a model with AWS's vLLM container, each a model, an endpoint configuration and an endpoint tagged with the unit and all three deleted when it goes, priced from the AWS Price List; Lens's model calls to it are signed with AWS Signature Version 4, never carry the secret key, stream as the container writes (InvokeEndpointWithResponseStream, read with botocore's own event stream decoder; whole when the key may only invoke), and embeddings and speech never borrow it | test_scaling_sagemaker_moto.py test_a_unit_is_a_model_a_configuration_and_an_endpoint_and_goes_as_all_three, test_an_endpoint_that_fails_says_sagemakers_reason, test_without_a_price_or_with_a_bad_setting_nothing_is_made, test_a_sagemaker_group_serves_lenss_model_calls_signed_until_it_scales_in; test_llm_sagemaker.py test_a_chat_goes_to_invoke_endpoint_signed_and_never_carries_the_secret, test_a_streamed_answer_comes_as_the_container_writes_it, test_a_model_error_mid_stream_says_so, test_keys_that_may_only_invoke_get_the_answer_whole, test_an_endpoint_serves_its_one_model_and_is_never_asked_for_a_list, test_a_key_that_isnt_an_aws_key_says_what_it_should_be, test_other_servers_still_get_a_bearer_token_at_chat_completions, test_embeddings_and_speech_never_borrow_the_aws_key |
| Amazon Bedrock as the LLM provider: chat, tool calls and their results, images, JSON answers and summaries go to Bedrock's Converse API signed with the AWS key, Bedrock's answers and refusals read as an OpenAI-compatible server's (a context too long, tools a model doesn't take), chat answers stream as they are written (ConverseStream; whole when the key may only invoke), and embeddings and speech never borrow the key; each request and answer is held to botocore's model of the API | test_llm_bedrock.py test_a_chat_is_a_converse_call_and_reads_as_an_openai_reply, test_tool_calls_and_their_results_go_as_converse_turns, test_a_json_answer_asks_for_the_schema_in_the_system_prompt, test_a_summary_goes_to_bedrock_too, test_a_streamed_answer_is_converse_stream_as_it_is_written, test_a_model_error_mid_stream_says_so, test_keys_that_may_only_invoke_get_the_answer_whole, test_bedrocks_refusals_read_as_lenss, test_bedrock_lends_its_key_to_nothing_and_lists_the_model_it_was_given |
| Amazon Bedrock provisioned throughput: made with no commitment, named and tagged with the unit, running once in service, failed ones gone with Bedrock's reason, found by name across pages and deleted when it goes; nothing launches without the admin's price; while it serves, Lens's model calls go to it signed; every request and answer held to AWS's model of Bedrock's API and its name patterns | test_scaling_bedrock.py test_a_unit_is_a_provisioned_throughput_held_until_it_goes, test_one_that_fails_says_bedrocks_reason_and_none_launch_without_a_price, test_a_long_name_keeps_its_number_and_bedrocks_pattern, test_a_bedrock_group_serves_lenss_model_calls_until_it_scales_in |
| A running model unit is Lens's LLM provider until none runs, then the provider from before comes back; a provider a person picks meanwhile stays | test_scaling_models.py test_a_running_model_unit_serves_lenss_model_calls_until_none_runs, test_a_provider_a_person_picks_while_a_unit_serves_stays |
| Model calls go to a group's running units in turn (one key per group, sealed on it); a unit that can't be reached hands its call on, and a spread left from another provider sends nothing elsewhere | test_scaling_models.py test_model_calls_go_to_a_groups_running_units_in_turn, test_llm_spread_is_checked |
| Lambda as a model host: GPU instances run vLLM over HTTPS with a certificate the group's own CA signed (its key sealed on the group), behind a firewall ruleset made once per region and the group's key; Lens's calls trust only that CA, and fail with none or another group's, tried against a real HTTPS server | test_scaling_clouds.py test_a_lambda_model_group_serves_https_with_a_certificate_lens_signed, test_lens_reads_only_fields_the_clouds_answer_with |
| Provider fakes take only the requests the real API would, answer only what it could, and Lens reads only fields the provider documents; AWS runs from launch to scale-in on moto | test_scaling_clouds.py test_lens_reads_only_fields_the_clouds_answer_with; test_scaling_models.py test_lens_reads_only_fields_runpod_answers_with; test_scaling_aws_moto.py test_a_spot_instance_launches_tagged_hardened_and_is_found_by_its_tag, test_an_instance_someone_stopped_is_terminated_so_its_disk_stops_costing, test_the_scaler_runs_a_group_on_moto_from_launch_to_scale_in |
| Agents manage scaling in the assistant and over MCP: proposals wait for the person, actions within the terms happen at once | test_scaling_agents.py test_the_assistant_proposes_and_the_person_approves_in_the_web_app, test_a_setup_conversation_makes_a_group_that_still_waits_for_the_person, test_only_admins_get_the_scaling_tools, test_mcp_clients_manage_scaling_but_never_approve |
Run on more machines¶
| Requirement | Checks |
|---|---|
Off until an admin adds a node, which makes the mesh's certificate authority and turns it on; nothing goes in .env |
test_mesh.py test_adding_a_node_makes_the_ca_a_token_and_turns_the_mesh_on; settings-mesh.test.tsx is off until a node is added |
| A machine joins with the install line and a one-time token that never crosses the wire, and its answer is sealed for that token; wrong or spent tokens are refused and repeated wrong joins turned away | test_mesh.py test_a_machine_joins_without_sending_the_token, test_a_join_answer_is_only_for_the_token_it_was_sealed_with, test_too_many_wrong_joins_from_one_address_are_turned_away; test_mesh_node.py test_a_machine_joins_and_follows_the_primary, test_a_spent_or_wrong_token_is_not_tried_again, test_the_install_line_names_this_install_and_the_token |
| After joining, a node talks to this install over the mesh only: it follows its config, renews its certificate and reports every minute, and a removed node is blocked everywhere until an undo | test_mesh.py test_a_node_reports_renews_and_follows_its_config; test_mesh_node.py test_a_machine_joins_and_follows_the_primary, test_a_primary_out_of_reach_is_tried_again_later, test_the_primarys_mesh_address_is_a_host_only_for_nodes_calls |
| Nebula runs in its own container, fetched once with its checksum checked against the one kept in Lens, and is started again when it stops | test_mesh_service.py test_the_sidecar_pins_the_nebula_lens_does, test_the_sidecar_downloads_nebula_only_with_its_checksum, test_the_keeper_and_the_sidecar_bring_up_the_primary, test_a_stopped_nebula_is_started_again_after_a_while; test_mesh.py test_nebula_is_fetched_once_with_its_checksum_checked |
| A worker node runs jobs as its own user of Lens's database, never root and never with the archive's secret, reading only its namespaces' files over the mesh, never a vault's | test_mesh_workers.py test_a_worker_node_runs_lens_worker_as_its_own_user_of_the_database, test_its_jobs_read_files_from_here_over_the_mesh, test_a_node_serving_some_namespaces_takes_only_their_jobs, test_only_a_worker_node_gets_the_database, test_a_node_runs_only_the_steps_a_node_can |
| A compute node runs the assistant's code on a gateway of its own, which Lens reaches over the mesh with mutual TLS, holding its certificate sealed | test_mesh_compute.py test_a_compute_nodes_install_line_starts_its_gateway, test_a_compute_node_hands_over_its_gateway_and_the_assistant_may_run_code_there, test_only_a_compute_node_gives_a_gateway_and_only_a_certificate, test_lens_reaches_the_nodes_gateway_with_its_certificate_checked_by_name |
| Settings › Nodes adds a machine with Enter, shows its install line once, lists each machine with how it's doing, renames in place and removes with Undo; Settings › Network shows the mesh and its settings | settings-mesh.test.tsx adds a node with Enter and shows the install line once, with its terms, lists each machine with how it's doing, removes a node without asking, and undoes it, renames a node with Enter, says whether a compute node's sandbox gateway runs, shows that it runs, and what Nebula said, checks the network and the hosts as the server does |
| Agents read the mesh and add, rename, remove, restore and withdraw over MCP and in the assistant; adding or removing a machine in the assistant waits for the admin's yes, and a join token's secret never reaches the model | test_mesh_agents.py test_mcp_clients_add_and_manage_mesh_nodes, test_the_assistant_adds_and_removes_machines_once_the_person_approves, test_the_assistant_renames_restores_and_withdraws_at_once, test_a_setup_conversation_still_asks_before_a_machine_joins_or_goes, test_only_admins_get_the_mesh_tools; approval-install-line.test.tsx shows its install line this once, until the person is done with it |
Keep it up to date and backed up¶
| Requirement | Checks |
|---|---|
| A daily check for a new release, told to admins by email or a notification | test_updates.py test_the_daily_check_runs_when_due_and_can_be_turned_off, test_a_new_release_is_announced_once |
| One click (or one API call) upgrades through the machine's agent, showing each step | test_updates.py test_one_click_upgrade_hands_the_release_to_the_agent; settings-updates.test.tsx shows each step of the upgrade as the machine goes; install.yml An upgrade asked for in the web app is installed by the machine's agent |
| A new version that doesn't start is rolled back | test_upgrade_agent.py test_a_release_that_doesnt_start_is_rolled_back |
| The same agent changes whether other devices reach Lens and which edition runs | test_install_settings.py test_the_agent_sets_up_the_version_running_with_the_change, test_the_agent_passes_the_change_to_install_sh; install-settings.test.tsx |
| Backups of the database and files: take, list, download and restore; unchanged files take no new room | test_backups.py test_take_download_and_restore, test_backups_keep_and_restore_the_files_too |
| Backups on a schedule, each copied, encrypted, to a storage connection | test_backups.py test_backups_on_a_schedule, test_copies_are_sealed_with_secret_key |
| A scheduled backup's copy reaching a storage connection, in CI | No check yet (#486) |
| Each release is first checked by upgrading the one before it, holding data: nothing lost, each pending step run once after a backup, a second run changes nothing | upgrade.yml Upgrade and check; release.yml Upgrade from the last release; test_cli.py test_a_worker_logs_the_upgrade_it_runs_as_it_starts |
Watch it run¶
| Requirement | Checks |
|---|---|
| Structured logs, read in Settings › Logs, and optionally sent to a syslog server | test_logs.py test_viewer_reads_the_file_newest_first_and_filters, test_syslog_off_by_default_then_sent_over_udp |
| Logs optionally sent straight to Grafana Loki or another HTTP endpoint, with sealed headers | test_logs.py test_http_sends_to_loki_in_batches_with_the_sealed_headers, test_http_test_line_as_ndjson_and_headers_dropped_for_a_new_host, test_unreachable_http_endpoint_never_blocks |
| Client certificates (mutual TLS) and a private CA for syslog over TLS and https log endpoints | test_logs.py test_client_certificates_for_syslog_over_tls_and_https |
| OpenTelemetry traces and metrics, off until turned on | test_telemetry.py test_off_by_default_sends_nothing, test_traces_and_metrics_go_to_the_endpoint |
A Prometheus /metrics endpoint, off until turned on |
test_prometheus.py test_off_by_default, test_needs_its_token |
| One switch starts Prometheus, node-exporter and Grafana next to Lens | test_monitoring.py test_turning_monitoring_on_and_off, test_the_agent_changes_compose_profiles |
| The job queue: clear it, reorder it by drag and drop, retry failed runs in bulk, in Activity and over the API and MCP | test_jobs.py test_clearing_reordering_and_retrying_the_queue, test_agents_work_the_queue_over_mcp; queue-controls.test.tsx; activity-model.test.ts |
| The Analytics page: active people, churn, retention and feature use, worked out on this machine | test_analytics.py test_metrics_active_churn_retention; analyticsPage.test.tsx |
| The same daily events to a Google Analytics 4 property of your own, off unless turned on | test_analytics.py test_events_kept_locally_with_ga_off, test_daily_events_once_a_day_counts_only |
| The space each source takes, its assets' own files and what Lens made of them, through the API and on System health | test_recording_list.py test_the_space_each_origin_takes |
Stay private and safe¶
| Requirement | Checks |
|---|---|
| Passkeys instead of passwords | test_passkeys.py test_first_admin_with_a_passkey_and_no_password, test_sign_in_with_a_passkey |
| A passkey at another of Lens's addresses (remote access, HTTPS) added from a signed-in one: Open signed in, then one fingerprint | test_passkeys.py test_taking_your_sign_in_to_another_address, test_the_addresses_lens_opens_at; passkeys-panel.test.tsx opens an address you have no passkey for signed in, to add one there; webauthn.test.ts points a failed sign-in to the address the passkey was made at |
| Encryption at rest, with a key per namespace | test_keyring.py test_each_namespace_gets_its_own_key_kept_only_wrapped; test_encryption.py test_a_new_archive_encrypts_and_still_plays_seeks_and_processes |
| Secret fields that refer to a Vaultwarden or Bitwarden vault, read by Lens and never shown to anyone, agents included | test_secrets.py test_a_secret_field_reads_its_value_from_the_vault, test_the_vault_is_read_again_and_what_vanishes_stops_being_used, test_the_vault_is_set_up_and_checked_in_the_app, test_agents_refer_to_secrets_and_never_read_them; settings-secrets.test.tsx is picked from the vault's items, and only the reference is saved, shows its reference and whether it was read, never a value |
| Roles per namespace: viewer, editor, owner, or custom roles made of permissions | test_roles.py test_builtin_roles_keep_their_meaning, test_custom_roles_allow_exactly_their_permissions; journeys.mjs person-add |
| Access that ends on a date: a role in a namespace or on a collection, or permission on an asset, opens nothing after its end and lists show it as expired | test_grant_ends.py test_asset_permission_holds_until_it_ends, test_namespace_role_holds_until_it_ends, test_collection_role_holds_until_it_ends; test_setup_assistant.py test_the_assistant_gives_a_role_until_a_date; access-until.test.ts sends the end of the day picked, and nothing for an empty field |
| A downloads switch per asset: off, only its owners save its files (its own file, its PDF, supplementary files) while everyone who sees it still plays it and reads its text | test_downloads.py test_downloads_off_refuses_a_viewers_download_but_still_plays, test_downloads_off_keeps_iiif_playing; access-model.test.ts sends the downloads switch only when it changes, on by default, says why the files can't be saved, only when they can't |
| A video's copy with faces blurred for sharing, made by ffmpeg over the faces' tracks only when asked (all, unnamed or chosen faces; API, MCP and the People tab), kept with its files, and played to visitors instead of the original only when an owner picks that | test_face_blur.py test_regions_cover_each_track_between_and_around_its_samples, test_the_filter_graph_blurs_each_region_for_its_time, test_blurring_faces_for_sharing; face-blur.test.ts asks for every face when all are ticked, else for the ones ticked in order, says how far it got, why it waits and why it failed |
| Email invitations, and removing someone for good | test_users.py test_invites_are_emailed_when_mail_is_set_up, test_admins_remove_people_for_good |
| An audit log | test_settings.py test_settings_are_editable_and_secret; test_files.py test_change_a_file_and_the_audit_log |
| Outside text reaches the model marked as data; text that reads like instructions is flagged, by patterns or an optional local classifier | test_prompt_injection.py test_what_tools_read_is_marked_as_data, test_a_result_that_reads_like_instructions_is_flagged; test_injection_model.py test_until_the_model_is_here_the_patterns_stand_alone |
| Once an answer has read outside text, sending anything out, or the browser acting on what the assistant wrote, waits for a yes | test_prompt_injection.py test_a_web_tool_asks_first_once_the_answer_read_outside_text; test_browser.py test_after_reading_a_page_the_browser_asks_before_what_the_person_didnt_write |
| Chat-room answers have the links they built broken up; agents over MCP get the archive as data | test_prompt_injection.py test_a_room_gets_the_links_the_answer_built_broken_up; test_mcp.py test_what_the_archive_says_reaches_agents_as_data |
| HTTPS set up in Settings, with certificates that renew, only for its own domains and optionally only for client certificates | test_https.py test_https_is_set_up_in_the_app, test_caddys_configuration_follows_the_settings, test_strict_sni_client_certificates_and_tls_13 |
| Remote access through a Cloudflare tunnel | test_tunnel.py test_remote_access_is_set_up_in_the_app, test_a_quick_tunnel_runs_in_one_process_and_stops_when_turned_off, test_a_managed_tunnel_is_made_routed_and_given_a_dns_record |
The S3 and WebDAV shares and the MQTT hub at the web app's address (/s3, /dav, /mqtt over WebSockets), each off until turned on |
test_s3.py test_at_the_web_apps_address_once_an_admin_allows_it; test_webdav.py test_at_the_web_apps_address_once_an_admin_allows_it; test_sensors.py test_broker_over_websockets, test_mqtt_over_websockets_takes_hub_logins_only; lens-relay.test.ts relays any method, path and body to the share, and its answer back, passes the handshake on with the visitor and the prefix, then joins the two connections |
| The threat model's three files agree, and a change to the prose comes with its companions, without a line every change edits | threat-model.yml validate, Validate the threat model, Test the validator; validate_threat_model.py; test_validate_threat_model.py test_prose_change_without_companions_fails, test_two_changes_to_the_prose_merge_cleanly |
| Owners make many assets public, restricted or private at once from the Library, with Undo putting each back | library-catalogue.test.ts touches only those not there yet, and remembers how to put each back; test_access_settings.py test_access_endpoint_and_defaults |
Use open standards¶
| Requirement | Checks |
|---|---|
| RDF and Dublin Core for every record | test_rdf.py test_recording_uri_public_and_members, test_import_round_trip_and_dublin_core |
| OAuth for apps and OpenID Connect sign-in | test_oauth.py test_discovery_says_where_everything_is, test_an_app_gets_access_and_acts_with_the_persons_roles; test_external_login.py test_signing_in_connects_the_account_with_the_same_verified_email |
| MQTT: a built-in broker, and a client for another one | test_sensors.py test_broker_is_an_ordinary_local_broker, test_bridge_client_reads_another_broker |
| IIIF for publishing | test_iiif.py test_manifests_collections_search_state_discovery, test_authorization_flow |
| Shared notes and comments as W3C Web Annotations in IIIF, for people with a role on the asset only (never unshared notes, never the public) | test_iiif.py test_notes_and_comments_as_web_annotations; test_documents.py test_notes_on_a_documents_pages |
| OpenTelemetry and Prometheus for monitoring | test_telemetry.py test_traces_and_metrics_go_to_the_endpoint; test_prometheus.py test_what_it_measures |
| An optional Fedora mirror | test_fedora.py test_off_by_default, test_sync_sends_changes_and_deletes |
| A CSV catalogue of the assets selected in the Library, one row each with a link to its page, safe to open in a spreadsheet | library-catalogue.test.ts has a row per asset with its people, tags and a link to its page, quotes what needs it and never lets a cell run as a formula |
Light by default¶
| Requirement | Checks |
|---|---|
| Heavy parts stay off or are fetched on first use: speech engines, a local LLM, document conversion, Apache Tika, the NSFW model, Fedora | test_components.py test_engines_are_fetched_on_first_use; test_local_llm.py test_settings_take_a_listed_model_or_any_gguf_on_hugging_face; test_convert.py test_libreoffice_and_chromium_are_installed_the_first_time_theyre_needed; test_tika.py test_off_by_default_nothing_changes; test_nsfw.py test_off_by_default_nothing_is_fetched_or_classified; test_fedora.py test_off_by_default |
| The MCP runner is off until turned on | test_mcp_servers.py test_a_server_that_runs_as_a_program_runs_in_the_runner |
| Setup asks only what has no default: the model, and a budget for a cloud model | journeys.mjs setup-wizard; baseline.json setup-wizard ≤ 1 click, ≤ 0 keys; test_setup.py test_the_wizard_asks_only_what_has_no_answer, test_a_paid_model_from_the_environment_still_asks_for_a_budget |
| Picking a cloud provider fills in its address and model; only the key is left to paste | journeys.mjs cloud-model; baseline.json cloud-model ≤ 3 clicks; test_setup.py test_offers_cloud_model_providers; settings-llm-presets.test.tsx switches to a cloud provider with one click and a pasted key |
| Settings are in named groups, a search finds any one by name, and rarely changed options stay folded | settings-find.test.tsx puts every section in exactly one group, finds a field by its label, and opens the section on it; settings-save.test.tsx are folded under a line saying what's in them, the main one left in view |
| A thin bar along the top of the web app runs while a page or its first data loads, not for a quick answer or a background refresh | top-progress.test.tsx runs along the top while a page's first data loads, doesn't flash for a quick answer, or show for refreshing what's on screen, runs from a page change until Next writes the new address, finishes a page change that lands back on the page showing, as a redirect can |
| The web app works on a phone: every journey is surveyed on one, and none may take more than its baseline | clicks.yml Survey on a phone; baseline-phone.json; phone-layout.test.tsx |
| Heavy steps kept to their hours and to so many at a time, off by default | test_quiet.py test_the_window_and_its_checks, test_heavy_steps_wait_for_their_hours_and_their_turn |
| The default install runs on a Raspberry Pi (arm64) with a cloud model | No check yet (#486) |