Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ MINDMAP_OAUTH_REDIRECT_URIS=https://mindmap.thoughtful-ai.com/
Restart the backend after setting them so startup provisioning creates or
updates the fixed client. Do not register localhost on the production server.

## Source layout

| Folder | Responsibility |
| --- | --- |
| `src/` | Entry point and shared definitions |
| `src/coach/` | One model turn: prompt, transport, response parsing, and turn loop |
| `src/grounding/` | Code checks on AI output |
| `src/map/` | Map, Source Bank, proposal state, and provenance |
| `src/session/` | Persistence, logging, and diagnostics |
| `src/platform/` | Writing Tools sign-in |
| `src/ui/` | React components, UI copy, and DOM helpers |
| `src/i18n/` | Translation dictionaries |

## Verification

```sh
Expand Down
2 changes: 1 addition & 1 deletion docs/login-bootstrap-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ automatic document reading. There is no `doc:read` scope any more.

### 1. Replace the launch gate with a login gate

Today `launchRequired()` in `src/platform-session.ts` refuses to start unless
Today `launchRequired()` in `src/platform/platform-session.ts` refuses to start unless
Writing Tools launched the app. Replace it with `loginRequired()` **keeping the
exact same shape**:

Expand Down
2 changes: 1 addition & 1 deletion docs/multilingual-grounding-pass.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ rebuilding; these donor modules already encode the right philosophy:
Reuse (near) as-is:
- `src/i18n/*.json` — **34 static UI dictionaries including `zh.json`**, keyed
by English source string. Chinese UI works day one.
- `src/ui-strings.ts` — static dictionary lookup with case-folded keys;
- `src/ui/ui-strings.ts` — static dictionary lookup with case-folded keys;
partial dictionary degrades in coverage, never correctness.
- `src/language.ts` — the write/view language split. Its own doc comment states
a view-language translation is "never written back, never harvested into the
Expand Down
2 changes: 1 addition & 1 deletion docs/polish-sweeps-pass.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ sync with no tool to regenerate. Port the donor's generator.
module does **not exist on `mindmap-main`** (the donor `language.ts` is on
the explicit-skip list) and there is **no `LANGUAGE_CODES` constant
anywhere** — `mindmap-main` discovers locales at build time via
`import.meta.glob("./i18n/*.json")` in `src/ui-strings.ts`. So the donor
`import.meta.glob("./i18n/*.json")` in `src/ui/ui-strings.ts`. So the donor
script throws immediately. **Fix:** rewrite `supportedLanguages()` to
enumerate the existing `src/i18n/*.json` filenames (excluding
`source.json`), e.g. read the `i18n` dir and strip `.json`. This keeps the
Expand Down
203 changes: 203 additions & 0 deletions docs/src-folder-refactor-pass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# `src/` Folder Refactor — Implementation Spec

Status: **SPEC READY — decided 2026-10-04 (Nhyira).** Audience: implementer (Codex).
Branch: `refactor/src-folders` (off `main` at `ae55265`). This spec is the first
commit on that branch.

## Goal

`src/` is one flat list of 41 modules plus their tests. Group them into folders
by responsibility so a reader can find the coach, the grounding checks, the map
state and the UI without opening every file.

**This is a move-only change.** After it, the app, the prompt, the validator,
the tests and the build behave exactly as before. Reviewers should be able to
confirm that from the diff alone.

## Non-goals (do not do any of these)

- Do not rename any file. Only its folder changes.
- Do not change, add or remove any export, function, type, comment or string.
- Do not delete the deprecated shims `controller.ts` and `map-commands.ts`.
Move them like any other file.
- Do not fix the `types.ts` ↔ `assistance-contract.ts` import cycle, or the
type-only import from `session-persistence.ts` into `Map.tsx`. Both are known
and out of scope.
- **Do not edit any regular expression**, anywhere. That includes the one in
`ui-strings.ts` and the ones in `ui-locale.test.ts`; see "Traps" below.
- Do not reformat, reorder imports, or touch lines other than import paths.
- Do not add a path alias (`@/…`), barrel `index.ts` files, or tsconfig/vite
changes.

## Target layout

Every module moves with its co-located test (`x.ts` → same folder as
`x.test.ts`). Files not listed stay where they are.

| Folder | Responsibility | Modules (tests move with them) |
| --- | --- | --- |
| `src/` (root) | entry point and shared definitions | `main.tsx`, `types.ts`, `config.ts`, `assistance-contract.ts` |
| `src/coach/` | one model turn: prompt, transport, response parsing, turn loop | `stage1-loop.ts`, `controller.ts`, `api.ts`, `provider-tools.ts`, `llm-contract.ts`, `assistant-response.ts`, `turn-shape.ts`, `language-context.ts` |
| `src/grounding/` | code checks on AI output | `validator.ts`, `normalize.ts`, `unicode-scripts.ts`, `action-gateway.ts`, `map-commands.ts` |
| `src/map/` | map, Source Bank and proposal state; provenance | `map-store.ts`, `store.ts`, `proposal-store.ts`, `map-layout.ts`, `map-export.ts`, `provenance-summary.ts`, `suggestion-adoption.ts` |
| `src/session/` | persistence, logging, diagnostics | `session-persistence.ts`, `event-ledger.ts`, `trace.ts`, `understanding.ts` |
| `src/platform/` | Writing Tools sign-in | `platform-session.ts`, `PlatformBootstrap.tsx`, `platform.css` |
| `src/ui/` | React components, UI copy, DOM helpers | `App.tsx`, `ControlRoom.tsx`, `Map.tsx`, `reader-view.tsx`, `reader-translation.ts`, `mutation-policy.tsx`, `useSpeechToText.ts`, `draft-anchor.ts`, `ui-locale.tsx`, `ui-strings.ts` |
| `src/i18n/` | translation dictionaries (unchanged) | existing `*.json` and `README.md` stay exactly where they are |

Tests whose names don't match a module:

| Test | Goes to |
| --- | --- |
| `candidate-store.test.ts` (tests `store.ts`) | `src/map/` |
| `proposal-fuzz.test.ts` | `src/map/` |
| `provider-tools.integration.test.ts` | `src/coach/` |
| `App.test.ts` | `src/ui/` |
| `useSpeechToText.test.tsx` | `src/ui/` |

`assistance-contract.ts` stays at the root because nearly every folder imports
it and it shares the import cycle with `types.ts`. Keeping both at the root
stops that cycle from crossing folders.

## Mechanics

1. Use `git mv` for every move so `git log --follow` and rename detection keep
each file's history.
2. Fix every relative path that the moves break. There are five kinds; search
for all of them, not only `from "./…"`:
- static imports and re-exports: `import … from "./x"`, `export … from "./x"`
- inline type imports: `import("./x").SomeType` (present in `action-gateway.ts`,
`api.ts`, `stage1-loop.ts`, `types.ts`, `App.tsx`, `Map.tsx`,
`stage1-loop.test.ts`; check with `grep -rn 'import("\.' src` after moving)
- Vitest mocks: `vi.mock("./api", …)` in `reader-view.test.ts`
- side-effect imports: `import "./platform.css"` in `main.tsx` becomes
`import "./platform/platform.css"`
- the Vite glob in `ui-strings.ts` (see Traps)
3. Fix imports outside `src/`: `eval/run.ts`, `eval/reporting.ts` and
`eval/scenarios/*.ts` import `../src/<file>` / `../../src/<file>`. Point them
at the new folders.
4. Do **not** change: `index.html` (still loads `/src/main.tsx`, which stays at
the root), `scripts/generate-i18n.mjs` (it writes to `src/i18n/`, which does
not move), `tsconfig*.json`, `vite.config.ts`, `.github/workflows/*`.
5. If `tsc -b` complains about stale paths, delete `tsconfig.tsbuildinfo` (a
build cache) and rebuild. Do not commit it if it is untracked.

## Traps

**1. `ui-strings.ts` translation glob (silent failure).** It loads dictionaries
with

```ts
const DICTIONARIES = import.meta.glob<Record<string, string>>("./i18n/*.json", { … });
…
const code = /\.\/i18n\/([\w-]+)\.json$/.exec(path)?.[1];
```

After moving to `src/ui/`, change **only** the glob string to
`"../i18n/*.json"`. Leave the regex untouched. The glob keys become
`"../i18n/zh.json"`, and the regex is not anchored at the start, so it still
matches `./i18n/zh.json` inside that key and extracts `zh`. If the glob is
wrong, every language silently falls back to English with no error. That is
why the guard test below is required.

**2. `ui-locale.test.ts` reads source files by relative URL.** It calls
`new URL("./App.tsx" | "./Map.tsx" | "./PlatformBootstrap.tsx", import.meta.url)`
and `new URL("./i18n/source.json", import.meta.url)`. After it moves to
`src/ui/`, `App.tsx` and `Map.tsx` stay as `./…`. Change the other two to
`"../platform/PlatformBootstrap.tsx"` and `"../i18n/source.json"`. Change only
the path strings, not the regexes in that test.

**3. Comment references.** Doc comments that mention a sibling path (e.g.
`ui-strings.ts`: "See ./i18n/README.md") may be updated to the new relative
path (`../i18n/README.md`). This is the only comment change allowed.

## Guard test (the one new test)

Add to `src/ui/ui-locale.test.ts`:

```ts
it("loads a dictionary for every translation file in src/i18n", () => {
const files = readdirSync(new URL("../i18n/", import.meta.url))
.filter((name) => name.endsWith(".json") && name !== "source.json")
.map((name) => name.slice(0, -".json".length))
.sort();
expect(files.length).toBeGreaterThan(0);
expect(translatedLanguages()).toEqual(files);
});
```

(Import `readdirSync` from `node:fs` and `translatedLanguages` from
`./ui-strings`, matching how the file already imports.) This fails if the glob
or the code extraction breaks, which today's tests only catch for Chinese.

## Docs

Update `src/<file>` path mentions in these docs to the new folders:
- `docs/login-bootstrap-spec.md:38` (`src/platform-session.ts`)
- `docs/multilingual-grounding-pass.md:40` (`src/ui-strings.ts`)
- `docs/polish-sweeps-pass.md:62` (`src/ui-strings.ts`)

Leave the other path mentions alone. They name files that no longer exist
(`src/language.ts`, `src/translate.ts`, …) or `prototype-mindmap/src/…` in the
old repo, so they are historical.

Add a short "Source layout" section to `README.md` with the folder table above
(folder + one-line responsibility only).

## Commits

1. `docs: spec the src folder refactor` (this file; already on the branch)
2. `refactor: group src modules into folders`: all `git mv`s plus import-path
fixes in **one** commit, so no commit leaves the tree broken
3. `test(i18n): guard that every dictionary file loads`
4. `docs: point path references at the new folders`

Do not push. Report back when done.

## Verification (all must pass; paste the output)

```bash
npm test
npx tsc --noEmit
npm run eval:typecheck
npm run build:pages
npm run verify:pages
```

- Baseline on `main` (`ae55265`): **37 test files, 393 tests**. After: 37 files,
**394 tests** (the guard test is the only addition).
- Optional if Playwright browsers are installed: `npm run test:e2e`.

**Move-only proof.** Rename detection must show every module as a rename:

```bash
git diff -M --summary main..HEAD -- src | grep -c "rename"
```

The count should equal the number of moved files (modules plus tests plus
`platform.css`).

Every changed line must be an import path, the glob string, a test-file path
string, the guard test, or the allowed comment path. This command should print
only the guard test's lines and the allowed comment/path-string lines:

```bash
git diff -M main..HEAD -- src eval | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' \
| grep -vE 'from "|import\(|vi\.mock\(|import\.meta\.glob|^[+-]\s*$'
```

**Picture.** If `codemap` is available:

```bash
codemap C:\Users\nhyir\Linux_Folder\mindmap-extract --name mindmap-folders
```

The Folders tab should show the seven folders. The only circular import should
still be `types.ts` ↔ `assistance-contract.ts`.

## Report format

- the commands above with their output
- the rename count, and the full output of the move-only grep
- any file you could not place by this table, and where you put it
- anything in this spec that turned out to be wrong
2 changes: 1 addition & 1 deletion eval/reporting.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AssistantResponse } from "../src/assistant-response";
import type { AssistantResponse } from "../src/coach/assistant-response";

export interface CsvRow {
[column: string]: string;
Expand Down
12 changes: 6 additions & 6 deletions eval/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { mkdir, writeFile } from "node:fs/promises";
import { execFileSync } from "node:child_process";
import { resolve } from "node:path";
import { contractForLevel, type AssistanceLevel } from "../src/assistance-contract";
import { historyForCurrentTurn, makeLLM, type ConversationMessage, type ProviderTrace } from "../src/api";
import type { AssistantResponse, TurnProgressStage } from "../src/assistant-response";
import { historyForCurrentTurn, makeLLM, type ConversationMessage, type ProviderTrace } from "../src/coach/api";
import type { AssistantResponse, TurnProgressStage } from "../src/coach/assistant-response";
import { defaultConfig } from "../src/config";
import { ThoughtUnitStore } from "../src/map-store";
import { containsWholePhrase, contentTokens, stem, stemSet } from "../src/normalize";
import type { ProviderTransport } from "../src/provider-tools";
import { createConversationState, processTurn } from "../src/stage1-loop";
import { ThoughtUnitStore } from "../src/map/map-store";
import { containsWholePhrase, contentTokens, stem, stemSet } from "../src/grounding/normalize";
import type { ProviderTransport } from "../src/coach/provider-tools";
import { createConversationState, processTurn } from "../src/coach/stage1-loop";
import { EVAL_SCENARIOS, MANIPULATION_CHECK_SCENARIOS, type EvalMemoryEvent, type EvalScenario } from "./scenarios";
import { encodeCsv, visibleResponseText } from "./reporting";

Expand Down
2 changes: 1 addition & 1 deletion eval/scenarios/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { containsWholePhrase } from "../../src/normalize";
import { containsWholePhrase } from "../../src/grounding/normalize";
import { EVAL_SCENARIOS, MANIPULATION_CHECK_SCENARIOS } from "./index";

describe("combined evaluation scenario set", () => {
Expand Down
2 changes: 1 addition & 1 deletion eval/scenarios/recall.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { containsWholePhrase } from "../../src/normalize";
import { containsWholePhrase } from "../../src/grounding/normalize";
import { RECALL_EVAL_SCENARIOS } from "./recall";

describe("recall eval scenario fixtures", () => {
Expand Down
2 changes: 1 addition & 1 deletion eval/scenarios/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AssistanceLevel } from "../../src/assistance-contract";
import type { ConversationMessage } from "../../src/api";
import type { ConversationMessage } from "../../src/coach/api";
import type { CandidateTarget } from "../../src/types";

export interface EvalMemoryEvent {
Expand Down
2 changes: 1 addition & 1 deletion src/assistance-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { ASSISTANCE_CONTRACTS, normalizeInfluenceTrace, snapshotContract } from "./assistance-contract";
import { parseAssistantResponse, renderContext } from "./api";
import { parseAssistantResponse, renderContext } from "./coach/api";
import { defaultConfig } from "./config";

describe("assistance contracts", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/api.test.ts → src/coach/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { historyForCurrentTurn, makeLLM, parseAssistantResponse, PROVIDER_TRANSPORT, renderContext, translateReaderText } from "./api";
import { defaultConfig } from "./config";
import { defaultConfig } from "../config";
import type { LLMContext } from "./llm-contract";
import { ThoughtUnitStore } from "./map-store";
import { ThoughtUnitStore } from "../map/map-store";
import { createConversationState, processTurn } from "./stage1-loop";
import { contractForLevel, snapshotContract } from "./assistance-contract";
import { contractForLevel, snapshotContract } from "../assistance-contract";

const context: LLMContext = {
language: { uiLocale: "en", latestUserLanguagePattern: "single" },
Expand Down
10 changes: 5 additions & 5 deletions src/api.ts → src/coach/api.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/// <reference types="vite/client" />

import type { MindmapConfig } from "./config";
import { defaultConfig } from "./config";
import type { MindmapConfig } from "../config";
import { defaultConfig } from "../config";
import type { LLMContext } from "./llm-contract";
import {
ModelResponseValidationError,
type AssistantModel,
type AssistantResponseEnvelope,
type StructuredRejection,
} from "./assistant-response";
import type { ProposedAction, ProposedRef } from "./action-gateway";
import type { GroundedClaim, GroundedRecap, MirrorClaim, MirrorReflection, SourceSpan, TranslationEvidencePhrase } from "./types";
import type { SourceBackedOption } from "./assistance-contract";
import type { ProposedAction, ProposedRef } from "../grounding/action-gateway";
import type { GroundedClaim, GroundedRecap, MirrorClaim, MirrorReflection, SourceSpan, TranslationEvidencePhrase } from "../types";
import type { SourceBackedOption } from "../assistance-contract";
import {
CONVERSATIONAL_TEXT_FORMAT,
MINDMAP_PROVIDER_TOOLS,
Expand Down
10 changes: 5 additions & 5 deletions src/assistant-response.ts → src/coach/assistant-response.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { LLMContext, QuestionStance } from "./llm-contract";
import type { CandidateStore, SourceBank } from "./store";
import type { ProposedAction } from "./action-gateway";
import type { GroundedRecap, MirrorReflection, TranslationEvidencePhrase } from "./types";
import type { Proposal } from "./proposal-store";
import type { SourceBackedOption } from "./assistance-contract";
import type { CandidateStore, SourceBank } from "../map/store";
import type { ProposedAction } from "../grounding/action-gateway";
import type { GroundedRecap, MirrorReflection, TranslationEvidencePhrase } from "../types";
import type { Proposal } from "../map/proposal-store";
import type { SourceBackedOption } from "../assistance-contract";
import type { LatestUserLanguagePattern } from "./language-context";

export interface RecallAnnotation {
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/language-context.ts → src/coach/language-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CJK_SCRIPT_RE } from "./unicode-scripts";
import { CJK_SCRIPT_RE } from "../grounding/unicode-scripts";

/**
* Conversational language hints are deliberately separate from evidence and
Expand Down
4 changes: 2 additions & 2 deletions src/llm-contract.ts → src/coach/llm-contract.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TurnShape } from "./turn-shape";
import type { CandidateStatus, CandidateTarget, SourceUtterance, ThoughtUnit, ThoughtUnitRole } from "./types";
import type { AssistanceContractSnapshot } from "./assistance-contract";
import type { CandidateStatus, CandidateTarget, SourceUtterance, ThoughtUnit, ThoughtUnitRole } from "../types";
import type { AssistanceContractSnapshot } from "../assistance-contract";
import type { LanguageContext } from "./language-context";

export type { LanguageContext } from "./language-context";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { makeLLM } from "./api";
import { ASSISTANCE_CONTRACTS } from "./assistance-contract";
import { defaultConfig } from "./config";
import { ThoughtUnitStore } from "./map-store";
import { ASSISTANCE_CONTRACTS } from "../assistance-contract";
import { defaultConfig } from "../config";
import { ThoughtUnitStore } from "../map/map-store";
import { createConversationState, processTurn } from "./stage1-loop";
import { resetIdCounter } from "./store";
import { resetIdCounter } from "../map/store";

afterEach(() => vi.unstubAllGlobals());
beforeEach(() => resetIdCounter());
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading