forgedev 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +57 -10
  2. package/bin/chainproof.js +126 -0
  3. package/bin/devforge.js +1 -1
  4. package/package.json +25 -7
  5. package/src/chainproof-bridge.js +330 -0
  6. package/src/ci-mode.js +85 -0
  7. package/src/claude-configurator.js +171 -78
  8. package/src/cli.js +30 -7
  9. package/src/composer.js +242 -214
  10. package/src/doctor-checks-chainproof.js +106 -0
  11. package/src/doctor-checks.js +39 -20
  12. package/src/doctor-prompts.js +9 -9
  13. package/src/doctor.js +37 -4
  14. package/src/guided.js +3 -3
  15. package/src/index.js +31 -10
  16. package/src/init-mode.js +76 -12
  17. package/src/menu.js +178 -0
  18. package/src/prompts.js +5 -12
  19. package/src/recommender.js +163 -30
  20. package/src/scanner.js +57 -2
  21. package/src/uat-generator.js +204 -189
  22. package/src/update-check.js +9 -4
  23. package/src/update.js +57 -13
  24. package/src/utils.js +162 -5
  25. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  29. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  34. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  35. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  36. package/templates/backend/express/Dockerfile.template +18 -0
  37. package/templates/backend/express/package.json.template +33 -0
  38. package/templates/backend/express/src/index.ts.template +34 -0
  39. package/templates/backend/express/src/routes/health.ts.template +27 -0
  40. package/templates/backend/express/tsconfig.json +17 -0
  41. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  42. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  44. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  45. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  46. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  47. package/templates/backend/hono/Dockerfile.template +18 -0
  48. package/templates/backend/hono/package.json.template +31 -0
  49. package/templates/backend/hono/src/index.ts.template +32 -0
  50. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  51. package/templates/backend/hono/tsconfig.json +18 -0
  52. package/templates/base/.gitignore.template +3 -0
  53. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
  54. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  55. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  56. package/templates/chainproof/base/.mcp.json +9 -0
  57. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  58. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  59. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  60. package/templates/claude-code/agents/architect.md +25 -11
  61. package/templates/claude-code/agents/build-error-resolver.md +19 -5
  62. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  63. package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
  64. package/templates/claude-code/agents/database-reviewer.md +15 -1
  65. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  66. package/templates/claude-code/agents/doc-updater.md +19 -5
  67. package/templates/claude-code/agents/docs-lookup.md +19 -5
  68. package/templates/claude-code/agents/e2e-runner.md +26 -12
  69. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  70. package/templates/claude-code/agents/frontend-builder.md +188 -0
  71. package/templates/claude-code/agents/harness-optimizer.md +36 -1
  72. package/templates/claude-code/agents/loop-operator.md +27 -13
  73. package/templates/claude-code/agents/planner.md +21 -7
  74. package/templates/claude-code/agents/product-strategist.md +24 -10
  75. package/templates/claude-code/agents/production-readiness.md +14 -0
  76. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  77. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  78. package/templates/claude-code/agents/security-reviewer.md +14 -0
  79. package/templates/claude-code/agents/spec-validator.md +15 -1
  80. package/templates/claude-code/agents/tdd-guide.md +21 -7
  81. package/templates/claude-code/agents/uat-validator.md +14 -0
  82. package/templates/claude-code/claude-md/base.md +14 -7
  83. package/templates/claude-code/claude-md/fastapi.md +8 -8
  84. package/templates/claude-code/claude-md/fullstack.md +6 -6
  85. package/templates/claude-code/claude-md/hono.md +18 -0
  86. package/templates/claude-code/claude-md/nextjs.md +5 -5
  87. package/templates/claude-code/claude-md/remix.md +18 -0
  88. package/templates/claude-code/commands/audit-security.md +14 -0
  89. package/templates/claude-code/commands/audit-spec.md +14 -0
  90. package/templates/claude-code/commands/audit-wiring.md +14 -0
  91. package/templates/claude-code/commands/build-fix.md +28 -0
  92. package/templates/claude-code/commands/build-ui.md +59 -0
  93. package/templates/claude-code/commands/code-review.md +53 -31
  94. package/templates/claude-code/commands/fix-loop.md +211 -0
  95. package/templates/claude-code/commands/full-audit.md +36 -8
  96. package/templates/claude-code/commands/generate-prd.md +1 -1
  97. package/templates/claude-code/commands/generate-sdd.md +74 -0
  98. package/templates/claude-code/commands/generate-uat.md +107 -35
  99. package/templates/claude-code/commands/help.md +68 -0
  100. package/templates/claude-code/commands/live-uat.md +268 -0
  101. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  102. package/templates/claude-code/commands/plan.md +3 -3
  103. package/templates/claude-code/commands/pre-pr.md +57 -19
  104. package/templates/claude-code/commands/product-strategist.md +21 -0
  105. package/templates/claude-code/commands/resume-session.md +10 -10
  106. package/templates/claude-code/commands/run-uat.md +59 -2
  107. package/templates/claude-code/commands/save-session.md +10 -10
  108. package/templates/claude-code/commands/simplify.md +36 -0
  109. package/templates/claude-code/commands/tdd.md +17 -18
  110. package/templates/claude-code/commands/verify-all.md +24 -0
  111. package/templates/claude-code/commands/verify-intent.md +55 -0
  112. package/templates/claude-code/commands/workflows.md +52 -40
  113. package/templates/claude-code/hooks/polyglot.json +10 -1
  114. package/templates/claude-code/hooks/python.json +10 -1
  115. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
  116. package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
  117. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
  118. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  119. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  120. package/templates/claude-code/hooks/typescript.json +10 -1
  121. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  122. package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
  123. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  124. package/templates/claude-code/skills/playwright/SKILL.md +5 -5
  125. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  126. package/templates/claude-code/skills/security-web/SKILL.md +1 -1
  127. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  128. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  129. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  130. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  131. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  132. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  133. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  134. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  135. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  136. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  137. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  138. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  139. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  140. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  141. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  142. package/templates/frontend/nextjs/package.json.template +3 -1
  143. package/templates/frontend/react/index.html.template +12 -0
  144. package/templates/frontend/react/package.json.template +34 -0
  145. package/templates/frontend/react/src/App.tsx.template +10 -0
  146. package/templates/frontend/react/src/index.css +1 -0
  147. package/templates/frontend/react/src/main.tsx +10 -0
  148. package/templates/frontend/react/tsconfig.json +17 -0
  149. package/templates/frontend/react/vite.config.ts.template +15 -0
  150. package/templates/frontend/react/vitest.config.ts +9 -0
  151. package/templates/frontend/remix/app/root.tsx.template +31 -0
  152. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  153. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  154. package/templates/frontend/remix/app/tailwind.css +1 -0
  155. package/templates/frontend/remix/package.json.template +39 -0
  156. package/templates/frontend/remix/tsconfig.json +18 -0
  157. package/templates/frontend/remix/vite.config.ts.template +7 -0
  158. package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
  159. package/templates/infra/k8s/k8s/deployment.yml.template +70 -0
  160. package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
  161. package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
  162. package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
  163. package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
  164. package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
  165. package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
  166. package/templates/infra/k8s/k8s/service.yml.template +15 -0
  167. package/templates/testing/load/k6/README.md.template +48 -0
  168. package/templates/testing/load/k6/load-test.js.template +57 -0
  169. package/docs/00-README.md +0 -310
  170. package/docs/01-universal-prompt-library.md +0 -1049
  171. package/docs/02-claude-code-mastery-playbook.md +0 -283
  172. package/docs/03-multi-agent-verification.md +0 -565
  173. package/docs/04-errata-and-verification-checklist.md +0 -284
  174. package/docs/05-universal-scaffolder-vision.md +0 -452
  175. package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
  176. package/docs/errata.md +0 -58
  177. package/docs/multi-agent-verification.md +0 -66
  178. package/docs/playbook.md +0 -95
  179. package/docs/prompt-library.md +0 -160
  180. package/docs/uat/UAT_CHECKLIST.csv +0 -9
  181. package/docs/uat/UAT_TEMPLATE.md +0 -163
  182. package/templates/claude-code/commands/done.md +0 -19
  183. /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
@@ -5,10 +5,10 @@ description: Git branching, commits, PR workflow, and conflict resolution patter
5
5
 
6
6
  ## Branching Strategy
7
7
 
8
- - `main` production-ready code, never commit directly
9
- - `feat/<name>` new features, branch from main
10
- - `fix/<name>` bug fixes, branch from main
11
- - `chore/<name>` maintenance, config changes, dependency updates
8
+ - `main` - production-ready code, never commit directly
9
+ - `feat/<name>` - new features, branch from main
10
+ - `fix/<name>` - bug fixes, branch from main
11
+ - `chore/<name>` - maintenance, config changes, dependency updates
12
12
 
13
13
  Always create a feature branch before starting work:
14
14
  ```bash
@@ -59,6 +59,6 @@ If rebase gets messy: `git rebase --abort` and start over.
59
59
 
60
60
  - Never force-push to `main`
61
61
  - Never commit `.env` files, credentials, or large binaries
62
- - Never rebase shared branches (`main`, `develop`) only rebase your own feature branches
62
+ - Never rebase shared branches (`main`, `develop`). Only rebase your own feature branches
63
63
  - Update feature branches with `git rebase origin/main`, not `git merge main` (force-push your branch after)
64
64
  - Always pull before pushing: `git pull --rebase` (pulls current branch's upstream)
@@ -33,7 +33,7 @@ description: Next.js 15 App Router patterns and best practices
33
33
  - Intercepting routes `(.)` for modals
34
34
 
35
35
  ## Common Mistakes
36
- - Avoid Client Components with `useEffect` for data fetching prefer async Server Components instead
36
+ - Avoid Client Components with `useEffect` for data fetching. Prefer async Server Components instead
37
37
  - Don't pass functions as props from Server to Client Components
38
38
  - Don't use `router.push` when `<Link>` works
39
39
  - Don't create API routes for data that Server Components can fetch directly
@@ -12,11 +12,11 @@ description: Playwright E2E testing patterns and best practices
12
12
  - Use page object pattern for complex pages
13
13
 
14
14
  ## Selector Priority
15
- 1. `getByRole()` buttons, links, headings (best for accessibility)
16
- 2. `getByLabel()` form inputs by label
17
- 3. `getByText()` visible text content
18
- 4. `getByTestId()` `data-testid` attributes (last resort)
19
- - Avoid CSS selectors or XPath they break when styles change
15
+ 1. `getByRole()`: buttons, links, headings (best for accessibility)
16
+ 2. `getByLabel()`: form inputs by label
17
+ 3. `getByText()`: visible text content
18
+ 4. `getByTestId()`: `data-testid` attributes (last resort)
19
+ - Avoid CSS selectors or XPath. They break when styles change
20
20
 
21
21
  ## Assertions
22
22
  - Use `expect(locator)` for element assertions
@@ -20,7 +20,7 @@ description: API security best practices
20
20
  - Validate file uploads (size, type, extension)
21
21
 
22
22
  ## SQL Injection Prevention
23
- - Use SQLAlchemy ORM never raw SQL strings
23
+ - Use SQLAlchemy ORM. Never raw SQL strings
24
24
  - If raw SQL needed, use `text()` with bound parameters
25
25
  - Never interpolate user input into queries
26
26
 
@@ -6,7 +6,7 @@ description: Web application security best practices
6
6
  # Web Application Security
7
7
 
8
8
  ## XSS Prevention
9
- - React escapes by default never use `dangerouslySetInnerHTML`
9
+ - React escapes by default. Never use `dangerouslySetInnerHTML`
10
10
  - Sanitize user input before rendering
11
11
  - Use Content Security Policy headers
12
12
  - Validate URLs before using in `href` or `src`
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: testing-patterns
3
- description: Universal testing principles test pyramid, AAA pattern, mocking strategies, and coverage targets
3
+ description: Universal testing principles - test pyramid, AAA pattern, mocking strategies, and coverage targets
4
4
  ---
5
5
 
6
6
  ## Test Pyramid
7
7
 
8
8
  ```
9
- / E2E \ Few, slow, high confidence
10
- / Integration \ Some, medium speed
11
- / Unit Tests \— Many, fast, focused
9
+ / E2E \ - Few, slow, high confidence
10
+ / Integration \ - Some, medium speed
11
+ / Unit Tests \- Many, fast, focused
12
12
  ```
13
13
 
14
14
  - **Unit tests** (70%): Test individual functions in isolation. Fast, many.
@@ -21,14 +21,14 @@ Every test follows this structure:
21
21
 
22
22
  ```
23
23
  test('should calculate total with tax', () => {
24
- // Arrange set up test data
24
+ // Arrange: set up test data
25
25
  const items = [{ price: 10 }, { price: 20 }];
26
26
  const taxRate = 0.1;
27
27
 
28
- // Act execute the function
28
+ // Act: execute the function
29
29
  const total = calculateTotal(items, taxRate);
30
30
 
31
- // Assert verify the result
31
+ // Assert: verify the result
32
32
  expect(total).toBe(33);
33
33
  });
34
34
  ```
@@ -52,7 +52,7 @@ test('should calculate total with tax', () => {
52
52
 
53
53
  | What | When to Mock |
54
54
  |------|-------------|
55
- | External APIs | Always — they're slow and unreliable |
55
+ | External APIs | Always. They're slow and unreliable |
56
56
  | Database | Integration tests use real DB, unit tests mock |
57
57
  | Time/Date | When testing time-dependent logic |
58
58
  | File system | When testing file operations |
@@ -84,7 +84,7 @@ Use descriptive names that explain the scenario:
84
84
 
85
85
  - **80% minimum** for all code
86
86
  - **100% required** for: auth logic, financial calculations, security-critical code
87
- - Coverage measures lines hit, not correctness high coverage with weak assertions is useless
87
+ - Coverage measures lines hit, not correctness. High coverage with weak assertions is useless
88
88
  - Focus on meaningful assertions, not just line coverage
89
89
 
90
90
  ## Common Anti-Patterns
@@ -1 +1,2 @@
1
+ # Local development only. Change credentials before deploying to production.
1
2
  DATABASE_URL="postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME_SNAKE}}"
@@ -1,2 +1,3 @@
1
+ # Local development only. Change credentials before deploying to production.
1
2
  DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/{{PROJECT_NAME_SNAKE}}"
2
3
  JWT_SECRET_KEY="change-me-generate-a-random-secret"
@@ -0,0 +1,201 @@
1
+ """Reads and organizes markdown files from the docs/ directory."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from dataclasses import dataclass, field
7
+
8
+ import bleach
9
+ import markdown
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ DOCS_ROOT = Path(__file__).resolve().parent.parent.parent.parent / "docs"
14
+
15
+ ALLOWED_HTML_TAGS = [
16
+ "a",
17
+ "p",
18
+ "ul",
19
+ "ol",
20
+ "li",
21
+ "strong",
22
+ "em",
23
+ "code",
24
+ "pre",
25
+ "blockquote",
26
+ "hr",
27
+ "br",
28
+ "h1",
29
+ "h2",
30
+ "h3",
31
+ "h4",
32
+ "h5",
33
+ "h6",
34
+ "table",
35
+ "thead",
36
+ "tbody",
37
+ "tr",
38
+ "th",
39
+ "td",
40
+ ]
41
+ ALLOWED_HTML_ATTRIBUTES = {
42
+ "a": ["href", "title"],
43
+ "th": ["align"],
44
+ "td": ["align"],
45
+ }
46
+ ALLOWED_PROTOCOLS = ["http", "https", "mailto"]
47
+
48
+ CATEGORY_META: dict[str, dict] = {
49
+ "prd": {"label": "Product", "description": "Product requirements and roadmap", "icon": "📋", "order": 1},
50
+ "sdd": {"label": "Architecture", "description": "System design and technical architecture", "icon": "🏗️", "order": 2},
51
+ "uat": {"label": "Testing", "description": "Test plans, results, and acceptance criteria", "icon": "✅", "order": 3},
52
+ "sessions": {"label": "Session Logs", "description": "Testing session results and bug reports", "icon": "📝", "order": 4},
53
+ }
54
+
55
+
56
+ @dataclass
57
+ class DocFile:
58
+ slug: str
59
+ title: str
60
+ content: str
61
+ html: str
62
+ category: str
63
+ category_label: str
64
+ last_modified: datetime
65
+
66
+
67
+ @dataclass
68
+ class DocCategory:
69
+ id: str
70
+ label: str
71
+ description: str
72
+ icon: str
73
+ order: int
74
+ docs: list[dict] = field(default_factory=list)
75
+
76
+
77
+ def _extract_title(content: str, filename: str) -> str:
78
+ for line in content.split("\n"):
79
+ line = line.strip()
80
+ if line.startswith("# ") and not line.startswith("##"):
81
+ return line[2:].strip()
82
+ return filename.replace(".md", "").replace("-", " ").replace("_", " ").title()
83
+
84
+
85
+ def _render_markdown(content: str) -> str:
86
+ md = markdown.Markdown(extensions=["tables", "fenced_code", "toc"])
87
+ html = md.convert(content)
88
+ return bleach.clean(
89
+ html,
90
+ tags=ALLOWED_HTML_TAGS,
91
+ attributes=ALLOWED_HTML_ATTRIBUTES,
92
+ protocols=ALLOWED_PROTOCOLS,
93
+ strip=True,
94
+ )
95
+
96
+
97
+ def get_categories() -> list[DocCategory]:
98
+ if not DOCS_ROOT.exists():
99
+ return []
100
+
101
+ categories: list[DocCategory] = []
102
+
103
+ for entry in sorted(DOCS_ROOT.iterdir()):
104
+ if not entry.is_dir():
105
+ continue
106
+
107
+ category_id = entry.name
108
+ meta = CATEGORY_META.get(category_id, {
109
+ "label": category_id.capitalize(),
110
+ "description": "",
111
+ "icon": "📄",
112
+ "order": 99,
113
+ })
114
+
115
+ md_files = sorted(entry.glob("*.md"))
116
+ if not md_files:
117
+ continue
118
+
119
+ docs = []
120
+ for f in md_files:
121
+ try:
122
+ content = f.read_text(encoding="utf-8")
123
+ docs.append({
124
+ "slug": f.stem,
125
+ "title": _extract_title(content, f.name),
126
+ })
127
+ except (OSError, UnicodeDecodeError) as exc:
128
+ logger.warning("Skipping docs file %s: %s", f.name, exc)
129
+ continue
130
+
131
+ categories.append(DocCategory(
132
+ id=category_id,
133
+ label=meta["label"],
134
+ description=meta["description"],
135
+ icon=meta["icon"],
136
+ order=meta["order"],
137
+ docs=docs,
138
+ ))
139
+
140
+ # Top-level markdown files
141
+ top_level = sorted(DOCS_ROOT.glob("*.md"))
142
+ if top_level:
143
+ docs = []
144
+ for f in top_level:
145
+ content = f.read_text(encoding="utf-8")
146
+ docs.append({
147
+ "slug": f.stem,
148
+ "title": _extract_title(content, f.name),
149
+ })
150
+ categories.append(DocCategory(
151
+ id="_general",
152
+ label="General",
153
+ description="Project documentation",
154
+ icon="📄",
155
+ order=99,
156
+ docs=docs,
157
+ ))
158
+
159
+ return sorted(categories, key=lambda c: c.order)
160
+
161
+
162
+ def get_doc(category: str, slug: str) -> DocFile | None:
163
+ if category == "_general":
164
+ file_path = DOCS_ROOT / f"{slug}.md"
165
+ else:
166
+ file_path = DOCS_ROOT / category / f"{slug}.md"
167
+
168
+ # Prevent path traversal — resolved path must stay within DOCS_ROOT
169
+ docs_root_resolved = DOCS_ROOT.resolve()
170
+ resolved = file_path.resolve()
171
+ try:
172
+ resolved.relative_to(docs_root_resolved)
173
+ except ValueError:
174
+ return None
175
+
176
+ if not resolved.exists():
177
+ return None
178
+
179
+ content = resolved.read_text(encoding="utf-8")
180
+ stat = resolved.stat()
181
+ meta = CATEGORY_META.get(category, {"label": "General"})
182
+
183
+ return DocFile(
184
+ slug=slug,
185
+ title=_extract_title(content, f"{slug}.md"),
186
+ content=content,
187
+ html=_render_markdown(content),
188
+ category=category,
189
+ category_label=meta["label"],
190
+ last_modified=datetime.fromtimestamp(stat.st_mtime),
191
+ )
192
+
193
+
194
+ def get_all_docs() -> list[DocFile]:
195
+ docs: list[DocFile] = []
196
+ for cat in get_categories():
197
+ for doc_meta in cat.docs:
198
+ doc = get_doc(cat.id, doc_meta["slug"])
199
+ if doc:
200
+ docs.append(doc)
201
+ return docs
@@ -0,0 +1,229 @@
1
+ """Renders portal pages as self-contained HTML. No external template engine required."""
2
+
3
+ from __future__ import annotations
4
+ from html import escape as esc
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from app.portal.docs_reader import DocCategory, DocFile
9
+
10
+
11
+ def _base(title: str, project: str, categories: list[DocCategory], content: str) -> str:
12
+ title = esc(title)
13
+ project = esc(project)
14
+ nav_items = []
15
+ for cat in categories:
16
+ dropdown_links = "".join(
17
+ f'<a href="/portal/{esc(cat.id)}/{esc(d["slug"])}" class="dropdown-item">{esc(d["title"])}</a>'
18
+ for d in cat.docs
19
+ )
20
+ nav_items.append(f"""
21
+ <div class="nav-dropdown">
22
+ <button class="nav-btn" aria-haspopup="true" aria-expanded="false">{esc(cat.icon)} {esc(cat.label)} ▾</button>
23
+ <div class="dropdown-content" role="menu">{dropdown_links}</div>
24
+ </div>
25
+ """)
26
+
27
+ nav_html = "".join(nav_items)
28
+
29
+ return f"""<!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1">
34
+ <title>{title} — {project}</title>
35
+ <style>
36
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
37
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1a1a1a; background: #f8f9fa; }}
38
+
39
+ /* Header */
40
+ .header {{ background: #fff; border-bottom: 1px solid #e5e7eb; position: sticky; top: 0; z-index: 40; }}
41
+ .header-inner {{ max-width: 1200px; margin: 0 auto; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; height: 64px; }}
42
+ .logo {{ display: flex; align-items: center; gap: 10px; text-decoration: none; color: #1a1a1a; font-weight: 600; font-size: 16px; }}
43
+ .logo-icon {{ width: 32px; height: 32px; background: #1a1a1a; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 14px; }}
44
+ .back-link {{ font-size: 14px; color: #6b7280; text-decoration: none; }}
45
+ .back-link:hover {{ color: #374151; }}
46
+
47
+ /* Nav */
48
+ .nav {{ display: flex; align-items: center; gap: 4px; }}
49
+ .nav a.overview {{ padding: 8px 12px; border-radius: 8px; font-size: 14px; font-weight: 500; color: #4b5563; text-decoration: none; }}
50
+ .nav a.overview:hover {{ background: #f3f4f6; color: #1a1a1a; }}
51
+ .nav-dropdown {{ position: relative; }}
52
+ .nav-btn {{ padding: 8px 12px; border-radius: 8px; font-size: 14px; font-weight: 500; color: #4b5563; background: none; border: none; cursor: pointer; }}
53
+ .nav-btn:hover {{ background: #f3f4f6; color: #1a1a1a; }}
54
+ .dropdown-content {{ display: none; position: absolute; top: 100%; left: 0; margin-top: 4px; min-width: 240px; background: #fff; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,.1); border: 1px solid #e5e7eb; padding: 8px 0; z-index: 50; }}
55
+ .nav-dropdown:hover .dropdown-content {{ display: block; }}
56
+ .dropdown-item {{ display: block; padding: 10px 16px; font-size: 14px; color: #374151; text-decoration: none; }}
57
+ .dropdown-item:hover {{ background: #f3f4f6; color: #1a1a1a; }}
58
+
59
+ /* Content */
60
+ .content {{ max-width: 1200px; margin: 0 auto; padding: 32px 24px; }}
61
+
62
+ /* Cards */
63
+ .card-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }}
64
+ .card {{ background: #fff; border-radius: 12px; border: 1px solid #e5e7eb; padding: 24px; transition: box-shadow .15s; }}
65
+ .card:hover {{ box-shadow: 0 4px 12px rgba(0,0,0,.08); }}
66
+ .card-icon {{ font-size: 28px; margin-bottom: 12px; }}
67
+ .card h2 {{ font-size: 18px; font-weight: 600; margin-bottom: 4px; }}
68
+ .card p {{ font-size: 14px; color: #6b7280; margin-bottom: 16px; }}
69
+ .card-link {{ display: block; font-size: 14px; color: #2563eb; text-decoration: none; padding: 2px 0; }}
70
+ .card-link:hover {{ color: #1d4ed8; }}
71
+
72
+ /* Doc article */
73
+ .article {{ background: #fff; border-radius: 12px; border: 1px solid #e5e7eb; padding: 48px; max-width: 900px; margin: 0 auto; }}
74
+ .breadcrumb {{ font-size: 14px; color: #6b7280; margin-bottom: 24px; }}
75
+ .breadcrumb a {{ color: #6b7280; text-decoration: none; }}
76
+ .breadcrumb a:hover {{ color: #374151; }}
77
+ .doc-meta {{ font-size: 13px; color: #9ca3af; margin-top: 8px; }}
78
+
79
+ /* Rendered markdown */
80
+ .prose h1 {{ font-size: 28px; font-weight: 700; margin: 32px 0 16px; }}
81
+ .prose h1:first-child {{ margin-top: 0; }}
82
+ .prose h2 {{ font-size: 22px; font-weight: 600; margin: 32px 0 12px; padding-bottom: 8px; border-bottom: 1px solid #e5e7eb; }}
83
+ .prose h3 {{ font-size: 18px; font-weight: 600; margin: 24px 0 8px; }}
84
+ .prose p {{ color: #374151; line-height: 1.7; margin: 12px 0; }}
85
+ .prose ul, .prose ol {{ margin: 12px 0; padding-left: 24px; color: #374151; }}
86
+ .prose li {{ margin: 6px 0; line-height: 1.6; }}
87
+ .prose a {{ color: #2563eb; text-decoration: underline; }}
88
+ .prose a:hover {{ color: #1d4ed8; }}
89
+ .prose code {{ background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-size: 13px; }}
90
+ .prose pre {{ background: #1e293b; color: #e2e8f0; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 16px 0; }}
91
+ .prose pre code {{ background: none; padding: 0; color: inherit; }}
92
+ .prose blockquote {{ border-left: 4px solid #bfdbfe; background: #eff6ff; padding: 12px 16px; border-radius: 0 8px 8px 0; margin: 16px 0; }}
93
+ .prose table {{ width: 100%; border-collapse: collapse; margin: 16px 0; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }}
94
+ .prose th {{ background: #f9fafb; padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: #4b5563; text-transform: uppercase; letter-spacing: .05em; }}
95
+ .prose td {{ padding: 12px 16px; font-size: 14px; color: #374151; border-top: 1px solid #f3f4f6; }}
96
+
97
+ /* Recent list */
98
+ .recent {{ background: #fff; border-radius: 12px; border: 1px solid #e5e7eb; padding: 24px; margin-top: 24px; }}
99
+ .recent h2 {{ font-size: 18px; font-weight: 600; margin-bottom: 16px; }}
100
+ .recent-item {{ display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-top: 1px solid #f3f4f6; }}
101
+ .recent-item:first-of-type {{ border-top: none; }}
102
+ .recent-item a {{ font-size: 14px; font-weight: 500; color: #1a1a1a; text-decoration: none; }}
103
+ .recent-item a:hover {{ color: #2563eb; }}
104
+ .recent-item .meta {{ font-size: 12px; color: #9ca3af; }}
105
+
106
+ /* Empty state */
107
+ .empty {{ text-align: center; padding: 80px 24px; }}
108
+ .empty .icon {{ font-size: 48px; margin-bottom: 16px; }}
109
+ .empty h1 {{ font-size: 24px; font-weight: 700; margin-bottom: 8px; }}
110
+ .empty p {{ color: #6b7280; max-width: 400px; margin: 0 auto; }}
111
+
112
+ @media (max-width: 768px) {{
113
+ .card-grid {{ grid-template-columns: 1fr; }}
114
+ .article {{ padding: 24px; }}
115
+ .nav {{ display: none; }}
116
+ }}
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <header class="header">
121
+ <div class="header-inner">
122
+ <div style="display:flex;align-items:center;gap:24px;">
123
+ <a href="/portal" class="logo">
124
+ <div class="logo-icon">{esc(project[0]) if project else "?"}</div>
125
+ {project}
126
+ </a>
127
+ <nav class="nav">
128
+ <a href="/portal" class="overview">Overview</a>
129
+ {nav_html}
130
+ </nav>
131
+ </div>
132
+ <a href="/docs" class="back-link">API docs</a>
133
+ </div>
134
+ </header>
135
+ <div class="content">
136
+ {content}
137
+ </div>
138
+ </body>
139
+ </html>"""
140
+
141
+
142
+ def render_portal_home(categories: list[DocCategory], recent: list[DocFile], project: str) -> str:
143
+ if not categories:
144
+ content = """
145
+ <div class="empty">
146
+ <div class="icon">📄</div>
147
+ <h1>No documentation yet</h1>
148
+ <p>Documentation will appear here once it has been generated. Ask your development team to run the documentation generators.</p>
149
+ </div>
150
+ """
151
+ return _base("Portal", project, categories, content)
152
+
153
+ cards = ""
154
+ for cat in categories:
155
+ links = "".join(
156
+ f'<a href="/portal/{esc(cat.id)}/{esc(d["slug"])}" class="card-link">{esc(d["title"])}</a>'
157
+ for d in cat.docs
158
+ )
159
+ cards += f"""
160
+ <div class="card">
161
+ <div class="card-icon">{esc(cat.icon)}</div>
162
+ <h2>{esc(cat.label)}</h2>
163
+ <p>{esc(cat.description)}</p>
164
+ {links}
165
+ </div>
166
+ """
167
+
168
+ recent_items = ""
169
+ for doc in recent:
170
+ date_str = doc.last_modified.strftime("%b %d, %Y")
171
+ recent_items += f"""
172
+ <div class="recent-item">
173
+ <div>
174
+ <a href="/portal/{esc(doc.category)}/{esc(doc.slug)}">{esc(doc.title)}</a>
175
+ <span class="meta" style="margin-left:8px;">{esc(doc.category_label)}</span>
176
+ </div>
177
+ <span class="meta">{date_str}</span>
178
+ </div>
179
+ """
180
+
181
+ content = f"""
182
+ <h1 style="font-size:28px;font-weight:700;margin-bottom:8px;">Project Documentation</h1>
183
+ <p style="color:#6b7280;margin-bottom:24px;">Everything you need to know about this project — requirements, architecture, test results, and more.</p>
184
+ <div class="card-grid">{cards}</div>
185
+ <div class="recent">
186
+ <h2>Recently Updated</h2>
187
+ {recent_items}
188
+ </div>
189
+ """
190
+ return _base("Portal", project, categories, content)
191
+
192
+
193
+ def render_category_page(cat: DocCategory, categories: list[DocCategory], project: str) -> str:
194
+ items = ""
195
+ for d in cat.docs:
196
+ items += f"""
197
+ <a href="/portal/{esc(cat.id)}/{esc(d["slug"])}" class="card" style="text-decoration:none;display:block;">
198
+ <h2 style="color:#1a1a1a;">{esc(d["title"])}</h2>
199
+ </a>
200
+ """
201
+
202
+ content = f"""
203
+ <div class="breadcrumb">
204
+ <a href="/portal">Portal</a> / <span>{esc(cat.label)}</span>
205
+ </div>
206
+ <h1 style="font-size:28px;font-weight:700;margin-bottom:8px;">{esc(cat.icon)} {esc(cat.label)}</h1>
207
+ <p style="color:#6b7280;margin-bottom:24px;">{esc(cat.description)}</p>
208
+ <div class="card-grid">{items}</div>
209
+ """
210
+ return _base(cat.label, project, categories, content)
211
+
212
+
213
+ def render_doc_page(doc: DocFile, categories: list[DocCategory], project: str) -> str:
214
+ date_str = doc.last_modified.strftime("%B %d, %Y")
215
+ # doc.html is already rendered markdown — titles and metadata need escaping
216
+ content = f"""
217
+ <div class="article">
218
+ <div class="breadcrumb">
219
+ <a href="/portal">Portal</a> /
220
+ <a href="/portal/{esc(doc.category)}">{esc(doc.category_label)}</a> /
221
+ <span>{esc(doc.title)}</span>
222
+ </div>
223
+ <h1 style="font-size:28px;font-weight:700;margin-bottom:4px;">{esc(doc.title)}</h1>
224
+ <p class="doc-meta">Last updated {date_str}</p>
225
+ <hr style="margin:24px 0;border:none;border-top:1px solid #e5e7eb;">
226
+ <div class="prose">{doc.html}</div>
227
+ </div>
228
+ """
229
+ return _base(doc.title, project, categories, content)
@@ -0,0 +1,35 @@
1
+ """Portal routes — serves project documentation as HTML pages."""
2
+
3
+ from fastapi import APIRouter, Request
4
+ from fastapi.responses import HTMLResponse
5
+
6
+ from app.portal.docs_reader import get_categories, get_doc, get_all_docs
7
+ from app.portal.html_renderer import render_portal_home, render_category_page, render_doc_page
8
+
9
+ router = APIRouter(prefix="/portal", tags=["portal"])
10
+
11
+
12
+ @router.get("/", response_class=HTMLResponse)
13
+ async def portal_home(request: Request):
14
+ categories = get_categories()
15
+ all_docs = get_all_docs()
16
+ recent = sorted(all_docs, key=lambda d: d.last_modified, reverse=True)[:5]
17
+ return render_portal_home(categories, recent, "{{PROJECT_NAME_PASCAL}}")
18
+
19
+
20
+ @router.get("/{category}", response_class=HTMLResponse)
21
+ async def category_page(category: str):
22
+ categories = get_categories()
23
+ cat = next((c for c in categories if c.id == category), None)
24
+ if not cat:
25
+ return HTMLResponse("<h1>Not found</h1>", status_code=404)
26
+ return render_category_page(cat, categories, "{{PROJECT_NAME_PASCAL}}")
27
+
28
+
29
+ @router.get("/{category}/{slug}", response_class=HTMLResponse)
30
+ async def doc_page(category: str, slug: str):
31
+ doc = get_doc(category, slug)
32
+ if not doc:
33
+ return HTMLResponse("<h1>Not found</h1>", status_code=404)
34
+ categories = get_categories()
35
+ return render_doc_page(doc, categories, "{{PROJECT_NAME_PASCAL}}")