diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7e90477..8f1a00b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -15,6 +15,7 @@ - Release automation is handled by `release-please`; keep `release-please-config.json` and `.release-please-manifest.json` aligned with the latest published tag, keep root tags in the existing plain `v*` format, and prefer Conventional Commit subjects (`fix:`, `feat:`, `deps:`) so release PRs are generated correctly. - Merged release PRs are finalized by `.github/workflows/release.yml`: the workflow detects `chore(main): release ink x.y.z` merges, creates the canonical `v*` tag plus a compatibility `ink-v*` tag, publishes the GitHub release from `v*`, and clears stale `autorelease:*` labels so the next release PR is not blocked. - Protected-branch repos should prefer a `RELEASE_PLEASE_TOKEN` PAT for the release workflow so bot-created release PRs trigger the normal PR checks. +- In this repo, `main` is protected and all changes land through PRs, so `.github/workflows/pr-checks.yml` is the validation gate, `.github/workflows/codeql.yml` runs on PRs plus schedule, and `.github/workflows/release.yml` should only assemble `ink-app.html` and publish the release instead of rerunning the full test suite after merge. - Contributor-facing workflow and validation steps now live in `CONTRIBUTING.md`; keep build, test, and single-file output guidance aligned with that document when project workflows change. - Declarative WebMCP note creation lives in the `#webmcpNoteForm` form in `ink.template.html`; keep its submit path wired to `createNoteFromTool()` in `src/app/workspace-io.ts` so agent-invoked note creation stays in the single-page app and does not navigate away. - Cogito Mode work is tracked in `openspec/changes/add-cogito-mode/`; preserve the strict three-question JSON contract and the markdown insertion block format (`> ### AI` then question text) when implementing it. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 69efcdb..9a7b485 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,8 +12,6 @@ name: "CodeQL Advanced" on: - push: - branches: [ "main" ] pull_request: branches: [ "main" ] schedule: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00739f4..876bb08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,13 +85,9 @@ jobs: if: ${{ steps.merged_release.outputs.is_release_merge == 'true' }} run: npm ci - - name: Build + - name: Assemble release artifact if: ${{ steps.merged_release.outputs.is_release_merge == 'true' }} - run: npm run build - - - name: Run tests - if: ${{ steps.merged_release.outputs.is_release_merge == 'true' }} - run: npm test + run: node build/compile-and-assemble.js - name: Create release tags if: ${{ steps.merged_release.outputs.is_release_merge == 'true' }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a07dab..1d80d8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,7 @@ Feature ideas are welcome. Open an issue describing the problem you want to solv - When a release PR is merged, the release workflow finalizes it by creating the plain `v*` tag, adding a compatibility `ink-v*` tag for release-please state reconciliation, publishing the GitHub release from `v*`, and clearing stale autorelease labels. - Merge the release PR through the normal protected-branch flow to create the Git tag and publish the GitHub release. - If you want the release PR to trigger the normal PR checks automatically, configure a `RELEASE_PLEASE_TOKEN` secret with a PAT that can open pull requests in this repository. +- CI validation should happen in PR workflows before merge; the release workflow is intentionally limited to regenerating `ink-app.html` for the tagged release artifact instead of rerunning the full build/test suite after the protected-branch merge. ## Style Guide diff --git a/repomix-output.xml b/repomix-output.xml index b8b34e6..c797739 100644 --- a/repomix-output.xml +++ b/repomix-output.xml @@ -27413,6 +27413,62 @@ ${t.trim()} } + + + + + + + + + + + ink + + + + + + + + + + + + + + + ink + + + + @@ -27444,6 +27500,10 @@ ${t.trim()} + +export {}; + + { "compilerOptions": { @@ -27839,6 +27899,240 @@ Users want to authenticate with their GitHub account to save their preferences a - [ ] 3.2 Create callback page for production deployment + +## ADDED Requirements +### Requirement: Canonical Project Logo Asset +The repository SHALL store a canonical project logo source file that is used for documentation and favicon generation. + +#### Scenario: Canonical logo source is available +- **WHEN** a maintainer inspects branding files in the repository +- **THEN** a single canonical logo source exists in a stable project path +- **AND** the source is suitable for deriving README and favicon assets + +### Requirement: README Logo Rendering +The project documentation SHALL render the project logo in `README.md` using repository-relative asset references. + +#### Scenario: README displays logo +- **WHEN** `README.md` is rendered by a GitHub-compatible markdown renderer +- **THEN** the logo is visible near the document header +- **AND** the logo reference resolves without external network dependencies + +### Requirement: Favicon Assets Derived from Logo +The project SHALL provide favicon assets derived from the canonical logo source. + +#### Scenario: Favicon assets are generated and available +- **WHEN** the favicon generation workflow is run +- **THEN** favicon files are produced in the documented output location +- **AND** generated filenames match those referenced by the application template + +### Requirement: Application Template Favicon References +The application HTML template SHALL reference project favicon assets so browser tabs display the project icon. + +#### Scenario: Browser tab uses project favicon +- **WHEN** a user opens the built application in a modern browser +- **THEN** the browser resolves favicon references from the application output +- **AND** the tab icon reflects the project logo branding + + + +# Change: Add Project Logo to README and Favicon Assets + +## Why +The repository currently lacks consistent branding assets in project documentation and browser metadata. Adding the provided logo to `README.md` and defining favicon outputs improves recognizability and presentation. + +## What Changes +- Add a canonical logo asset in the repository based on `~/Downloads/ink_logo.svg`. +- Update `README.md` to render the project logo near the top of the document. +- Define and implement favicon outputs derived from the same logo source. +- Wire favicon references into the app HTML template so browser tabs use project branding. +- Document the asset location and favicon generation/update workflow. + +## Impact +- Affected specs: `branding-assets` +- Affected code: + - `README.md` + - `ink.template.html` + - `build/` scripts (if favicon generation is automated in build) + - `dist/` favicon artifacts + - `assets/` branding source files (new) + + + +## 1. Asset Setup +- [x] 1.1 Add the provided logo SVG to a canonical repository path for shared branding usage. +- [x] 1.2 Define favicon output formats and filenames derived from the canonical logo source. + +## 2. Documentation and Template Integration +- [x] 2.1 Update `README.md` to display the project logo with stable relative linking. +- [x] 2.2 Update the HTML template to include favicon references that resolve in the built output. + +## 3. Build and Distribution +- [x] 3.1 Implement or document the process to generate favicon artifacts from the SVG source. +- [x] 3.2 Ensure generated favicon assets are available in expected output locations. + +## 4. Validation +- [x] 4.1 Verify `README.md` renders the logo correctly on GitHub-compatible markdown viewers. +- [x] 4.2 Verify the built app/tab displays the new favicon in a modern browser. +- [x] 4.3 Verify documentation describes how to refresh logo-derived assets when the SVG changes. + + + +## ADDED Requirements +### Requirement: Prioritized Maintainability Refactor Plan +The project SHALL define a maintainability refactor plan with explicit High, Medium, and Low priority tiers based on engineering risk and value. + +#### Scenario: Plan is prioritized and actionable +- **WHEN** a maintainer reviews the approved change proposal +- **THEN** the proposal includes clearly separated High, Medium, and Low priority work +- **AND** each tier contains concrete codebase targets + +### Requirement: Evidence-Based Refactor Scope +Refactor recommendations MUST be grounded in current repository evidence and MUST avoid unnecessary changes. + +#### Scenario: Recommendations are justified +- **WHEN** a recommendation proposes delete, rename, or restructure actions +- **THEN** it references observable repository state (for example dead code, missing script targets, or duplicated logic) +- **AND** the proposal explicitly identifies areas that should remain unchanged when already aligned + +### Requirement: Behavior-Preserving Refactor Guardrails +Maintainability refactors SHALL preserve existing user-visible behavior unless a separate feature change is approved. + +#### Scenario: Refactor keeps current behavior stable +- **WHEN** maintainability refactor tasks are implemented +- **THEN** build and test workflows continue to pass +- **AND** core authoring flow behavior (open workspace, create note, edit, save) remains unchanged + +### Requirement: Tooling and Documentation Consistency +Documented commands and scripts MUST map to files and behavior that exist in the repository. + +#### Scenario: Script/docs mismatch is resolved +- **WHEN** package scripts or README commands are documented +- **THEN** each command resolves to existing code paths +- **AND** stale or broken command references are removed or restored + + + +## Context +Ink is intentionally lightweight and single-file at runtime. The refactor must preserve behavior while making source code easier for LLMs to read, regenerate, and safely modify. + +## Goals / Non-Goals +- Goals: + - Reduce cognitive load in `src/app.ts` by splitting by feature boundaries. + - Remove stale/dead paths that create false complexity. + - Improve naming so build and runtime responsibilities are explicit. + - Keep behavior and user workflow unchanged. +- Non-Goals: + - Framework migration. + - UX redesign. + - New product features. + +## Decisions +- Decision: Refactor in phases (High -> Medium -> Low) with behavior-preserving checkpoints. + - Rationale: minimizes regression risk and keeps scope manageable. +- Decision: Keep a thin composition entrypoint and move concerns into small modules. + - Rationale: supports predictable regeneration and targeted testing. +- Decision: Do not force changes where current code is already clear (for example `src/tags.ts`). + - Rationale: avoids churn and preserves momentum. + +## Risks / Trade-offs +- Risk: File splits can break implicit state assumptions. + - Mitigation: define explicit typed state contracts and test around open/save/refresh flows. +- Risk: Renaming scripts can disrupt local habits. + - Mitigation: keep temporary aliases and document migration in README. + +## Migration Plan +1. Remove dead code and duplicate timers in `src/app.ts`. +2. Introduce module boundaries for workspace scan, editor operations, and rendering. +3. Rename build scripts/files with compatibility aliases. +4. Re-run build + QUnit + Cypress and update docs. + +## Open Questions +- Should `sync:from-ink-app` be restored (by adding script) or removed from docs/scripts? +- Should `src/storage.ts` remain a test fixture in `src/` or move to a dedicated test-support path? + + + +# Change: LLM-Aligned Maintainability Refactor Plan + +## Why +The current codebase works, but it has several maintainability risks that reduce predictability and regenerability for LLM-driven development. The biggest issue is concentration of behavior in one large file, plus stale scripts and dead code paths that make intent harder to reason about. + +## What Changes +- Create a phased refactor plan aligned to project coding principles: clarity, pragmatism, rigor. +- Prioritize only high-value changes and explicitly avoid churn where behavior is already clear and stable. +- Define concrete rename/restructure/delete targets backed by repository evidence. +- Add acceptance criteria so refactor work can be verified without changing product behavior. + +### High Priority +- Split `src/app.ts` into small feature modules with a thin entrypoint (`main` + feature-oriented files). +- Remove duplicated auto-refresh behavior (currently both `startAutoRefresh()` timer and a separate global `setInterval` run every 10s). +- Fix stale/broken tooling contract: `sync:from-ink-app` references `build/sync-from-ink-app.js`, but the file is missing. +- Remove dead fields/utilities in `src/app.ts`: + - `searchScanDebounceTimer` + - `lastContentSearchToken` + - `sleep()` + - `humanDate()` + - `debounce()` + +### Medium Priority +- Rename ambiguous build scripts for intent clarity: + - `build/inject.js` -> `build/assemble-single-file.js` + - `build/build.js` -> `build/compile-and-assemble.js` +- Extract File System Access API handling into a dedicated module (permissions, handles, scan traversal) to reduce coupling with UI rendering. +- Replace broad `any` usage in app state with narrow interfaces for notes, tree nodes, and workspace handles. +- Remove redundant DOM lookups in `src/app.ts` where elements are already captured once. + +### Low Priority +- Decide whether test-only storage logic should be renamed for intent: + - `src/storage.ts` -> `src/test-support/storage-fixture.ts` (if kept test-only) +- Decide whether generated test bundles should stay committed: + - `dist/test/*` can be generated during `npm run build:test` and excluded from source control. +- Tighten Cypress scaffolding comments/config only if it improves signal-to-noise (avoid cosmetic edits). + +### Keep As-Is (No Change Needed) +- `src/tags.ts` is already small, explicit, and testable. +- Build pipeline remains lightweight and appropriate for single-file distribution constraints. +- Existing QUnit and Cypress coverage should be retained and expanded only around touched refactor areas. + +## Impact +- Affected specs: `codebase-maintainability` +- Affected code: + - `src/app.ts` + - `src/storage.ts` (rename/rehome decision) + - `build/build.js` + - `build/inject.js` + - `package.json` + - `README.md` + - `Makefile` (if command wrappers change) + - `tests/qunit/*` + - `cypress/*` + + + +## 1. High Priority (Safety + Clarity) +- [x] 1.1 Remove duplicate auto-refresh scheduling and keep one refresh control path. +- [x] 1.2 Remove dead fields/utilities from `src/app.ts` (`searchScanDebounceTimer`, `lastContentSearchToken`, `sleep`, `humanDate`, `debounce`). +- [x] 1.3 Resolve broken `sync:from-ink-app` contract by either restoring `build/sync-from-ink-app.js` or removing stale script/docs. +- [x] 1.4 Split `src/app.ts` into a small entrypoint plus feature modules with explicit interfaces. + +## 2. Medium Priority (Structure + Naming) +- [x] 2.1 Rename build scripts/files for clearer responsibility and keep compatibility aliases during migration. +- [x] 2.2 Extract File System Access API logic into a dedicated module. +- [x] 2.3 Replace broad `any` usage in app state/types with narrower interfaces. +- [x] 2.4 Remove redundant element re-queries in app initialization. + +## 3. Low Priority (Cleanup) +- [x] 3.1 Decide whether to rename/rehome `src/storage.ts` as test-support-only code. +- [x] 3.2 Decide whether `dist/test/*` should be generated-only and excluded from source control. (Decision: keep committed for now to preserve current repository pattern with generated distribution artifacts under `dist/`.) +- [x] 3.3 Clean minor Cypress scaffolding noise only when it improves maintainability. + +## 4. Validation +- [x] 4.1 `npm run build` succeeds and produces a working `ink-app.html`. +- [x] 4.2 `npm run test:qunit` passes. +- [x] 4.3 `npm run test:cypress` passes. +- [x] 4.4 README and script references match actual files/commands. + + # OpenSpec Instructions @@ -29090,39 +29384,154 @@ export function createUserManager() { const data: GitHubApiUserResponse = await response.json(); - cachedUser = { - login: data.login, - id: data.id, - avatarUrl: data.avatar_url, - name: data.name, - }; + cachedUser = { + login: data.login, + id: data.id, + avatarUrl: data.avatar_url, + name: data.name, + }; + + return cachedUser; + } + + /** + * Clears cached user data + * Called on logout + */ + function clearCache(): void { + cachedUser = null; + } + + /** + * Gets cached user without fetching + */ + function getCachedUser(): GitHubUser | null { + return cachedUser; + } + + return { + fetchUser, + clearCache, + getCachedUser, + }; +} + +export type GitHubUserManager = ReturnType; + + + +export interface KeyValueStorage { + readonly length: number; + key(index: number): string | null; + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; +} + +const STORAGE_PREFIX = "ink.workspace."; + +function workspaceKey(name: string): string { + return `${STORAGE_PREFIX}${name}`; +} + +function normalizeWorkspaceName(name: string): string { + return name.trim(); +} + +function parseWorkspace(value: string | null): Record { + if (!value) { + return {}; + } + + try { + const parsed = JSON.parse(value) as unknown; + if (!parsed || typeof parsed !== "object") { + return {}; + } + + const files: Record = {}; + for (const [key, content] of Object.entries(parsed as Record)) { + if (typeof key === "string" && typeof content === "string") { + files[key] = content; + } + } + + return files; + } catch { + return {}; + } +} + +function writeWorkspace(storage: KeyValueStorage, workspace: string, files: Record): void { + storage.setItem(workspaceKey(workspace), JSON.stringify(files)); +} + +export function listWorkspaces(storage: KeyValueStorage): string[] { + const workspaces: string[] = []; + + for (let index = 0; index < storage.length; index += 1) { + const key = storage.key(index); + if (!key || !key.startsWith(STORAGE_PREFIX)) { + continue; + } + + workspaces.push(key.slice(STORAGE_PREFIX.length)); + } + + return workspaces.sort((a, b) => a.localeCompare(b)); +} + +export function ensureWorkspace(storage: KeyValueStorage, workspaceName: string): string { + const normalizedName = normalizeWorkspaceName(workspaceName); + if (!normalizedName) { + throw new Error("Workspace name cannot be empty."); + } + + const key = workspaceKey(normalizedName); + if (storage.getItem(key) === null) { + writeWorkspace(storage, normalizedName, {}); + } + + return normalizedName; +} + +export function listFiles(storage: KeyValueStorage, workspaceName: string): string[] { + const files = parseWorkspace(storage.getItem(workspaceKey(workspaceName))); + return Object.keys(files).sort((a, b) => a.localeCompare(b)); +} - return cachedUser; +export function createFile(storage: KeyValueStorage, workspaceName: string, fileName: string): string { + const normalizedFileName = fileName.trim(); + if (!normalizedFileName) { + throw new Error("File name cannot be empty."); } - /** - * Clears cached user data - * Called on logout - */ - function clearCache(): void { - cachedUser = null; + const workspace = ensureWorkspace(storage, workspaceName); + const files = parseWorkspace(storage.getItem(workspaceKey(workspace))); + if (!(normalizedFileName in files)) { + files[normalizedFileName] = ""; + writeWorkspace(storage, workspace, files); } - /** - * Gets cached user without fetching - */ - function getCachedUser(): GitHubUser | null { - return cachedUser; - } + return normalizedFileName; +} - return { - fetchUser, - clearCache, - getCachedUser, - }; +export function readFile(storage: KeyValueStorage, workspaceName: string, fileName: string): string { + const files = parseWorkspace(storage.getItem(workspaceKey(workspaceName))); + return files[fileName] ?? ""; } -export type GitHubUserManager = ReturnType; +export function saveFile( + storage: KeyValueStorage, + workspaceName: string, + fileName: string, + content: string, +): void { + const workspace = ensureWorkspace(storage, workspaceName); + const files = parseWorkspace(storage.getItem(workspaceKey(workspace))); + files[fileName] = content; + writeWorkspace(storage, workspace, files); +} @@ -29610,62 +30019,6 @@ npx repomix@latest git add repomix-output.xml - - - - - - - - - - - ink - - - - - - - - - - - - - - - ink - - - - function createFakeFileHandle(name, initialContent = "") { let content = initialContent; @@ -29793,179 +30146,47 @@ describe("cogito mode", () => { }; }, }, - }, - }; - }, - }; - }, - }); - }); - - it("opens the panel, switches model, generates questions, and inserts one into the editor", () => { - cy.get("[data-action=\"open-workspace\"]").click({ force: true }); - cy.get("[data-action=\"new-note\"]").click({ force: true }); - cy.get("#editor").clear().type(markdown); - - cy.get("#cogitoToggleBtn") - .should("have.attr", "aria-expanded", "false") - .click() - .should("have.attr", "aria-expanded", "true"); - cy.get("#cogitoPanel").should("be.visible"); - cy.get(".split").should("have.class", "with-cogito"); - - cy.get("#cogitoDeepBtn").click().should("have.class", "active"); - cy.get("#cogitoLiteBtn").should("not.have.class", "active"); - - cy.get("#cogitoGenerateBtn").click(); - - cy.get("#statusBadge").should("contain", "Cogito questions ready"); - cy.get("#cogitoStatus").should("contain", "Questions ready"); - cy.get("#cogitoQuestionList .cogitoQuestionItem").should("have.length", 3); - cy.get("#cogitoQuestionList .cogitoQuestionText") - .eq(0) - .should("contain", "What problem does local-first editing solve here?"); - - cy.window().then((win) => { - expect(win.__cogitoCreateEngineCalls).to.deep.equal(["Qwen3-8B-q4f16_1-MLC"]); - expect(win.__cogitoCompletions).to.have.length(1); - expect(win.__cogitoCompletions[0].messages[1].content).to.contain(markdown); - }); - - cy.contains("#cogitoQuestionList .cogitoInsertBtn", "Insert").first().click(); - cy.get("#statusBadge").should("contain", "Inserted AI question"); - cy.get("#editor").should("have.value", `${markdown}> ### AI\nWhat problem does local-first editing solve here?\n`); - }); -}); - - - -function createFakeFileHandle(name, initialContent = "") { - let content = initialContent; - let lastModified = Date.now(); - - return { - kind: "file", - name, - async queryPermission() { - return "granted"; - }, - async requestPermission() { - return "granted"; - }, - async getFile() { - return new File([content], name, { type: "text/markdown", lastModified }); - }, - async createWritable() { - return { - async write(nextContent) { - content = String(nextContent); - lastModified = Date.now(); - }, - async close() {}, - }; - }, - __read() { - return content; - }, - }; -} - -function createFakeDirectoryHandle(name) { - const entries = new Map(); - - return { - kind: "directory", - name, - async queryPermission() { - return "granted"; - }, - async requestPermission() { - return "granted"; - }, - async *entries() { - for (const entry of entries.entries()) { - yield entry; - } - }, - async getFileHandle(fileName, options = {}) { - const existing = entries.get(fileName); - if (existing) { - return existing; - } - if (!options.create) { - throw new Error(`File not found: ${fileName}`); - } - const handle = createFakeFileHandle(fileName); - entries.set(fileName, handle); - return handle; - }, - async getDirectoryHandle(dirName, options = {}) { - const existing = entries.get(dirName); - if (existing) { - return existing; - } - if (!options.create) { - throw new Error(`Directory not found: ${dirName}`); - } - const handle = createFakeDirectoryHandle(dirName); - entries.set(dirName, handle); - return handle; - }, - }; -} - -describe("document linter export", () => { - const workspaceName = "linter-workspace"; - const noteName = "linter-note"; - const noteContent = "# Linter export test\n\nThis sentence is intentionally very long so the readability rule has something to report."; - - beforeEach(() => { - cy.visit("/ink-app.html", { - onBeforeLoad(win) { - const root = createFakeDirectoryHandle(workspaceName); - - win.prompt = (message) => { - if (message.includes("New note name")) { - return noteName; - } - return null; - }; - - win.confirm = () => true; - win.FileSystemHandle = function FileSystemHandle() {}; - win.showDirectoryPicker = async () => root; - win.__fakeWorkspace = root; - win.__linterExportBlobs = []; - win.URL.createObjectURL = (blob) => { - win.__linterExportBlobs.push(blob); - return "blob:linter-report"; + }, + }; + }, }; - win.URL.revokeObjectURL = () => {}; }, }); }); - it("exports the current linter report as markdown", () => { + it("opens the panel, switches model, generates questions, and inserts one into the editor", () => { cy.get("[data-action=\"open-workspace\"]").click({ force: true }); cy.get("[data-action=\"new-note\"]").click({ force: true }); - cy.get("#editor").clear().type(noteContent); + cy.get("#editor").clear().type(markdown); - cy.get("#documentLinterToggleBtn").click(); - cy.get("#documentLinterAnalyzeBtn").click(); - cy.get("#statusBadge").should("contain", "Analysis complete"); + cy.get("#cogitoToggleBtn") + .should("have.attr", "aria-expanded", "false") + .click() + .should("have.attr", "aria-expanded", "true"); + cy.get("#cogitoPanel").should("be.visible"); + cy.get(".split").should("have.class", "with-cogito"); - cy.get("#documentLinterExportBtn").click(); - cy.get("#statusBadge").should("contain", "Exported report"); + cy.get("#cogitoDeepBtn").click().should("have.class", "active"); + cy.get("#cogitoLiteBtn").should("not.have.class", "active"); - cy.window().then(async (win) => { - expect(win.__linterExportBlobs).to.have.length(1); - const report = await win.__linterExportBlobs[0].text(); - expect(report).to.contain("# Document Linter Review"); - expect(report).to.contain("## Overall"); - expect(report).to.contain("Overall score:"); - expect(report).to.contain("Readability"); - expect(report).to.contain("Opening is informative, not directive"); + cy.get("#cogitoGenerateBtn").click(); + + cy.get("#statusBadge").should("contain", "Cogito questions ready"); + cy.get("#cogitoStatus").should("contain", "Questions ready"); + cy.get("#cogitoQuestionList .cogitoQuestionItem").should("have.length", 3); + cy.get("#cogitoQuestionList .cogitoQuestionText") + .eq(0) + .should("contain", "What problem does local-first editing solve here?"); + + cy.window().then((win) => { + expect(win.__cogitoCreateEngineCalls).to.deep.equal(["Qwen3-8B-q4f16_1-MLC"]); + expect(win.__cogitoCompletions).to.have.length(1); + expect(win.__cogitoCompletions[0].messages[1].content).to.contain(markdown); }); + + cy.contains("#cogitoQuestionList .cogitoInsertBtn", "Insert").first().click(); + cy.get("#statusBadge").should("contain", "Inserted AI question"); + cy.get("#editor").should("have.value", `${markdown}> ### AI\nWhat problem does local-first editing solve here?\n`); }); }); @@ -30094,8 +30315,11 @@ describe("editor view mode toggle", () => { }); - -export {}; + +import "./commands"; +import "@cypress/code-coverage/support"; + +Cypress.on("uncaught:exception", () => false); @@ -30700,83 +30924,6 @@ The project lacks consistent code quality enforcement. Adding ESLint will help c - [x] 1.5 Integrate lint check into build process - -## ADDED Requirements -### Requirement: Canonical Project Logo Asset -The repository SHALL store a canonical project logo source file that is used for documentation and favicon generation. - -#### Scenario: Canonical logo source is available -- **WHEN** a maintainer inspects branding files in the repository -- **THEN** a single canonical logo source exists in a stable project path -- **AND** the source is suitable for deriving README and favicon assets - -### Requirement: README Logo Rendering -The project documentation SHALL render the project logo in `README.md` using repository-relative asset references. - -#### Scenario: README displays logo -- **WHEN** `README.md` is rendered by a GitHub-compatible markdown renderer -- **THEN** the logo is visible near the document header -- **AND** the logo reference resolves without external network dependencies - -### Requirement: Favicon Assets Derived from Logo -The project SHALL provide favicon assets derived from the canonical logo source. - -#### Scenario: Favicon assets are generated and available -- **WHEN** the favicon generation workflow is run -- **THEN** favicon files are produced in the documented output location -- **AND** generated filenames match those referenced by the application template - -### Requirement: Application Template Favicon References -The application HTML template SHALL reference project favicon assets so browser tabs display the project icon. - -#### Scenario: Browser tab uses project favicon -- **WHEN** a user opens the built application in a modern browser -- **THEN** the browser resolves favicon references from the application output -- **AND** the tab icon reflects the project logo branding - - - -# Change: Add Project Logo to README and Favicon Assets - -## Why -The repository currently lacks consistent branding assets in project documentation and browser metadata. Adding the provided logo to `README.md` and defining favicon outputs improves recognizability and presentation. - -## What Changes -- Add a canonical logo asset in the repository based on `~/Downloads/ink_logo.svg`. -- Update `README.md` to render the project logo near the top of the document. -- Define and implement favicon outputs derived from the same logo source. -- Wire favicon references into the app HTML template so browser tabs use project branding. -- Document the asset location and favicon generation/update workflow. - -## Impact -- Affected specs: `branding-assets` -- Affected code: - - `README.md` - - `ink.template.html` - - `build/` scripts (if favicon generation is automated in build) - - `dist/` favicon artifacts - - `assets/` branding source files (new) - - - -## 1. Asset Setup -- [x] 1.1 Add the provided logo SVG to a canonical repository path for shared branding usage. -- [x] 1.2 Define favicon output formats and filenames derived from the canonical logo source. - -## 2. Documentation and Template Integration -- [x] 2.1 Update `README.md` to display the project logo with stable relative linking. -- [x] 2.2 Update the HTML template to include favicon references that resolve in the built output. - -## 3. Build and Distribution -- [x] 3.1 Implement or document the process to generate favicon artifacts from the SVG source. -- [x] 3.2 Ensure generated favicon assets are available in expected output locations. - -## 4. Validation -- [x] 4.1 Verify `README.md` renders the logo correctly on GitHub-compatible markdown viewers. -- [x] 4.2 Verify the built app/tab displays the new favicon in a modern browser. -- [x] 4.3 Verify documentation describes how to refresh logo-derived assets when the SVG changes. - - ## ADDED Requirements @@ -31765,218 +31912,61 @@ The system SHALL provide a linting and review tool for markdown documents that a #### Scenario: Check style quality - **WHEN** the system analyzes prose -- **THEN** it identifies spelling errors and style consistency issues - -#### Scenario: Export suggestions as markdown -- **WHEN** the user requests export -- **THEN** the system outputs the suggestions in markdown format -- **AND** the export includes a concise summary, prioritized fixes, and section-level detail - - - -# Change: Improve Document Linter Output Quality - -## Why -The current Document Linter output is technically correct but too generic to be useful on real study and writing notes. It over-flags list items as long sentences, repeats low-signal style warnings, and does not surface a clear priority order or a concise summary that helps the reader act quickly. - -## What Changes -- Upgrade the linter report to prioritize the most useful feedback first -- Make sentence and paragraph analysis markdown-aware so list items, headings, and quoted callouts are not misclassified as generic prose -- Add a stronger summary layer that highlights the top issues, strongest sections, and what to fix first -- Add section-level analysis so the linter can reason about headings, label-style section starts, and dense sub-parts of a document -- Reduce dull, repetitive phrasing in favor of more specific, content-aware suggestions -- Keep the report export in markdown, but make the exported structure more editorial and easier to scan - -## Impact -- Affected specs: document-linter -- Affected code: Document Linter analysis pipeline, markdown report builder, export output -- Runtime dependency: None -- Breaking changes: none - - - -## 1. Implementation -- [x] 1.1 Improve markdown-aware parsing so headings, bullets, and callouts are not treated like plain prose -- [x] 1.2 Rewrite the report builder to include a concise summary, top issues, and prioritized fixes -- [x] 1.3 Replace generic suggestion phrasing with more specific, content-aware language -- [x] 1.4 Keep export output in markdown while making it more readable and action-oriented -- [x] 1.5 Add tests that cover the new report structure and the markdown-aware classification rules - - - -# Change: Migrate marked to npm Dependency - -## Why -The `marked` library is currently inlined as a minified script block inside `ink.template.html`, manually downloaded from jsDelivr. This makes updates error-prone (requires manual copy-paste of minified code), bloats the template file, and prevents TypeScript from using typed imports. Since the project already uses esbuild for bundling, adding `marked` as an npm dependency is the correct approach: esbuild will bundle it into `dist/app.min.js` at build time, preserving the self-contained nature of the final output with no runtime external dependencies. - -## What Changes -- Add `marked` as an npm dependency. -- Replace `window.marked` usages in `src/app/bootstrap.ts` with a direct TypeScript import. -- Remove the inlined `