docs(phase-15): add pattern map
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
# Phase 15: Doc-Only CI Skip + Markdown Lint - Pattern Map
|
||||
|
||||
**Mapped:** 2026-06-12
|
||||
**Files analyzed:** 4 (2 modified, 1 created, N docs fixed)
|
||||
**Analogs found:** 3 / 4 (`.markdownlint-cli2.jsonc` has no role-match analog — closest is `.prettierrc`)
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|---|---|---|---|---|
|
||||
| `.gitea/workflows/ci.yml` | CI workflow | event-driven | itself (existing job blocks) | exact — new jobs modeled on existing jobs in same file |
|
||||
| `.markdownlint-cli2.jsonc` | tool config | — | `.prettierrc` (root JSON tool config) | structural-match (same layer, no content analog) |
|
||||
| `package.json` (root) | package manifest | — | itself (existing `format:check` script + `prettier` devDep) | exact — same script/dep pattern |
|
||||
| `docs/**/*.md`, `README.md`, `apps/*/README.md` | docs content | — | the violating files themselves | exact — in-place fixes |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `.gitea/workflows/ci.yml` — `changes` job (new)
|
||||
|
||||
**Analog:** The existing `fast-checks` job in the same file, lines 8–38.
|
||||
|
||||
**Job skeleton pattern** (ci.yml lines 8–11 — `fast-checks` header as template):
|
||||
```yaml
|
||||
fast-checks:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
```
|
||||
|
||||
**New `changes` job — copy this structure, replacing steps with the paths-filter action:**
|
||||
```yaml
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.js'
|
||||
- '**/*.json'
|
||||
- '**/*.yaml'
|
||||
- '**/*.yml'
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'Dockerfile'
|
||||
- 'docker-compose*.yml'
|
||||
```
|
||||
|
||||
**Key design notes:**
|
||||
- No `actions/checkout` step — `dorny/paths-filter@v4` uses the Gitea/GitHub REST API on PR events (no fetch-depth needed).
|
||||
- `permissions: pull-requests: read` is required by v4; declare it at job scope (not workflow scope).
|
||||
- Define the positive `code` filter (not a `docs` filter) — `code == 'false'` means "only docs changed", and ambiguous new file types safely default to the full test run.
|
||||
- Insert this job **before** `fast-checks` in the file so the job ordering in the UI is logical (changes → fast-checks / api / harness → gate).
|
||||
|
||||
---
|
||||
|
||||
### `.gitea/workflows/ci.yml` — `api` job modification
|
||||
|
||||
**Analog:** Existing `api` job, ci.yml lines 40–125.
|
||||
|
||||
**Current header** (lines 40–44):
|
||||
```yaml
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
# Runs in PARALLEL with fast-checks (D-03) — no needs: dependency.
|
||||
services:
|
||||
```
|
||||
|
||||
**Modified header — add `needs` and extend the `if` condition:**
|
||||
```yaml
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes]
|
||||
if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'
|
||||
# Runs in PARALLEL with fast-checks (D-03) — skipped for doc-only PRs.
|
||||
services:
|
||||
```
|
||||
|
||||
**Nothing else in the `api` job body changes.** The existing comment at line 43 should be updated to reflect conditional skip behavior.
|
||||
|
||||
---
|
||||
|
||||
### `.gitea/workflows/ci.yml` — `harness` job modification
|
||||
|
||||
**Analog:** Existing `harness` job, ci.yml lines 126–314.
|
||||
|
||||
**Current header** (lines 126–129):
|
||||
```yaml
|
||||
harness:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
# Runs in PARALLEL with fast-checks + api (D-03) — no needs: dependency.
|
||||
services:
|
||||
```
|
||||
|
||||
**Modified header — same pattern as `api`:**
|
||||
```yaml
|
||||
harness:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes]
|
||||
if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'
|
||||
# Runs in PARALLEL with fast-checks (D-03) — skipped for doc-only PRs.
|
||||
services:
|
||||
```
|
||||
|
||||
**Nothing else in the `harness` job body changes.**
|
||||
|
||||
---
|
||||
|
||||
### `.gitea/workflows/ci.yml` — `gate` job (new)
|
||||
|
||||
**Analog:** `publish.yml` line 94 — `if: always()` pattern on the `Docker logout` step; same construct used at job scope here.
|
||||
|
||||
**Insert after `harness` job, at end of file:**
|
||||
```yaml
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [fast-checks, changes, api, harness]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check all required jobs passed or were skipped
|
||||
run: |
|
||||
# fast-checks always runs — must be success
|
||||
if [ "${{ needs.fast-checks.result }}" != "success" ]; then
|
||||
echo "fast-checks: ${{ needs.fast-checks.result }}"
|
||||
exit 1
|
||||
fi
|
||||
# api and harness are conditionally skipped — success OR skipped are both acceptable
|
||||
for result in "${{ needs.api.result }}" "${{ needs.harness.result }}"; do
|
||||
if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then
|
||||
echo "Heavy job failed or was cancelled: $result"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Gate passed."
|
||||
```
|
||||
|
||||
**Critical Gitea 1.26.2 constraint:** Do NOT use `contains(needs.*.result, 'success')` — Gitea issue #31007 confirms this returns `false` even when jobs succeed. Individual `needs.X.result` string comparisons are the only reliable approach on this instance.
|
||||
|
||||
**Why `if: always()`:** Without it, a skipped upstream job causes `gate` to also skip — branch protection waiting for `CI / gate` would deadlock. The `if: always()` deadlock bug (Gitea #27906) was fixed in 1.21.8; this instance runs 1.26.2.
|
||||
|
||||
---
|
||||
|
||||
### `.gitea/workflows/ci.yml` — Markdown lint step in `fast-checks`
|
||||
|
||||
**Analog:** Existing `Format check` step, ci.yml lines 31–32:
|
||||
```yaml
|
||||
- name: Format check
|
||||
run: pnpm format:check
|
||||
```
|
||||
|
||||
**New step — insert after `Format check`, before `Typecheck`:**
|
||||
```yaml
|
||||
- name: Markdown lint
|
||||
run: pnpm md:lint
|
||||
```
|
||||
|
||||
**Slot:** After line 32 (`run: pnpm format:check`), before line 34 (`- name: Typecheck`). One line of YAML mirrors the `format:check` step pattern exactly — a named step calling a root pnpm script.
|
||||
|
||||
---
|
||||
|
||||
### `package.json` (root) — `md:lint` script + devDependency
|
||||
|
||||
**Analog:** Existing `format:check` script and `prettier` devDependency, package.json lines 15 and 21.
|
||||
|
||||
**Current scripts block** (lines 6–16):
|
||||
```json
|
||||
"scripts": {
|
||||
"dev:api": "pnpm --filter @familysync/api dev",
|
||||
"dev:pwa": "pnpm --filter @familysync/pwa dev",
|
||||
"build": "pnpm --filter @familysync/api build && pnpm --filter @familysync/pwa build",
|
||||
"test": "pnpm --filter @familysync/api test",
|
||||
"test:e2e": "pnpm --filter @familysync/pwa test:e2e",
|
||||
"lint": "pnpm -r --if-present lint",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
```
|
||||
|
||||
**Add `md:lint` after `format:check` (alphabetical/logical grouping matches existing order):**
|
||||
```json
|
||||
"format:check": "prettier --check .",
|
||||
"md:lint": "markdownlint-cli2"
|
||||
```
|
||||
|
||||
**Note:** No glob args on the CLI — globs and ignores live entirely in `.markdownlint-cli2.jsonc` (see Open Question #3 in RESEARCH.md resolved: config-file globs make `pnpm md:lint` and direct `npx markdownlint-cli2` behave identically).
|
||||
|
||||
**Current devDependencies block** (lines 17–25):
|
||||
```json
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.4",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"prettier": "3.8.4",
|
||||
"typescript-eslint": "8.61.0"
|
||||
}
|
||||
```
|
||||
|
||||
**Add `markdownlint-cli2` (alphabetical, between `eslint-plugin-react-hooks` and `prettier`):**
|
||||
```json
|
||||
"markdownlint-cli2": "0.22.1",
|
||||
"prettier": "3.8.4",
|
||||
```
|
||||
|
||||
**Install command (must run at workspace root):**
|
||||
```bash
|
||||
pnpm add -D markdownlint-cli2@0.22.1 --workspace-root
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `.markdownlint-cli2.jsonc` (new file)
|
||||
|
||||
**Analog:** `.prettierrc` (root JSON tool config, lines 1–7) — same layer (repo root), same purpose (configure a formatting/lint tool), same JSON format. No `.markdownlint-cli2.jsonc` exists yet.
|
||||
|
||||
**`.prettierrc` structural pattern** (lines 1–7):
|
||||
```json
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
```
|
||||
|
||||
**New file — use JSONC format (`.jsonc` extension supports comments), place at repo root:**
|
||||
```jsonc
|
||||
// .markdownlint-cli2.jsonc
|
||||
{
|
||||
"config": {
|
||||
// Disable all rules that conflict with Prettier (23 rules — line-length, list-indent,
|
||||
// blanks-around-fences, emphasis-style, etc.)
|
||||
"extends": "markdownlint/style/prettier",
|
||||
|
||||
// Content rules to KEEP:
|
||||
"MD001": true, // heading-increment: no skipping h1→h3
|
||||
"MD024": true, // no-duplicate-heading
|
||||
"MD040": true, // fenced-code-language: all fences must declare a language
|
||||
"MD031": true, // blanks-around-fences (re-enabled — see Pitfall 4 in RESEARCH.md)
|
||||
"MD051": true, // link-fragments: broken anchor links
|
||||
"MD052": true, // reference-links-images: undefined link references
|
||||
|
||||
// Rules DISABLED (Prettier owns these OR they fire on non-author-controlled files):
|
||||
"MD041": false, // first-line-h1: CLAUDE.md legitimately starts with ## Project
|
||||
"MD034": false, // no-bare-urls: CLAUDE.md version table uses pkg@version syntax
|
||||
"MD036": false // no-emphasis-as-heading: docs/API.md uses **Response 200** as label
|
||||
},
|
||||
"globs": [
|
||||
"docs/**/*.md",
|
||||
"*.md",
|
||||
"apps/**/*.md"
|
||||
],
|
||||
"ignores": [
|
||||
".planning/**",
|
||||
"node_modules/**",
|
||||
"**/node_modules/**",
|
||||
".pnpm-store/**"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Why `extends` resolves:** `markdownlint-cli2` depends on `markdownlint`, which ships `style/prettier.json`. After `pnpm add -D markdownlint-cli2 --workspace-root`, the file exists at `node_modules/markdownlint/style/prettier.json`.
|
||||
|
||||
**Glob placement in config, not CLI:** Putting `globs` and `ignores` in the config file means `pnpm md:lint` (just `markdownlint-cli2`, no args) and a developer running `npx markdownlint-cli2` directly both use the same scope automatically.
|
||||
|
||||
---
|
||||
|
||||
### Docs content fixes — 13 baseline violations (MD040 / MD031)
|
||||
|
||||
**Analog:** The violating files themselves. All fixes are mechanical one-liners.
|
||||
|
||||
**Files and fix type:**
|
||||
|
||||
| File | Rule | Fix |
|
||||
|------|------|-----|
|
||||
| `apps/api/README.md` | MD040 (1) | Add language tag to bare ` ``` ` |
|
||||
| `apps/pwa/e2e/README.md` | MD040 (1) | Add language tag to bare ` ``` ` |
|
||||
| `apps/pwa/README.md` | MD040 (1) | Add language tag to bare ` ``` ` |
|
||||
| `docs/API.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks |
|
||||
| `docs/ARCHITECTURE.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks |
|
||||
| `docs/DEVELOPMENT.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks |
|
||||
| `docs/GETTING-STARTED.md` | MD031 (2) | Add blank lines around 1 fence pair |
|
||||
| `README.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks |
|
||||
|
||||
**MD040 fix pattern** — change:
|
||||
````
|
||||
```
|
||||
some content
|
||||
```
|
||||
````
|
||||
to (pick the language that matches: `bash`, `text`, `json`, `yaml`, etc.):
|
||||
````
|
||||
```bash
|
||||
some content
|
||||
```
|
||||
````
|
||||
|
||||
**MD031 fix pattern** — change:
|
||||
````
|
||||
- list item
|
||||
```bash
|
||||
code
|
||||
```
|
||||
next paragraph
|
||||
````
|
||||
to:
|
||||
````
|
||||
- list item
|
||||
|
||||
```bash
|
||||
code
|
||||
```
|
||||
|
||||
next paragraph
|
||||
````
|
||||
|
||||
**Verification command (run locally after fixes, before pushing):**
|
||||
```bash
|
||||
pnpm md:lint
|
||||
pnpm format:check
|
||||
```
|
||||
Run both — see RESEARCH.md Pitfall 4: MD031 fixes and Prettier must not conflict.
|
||||
|
||||
---
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### `runs-on` label
|
||||
**Source:** ci.yml line 9, 41, 127 — `runs-on: ubuntu-latest`
|
||||
**Apply to:** All new jobs (`changes`, `gate`)
|
||||
```yaml
|
||||
runs-on: ubuntu-latest
|
||||
```
|
||||
The runner advertises `ubuntu-latest`. Never use `self-hosted`.
|
||||
|
||||
### `if: github.event_name == 'pull_request'` guard
|
||||
**Source:** ci.yml lines 10, 41, 128 — existing jobs all carry this guard
|
||||
**Apply to:** `changes` job (same guard); `api` and `harness` extend it with `&&`
|
||||
```yaml
|
||||
if: github.event_name == 'pull_request'
|
||||
```
|
||||
|
||||
### Step ordering in `fast-checks`
|
||||
**Source:** ci.yml lines 11–38 — current step sequence:
|
||||
1. `actions/checkout@v4`
|
||||
2. `actions/setup-node@v4` (node 22)
|
||||
3. `Enable pnpm` (corepack)
|
||||
4. `Install dependencies` (pnpm install --frozen-lockfile)
|
||||
5. `Lint`
|
||||
6. `Format check`
|
||||
7. **← insert `Markdown lint` here**
|
||||
8. `Typecheck`
|
||||
9. `PWA unit tests`
|
||||
|
||||
The new `Markdown lint` step slots between `Format check` and `Typecheck` — logically grouped with format/style checks, before compilation checks.
|
||||
|
||||
### `if: always()` usage
|
||||
**Source:** publish.yml line 94 — `if: always()` on the `Docker logout` step
|
||||
**Apply to:** `gate` job (job-level `if: always()`, not step-level)
|
||||
```yaml
|
||||
if: always()
|
||||
```
|
||||
This is the only safe way to ensure `gate` always reports a commit-status regardless of upstream job outcomes.
|
||||
|
||||
### No `actions/cache@v4`
|
||||
**Source:** ci.yml lines 22–23, 75–76, 161–162 — comment in every job:
|
||||
```yaml
|
||||
# actions/cache@v4 is intentionally omitted — probe (D-PROBE-04) showed it
|
||||
# times out on this runner (socket hang-up between runner container and job
|
||||
# container cache server). pnpm install without cache takes ~30s; acceptable.
|
||||
```
|
||||
**Apply to:** `changes` job has no install step so this is not relevant. `gate` job has no install step. No new jobs require caching.
|
||||
|
||||
---
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File | Role | Data Flow | Reason |
|
||||
|------|------|-----------|--------|
|
||||
| `.markdownlint-cli2.jsonc` | tool config | — | No existing markdownlint config in repo; `.prettierrc` used as structural analog only (JSON root tool config pattern) |
|
||||
|
||||
---
|
||||
|
||||
## publish.yml Comment Update (advisory)
|
||||
|
||||
**Source:** publish.yml lines 16–17 — the file's safety-gate comment names `CI / api` and `CI / harness` as the required checks:
|
||||
```yaml
|
||||
# The PR test jobs (fast-checks, api, harness in ci.yml) run on pull_request — they never
|
||||
# run in the same workflow invocation as publish.yml. Tests gate the PR; main is trusted to
|
||||
# be green because direct push and force push are blocked and the three required checks
|
||||
# (CI / fast-checks, CI / api, CI / harness) must pass before merge.
|
||||
```
|
||||
After this phase, the required checks change to `CI / fast-checks` + `CI / gate`. The planner should include a step to update this comment to reflect the new required-check names.
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `.gitea/workflows/`, repo root (config files), `package.json`
|
||||
**Files scanned:** 4 (ci.yml, publish.yml, package.json, .prettierrc)
|
||||
**Pattern extraction date:** 2026-06-12
|
||||
Reference in New Issue
Block a user