# Q-Blog v0.0.1 — Source Audit _Generated: 2025-08-21T19:40:03_ ## Inventory Summary - Extracted to: `/mnt/data/q-blog_v0.0.1_source` - Total files: **257** - Total size: **2.47 MB** - Top-level directories: - src: 121 files - scripts: 42 files - docs: 31 files - (root): 21 files - issues: 15 files - reports: 12 files - .gitea: 5 files - tests: 5 files - pr: 4 files - public: 1 files - File types (top 20): - .tsx: 76 - .md: 59 - .sh: 40 - .ts: 33 - .json: 9 - .txt: 9 - .png: 8 - (noext): 5 - .ttf: 5 - .css: 2 - .js: 2 - .svg: 2 - .bak: 1 - .html: 1 - .ico: 1 - .log: 1 - .mjs: 1 - .yaml: 1 - .yml: 1 Inventory CSV: `/mnt/data/q-blog_inventory_v0.0.1.csv` ## Key Files Presence - package.json: ✅ - pnpm-lock.yaml: ❌ - yarn.lock: ❌ - package-lock.json: ✅ - tsconfig.json: ✅ - vite.config.ts: ✅ - vitest.config.ts: ✅ - jest.config.js: ❌ - README.md: ❌ - docs/ARCHITECTURE.md: ❌ - docs/TESTING.md: ✅ - docs/ACCESSIBILITY.md: ❌ - docs/SECURITY.md: ❌ - docs/USER_JOURNEYS.md: ✅ - docs/GLOSSARY.md: ❌ - docs/ROADMAP_DEPENDENCIES.md: ✅ - docs/DECISIONS/README.md: ❌ - src/main.tsx: ✅ - src/App.tsx: ✅ - src/index.css: ✅ - src/routes.tsx: ❌ - src/router.tsx: ❌ - src/components/BlogSwitcher.tsx: ❌ - src/components/PostEditor.tsx: ❌ - src/components/PostList.tsx: ❌ - src/components/MembersPanel.tsx: ❌ - src/components/HeaderNav.tsx: ❌ - src/store/index.ts: ❌ - src/store/api.ts: ❌ - src/store/slices/postsSlice.ts: ❌ - src/store/slices/blogsSlice.ts: ❌ - src/store/slices/authSlice.ts: ❌ - src/i18n.ts: ❌ - src/theme.ts: ❌ - tests/setup.ts: ✅ - tests/App.test.tsx: ❌ - tests/components/PostEditor.test.tsx: ❌ - scripts/release/create-gitea-release.sh: ✅ - scripts/release/build-archive.sh: ✅ - docs/RELEASE_NOTES_v0.0.1.md: ✅ - .gitea/workflows/ci.yml: ✅ - .gitea/workflows/release.yml: ❌ ## package.json Snapshot ```json { "name": "q-blog", "version": "0.0.1", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint .", "format": "prettier --check .", "format:write": "prettier --write .", "test": "vitest", "test:run": "vitest run", "typecheck": "tsc -p tsconfig.json -noEmit", "lint:phase0": "LINT_SCOPE=phase0 eslint .", "lint:full": "LINT_SCOPE=full eslint .", "check": "npm run typecheck && npm run lint:phase0 && npm run test:run", "scan:phase1": "bash scripts/dev/phase1-scan.sh", "report:phase1": "bash scripts/dev/phase1-report.sh" }, "dependencies": { "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.13", "@reduxjs/toolkit": "^1.9.3", "@types/react-grid-layout": "^1.3.2", "axios": "^1.3.4", "compressorjs": "^1.2.1", "localforage": "^1.10.0", "moment": "^2.29.4", "philliplm-react-modern-audio-player": "^1.4.6", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-grid-layout": "^1.3.4", "react-intersection-observer": "^9.4.3", "react-masonry-css": "^1.0.16", "react-redux": "^8.0.5", "react-resize-detector": "^8.0.4", "react-router-dom": "^6.9.0", "react-toastify": "^9.1.2", "react-virtuoso": "^4.3.3", "short-unique-id": "^4.4.4", "slate": "^0.91.4", "slate-history": "^0.86.0", "slate-react": "^0.91.11", "ts-key-enum": "^2.0.12" }, "devDependencies": { "@eslint/js": "^9.33.0", "@mui/types": "^7.2.3", "@testing-library/jest-dom": "^6.7.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/node": "^24.3.0", "@types/react": "^18.3.23", "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react-swc": "^3.2.0", "@vitest/coverage-v8": "^3.2.4", "axe-core": "^4.10.3", "eslint": "^9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react-hooks": "^5.2.0", "jest-axe": "^10.0.0", "jsdom": "^26.1.0", "msw": "^2.10.5", "prettier": "^2.8.8", "typescript": "^4.9.5", "typescript-eslint": "^8.39.1", "vite": "^4.2.0", "vitest": "^3.2.4", "worker-loader": "^3.0.8" } } ``` ## tsconfig.json (key fields) ```json {} ``` ## Tooling Signals in Source - redux_toolkit: 6 files - rtk_query: 0 files - slate: 6 files - mui: 51 files - i18n: 0 files - vite: 0 files - dompurify: 0 files - zod: 0 files - jest_axe: 0 files - msw: 0 files - rtl: 0 files - virtualize: 0 files - sample `mui`: src/App.tsx, src/pages/BlogIndividualPost/BlogIndividualPost.tsx, src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx, src/pages/CreatePost/CreatePost.tsx, src/pages/CreatePost/CreatePostMinimal.tsx, src/pages/CreatePost/CreatePost-styles.ts ... - sample `slate`: src/pages/CreatePost/CreatePostMinimal.tsx, src/pages/CreatePost/CreatePostBuilder.tsx, src/pages/EditPost/EditPost.tsx, src/components/editor/BlogEditor.tsx, src/components/editor/ReadOnlySlate.tsx, src/components/editor/customTypes.ts - sample `redux_toolkit`: src/state/store.ts, src/state/features/globalSlice.ts, src/state/features/blogSlice.ts, src/state/features/authSlice.ts, src/state/features/mailSlice.ts, src/state/features/notificationsSlice.ts ## Routes & Landmarks (heuristic) - src/main.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/App.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/BlogIndividualPost/BlogIndividualPost.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/CreatePost/CreatePost.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/CreatePost/CreatePostMinimal.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/CreatePost/CreatePost-styles.ts: h1=0, header=False, nav=False, main=False, footer=False - src/pages/CreatePost/CreatePostBuilder.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/CreatePost/components/Toolbar/EditorToolbar.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/CreatePost/components/Navbar/NavbarBuilder.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/EditPost/EditPost.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/Home/Home.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/CreateEditProfile/CreatEditProfile.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/BlogList/PostPreview-styles.ts: h1=0, header=False, nav=False, main=False, footer=False - src/pages/BlogList/PostPreview.tsx: h1=0, header=False, nav=False, main=False, footer=False - src/pages/BlogList/BlogList.tsx: h1=0, header=False, nav=False, main=False, footer=False ## Release Scripts — quick notes ### scripts/release/create-gitea-release.sh (excerpt) ```bash #!/usr/bin/env bash set -euo pipefail # create-gitea-release.sh # Creates/updates a Gitea release for a given version/tag and uploads zips from release/. # # Env required: GITEA_BASE_URL, GITEA_TOKEN, OWNER, REPO # Usage: # bash scripts/release/create-gitea-release.sh 0.0.1 \ # --title "Phase 0 — v0.0.1" \ # --notes docs/RELEASE_NOTES_v0.0.1.md \ # --branch update \ # [--draft] [--prerelease] [--assets 'release/*.zip'] # # Defaults: # title: "v" # notes: docs/RELEASE_NOTES_v.md if exists, else empty # branch: current git branch (fallback: main) # assets: release/*.zip # # Requires: curl, jq require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; } require curl require jq : "${GITEA_BASE_URL:?Set GITEA_BASE_URL (e.g., https://gitea.example.com)}" : "${GITEA_TOKEN:?Set GITEA_TOKEN}" : "${OWNER:?Set OWNER}" : "${REPO:?Set REPO}" if [[ $# -lt 1 ]]; then echo "Usage: $0 [--title TITLE] [--notes FILE] [--branch BRANCH] [--draft] [--prerelease] [--assets GLOB]" >&2 exit 2 fi VER_RAW="$1"; shift TAG="${VER_RAW}" [[ "${TAG}" != v* ]] && TAG="v${TAG}" TITLE="${TAG}" NOTES_FILE="" BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)" DRAFT=false PRERELEASE=false ASSETS_GLOB="release/*.zip" while [[ $# -gt 0 ]]; do case "$1" in --title) TITLE="${2:-${TITLE}}"; shift 2;; --notes) NOTES_FILE="${2:-}"; shift 2;; --branch) BRANCH="${2:-${BRANCH}}"; shift 2;; --draft) DRAFT=true; shift;; --prerelease) PRERELEASE=true; shift;; --assets) ASSETS_GLOB="${2:-${ASSETS_GLOB}}"; shift 2;; *) echo "Unknown arg: $1" >&2; exit 2;; esac done if [[ -z "${NOTES_FILE}" ]]; then CANDIDATE="docs/RELEASE_NOTES_${TAG}.md" if [[ -f "${CANDIDATE}" ]]; then NOTES_FILE="${CANDIDATE}" else CANDIDATE="docs/RELEASE_NOTES_${TAG#v}.md" [[ -f "${CANDIDATE}" ]] && NOTES_FILE="${CANDIDATE}" || NOTES_FILE="" fi fi BODY="" if [[ -n "${NOTES_FILE}" && -f "${NOTES_FILE}" ]]; then BODY="$(cat "${NOTES_FILE}")" fi API="${GITEA_BASE_URL%/}/api/v1" auth() { curl -sS -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json"; } auth_up() { curl -sS -H "Authorization: token ${GITEA_TOKEN}"; } echo "== Looking up release by tag ${TAG} ==" GET_URL="${API}/repos/${OWNER}/${REPO}/releases/tags/${TAG}" set +e EXIST_JSON="$(auth GET "${GET_URL}" 2>/dev/null)" RC=$? set -e RELEASE_ID="" if [[ ${RC} -eq 0 && -n "${EXIST_JSON}" && "$(echo "${EXIST_JSON}" | jq -r '.id // empty')" != "" ]]; then RELEASE_ID="$(echo "${EXIST_JSON}" | jq -r '.id')" echo "Found existing release id=${RELEASE_ID}; will PATCH" PAYLOAD="$(jq -n \ --arg name "${TITLE}" \ --arg body "${BODY}" \ --argjson draft ${DRAFT} \ --argjson prerelease ${PRERELEASE} \ '{name:$name, body:$body, draft:$draft, prerelease:$prerelease}')" REL_JSON="$(echo "${PAYLOAD}" | auth -X PATCH "${API}/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}" -d @-)" else echo "No existing release; creating" PAYLOAD="$(jq -n \ --arg tag_name "${TAG}" \ --arg target_commitish "${BRANCH}" \ --arg name "${TITLE}" \ --arg body "${BODY}" \ --argjson draft ${DRAFT} \ --argjson prerelease ${PRERELEASE} \ '{tag_name:$tag_name, target_commitish:$target_commitish, name:$name, body:$body, draft:$draft, prerelease:$prerelease}')" REL_JSON="$(echo "${PAYLOAD}" | auth -X POST "${API}/repos/${OWNER}/${REPO}/releases" -d @-)" RELEASE_ID="$(echo "${REL_JSON}" | jq -r '.id // empty')" fi if [[ -z "${RELEASE_ID}" ]]; then echo "ERROR: could not determine release id" >&2 echo "${REL_JSON:-"(no server response)"}" >&2 exit 1 fi HTML_URL="$(echo "${REL_JSON}" | jq -r '.html_url // .url // empty')" echo "Release ready (id=${RELEASE_ID}) ${HTML_URL}" echo "== Uploading assets from ${ASSETS_GLOB} ==" ``` ### scripts/release/build-archive.sh (excerpt) ```bash #!/usr/bin/env bash set -euo pipefail # build-archive.sh # Creates source zip (always) and an optional dist zip if --with-build succeeds. # Usage: # bash scripts/release/build-archive.sh [--with-build] [--outdir release] [--name q-blog] # # Notes: # - By default, NO TypeScript build is executed to avoid failing on app code during Phase 0. # - Pass --with-build to attempt `npm ci` + `npm run build`; if dist/ exists afterwards, a dist zip is created. WITH_BUILD=0 OUTDIR="release" NAME="" while [[ $# -gt 0 ]]; do case "$1" in --with-build) WITH_BUILD=1; shift;; --outdir) OUTDIR="${2:-release}"; shift 2;; --name) NAME="${2:-}"; shift 2;; *) echo "Unknown arg: $1" >&2; exit 2;; esac done require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; } require jq require zip if [[ -z "${NAME}" ]]; then if [[ -f package.json ]]; then NAME="$(jq -r '.name // empty' package.json)" fi [[ -z "${NAME}" ]] && NAME="$(basename "$PWD")" fi if [[ -f package.json ]]; then VERSION="$(jq -r '.version // "0.0.0"' package.json)" else VERSION="0.0.0" fi mkdir -p "${OUTDIR}" TAG="v${VERSION}" SRC_ZIP="${OUTDIR}/${NAME}-${TAG}-src.zip" DIST_ZIP="${OUTDIR}/${NAME}-${TAG}-dist.zip" echo "== Creating source archive ==" # Use zip with excludes (works even without git) zip -q -9 -r "${SRC_ZIP}" . \ -x "node_modules/*" ".git/*" "${OUTDIR}/*" "dist/*" ".vite/*" "coverage/*" "*.log" "*.zip" if [[ ${WITH_BUILD} -eq 1 ]]; then echo "== Build step (best-effort) ==" if jq -e '.scripts.build' package.json >/dev/null 2>&1; then # prefer ci if present, otherwise install if jq -e '.scripts.ci' package.json >/dev/null 2>&1; then npm run ci || true else (command -v npm >/dev/null 2>&1 && npm ci) || (npm install || true) fi npm run build || echo "(build failed or not configured — continuing)" else echo "No build script in package.json — skipping build" fi if [[ -d dist ]]; then echo "== Creating dist archive ==" (cd dist && zip -q -9 -r "../${DIST_ZIP##*/}" .) else echo "dist/ not found — skipping dist zip." fi else echo "(Skipping build; source zip only. Use --with-build to attempt a build.)" fi echo "Artifacts:" echo " - ${SRC_ZIP}" [[ -f "${DIST_ZIP}" ]] && echo " - ${DIST_ZIP}" ``` **Potential issues detected:** - Release script may not validate required args (title/notes); add guards or defaults to avoid empty curl parameters. ## Docs vs Code — spot verification - README.md mentions Redux Toolkit: ℹ️ code present but not documented - README.md mentions RTK Query: — - README.md mentions Slate editor: ℹ️ code present but not documented - README.md mentions MUI v5: ℹ️ code present but not documented - README.md mentions i18n: — - README.md mentions DOMPurify: — - README.md mentions Zod validation: — - README.md mentions MSW/jest-axe: — ## Priority Findings & Questions 1. Content sanitization (DOMPurify) not detected in source scan. Verify sanitize-on-save and sanitize-on-render are implemented. 2. Zod validation not detected; confirm validation at form and API boundaries or update docs. 3. Testing stack may be incomplete (MSW/jest-axe). Ensure test coverage for autosave, a11y smoke, and error paths. 4. RTK Query not detected; if used, ensure endpoints are defined with blog-scoped cache keys. ## Next Steps (thin vertical slice) 1. Run `pnpm i` (or npm/yarn) and `pnpm test` to confirm toolchain and a11y smoke tests. 2. Verify PostEditor autosave path, live region announcements, and sanitize pipeline. 3. Resolve release script guard/arg issues; add CI job to create release on tag. 4. Pick a small feature change and map the affected slice (state → API → UI → tests).