From 3d5ca34d209d9f566c39e9cc2bd17152fce3ed58 Mon Sep 17 00:00:00 2001 From: Uniskela <104075208+uniskela@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:59:56 +0000 Subject: [PATCH] docs: separate public, internal and agent documentation Keep published setup, deployment, backup and security pages under docs/public with the same slugs, and move plans and architecture decisions out of the import path. Co-authored-by: Cursor --- .github/docs-sync.md | 10 +- AGENTS.md | 22 +++-- README.md | 22 ++--- docs/agents/README.md | 7 ++ docs/{ => internal}/DEPLOYMENT_ROUTING.md | 0 docs/{ => internal}/DEPLOYMENT_WATCH.md | 0 docs/{ => internal}/INDEX.md | 43 +++++---- docs/{ => internal}/PRIOR_ART.md | 0 docs/{ => internal}/UX.md | 0 docs/{ => internal}/adr/0001-tech-stack.md | 0 .../{ => internal}/adr/0002-persisted-jobs.md | 0 .../adr/0003-editor-codemirror.md | 0 .../adr/0004-modular-monolith.md | 0 .../adr/0005-product-boundary.md | 0 docs/{ => internal}/domain/DATA_MODEL.md | 0 ...-09-30-dashboard-stacks-settings-design.md | 0 ...ashboard-stacks-settings-implementation.md | 8 +- .../2026-09-30-stacks-ui-audit-and-plan.md | 10 +- docs/{ => internal}/plans/UI_UX_PLAN.md | 2 +- .../providers/DEPLOYMENT_PROVIDER.md | 0 docs/{ => internal}/providers/GIT_PROVIDER.md | 2 +- .../providers/RUNTIME_PROVIDER.md | 0 .../providers/SECRET_PROVIDER.md | 0 docs/manifest.json | 28 +++--- docs/{ => public}/ARCHITECTURE.md | 2 +- docs/{ => public}/AUTH_AND_CREDENTIALS.md | 0 docs/{ => public}/BACKUP_RESTORE.md | 0 docs/{ => public}/GIT_WORKFLOW.md | 0 docs/{ => public}/PRODUCT.md | 0 docs/{ => public}/RELEASING.md | 0 docs/{ => public}/SECURITY.md | 0 docs/{ => public}/SELF_HOSTING.md | 2 +- docs/{ => public}/STACK_DISCOVERY.md | 2 +- docs/{ => public}/TESTING.md | 0 docs/{ => public}/getting-started.md | 0 docs/{ => public}/index.md | 0 docs/{ => public}/installation.md | 2 +- docs/{ => public}/plans/MVP_PLAN.md | 0 next.config.ts | 2 +- .../stacks/[stackId]/environment/page.tsx | 2 +- src/server/application/credential-service.ts | 2 +- src/server/application/source-service.ts | 2 +- src/server/application/stack-service.ts | 2 +- src/server/domain/git-repository.ts | 4 +- src/server/domain/job.ts | 2 +- src/server/domain/stack.ts | 2 +- src/server/persistence/schema.ts | 4 +- src/server/providers/git/types.ts | 2 +- src/server/security/network-policy.ts | 2 +- src/server/security/password.ts | 2 +- src/ui/source/stack-discovery.tsx | 2 +- src/ui/source/stack-editor.tsx | 2 +- src/ui/source/stack-tabs.tsx | 2 +- tests/unit/docs-layout.test.ts | 96 +++++++++++++++++++ 54 files changed, 203 insertions(+), 89 deletions(-) create mode 100644 docs/agents/README.md rename docs/{ => internal}/DEPLOYMENT_ROUTING.md (100%) rename docs/{ => internal}/DEPLOYMENT_WATCH.md (100%) rename docs/{ => internal}/INDEX.md (52%) rename docs/{ => internal}/PRIOR_ART.md (100%) rename docs/{ => internal}/UX.md (100%) rename docs/{ => internal}/adr/0001-tech-stack.md (100%) rename docs/{ => internal}/adr/0002-persisted-jobs.md (100%) rename docs/{ => internal}/adr/0003-editor-codemirror.md (100%) rename docs/{ => internal}/adr/0004-modular-monolith.md (100%) rename docs/{ => internal}/adr/0005-product-boundary.md (100%) rename docs/{ => internal}/domain/DATA_MODEL.md (100%) rename docs/{ => internal}/plans/2026-09-30-dashboard-stacks-settings-design.md (100%) rename docs/{ => internal}/plans/2026-09-30-dashboard-stacks-settings-implementation.md (98%) rename docs/{ => internal}/plans/2026-09-30-stacks-ui-audit-and-plan.md (97%) rename docs/{ => internal}/plans/UI_UX_PLAN.md (98%) rename docs/{ => internal}/providers/DEPLOYMENT_PROVIDER.md (100%) rename docs/{ => internal}/providers/GIT_PROVIDER.md (97%) rename docs/{ => internal}/providers/RUNTIME_PROVIDER.md (100%) rename docs/{ => internal}/providers/SECRET_PROVIDER.md (100%) rename docs/{ => public}/ARCHITECTURE.md (98%) rename docs/{ => public}/AUTH_AND_CREDENTIALS.md (100%) rename docs/{ => public}/BACKUP_RESTORE.md (100%) rename docs/{ => public}/GIT_WORKFLOW.md (100%) rename docs/{ => public}/PRODUCT.md (100%) rename docs/{ => public}/RELEASING.md (100%) rename docs/{ => public}/SECURITY.md (100%) rename docs/{ => public}/SELF_HOSTING.md (98%) rename docs/{ => public}/STACK_DISCOVERY.md (98%) rename docs/{ => public}/TESTING.md (100%) rename docs/{ => public}/getting-started.md (100%) rename docs/{ => public}/index.md (100%) rename docs/{ => public}/installation.md (96%) rename docs/{ => public}/plans/MVP_PLAN.md (100%) create mode 100644 tests/unit/docs-layout.test.ts diff --git a/.github/docs-sync.md b/.github/docs-sync.md index 1760de1..d54ef6b 100644 --- a/.github/docs-sync.md +++ b/.github/docs-sync.md @@ -4,8 +4,8 @@ stack-manager's user documentation is published at [uniskela.com/docs/stack-mana by `uniskela/.com`, the same way as adhd-hub and ts6-manager. - [`docs/manifest.json`](../docs/manifest.json) lists the pages to publish: `source` file, URL `slug`, sidebar `title` - and `group`. Design docs, ADRs and plans that are not listed stay on GitHub only; links to them fall back to GitHub - URLs on the site. + and `group`. Published pages live under `docs/public/`. `docs/internal/` (plans, architecture decisions) and + `docs/agents/` are not imported; links to them fall back to GitHub URLs on the site. - The `Notify Uniskela documentation` workflow sends an update notification after README/docs changes merge to `main` (and on each published release). It does not publish the website directly: `uniskela/.com` imports the content, runs its checks and proposes a draft PR for review. @@ -27,5 +27,7 @@ Never put the App private key in repository files, PRs or chats. ## Adding a page -Write it in `docs/`, link it from [`docs/index.md`](../docs/index.md) and add an entry to `docs/manifest.json`. Use -relative links between docs so they work both on GitHub and on the site. +Write it in `docs/public/`, link it from [`docs/public/index.md`](../docs/public/index.md) and add an entry to +`docs/manifest.json`. The slug is the public URL and does not include `public`. Use relative links between docs so +they work both on GitHub and on the site. Put implementation plans and architecture decisions in `docs/internal/`, +and agent-only notes in `docs/agents/`. diff --git a/AGENTS.md b/AGENTS.md index f4dacc3..595edff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Guidance for coding agents working on **stack-manager**. ## Product boundary -Read [docs/PRODUCT.md](docs/PRODUCT.md) and [docs/adr/0005-product-boundary.md](docs/adr/0005-product-boundary.md) before adding features. +Read [docs/public/PRODUCT.md](docs/public/PRODUCT.md) and [docs/internal/adr/0005-product-boundary.md](docs/internal/adr/0005-product-boundary.md) before adding features. This app manages Git-backed **source/configuration** for Compose stacks. It is **not** a Portainer/Komodo/Arcane replacement. No Containers/Images/Networks/Volumes management pages. Runtime is optional and read-only. @@ -15,14 +15,14 @@ forge webhook receiver, relevance rules, selective deploy). PR #2 (application f and PR #4 (Git workflow: commit, safe push, conflict protection, stack history, Changes UI; released in v0.5.0) are done. Do not build secrets providers, runtime watch or PWA work ahead of their phases. -See [docs/plans/MVP_PLAN.md](docs/plans/MVP_PLAN.md) and the code layout in -[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#code-layout-implemented-in-pr-2-extended-through-v050). +See [docs/public/plans/MVP_PLAN.md](docs/public/plans/MVP_PLAN.md) and the code layout in +[docs/public/ARCHITECTURE.md](docs/public/ARCHITECTURE.md#code-layout-implemented-in-pr-2-extended-through-v050). ## Working in the code - `pnpm check` must pass (format, lint, typecheck, tests). `pnpm build` must succeed. - PR titles (and so squash-merge commits) must be Conventional Commits (`feat:`, `fix:`, `docs:`, …); Release - Please derives versions and the changelog from them. See [docs/RELEASING.md](docs/RELEASING.md). + Please derives versions and the changelog from them. See [docs/public/RELEASING.md](docs/public/RELEASING.md). - Schema changes: edit `src/server/persistence/schema.ts`, run `pnpm db:generate`, commit the migration. - API routes: always wrap handlers with `defineRoute` (auth is default-deny; a coverage test enforces this). - Services receive dependencies through `src/server/container.ts`; depend on ports, not Drizzle. @@ -32,9 +32,11 @@ See [docs/plans/MVP_PLAN.md](docs/plans/MVP_PLAN.md) and the code layout in - Read repository content through `SourceTreeReader` (Git objects, never the working tree). Never return the contents of secret-looking files (`isSecretPath` in `src/shared/source/paths.ts`) or accept drafts for them. - `src/shared` is pure code shared by server and browser (no Node, React or server imports; lint-enforced). -- User docs are published to uniskela.com from `docs/manifest.json` (see `.github/docs-sync.md`). When a feature - changes what users see, update `docs/getting-started.md`/`docs/installation.md`; list new user-facing pages in the - manifest. +- User docs are published to uniskela.com from `docs/manifest.json` (see `.github/docs-sync.md`). Write them under + `docs/public/` (setup, deployment, backups, security and other user-facing references). Implementation plans and + architecture decisions go under `docs/internal/`. Agent-only notes go under `docs/agents/`; this file stays the + canonical agent guide. When a feature changes what users see, update `docs/public/getting-started.md` / + `docs/public/installation.md` and list new pages in the manifest. Do not change published slugs. - UI tests: `pnpm build && pnpm test:e2e` (set `E2E_GIT_REMOTE`/`E2E_STACK_ROOT` to a public repo and stack folder to include the editor tests). ## Tech direction (locked) @@ -47,12 +49,12 @@ See [docs/plans/MVP_PLAN.md](docs/plans/MVP_PLAN.md) and the code layout in ## Prior art -`uniskela/stackwise` is an unrelated inventory SPA. Do not import its code. See [docs/PRIOR_ART.md](docs/PRIOR_ART.md). +`uniskela/stackwise` is an unrelated inventory SPA. Do not import its code. See [docs/internal/PRIOR_ART.md](docs/internal/PRIOR_ART.md). ## Security -Never log or commit credentials. Encrypt provider secrets at rest. Redact audit events. See [docs/SECURITY.md](docs/SECURITY.md). +Never log or commit credentials. Encrypt provider secrets at rest. Redact audit events. See [docs/public/SECURITY.md](docs/public/SECURITY.md). ## Docs map -[docs/INDEX.md](docs/INDEX.md) +[docs/internal/INDEX.md](docs/internal/INDEX.md) diff --git a/README.md b/README.md index fd223ce..d868414 100644 --- a/README.md +++ b/README.md @@ -19,29 +19,29 @@ drafts and Compose validation, stack docs and an environment inventory. **New in v0.5.0, the Git workflow:** review drafts on the Changes tab, commit them with your own Git identity (validation errors block, warnings need acknowledgement), and push safely to the tracked branch. Stack Manager never force pushes, stops when the remote branch has changed, and keeps your drafts whatever happens. Each stack has a -History tab with the commits and diffs that touched it. See [Git workflow](docs/GIT_WORKFLOW.md). +History tab with the commits and diffs that touched it. See [Git workflow](docs/public/GIT_WORKFLOW.md). **Next:** deployment routing, which deploys only the stacks a commit changed through Portainer or a generic -webhook. See the [roadmap](docs/plans/MVP_PLAN.md). +webhook. See the [roadmap](docs/public/plans/MVP_PLAN.md). **Documentation:** [uniskela.com/docs/stack-manager](https://uniskela.com/docs/stack-manager/) (source in -[docs/](docs/index.md)). +[docs/](docs/public/index.md)). | Document | Purpose | | --- | --- | -| [Installation](docs/installation.md) | Run the published container image with Docker Compose | -| [Getting started](docs/getting-started.md) | First run, connecting a repository, stacks and the editor | -| [Git workflow](docs/GIT_WORKFLOW.md) | Commit, push, remote changes and stack history | -| [Self-hosting reference](docs/SELF_HOSTING.md) | Configuration, data directory, reverse proxy, upgrades | -| [Product](docs/PRODUCT.md) / [Architecture](docs/ARCHITECTURE.md) | What it is (and is not), and how it is built | -| [Docs index](docs/INDEX.md) | Every design, planning and operations document | +| [Installation](docs/public/installation.md) | Run the published container image with Docker Compose | +| [Getting started](docs/public/getting-started.md) | First run, connecting a repository, stacks and the editor | +| [Git workflow](docs/public/GIT_WORKFLOW.md) | Commit, push, remote changes and stack history | +| [Self-hosting reference](docs/public/SELF_HOSTING.md) | Configuration, data directory, reverse proxy, upgrades | +| [Product](docs/public/PRODUCT.md) / [Architecture](docs/public/ARCHITECTURE.md) | What it is (and is not), and how it is built | +| [Docs index](docs/internal/INDEX.md) | Every design, planning and operations document | ## Using this project with AI coding agents If you use Codex, Cursor, Claude Code, or another assistant with [Context7 MCP](https://context7.com/), you can ask it to consult Stack Manager's documentation before making changes. Context7 is optional and is separate from the application's Git and deployment integrations. -> Use Context7 MCP to resolve the official documentation library for `uniskela/stack-manager` (expected ID: `/uniskela/stack-manager`), then retrieve guidance for installation, repository connections, the editor and Git workflow, environment inventory, self-hosting, and troubleshooting. Match the guidance to the Stack Manager version I'm running. If Context7 is unavailable, the lookup returns an error, the library is still indexing, or the library doesn't cover that version, consult the [maintained documentation](https://uniskela.com/docs/stack-manager/) and [source Markdown](docs/index.md) instead. +> Use Context7 MCP to resolve the official documentation library for `uniskela/stack-manager` (expected ID: `/uniskela/stack-manager`), then retrieve guidance for installation, repository connections, the editor and Git workflow, environment inventory, self-hosting, and troubleshooting. Match the guidance to the Stack Manager version I'm running. If Context7 is unavailable, the lookup returns an error, the library is still indexing, or the library doesn't cover that version, consult the [maintained documentation](https://uniskela.com/docs/stack-manager/) and [source Markdown](docs/public/index.md) instead. ## Quick start (Docker Compose) @@ -63,7 +63,7 @@ Put stack-manager behind a TLS-terminating reverse proxy and set `STACK_MANAGER_ For plain-HTTP testing on localhost set `STACK_MANAGER_COOKIE_SECURE=false`. The container runs as a non-root user with a read-only root filesystem; all state lives in the `/data` -volume. No Docker socket is mounted. See [docs/installation.md](docs/installation.md). +volume. No Docker socket is mounted. See [docs/public/installation.md](docs/public/installation.md). ## Development diff --git a/docs/agents/README.md b/docs/agents/README.md new file mode 100644 index 0000000..64ba26d --- /dev/null +++ b/docs/agents/README.md @@ -0,0 +1,7 @@ +# Agent notes + +Canonical instructions for this repository are [AGENTS.md](../../AGENTS.md). Do not copy them here. + +Product boundary: [PRODUCT.md](../public/PRODUCT.md) and [ADR 0005](../internal/adr/0005-product-boundary.md). + +Maintainer index: [INDEX.md](../internal/INDEX.md). diff --git a/docs/DEPLOYMENT_ROUTING.md b/docs/internal/DEPLOYMENT_ROUTING.md similarity index 100% rename from docs/DEPLOYMENT_ROUTING.md rename to docs/internal/DEPLOYMENT_ROUTING.md diff --git a/docs/DEPLOYMENT_WATCH.md b/docs/internal/DEPLOYMENT_WATCH.md similarity index 100% rename from docs/DEPLOYMENT_WATCH.md rename to docs/internal/DEPLOYMENT_WATCH.md diff --git a/docs/INDEX.md b/docs/internal/INDEX.md similarity index 52% rename from docs/INDEX.md rename to docs/internal/INDEX.md index 27353a8..297f229 100644 --- a/docs/INDEX.md +++ b/docs/internal/INDEX.md @@ -1,26 +1,33 @@ # Documentation index -Architecture, planning and operations docs for **stack-manager**. User documentation starts at [index.md](index.md) -and is published at [uniskela.com/docs/stack-manager](https://uniskela.com/docs/stack-manager/); the pages it publishes -are listed in [manifest.json](manifest.json) (see [../.github/docs-sync.md](../.github/docs-sync.md)). +Architecture, planning and operations docs for **stack-manager**. User documentation lives in +[public/](../public/index.md) and is published at +[uniskela.com/docs/stack-manager](https://uniskela.com/docs/stack-manager/); the pages it publishes +are listed in [manifest.json](../manifest.json) (see [docs-sync](../../.github/docs-sync.md)). + +| Tree | What belongs there | +| --- | --- | +| [public/](../public/index.md) | Setup, deployment, backups, security and other user-facing references | +| internal/ (this file) | Implementation plans, architecture decisions and maintainer references | +| [agents/](../agents/README.md) | Agent notes. Canonical instructions stay in [AGENTS.md](../../AGENTS.md) | ## User guide | Doc | Summary | | --- | --- | -| [index.md](index.md) | Overview: what stack-manager does and does not do | -| [installation.md](installation.md) | Docker Compose install, image tags, upgrades | -| [getting-started.md](getting-started.md) | First run, repositories, stacks, editor, drafts, commit and push | -| [GIT_WORKFLOW.md](GIT_WORKFLOW.md) | Commit, safe push, remote changes, stack history; API reference | +| [index.md](../public/index.md) | Overview: what stack-manager does and does not do | +| [installation.md](../public/installation.md) | Docker Compose install, image tags, upgrades | +| [getting-started.md](../public/getting-started.md) | First run, repositories, stacks, editor, drafts, commit and push | +| [GIT_WORKFLOW.md](../public/GIT_WORKFLOW.md) | Commit, safe push, remote changes, stack history; API reference | ## Product and architecture | Doc | Summary | | --- | --- | -| [PRODUCT.md](PRODUCT.md) | Definition, hierarchy, non-goals, success criteria | -| [ARCHITECTURE.md](ARCHITECTURE.md) | Modular monolith, providers, request flows | +| [PRODUCT.md](../public/PRODUCT.md) | Definition, hierarchy, non-goals, success criteria | +| [ARCHITECTURE.md](../public/ARCHITECTURE.md) | Modular monolith, providers, request flows | | [PRIOR_ART.md](PRIOR_ART.md) | Portainer, Komodo, Arcane, Dockge, stackwise, etc. | -| [plans/MVP_PLAN.md](plans/MVP_PLAN.md) | PR #2–#8 implementation sequence | +| [plans/MVP_PLAN.md](../public/plans/MVP_PLAN.md) | Published roadmap: PR #2–#8 sequence | | [plans/UI_UX_PLAN.md](plans/UI_UX_PLAN.md) | UI/UX review of PR #2 and phased improvement plan | | [plans/2026-09-30-dashboard-stacks-settings-design.md](plans/2026-09-30-dashboard-stacks-settings-design.md) | Dashboard home, organised stacks, settings split (slice A) | | [plans/2026-09-30-dashboard-stacks-settings-implementation.md](plans/2026-09-30-dashboard-stacks-settings-implementation.md) | Implementation plan for slice A (nav, dashboard, stacks, settings, account) | @@ -35,22 +42,22 @@ are listed in [manifest.json](manifest.json) (see [../.github/docs-sync.md](../. | [providers/DEPLOYMENT_PROVIDER.md](providers/DEPLOYMENT_PROVIDER.md) | Deploy-only providers | | [providers/RUNTIME_PROVIDER.md](providers/RUNTIME_PROVIDER.md) | Optional read-only runtime | | [providers/SECRET_PROVIDER.md](providers/SECRET_PROVIDER.md) | Secret metadata / references | -| [STACK_DISCOVERY.md](STACK_DISCOVERY.md) | Repository → stack scopes | +| [STACK_DISCOVERY.md](../public/STACK_DISCOVERY.md) | Repository → stack scopes | | [DEPLOYMENT_ROUTING.md](DEPLOYMENT_ROUTING.md) | Path relevance and push router | -| [GIT_WORKFLOW.md](GIT_WORKFLOW.md) | Edit → validate → commit → push → history (implemented in v0.5.0) | +| [GIT_WORKFLOW.md](../public/GIT_WORKFLOW.md) | Edit → validate → commit → push → history (implemented in v0.5.0) | | [DEPLOYMENT_WATCH.md](DEPLOYMENT_WATCH.md) | CI-like deploy feedback | ## Operations and UX | Doc | Summary | | --- | --- | -| [SELF_HOSTING.md](SELF_HOSTING.md) | Configuration, data directory, Docker, upgrades | -| [RELEASING.md](RELEASING.md) | CI workflows, Conventional Commits, Release Please, GHCR images | -| [AUTH_AND_CREDENTIALS.md](AUTH_AND_CREDENTIALS.md) | Auth, encryption at rest | -| [SECURITY.md](SECURITY.md) | Threat model and controls | +| [SELF_HOSTING.md](../public/SELF_HOSTING.md) | Configuration, data directory, Docker, upgrades | +| [RELEASING.md](../public/RELEASING.md) | CI workflows, Conventional Commits, Release Please, GHCR images | +| [AUTH_AND_CREDENTIALS.md](../public/AUTH_AND_CREDENTIALS.md) | Auth, encryption at rest | +| [SECURITY.md](../public/SECURITY.md) | Threat model and controls | | [UX.md](UX.md) | Desktop / mobile / PWA | -| [BACKUP_RESTORE.md](BACKUP_RESTORE.md) | Backup and restore | -| [TESTING.md](TESTING.md) | Testing strategy | +| [BACKUP_RESTORE.md](../public/BACKUP_RESTORE.md) | Backup and restore | +| [TESTING.md](../public/TESTING.md) | Testing strategy | ## Architecture decision records diff --git a/docs/PRIOR_ART.md b/docs/internal/PRIOR_ART.md similarity index 100% rename from docs/PRIOR_ART.md rename to docs/internal/PRIOR_ART.md diff --git a/docs/UX.md b/docs/internal/UX.md similarity index 100% rename from docs/UX.md rename to docs/internal/UX.md diff --git a/docs/adr/0001-tech-stack.md b/docs/internal/adr/0001-tech-stack.md similarity index 100% rename from docs/adr/0001-tech-stack.md rename to docs/internal/adr/0001-tech-stack.md diff --git a/docs/adr/0002-persisted-jobs.md b/docs/internal/adr/0002-persisted-jobs.md similarity index 100% rename from docs/adr/0002-persisted-jobs.md rename to docs/internal/adr/0002-persisted-jobs.md diff --git a/docs/adr/0003-editor-codemirror.md b/docs/internal/adr/0003-editor-codemirror.md similarity index 100% rename from docs/adr/0003-editor-codemirror.md rename to docs/internal/adr/0003-editor-codemirror.md diff --git a/docs/adr/0004-modular-monolith.md b/docs/internal/adr/0004-modular-monolith.md similarity index 100% rename from docs/adr/0004-modular-monolith.md rename to docs/internal/adr/0004-modular-monolith.md diff --git a/docs/adr/0005-product-boundary.md b/docs/internal/adr/0005-product-boundary.md similarity index 100% rename from docs/adr/0005-product-boundary.md rename to docs/internal/adr/0005-product-boundary.md diff --git a/docs/domain/DATA_MODEL.md b/docs/internal/domain/DATA_MODEL.md similarity index 100% rename from docs/domain/DATA_MODEL.md rename to docs/internal/domain/DATA_MODEL.md diff --git a/docs/plans/2026-09-30-dashboard-stacks-settings-design.md b/docs/internal/plans/2026-09-30-dashboard-stacks-settings-design.md similarity index 100% rename from docs/plans/2026-09-30-dashboard-stacks-settings-design.md rename to docs/internal/plans/2026-09-30-dashboard-stacks-settings-design.md diff --git a/docs/plans/2026-09-30-dashboard-stacks-settings-implementation.md b/docs/internal/plans/2026-09-30-dashboard-stacks-settings-implementation.md similarity index 98% rename from docs/plans/2026-09-30-dashboard-stacks-settings-implementation.md rename to docs/internal/plans/2026-09-30-dashboard-stacks-settings-implementation.md index e4524e9..0cc6a04 100644 --- a/docs/plans/2026-09-30-dashboard-stacks-settings-implementation.md +++ b/docs/internal/plans/2026-09-30-dashboard-stacks-settings-implementation.md @@ -516,9 +516,9 @@ Never return hashes/tokens. Session `id` in API responses is the **hashed** sess ### Task 8: Docs + e2e polish + final verification **Files:** -- `docs/getting-started.md` / `docs/UX.md` — mention Dashboard home and settings tabs if user-facing -- `docs/INDEX.md` — link this plan if not already -- `docs/plans/2026-09-30-dashboard-stacks-settings-design.md` — status → implemented (when done) +- `docs/public/getting-started.md` / `docs/internal/UX.md` — mention Dashboard home and settings tabs if user-facing +- `docs/internal/INDEX.md` — link this plan if not already +- `docs/internal/plans/2026-09-30-dashboard-stacks-settings-design.md` — status → implemented (when done) - `tests/e2e/shell.spec.ts` — full nav order assertion - [ ] **Step 1: Update docs for IA change** @@ -560,7 +560,7 @@ await expect(nav.getByRole('link', { name: 'Settings' })).toBeVisible(); ## Execution handoff -Plan saved to `docs/plans/2026-09-30-dashboard-stacks-settings-implementation.md`. +Plan saved to `docs/internal/plans/2026-09-30-dashboard-stacks-settings-implementation.md`. **Two execution options:** diff --git a/docs/plans/2026-09-30-stacks-ui-audit-and-plan.md b/docs/internal/plans/2026-09-30-stacks-ui-audit-and-plan.md similarity index 97% rename from docs/plans/2026-09-30-stacks-ui-audit-and-plan.md rename to docs/internal/plans/2026-09-30-stacks-ui-audit-and-plan.md index 221e0f7..687f1c9 100644 --- a/docs/plans/2026-09-30-stacks-ui-audit-and-plan.md +++ b/docs/internal/plans/2026-09-30-stacks-ui-audit-and-plan.md @@ -10,7 +10,7 @@ The Stacks page should be **clean**, **predictable to scan** (ordering you can r ## Method and limits -Read `src/app/w/[workspaceId]/stacks/page.tsx`, `src/ui/stacks/organised-stacks.tsx`, `src/shared/stacks/group-stacks.ts`, the `.list`/`.list-row`/`.pill`/`.search-field` CSS, the stack layout/tabs, `StackService`, `docs/UX.md` and the earlier UI plans. +Read `src/app/w/[workspaceId]/stacks/page.tsx`, `src/ui/stacks/organised-stacks.tsx`, `src/shared/stacks/group-stacks.ts`, the `.list`/`.list-row`/`.pill`/`.search-field` CSS, the stack layout/tabs, `StackService`, `docs/internal/UX.md` and the earlier UI plans. The first pass was read-only, so visual findings were inferred from markup and CSS. Step 0 then ran a production build with a seeded dataset and checked them against screenshots (results below). @@ -231,8 +231,8 @@ monorepo paths, 8 drafts, one repository in `error` with no fetch), captured wit ### Phase S5 — Finish (docs/test) - Axe on `/stacks` at 390 px and 1280 px, light/dark, with filters open and with a chip row; check focus order and the live region. -- Update `docs/UX.md` ("Stacks — organised inventory (groups, search, filters, sort)"), `docs/getting-started.md` (how to find a stack), and mark this plan's status. List nothing new in `docs/manifest.json` unless a new user-facing page appears. -- Add this plan to `docs/INDEX.md` (note: that file currently has uncommitted changes in the working tree, so reconcile before editing). +- Update `docs/internal/UX.md` ("Stacks — organised inventory (groups, search, filters, sort)"), `docs/public/getting-started.md` (how to find a stack), and mark this plan's status. List nothing new in `docs/manifest.json` unless a new user-facing page appears. +- Add this plan to `docs/internal/INDEX.md` (note: that file currently has uncommitted changes in the working tree, so reconcile before editing). - `pnpm check` and `pnpm build` green; `pnpm test:e2e` with `E2E_GIT_REMOTE`/`E2E_STACK_ROOT` for the editor-dependent tests. **Done.** @@ -240,9 +240,9 @@ monorepo paths, 8 drafts, one repository in `error` with no fetch), captured wit - Axe runs in e2e on `/stacks` in its default, no-match, collapsed-folder and filters-open states (the mobile disclosure included), at desktop and Pixel 7 sizes, light and dark. There is no chip row (see S3). The switcher header was checked separately (S4). -- `docs/getting-started.md` gains a "Find a stack" section; `docs/UX.md` describes the new page and links here. No new +- `docs/public/getting-started.md` gains a "Find a stack" section; `docs/internal/UX.md` describes the new page and links here. No new user-facing page, so `docs/manifest.json` is unchanged. -- Not done: the `docs/INDEX.md` entry. That file has an unrelated uncommitted rewrite in the owner's working tree; +- Not done: the `docs/internal/INDEX.md` entry. That file has an unrelated uncommitted rewrite in the owner's working tree; add this plan when that lands. ## Outcome against the success criteria diff --git a/docs/plans/UI_UX_PLAN.md b/docs/internal/plans/UI_UX_PLAN.md similarity index 98% rename from docs/plans/UI_UX_PLAN.md rename to docs/internal/plans/UI_UX_PLAN.md index c18a4e3..e310083 100644 --- a/docs/plans/UI_UX_PLAN.md +++ b/docs/internal/plans/UI_UX_PLAN.md @@ -1,6 +1,6 @@ # UI/UX review and improvement plan -Review of the UI shipped in PR #2 (application foundation), with a phased plan to make it **user friendly, efficient and professional** without breaking the product boundary ([PRODUCT.md](../PRODUCT.md), [ADR 0005](../adr/0005-product-boundary.md)) or pulling later-phase features forward ([MVP_PLAN.md](MVP_PLAN.md)). +Review of the UI shipped in PR #2 (application foundation), with a phased plan to make it **user friendly, efficient and professional** without breaking the product boundary ([PRODUCT.md](../../public/PRODUCT.md), [ADR 0005](../adr/0005-product-boundary.md)) or pulling later-phase features forward ([MVP_PLAN.md](../../public/plans/MVP_PLAN.md)). Companion to [UX.md](../UX.md), which remains the source of truth for UX _principles_. This document covers the _current state_ and _concrete work_. diff --git a/docs/providers/DEPLOYMENT_PROVIDER.md b/docs/internal/providers/DEPLOYMENT_PROVIDER.md similarity index 100% rename from docs/providers/DEPLOYMENT_PROVIDER.md rename to docs/internal/providers/DEPLOYMENT_PROVIDER.md diff --git a/docs/providers/GIT_PROVIDER.md b/docs/internal/providers/GIT_PROVIDER.md similarity index 97% rename from docs/providers/GIT_PROVIDER.md rename to docs/internal/providers/GIT_PROVIDER.md index 450a12c..a4facd9 100644 --- a/docs/providers/GIT_PROVIDER.md +++ b/docs/internal/providers/GIT_PROVIDER.md @@ -86,7 +86,7 @@ One repository webhook → stack-manager: - Forge pull requests and commit statuses are not implemented (`capabilities.pullRequests` / `commitStatuses` are `false`); the v0.5.0 workflow commits directly to the tracked branch. - Commit inputs map selected drafts to `{ path, content, baseBlobSha }` with an expected branch HEAD and author identity; `content: null` supports controlled deletion. Push requires an exact retained commit and expected remote SHA. Neither operation touches draft persistence. -- See [Git workflow](../GIT_WORKFLOW.md#implementation-overview-v050) for remote protection, cleanup and crash-recovery limits. +- See [Git workflow](../../public/GIT_WORKFLOW.md#implementation-overview-v050) for remote protection, cleanup and crash-recovery limits. ## Non-goals diff --git a/docs/providers/RUNTIME_PROVIDER.md b/docs/internal/providers/RUNTIME_PROVIDER.md similarity index 100% rename from docs/providers/RUNTIME_PROVIDER.md rename to docs/internal/providers/RUNTIME_PROVIDER.md diff --git a/docs/providers/SECRET_PROVIDER.md b/docs/internal/providers/SECRET_PROVIDER.md similarity index 100% rename from docs/providers/SECRET_PROVIDER.md rename to docs/internal/providers/SECRET_PROVIDER.md diff --git a/docs/manifest.json b/docs/manifest.json index 3d637ff..0d85a40 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -7,98 +7,98 @@ "kind": "readme" }, { - "source": "docs/index.md", + "source": "docs/public/index.md", "slug": "index", "title": "Overview", "group": "Start here", "kind": "doc" }, { - "source": "docs/installation.md", + "source": "docs/public/installation.md", "slug": "installation", "title": "Installation", "group": "Start here", "kind": "doc" }, { - "source": "docs/getting-started.md", + "source": "docs/public/getting-started.md", "slug": "getting-started", "title": "Getting started", "group": "Start here", "kind": "doc" }, { - "source": "docs/GIT_WORKFLOW.md", + "source": "docs/public/GIT_WORKFLOW.md", "slug": "git-workflow", "title": "Git workflow: commit, push and history", "group": "Start here", "kind": "doc" }, { - "source": "docs/SELF_HOSTING.md", + "source": "docs/public/SELF_HOSTING.md", "slug": "self-hosting", "title": "Self-hosting reference", "group": "Deploy & operate", "kind": "doc" }, { - "source": "docs/BACKUP_RESTORE.md", + "source": "docs/public/BACKUP_RESTORE.md", "slug": "backup-restore", "title": "Backup and restore", "group": "Deploy & operate", "kind": "doc" }, { - "source": "docs/AUTH_AND_CREDENTIALS.md", + "source": "docs/public/AUTH_AND_CREDENTIALS.md", "slug": "auth-and-credentials", "title": "Authentication and credentials", "group": "Deploy & operate", "kind": "doc" }, { - "source": "docs/SECURITY.md", + "source": "docs/public/SECURITY.md", "slug": "security", "title": "Security", "group": "Deploy & operate", "kind": "doc" }, { - "source": "docs/PRODUCT.md", + "source": "docs/public/PRODUCT.md", "slug": "product", "title": "Product scope", "group": "Project", "kind": "doc" }, { - "source": "docs/STACK_DISCOVERY.md", + "source": "docs/public/STACK_DISCOVERY.md", "slug": "stack-discovery", "title": "Stack discovery", "group": "Project", "kind": "doc" }, { - "source": "docs/ARCHITECTURE.md", + "source": "docs/public/ARCHITECTURE.md", "slug": "architecture", "title": "Architecture", "group": "Project", "kind": "doc" }, { - "source": "docs/RELEASING.md", + "source": "docs/public/RELEASING.md", "slug": "releasing", "title": "Releases and CI", "group": "Project", "kind": "doc" }, { - "source": "docs/TESTING.md", + "source": "docs/public/TESTING.md", "slug": "testing", "title": "Testing", "group": "Project", "kind": "doc" }, { - "source": "docs/plans/MVP_PLAN.md", + "source": "docs/public/plans/MVP_PLAN.md", "slug": "plans/mvp-plan", "title": "Roadmap", "group": "Project", diff --git a/docs/ARCHITECTURE.md b/docs/public/ARCHITECTURE.md similarity index 98% rename from docs/ARCHITECTURE.md rename to docs/public/ARCHITECTURE.md index 1ccdc26..5882f98 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/public/ARCHITECTURE.md @@ -157,4 +157,4 @@ Next.js compiles `instrumentation.ts` and route bundles separately. The containe ## Stopping rule -If implementation begins resembling a Docker host manager, re-read [PRODUCT.md](PRODUCT.md) and [adr/0005-product-boundary.md](adr/0005-product-boundary.md). +If implementation begins resembling a Docker host manager, re-read [PRODUCT.md](PRODUCT.md) and [adr/0005-product-boundary.md](../internal/adr/0005-product-boundary.md). diff --git a/docs/AUTH_AND_CREDENTIALS.md b/docs/public/AUTH_AND_CREDENTIALS.md similarity index 100% rename from docs/AUTH_AND_CREDENTIALS.md rename to docs/public/AUTH_AND_CREDENTIALS.md diff --git a/docs/BACKUP_RESTORE.md b/docs/public/BACKUP_RESTORE.md similarity index 100% rename from docs/BACKUP_RESTORE.md rename to docs/public/BACKUP_RESTORE.md diff --git a/docs/GIT_WORKFLOW.md b/docs/public/GIT_WORKFLOW.md similarity index 100% rename from docs/GIT_WORKFLOW.md rename to docs/public/GIT_WORKFLOW.md diff --git a/docs/PRODUCT.md b/docs/public/PRODUCT.md similarity index 100% rename from docs/PRODUCT.md rename to docs/public/PRODUCT.md diff --git a/docs/RELEASING.md b/docs/public/RELEASING.md similarity index 100% rename from docs/RELEASING.md rename to docs/public/RELEASING.md diff --git a/docs/SECURITY.md b/docs/public/SECURITY.md similarity index 100% rename from docs/SECURITY.md rename to docs/public/SECURITY.md diff --git a/docs/SELF_HOSTING.md b/docs/public/SELF_HOSTING.md similarity index 98% rename from docs/SELF_HOSTING.md rename to docs/public/SELF_HOSTING.md index 899898e..fcc9006 100644 --- a/docs/SELF_HOSTING.md +++ b/docs/public/SELF_HOSTING.md @@ -1,6 +1,6 @@ # Self-hosting -stack-manager ships as a single long-running Node.js container (see [adr/0004-modular-monolith.md](adr/0004-modular-monolith.md)). +stack-manager ships as a single long-running Node.js container (see [adr/0004-modular-monolith.md](../internal/adr/0004-modular-monolith.md)). It needs no database server, no Redis and **no Docker socket**. ## Container image diff --git a/docs/STACK_DISCOVERY.md b/docs/public/STACK_DISCOVERY.md similarity index 98% rename from docs/STACK_DISCOVERY.md rename to docs/public/STACK_DISCOVERY.md index 3661ca0..f5ffc81 100644 --- a/docs/STACK_DISCOVERY.md +++ b/docs/public/STACK_DISCOVERY.md @@ -34,7 +34,7 @@ When connecting a repository: Each stack has: - `rootPath` — e.g. `apps/wiki` -- Path policy globs for deploy relevance (see [DEPLOYMENT_ROUTING.md](DEPLOYMENT_ROUTING.md)) +- Path policy globs for deploy relevance (see [DEPLOYMENT_ROUTING.md](../internal/DEPLOYMENT_ROUTING.md)) - Optional docs globs under root Scope matching for routing uses glob semantics (document library choice in PR #5, e.g. `picomatch` / `micromatch`). diff --git a/docs/TESTING.md b/docs/public/TESTING.md similarity index 100% rename from docs/TESTING.md rename to docs/public/TESTING.md diff --git a/docs/getting-started.md b/docs/public/getting-started.md similarity index 100% rename from docs/getting-started.md rename to docs/public/getting-started.md diff --git a/docs/index.md b/docs/public/index.md similarity index 100% rename from docs/index.md rename to docs/public/index.md diff --git a/docs/installation.md b/docs/public/installation.md similarity index 96% rename from docs/installation.md rename to docs/public/installation.md index 87beef8..7da23eb 100644 --- a/docs/installation.md +++ b/docs/public/installation.md @@ -12,7 +12,7 @@ access to the Docker socket. ## 1. Get the Compose file -Download [`docker-compose.yml`](../docker-compose.yml) and [`.env.example`](../.env.example) into an empty folder, or +Download [`docker-compose.yml`](../../docker-compose.yml) and [`.env.example`](../../.env.example) into an empty folder, or clone the repository: ```sh diff --git a/docs/plans/MVP_PLAN.md b/docs/public/plans/MVP_PLAN.md similarity index 100% rename from docs/plans/MVP_PLAN.md rename to docs/public/plans/MVP_PLAN.md diff --git a/next.config.ts b/next.config.ts index 9544158..8f0fe25 100644 --- a/next.config.ts +++ b/next.config.ts @@ -14,7 +14,7 @@ const securityHeaders = [ ]; const nextConfig: NextConfig = { - // Self-hosted, long-running Node server (see docs/adr/0004-modular-monolith.md). + // Self-hosted, long-running Node server (see docs/internal/adr/0004-modular-monolith.md). output: 'standalone', poweredByHeader: false, reactStrictMode: true, diff --git a/src/app/w/[workspaceId]/stacks/[stackId]/environment/page.tsx b/src/app/w/[workspaceId]/stacks/[stackId]/environment/page.tsx index 72ec6a9..dab711b 100644 --- a/src/app/w/[workspaceId]/stacks/[stackId]/environment/page.tsx +++ b/src/app/w/[workspaceId]/stacks/[stackId]/environment/page.tsx @@ -12,7 +12,7 @@ import { SectionTitle } from '@/ui/primitives/section'; export const metadata: Metadata = { title: 'Environment' }; /** - * Environment inventory derived from source only (docs/STACK_DISCOVERY.md): variables referenced by the + * Environment inventory derived from source only (docs/public/STACK_DISCOVERY.md): variables referenced by the * Compose file, whether `.env.example` documents them, and env files the stack expects. Values are * never read; secret files are only listed. */ diff --git a/src/server/application/credential-service.ts b/src/server/application/credential-service.ts index 6fc2b8e..1d5b431 100644 --- a/src/server/application/credential-service.ts +++ b/src/server/application/credential-service.ts @@ -32,7 +32,7 @@ export const credentialAad = (workspaceId: string, credentialId: string) => `stack-manager:credential:v1:${workspaceId}:${credentialId}`; /** - * Encrypted provider credentials (docs/AUTH_AND_CREDENTIALS.md). + * Encrypted provider credentials (docs/public/AUTH_AND_CREDENTIALS.md). * * Public methods return `CredentialView` only — metadata and a masked hint. Plaintext is available * solely through `withPlaintext`, which is used server-side for connection tests and Git operations. diff --git a/src/server/application/source-service.ts b/src/server/application/source-service.ts index f0b93b7..d7ea49d 100644 --- a/src/server/application/source-service.ts +++ b/src/server/application/source-service.ts @@ -26,7 +26,7 @@ const utf8Bytes = (s: string) => Buffer.byteLength(s, 'utf8'); * Read access to committed repository content plus draft edits (PR #3). Everything is scoped to a * directory (`root`, the stack root) so a stack page can only see and edit its own files. * - * Secret-looking files (docs/SECURITY.md) are listed but their contents are never returned and drafts + * Secret-looking files (docs/public/SECURITY.md) are listed but their contents are never returned and drafts * for them are refused. Committed content comes from Git objects, never the working tree. */ export class SourceService { diff --git a/src/server/application/stack-service.ts b/src/server/application/stack-service.ts index 7b93f23..b0a7d98 100644 --- a/src/server/application/stack-service.ts +++ b/src/server/application/stack-service.ts @@ -61,7 +61,7 @@ const IGNORED_SEGMENTS = new Set([ const MAX_SUGGESTIONS = 2000; /** - * Explicit stacks inside connected repositories (docs/STACK_DISCOVERY.md). Discovery only suggests; + * Explicit stacks inside connected repositories (docs/public/STACK_DISCOVERY.md). Discovery only suggests; * stacks exist once the operator confirms them, and never imply deploy/runtime bindings. */ export class StackService { diff --git a/src/server/domain/git-repository.ts b/src/server/domain/git-repository.ts index c827d42..b40c364 100644 --- a/src/server/domain/git-repository.ts +++ b/src/server/domain/git-repository.ts @@ -18,7 +18,7 @@ export interface GitRepositoryConnection { lastSyncError: string | null; headSha: string | null; lastFetchedAt: Date | null; - /** Register every Compose folder as a stack after each successful fetch (docs/STACK_DISCOVERY.md). */ + /** Register every Compose folder as a stack after each successful fetch (docs/public/STACK_DISCOVERY.md). */ autoAddStacks: boolean; createdAt: Date; updatedAt: Date; @@ -28,7 +28,7 @@ export interface GitRepositoryConnection { * Validates and normalises a Git remote URL. * * PR #2 accepts HTTPS remotes only: credentials are sent as an HTTP header, so plaintext - * `http://` and `git://` are rejected up-front (docs/SECURITY.md). SSH remotes are deferred until + * `http://` and `git://` are rejected up-front (docs/public/SECURITY.md). SSH remotes are deferred until * host-key management exists. Embedded credentials (`https://user:token@…`) are rejected so * tokens are never persisted in URLs, logs or `.git/config`. */ diff --git a/src/server/domain/job.ts b/src/server/domain/job.ts index 9a10696..2fd6f4f 100644 --- a/src/server/domain/job.ts +++ b/src/server/domain/job.ts @@ -19,7 +19,7 @@ export interface Job

= Record finishedAt: Date | null; } -/** Job lease/heartbeat constants (docs/adr/0002-persisted-jobs.md). */ +/** Job lease/heartbeat constants (docs/internal/adr/0002-persisted-jobs.md). */ export const JOB_LEASE_TTL_MS = 60_000; export const JOB_HEARTBEAT_INTERVAL_MS = 10_000; export const JOB_POLL_INTERVAL_MS = 1_000; diff --git a/src/server/domain/stack.ts b/src/server/domain/stack.ts index 441ec91..a33a18f 100644 --- a/src/server/domain/stack.ts +++ b/src/server/domain/stack.ts @@ -4,7 +4,7 @@ import { slugify } from './workspace'; /** * An explicit stack: a directory in a repository plus its primary Compose file - * (docs/STACK_DISCOVERY.md). Identity is this record, never a folder-name heuristic. + * (docs/public/STACK_DISCOVERY.md). Identity is this record, never a folder-name heuristic. */ export interface Stack { id: string; diff --git a/src/server/persistence/schema.ts b/src/server/persistence/schema.ts index c584b8f..6ab48fc 100644 --- a/src/server/persistence/schema.ts +++ b/src/server/persistence/schema.ts @@ -2,7 +2,7 @@ import { sql } from 'drizzle-orm'; import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'; /** - * Physical schema for the PR #2 foundation entities and the PR #3 source workspace (docs/domain/DATA_MODEL.md). + * Physical schema for the PR #2 foundation entities and the PR #3 source workspace (docs/internal/domain/DATA_MODEL.md). * * Portability rules (keep a PostgreSQL port practical): * - ids are application-generated UUID strings, never autoincrement integers; @@ -171,7 +171,7 @@ export const auditEvents = sqliteTable( ], ); -/** PR #3: explicit stacks inside a connected repository (docs/STACK_DISCOVERY.md). */ +/** PR #3: explicit stacks inside a connected repository (docs/public/STACK_DISCOVERY.md). */ export const stacks = sqliteTable( 'stacks', { diff --git a/src/server/providers/git/types.ts b/src/server/providers/git/types.ts index d1ecfe7..c6555ac 100644 --- a/src/server/providers/git/types.ts +++ b/src/server/providers/git/types.ts @@ -1,5 +1,5 @@ /** - * GitProvider capability interface (docs/providers/GIT_PROVIDER.md). + * GitProvider capability interface (docs/internal/providers/GIT_PROVIDER.md). * * Connection, source reads and isolated commit/push operations. Webhook * verification/parsing (PR #5) extend this interface later; `capabilities` advertises optional diff --git a/src/server/security/network-policy.ts b/src/server/security/network-policy.ts index 9009ec5..acd7a8f 100644 --- a/src/server/security/network-policy.ts +++ b/src/server/security/network-policy.ts @@ -3,7 +3,7 @@ import net from 'node:net'; import { ValidationError } from '@/server/domain/errors'; /** - * Outbound host policy for credential-bearing requests (SSRF control, docs/SECURITY.md). + * Outbound host policy for credential-bearing requests (SSRF control, docs/public/SECURITY.md). * * Always denied: loopback, unspecified, link-local (incl. cloud metadata 169.254.169.254), * multicast/reserved. Private ranges (RFC 1918, CGNAT, IPv6 ULA) are denied unless the operator diff --git a/src/server/security/password.ts b/src/server/security/password.ts index acd0b7c..23e5ce7 100644 --- a/src/server/security/password.ts +++ b/src/server/security/password.ts @@ -1,7 +1,7 @@ import { hash, verify, type Algorithm } from '@node-rs/argon2'; /** - * Argon2id password hashing (docs/AUTH_AND_CREDENTIALS.md). + * Argon2id password hashing (docs/public/AUTH_AND_CREDENTIALS.md). * Parameters exceed the OWASP minimum (m=19 MiB, t=2, p=1); a single-operator instance can afford it. */ const PARAMS = { diff --git a/src/ui/source/stack-discovery.tsx b/src/ui/source/stack-discovery.tsx index f7b745f..a45da6e 100644 --- a/src/ui/source/stack-discovery.tsx +++ b/src/ui/source/stack-discovery.tsx @@ -27,7 +27,7 @@ export const STACKS_PAGE_SIZE = 25; const label = (s: Suggestion) => s.rootPath || '(repository root)'; /** - * Stacks of one repository (docs/STACK_DISCOVERY.md). Every folder with a Compose file is listed; with + * Stacks of one repository (docs/public/STACK_DISCOVERY.md). Every folder with a Compose file is listed; with * auto-add on (the default) they are registered after each fetch, otherwise the operator adds all of * them at once or picks some. Folders can also be added by path. */ diff --git a/src/ui/source/stack-editor.tsx b/src/ui/source/stack-editor.tsx index 4926a39..d057170 100644 --- a/src/ui/source/stack-editor.tsx +++ b/src/ui/source/stack-editor.tsx @@ -131,7 +131,7 @@ export function StackEditor(props: StackEditorProps) { [problems], ); - // Phones: soft-wrap on and the problems list collapsed, so the editor keeps most of the screen (docs/UX.md). + // Phones: soft-wrap on and the problems list collapsed, so the editor keeps most of the screen (docs/internal/UX.md). useEffect(() => { const narrow = window.matchMedia('(max-width: 900px)').matches; setWrap(narrow); diff --git a/src/ui/source/stack-tabs.tsx b/src/ui/source/stack-tabs.tsx index 35bb22a..19dab52 100644 --- a/src/ui/source/stack-tabs.tsx +++ b/src/ui/source/stack-tabs.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { useEffect } from 'react'; -/** Stack page sections (docs/UX.md). Deployments, runtime and secrets join in later phases. */ +/** Stack page sections (docs/internal/UX.md). Deployments, runtime and secrets join in later phases. */ export function StackTabs({ base, drafts }: { base: string; drafts: number }) { const pathname = usePathname(); const router = useRouter(); diff --git a/tests/unit/docs-layout.test.ts b/tests/unit/docs-layout.test.ts new file mode 100644 index 0000000..edc19b7 --- /dev/null +++ b/tests/unit/docs-layout.test.ts @@ -0,0 +1,96 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const root = process.cwd(); + +/** Published URL slugs. Moving a file must not change these. */ +const PUBLIC_SLUGS = [ + 'readme', + 'index', + 'installation', + 'getting-started', + 'git-workflow', + 'self-hosting', + 'backup-restore', + 'auth-and-credentials', + 'security', + 'product', + 'stack-discovery', + 'architecture', + 'releasing', + 'testing', + 'plans/mvp-plan', +]; + +type Page = { source: string; slug: string; kind: string }; + +function markdownFiles(dir: string): string[] { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return markdownFiles(full); + return entry.name.endsWith('.md') ? [full] : []; + }); +} + +function relativeLinks(file: string): string[] { + const text = fs + .readFileSync(file, 'utf8') + .replace(/```[\s\S]*?```/g, '') + .replace(/`[^`\n]*`/g, ''); + return [...text.matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)].map((match) => match[1] ?? ''); +} + +describe('docs audience split', () => { + const manifest = JSON.parse(fs.readFileSync(path.join(root, 'docs/manifest.json'), 'utf8')) as { + schemaVersion: number; + pages: Page[]; + }; + + it('publishes the same slugs from docs/public and README only', () => { + expect(manifest.schemaVersion).toBe(1); + expect(manifest.pages.map((page) => page.slug)).toEqual(PUBLIC_SLUGS); + const publicDocs = markdownFiles(path.join(root, 'docs/public')).map((file) => + path.relative(root, file).split(path.sep).join('/'), + ); + expect(manifest.pages.map((page) => page.source).sort()).toEqual(['README.md', ...publicDocs].sort()); + for (const page of manifest.pages) { + expect(page.source.startsWith('docs/internal/') || page.source.startsWith('docs/agents/')).toBe(false); + expect(fs.existsSync(path.join(root, page.source))).toBe(true); + } + }); + + it('keeps the product boundary and root agent guide in place', () => { + const agents = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'); + expect(agents).toContain('](docs/public/PRODUCT.md)'); + expect(agents).toContain('](docs/internal/adr/0005-product-boundary.md)'); + expect(fs.existsSync(path.join(root, 'docs/internal/adr/0005-product-boundary.md'))).toBe(true); + const notes = fs.readFileSync(path.join(root, 'docs/agents/README.md'), 'utf8'); + expect(notes).toContain('](../../AGENTS.md)'); + expect(notes).not.toContain('Portainer/Komodo/Arcane replacement'); + }); + + it('resolves relative markdown links', () => { + const files = [ + ...markdownFiles(path.join(root, 'docs')), + path.join(root, 'README.md'), + path.join(root, 'AGENTS.md'), + path.join(root, '.github/docs-sync.md'), + ]; + const missing: string[] = []; + for (const file of files) { + for (const link of relativeLinks(file)) { + if (/^(https?:|mailto:)/.test(link) || link.startsWith('#')) continue; + const target = decodeURIComponent(link.split('#')[0]?.split('?')[0] ?? ''); + if (!target) continue; + const resolved = path.resolve(path.dirname(file), target); + if (!resolved.startsWith(root + path.sep) && resolved !== root) { + missing.push(`${path.relative(root, file)} -> ${link}`); + continue; + } + if (!fs.existsSync(resolved)) missing.push(`${path.relative(root, file)} -> ${link}`); + } + } + expect(missing).toEqual([]); + }); +});