From e84911de10215f890ee01aacaaf388a874802f15 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:17:53 -0400 Subject: [PATCH 01/16] docs(15): research phase - doc-only CI skip + markdown lint --- .../15-RESEARCH.md | 638 ++++++++++++++++++ 1 file changed, 638 insertions(+) create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md new file mode 100644 index 0000000..7f4c59b --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md @@ -0,0 +1,638 @@ +# Phase 15: Doc-Only CI Skip + Markdown Lint - Research + +**Researched:** 2026-06-12 +**Domain:** Gitea Actions YAML / CI gate patterns / markdownlint-cli2 +**Confidence:** HIGH + +## Summary + +This phase has two independent sub-problems: (1) skip the slow `api` and `harness` jobs when a PR only touches docs, without deadlocking branch protection; and (2) add `markdownlint-cli2` to `fast-checks` so docs get a real lint gate. Both problems have well-understood solutions, but each has a Gitea-specific wrinkle that must be handled correctly. + +**Sub-problem 1 — doc-only skip + gate:** The mandatory pattern is a `changes` job (using `dorny/paths-filter@v4`) that detects doc-only PRs, combined with job-level `if:` conditions on `api`/`harness`, and an always-running `gate` aggregate job that branch protection requires instead of requiring `api`/`harness` directly. Gitea 1.26.2 (this instance) has a confirmed bug where `contains(needs.*.result, 'success')` returns `false` in certain contexts; the safe workaround is to check each job result individually using `&&` rather than the wildcard `contains()`. The `if: always()` deadlock bug (issue #27906) was fixed in Gitea 1.21.8; this instance at 1.26.2 is past that fix. + +**Sub-problem 2 — markdownlint:** With the `markdownlint/style/prettier` preset disabling all formatting-rules that Prettier owns, 13 content violations remain across 7 files — all trivially fixable (add language tags to bare fenced blocks, add blank lines around one fence pair). The baseline cleanup is the same order of magnitude as a single commit. + +**Primary recommendation:** Use `dorny/paths-filter@v4` in a `changes` job + individual `needs.X.result` checks in the `gate` job. Add `markdownlint-cli2` as a root devDependency. Update branch protection to require `CI / fast-checks` + `CI / gate` only (drop direct `CI / api` and `CI / harness` requirements). + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Doc-only detection | CI workflow | — | Pure YAML — a `changes` job runs `dorny/paths-filter` against the PR diff | +| Heavy-job skip | CI workflow | — | Job-level `if: needs.changes.outputs.code == 'true'` | +| Branch-protection gate | CI workflow | Gitea admin (manual) | `gate` job in YAML; required-check config is a Gitea UI/API action | +| Markdown linting | CI `fast-checks` job | Root workspace | A step in the existing job; markdownlint-cli2 installed at root | + +## Standard Stack + +### Core + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| markdownlint-cli2 | 0.22.1 | Markdown lint runner | CLI2 variant is config-driven; faster startup than markdownlint-cli; actively maintained by DavidAnson [VERIFIED: npm registry] | +| dorny/paths-filter | v4 | Changed-files detection for conditional jobs | Official GitHub Action mirrored on Gitea; uses GitHub/Gitea REST API on PR events (no fetch-depth needed); v4 is the current major [CITED: github.com/dorny/paths-filter] | + +### Supporting + +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| markdownlint/style/prettier | (bundled with markdownlint) | Rule preset disabling all Prettier-owned rules | Use via `extends: "markdownlint/style/prettier"` in the config; resolves from `node_modules/markdownlint/style/prettier.json` since markdownlint is a transitive dep of markdownlint-cli2 [VERIFIED: confirmed present in npm registry bundle] | + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| dorny/paths-filter@v4 | workflow-level `on.paths` filter | `on.paths` triggers a completely different workflow run — a required check gated on it never reports for a non-matching PR, causing a permanent deadlock on branch protection. Never use for required checks. | +| dorny/paths-filter@v4 | `git diff --name-only` in a setup job | Works but requires careful fetch-depth setup and manual glob matching; paths-filter handles this cleanly with the API | +| gate job with `if: always()` | Marking api/harness directly as required + optional | Gitea does not support "optional when skipped" on required checks; skipped jobs may not emit a commit-status context at all | + +**Installation (root workspace only):** + +```bash +pnpm add -D markdownlint-cli2 --workspace-root +``` + +**Version verification:** + +```bash +npm view markdownlint-cli2 version +# 0.22.1 — confirmed 2026-06-12 +``` + +## Package Legitimacy Audit + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| markdownlint-cli2 | npm | ~4 yrs | ~3M/wk | github.com/DavidAnson/markdownlint-cli2 | OK | Approved | +| dorny/paths-filter | GitHub Action | ~5 yrs | widely used | github.com/dorny/paths-filter | OK | Approved — not a package, used via `uses:` | + +**Packages removed due to [SLOP] verdict:** none +**Packages flagged as suspicious [SUS]:** none + +## Architecture Patterns + +### System Architecture Diagram + +``` +PR pushed to main + │ + ▼ +┌─────────────┐ +│ changes │ (new job — always runs) +│ job │ dorny/paths-filter@v4 +│ PR API diff │ outputs: code=true|false +└──────┬──────┘ + │ + ├──────────────────────┬──────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ fast-checks │ │ api │ │ harness │ +│ (always) │ │ if: code== │ │ if: code== │ +│ lint/fmt/type│ │ 'true' │ │ 'true' │ +│ + md-lint │ │ │ │ │ +└──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ + │ │ │ + └──────────┬────────┴──────────────────────┘ + │ + ▼ + ┌──────────────┐ + │ gate │ (new job — if: always()) + │ needs: all 4 │ passes if each job is + │ always runs │ success OR skipped; + │ │ fails if any failure/cancel + └──────┬────────┘ + │ + ▼ + Branch protection requires: + CI / fast-checks (direct — always runs) + CI / gate (aggregate — always runs) + DROP direct: CI / api, CI / harness +``` + +### Recommended Project Structure + +No new directories. All changes are in: + +``` +.gitea/ +└── workflows/ + └── ci.yml # modified: add changes + gate jobs; add if: conditions to api/harness +.markdownlint-cli2.jsonc # new: config at repo root +package.json # modified: add markdownlint-cli2 devDep + md:lint script +``` + +### Pattern 1: Changes Job (dorny/paths-filter@v4) + +**What:** A setup job that always runs, detects whether the PR touches code or only docs, and exports an output used by downstream jobs. + +**When to use:** Any workflow where some jobs should be skippable based on which files changed. + +**Example (exact YAML for this repo):** + +```yaml +# Source: github.com/dorny/paths-filter (v4 README) +jobs: + 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 choice:** Define `code` as the positive filter (files that ARE code), not `docs`. This means `code == 'false'` means "only docs changed" — safe, because any ambiguous or new file type is treated as code and gets the full gate. A doc-only PR will have `code == 'false'` only if every changed file is excluded from the `code` pattern. + +**No `actions/checkout` needed:** `dorny/paths-filter@v4` on a `pull_request` event uses the GitHub/Gitea REST API to fetch the diff, so no checkout step is required in the `changes` job. [CITED: github.com/dorny/paths-filter] + +**No `fetch-depth` needed:** API-based detection for PR events does not depend on git history depth. [CITED: github.com/dorny/paths-filter] + +### Pattern 2: Conditional Heavy Jobs + +**What:** `api` and `harness` jobs add `needs: [changes]` and a job-level `if` condition. + +**Example:** + +```yaml + api: + runs-on: ubuntu-latest + needs: [changes] + if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true' + # ... rest unchanged +``` + +**Important:** The existing `if: github.event_name == 'pull_request'` condition is already present; it must be combined with the new `&& needs.changes.outputs.code == 'true'` clause. The `needs: [changes]` declaration is new. + +### Pattern 3: Always-Running Gate Job + +**What:** An aggregate job that runs after all other jobs (`needs: [fast-checks, changes, api, harness]`), always executes regardless of upstream results, and fails if any non-skipped job failed or was cancelled. + +**Gitea 1.26.2 specific:** Gitea has a known bug (issue #31007, still open as of mid-2025) where `contains(needs.*.result, 'success')` returns `false` even when jobs succeed. **Do NOT use `contains(needs.*.result, ...)`.** Instead check each job's result individually. [CITED: github.com/go-gitea/gitea/issues/31007] + +**Recommended gate pattern (safe for Gitea 1.26.2):** + +```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 failed or was cancelled: ${{ needs.fast-checks.result }}" + exit 1 + fi + # api and harness are conditionally skipped — success OR skipped are both acceptable + for job_result in "${{ needs.api.result }}" "${{ needs.harness.result }}"; do + if [ "$job_result" != "success" ] && [ "$job_result" != "skipped" ]; then + echo "A required job failed or was cancelled: $job_result" + exit 1 + fi + done + echo "All required jobs passed or were intentionally skipped." +``` + +**Why not `contains(needs.*.result, ...)`:** Gitea 1.26.2 has confirmed incompatibility with the wildcard expression for job result aggregation. Individual `needs.X.result` references work correctly. [CITED: github.com/go-gitea/gitea/issues/31007] + +**Why `if: always()`:** Without this, if any upstream job is skipped or fails, the `gate` job is also skipped — and branch protection waiting for `CI / gate` would deadlock. The `always()` function ensures `gate` runs in every scenario. The `if: always()` deadlock bug (Gitea #27906) was fixed in 1.21.8; this instance runs 1.26.2. [CITED: github.com/go-gitea/gitea/issues/27906] + +### Pattern 4: markdownlint-cli2 in fast-checks + +**What:** A new step in the existing `fast-checks` job that runs after `format:check` and before `typecheck`. No separate job needed — it's fast (< 2s) and belongs in the same "format/style" bucket as Prettier. + +**Example step (slots into fast-checks after "Format check"):** + +```yaml + - name: Markdown lint + run: pnpm exec markdownlint-cli2 "docs/**/*.md" "*.md" "apps/**/*.md" "#.planning/**" "#node_modules/**" "#**/node_modules/**" +``` + +Or, preferred via a root script: + +```json +"scripts": { + "md:lint": "markdownlint-cli2 \"docs/**/*.md\" \"*.md\" \"apps/**/*.md\" \"#.planning/**\" \"#node_modules/**\" \"#**/node_modules/**\"" +} +``` + +Then CI step is `pnpm md:lint`. + +**Why root workspace, not per-app:** markdownlint-cli2 lints the whole repo's docs, not a specific app's source. The `fast-checks` job already runs `pnpm install --frozen-lockfile` at root, so a root devDependency is immediately available. + +### Pattern 5: markdownlint-cli2 Config File (.markdownlint-cli2.jsonc) + +**File:** `.markdownlint-cli2.jsonc` at repo root (takes highest priority over `.markdownlint.jsonc`). + +**Recommended config:** + +```jsonc +// .markdownlint-cli2.jsonc +{ + "config": { + // Disable all rules that conflict with Prettier (line-length, list-indent, + // blanks-around-fences, emphasis-style, etc. — 23 rules total) + "extends": "markdownlint/style/prettier", + + // Content rules to KEEP: + "MD001": true, // heading-increment: no skipping h1→h3 + "MD024": true, // no-duplicate-heading: duplicate headings in same section + "MD040": true, // fenced-code-language: all fences must declare a language + "MD031": true, // blanks-around-fences: blank lines around code fences + "MD051": true, // link-fragments: broken anchor links + "MD052": true, // reference-links-images: undefined link references + + // Rules to DISABLE (Prettier handles these OR they fire on CLAUDE.md/README + // which is not author-controlled for the linting purposes of this gate): + "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 bold for HTTP response labels + } +} +``` + +**Why `extends: "markdownlint/style/prettier"` resolves at install time:** `markdownlint-cli2` depends on `markdownlint`, which ships `style/prettier.json`. When `markdownlint-cli2` is installed as a root devDep (`pnpm add -D markdownlint-cli2 --workspace-root`), the `markdownlint` package lands in the root `node_modules` and the `extends` path resolves correctly. [VERIFIED: confirmed via npx run — markdownlint found at `node_modules/markdownlint/style/prettier.json`] + +**Key rule decisions:** + +| Rule | Decision | Rationale | +|------|----------|-----------| +| MD013 line-length | DISABLED (via prettier preset) | Prettier sets `printWidth: 100`; markdownlint's 80-char default creates 700+ false violations | +| MD036 no-emphasis-as-heading | DISABLED | `docs/API.md` uses `**Response 200**` as a semantic label (not a heading); 25 violations; fixing changes doc structure significantly | +| MD041 first-line-h1 | DISABLED | `CLAUDE.md` legitimately starts with `## Project` (a project instructions file, not a user doc) | +| MD034 no-bare-urls | DISABLED | `CLAUDE.md` version-compatibility table uses `pkg@version` syntax that triggers this; these are not URLs | +| MD040 fenced-code-language | ENABLED | 11 fences missing language tags — easy fixes, real content rule | +| MD031 blanks-around-fences | ENABLED | 2 violations in `docs/GETTING-STARTED.md` — easy fix | +| MD001 heading-increment | ENABLED | Content rule: headings must not skip levels | +| MD024 no-duplicate-heading | ENABLED | Content rule: duplicate headings in same section | +| MD051 link-fragments | ENABLED | Catches broken anchor links | +| MD052 reference-links | ENABLED | Catches undefined reference-style links | + +### Anti-Patterns to Avoid + +- **Workflow-level `on: paths` filter on required jobs:** If `fast-checks` (or any required check) has an `on: pull_request: paths:` filter, PRs that don't match the path never trigger the workflow, the required check never reports, and the PR is permanently blocked. Never path-filter a required-check workflow. [CITED: github.com/go-gitea/gitea/issues/36895] +- **Marking `api`/`harness` directly as required after adding skip logic:** Once these jobs can be skipped, a skipped job may not emit a commit status in Gitea. Any PR that triggers a skip will find the required check "missing" and be blocked forever. The `gate` job is the only correct gating surface for skippable jobs. +- **`contains(needs.*.result, 'success')` in Gitea:** Returns `false` even when jobs succeed in Gitea 1.26.2. Use individual `needs.X.result` string comparisons. [CITED: github.com/go-gitea/gitea/issues/31007] +- **Not combining `github.event_name` check with `needs.changes.outputs.code`:** The `api`/`harness` jobs' existing `if` condition is `github.event_name == 'pull_request'`. This must be combined (&&) with the new output check, not replaced — otherwise the event type guard is lost. +- **Installing markdownlint-cli2 per-app instead of at root:** The lint covers cross-repo docs, not a specific app's source. Putting it in `apps/pwa` or `apps/api` packages adds unnecessary coupling. +- **Including `.planning/**` in the lint glob:** `.planning/**` files are written by GSD tooling on every workflow run, are never human-authored to a lint standard, and bypass branch protection via the Unprotected pattern. Linting them would add noise with no signal and pollute the baseline indefinitely. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Detecting changed files in a PR | Shell script parsing `git diff --name-only` | `dorny/paths-filter@v4` | Handles shallow clones, merge commits, API fallback, glob matching, and outputs wiring cleanly | +| Aggregate gate for conditional jobs | Complex `if:` expression checking `contains()` | Simple `bash` script in a `gate` job checking individual `needs.X.result` | Gitea has bugs with `contains(needs.*.result, ...)` wildcard; individual checks are reliable | +| Disabling Prettier-conflicting lint rules | Manually listing all 23 rules | `extends: "markdownlint/style/prettier"` | The preset is maintained by DavidAnson (markdownlint author) and tracks Prettier compatibility | + +**Key insight:** The gate pattern is the only safe design for conditionally skipped jobs with branch protection. Anything that directly requires a skippable job as a "required status check" will deadlock. + +## Runtime State Inventory + +Not applicable — this phase is a CI workflow change only. No runtime state is involved. + +## Common Pitfalls + +### Pitfall 1: Required-check deadlock from skipped jobs + +**What goes wrong:** `api` or `harness` is still listed as a required status check in Gitea branch protection. After the skip logic lands, a doc-only PR skips these jobs. Gitea does not emit a commit-status for a skipped job (behavior confirmed in issue #36895 pattern — the check simply never appears). Branch protection sees the required check as "missing" and blocks the PR permanently. + +**Why it happens:** Gitea's branch protection model requires that the named status context appear with a `success` result. A skipped job may report nothing, or may report `skipped` — neither satisfies the requirement. + +**How to avoid:** Remove `CI / api` and `CI / harness` from branch protection's required checks. Add `CI / gate` instead. The `gate` job always runs (`if: always()`), always reports, and passes only when heavy jobs are `success` or `skipped`. + +**Warning signs:** Doc-only PR stuck at "Some required checks are missing" even though the workflow ran and `api`/`harness` show as skipped in the Actions UI. + +### Pitfall 2: `contains(needs.*.result, 'success')` returns false on Gitea + +**What goes wrong:** The gate job logic uses `contains(needs.*.result, 'success')` to check if any upstream job passed. On Gitea 1.26.2, this wildcard expression returns `false` even when jobs succeed, causing the gate to always fail. + +**Why it happens:** Gitea's expression engine has an incompatibility with the GitHub Actions wildcard `needs.*.result` spread when used with `contains()`. Issue #31007 is labeled `type/upstream` (an act runner issue) and was open as of mid-2025. [CITED: github.com/go-gitea/gitea/issues/31007] + +**How to avoid:** Check each upstream job individually: +```bash +if [ "${{ needs.fast-checks.result }}" != "success" ]; then exit 1; fi +``` +Not: +```yaml +if: contains(needs.*.result, 'failure') +``` + +**Warning signs:** Gate job always fails even on a green code PR where all three jobs passed. + +### Pitfall 3: `dorny/paths-filter@v4` needs `permissions: pull-requests: read` + +**What goes wrong:** The `changes` job fails with a permission error when `dorny/paths-filter` tries to call the GitHub/Gitea REST API to list changed files. + +**Why it happens:** v3+ of the action requires explicit `pull-requests: read` permission on the job. [CITED: github.com/dorny/paths-filter] + +**How to avoid:** Add `permissions: pull-requests: read` to the `changes` job (not at the workflow level — keeping it scoped). + +**Warning signs:** `changes` job fails with `403` or `Resource not accessible by integration` in the step log. + +### Pitfall 4: Prettier and markdownlint both own blank-lines-around-fences (MD031) + +**What goes wrong:** The `markdownlint/style/prettier` preset disables `blanks-around-fences` (MD031). If you re-enable it, markdownlint and Prettier may disagree on whether a blank line before a fence is required in certain contexts. + +**Why it happens:** MD031 is in the prettier preset's disable list because Prettier has its own opinion about blank lines around code fences in markdown. + +**How to avoid:** For this repo, MD031 is RE-ENABLED in the config because `docs/GETTING-STARTED.md` has a genuine structural issue (a fence immediately following a list item with no blank line), and fixing it (add blank lines) is also what Prettier would want. **Verify with `pnpm format:check` that fixing the MD031 violations does not introduce a Prettier conflict.** If conflicts emerge, disable MD031. + +**Warning signs:** After fixing MD031 violations, `pnpm format:check` reports new failures on the same files. + +### Pitfall 5: markdownlint-cli2 glob negation syntax + +**What goes wrong:** Writing `!.planning/**` in the CLI invocation instead of `#.planning/**` causes a shell glob expansion error. + +**Why it happens:** markdownlint-cli2 uses `#` as the glob negation prefix (not `!`). In shell, `!` is a history expansion character. On the command line, use `#`. In the config file `ignores:` array, `!` is also invalid — use the `ignores` array (no prefix needed, since ignores are always exclusions). [CITED: github.com/DavidAnson/markdownlint-cli2 README] + +**How to avoid:** Use `pnpm md:lint` which calls the script from `package.json` where the `#` prefix is safely quoted. Or use the `globs`/`ignores` properties in `.markdownlint-cli2.jsonc`. + +### Pitfall 6: `.pnpm-store/` in the markdown glob + +**What goes wrong:** `pnpm`'s content-addressable store lands inside the workspace on this CI runner (confirmed in Phase 13 — Prettier's `--check .` hit 39 `.pnpm-store/` files). If the markdownlint glob is `**/*.md` without excluding `.pnpm-store/`, it will scan `.pnpm-store/` and either find no issues (luck) or find lint violations in third-party packages' markdown files. + +**How to avoid:** The `ignores` list must include `node_modules/**` and `**/node_modules/**`. The pnpm store at `.pnpm-store/` does not contain `.md` files (it stores content-addressed binaries), but for safety, add `.pnpm-store/**` to ignores as well. [ASSUMED — the store likely doesn't contain .md files, but exclude it defensively] + +**Warning signs:** `pnpm md:lint` in CI reports violations in paths starting with `.pnpm-store/` or `node_modules/`. + +## Code Examples + +### Complete `changes` job + +```yaml +# Source: github.com/dorny/paths-filter (v4 README) — adapted for this repo + 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' +``` + +### Gate job + +```yaml +# Source: devopsdirective.com/posts/2025/08/github-actions-required-checks-for-conditional-jobs +# Adapted: use individual needs.X.result (not contains()) due to Gitea #31007 + gate: + runs-on: ubuntu-latest + needs: [fast-checks, changes, api, harness] + if: always() + steps: + - name: Check all required jobs passed or were skipped + run: | + if [ "${{ needs.fast-checks.result }}" != "success" ]; then + echo "fast-checks: ${{ needs.fast-checks.result }}" + exit 1 + fi + 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." +``` + +### Markdown lint step in fast-checks + +```yaml + - name: Markdown lint + run: pnpm md:lint +``` + +### Root package.json script addition + +```json +"md:lint": "markdownlint-cli2 \"docs/**/*.md\" \"*.md\" \"apps/**/*.md\" \"#.planning/**\" \"#node_modules/**\" \"#**/node_modules/**\"" +``` + +### `.markdownlint-cli2.jsonc` + +```jsonc +{ + "config": { + "extends": "markdownlint/style/prettier", + "MD001": true, + "MD024": true, + "MD040": true, + "MD031": true, + "MD051": true, + "MD052": true, + "MD041": false, + "MD034": false, + "MD036": false + } +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Require all 3 jobs directly in branch protection | Require `fast-checks` + always-running `gate` aggregate | This phase | Conditional jobs can be skipped without deadlocking branch protection | +| All PRs run full CI regardless of diff | Doc-only PRs skip `api`/`harness` | This phase | Doc PRs: ~30s instead of ~5min | +| No markdown lint | markdownlint-cli2 in fast-checks | This phase | Docs get a real format+lint gate | + +**Deprecated/outdated:** + +- `CI / api` and `CI / harness` as direct required checks — replaced by `CI / gate` aggregate after this phase. + +## Baseline Violation Assessment + +Running the recommended config against the current repo (12 files scanned, excluding `.planning/**`): + +**Result: 13 violations across 7 files.** [VERIFIED: run 2026-06-12 via npx markdownlint-cli2] + +| File | Rule | Count | 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 | + +**Total: 13 violations, all mechanical one-liner fixes.** Sizing: one commit, 5–10 minutes of editing. + +**Files NOT in scope (correctly excluded):** + +- `.planning/**` — GSD tooling output, bypasses branch protection, excluded by design +- `CLAUDE.md` — project instructions file; MD041/MD034/MD036 violations exempted by rule config decisions above + +**The baseline cleanup task is small and entirely mechanical** — it does not require judgment calls or restructuring. + +## Gitea-Specific Behavior Notes + +All items below are [CITED] from Gitea issue tracker or confirmed from Phase 8 probe results. + +1. **Skipped job commit-status:** Gitea may not emit a commit-status context for a job skipped via job-level `if:`. A skipped job simply does not appear in the branch protection check list. This is the core reason the `gate` job is mandatory — it is the only always-reporting surface. [CITED: github.com/go-gitea/gitea/issues/36895] + +2. **`if: always()` deadlock bug:** Fixed in Gitea 1.21.8 (PR #29464). This instance runs 1.26.2 — the fix is in place. `if: always()` on a job that `needs:` skipped upstream jobs will correctly run. [CITED: github.com/go-gitea/gitea/issues/27906] + +3. **`contains(needs.*.result, ...)` bug:** Issue #31007 is labeled `type/upstream` and was still open as of mid-2025 with no confirmed fix in 1.26.2. Individual `needs.X.result` comparisons are safe and unaffected. [CITED: github.com/go-gitea/gitea/issues/31007] + +4. **Branch protection required-check update is a MANUAL Gitea admin step:** The YAML changes alone are insufficient. An operator must visit Gitea → Repository Settings → Branches → Edit protection rule for `main`, remove `CI / api` and `CI / harness` from required checks, and add `CI / gate`. This cannot be done via a workflow file. (`tea` CLI can update branch protection via the API but requires `tea` setup and the relevant endpoint.) [ASSUMED — based on prior phase knowledge; confirm against the Gitea admin UI] + +5. **`dorny/paths-filter@v4` resolution:** Actions in `.gitea/workflows/` using `uses: dorny/paths-filter@v4` resolve from `github.com` (confirmed in Phase 8 probe — `actions/checkout@v4` and `actions/setup-node@v4` clone from github.com on first run, ~60–75s cold start). No local mirror needed. [CITED: Phase 8 08-01-SUMMARY.md D-PROBE-08] + +6. **`runs-on: ubuntu-latest`:** The runner advertises `ubuntu-latest`, not `self-hosted`. All jobs must use `runs-on: ubuntu-latest`. [CITED: Phase 8 08-01-SUMMARY.md D-PROBE-01] + +## Open Questions + +1. **`dorny/paths-filter@v4` requires `pull-requests: read` — does this repo's GITHUB_TOKEN have it?** + - What we know: Gitea's `GITHUB_TOKEN` equivalent is auto-injected; the permission level depends on Gitea's token permission model. + - What's unclear: Whether the default Gitea Actions token has `pull-requests: read` or if the `permissions:` declaration is required to elevate it. + - Recommendation: Include `permissions: pull-requests: read` on the `changes` job explicitly regardless. If the token already has it, this is a no-op. If it doesn't, this is necessary. + +2. **Does Gitea 1.26.2 emit a commit-status for `skipped` jobs?** + - What we know: Issue #36895 documents the behavior where skipped jobs don't block but also don't report; issue #23599 (display-as-failed) was fixed in 1.19.1. + - What's unclear: The exact behavior in 1.26.2 — skipped jobs may show in the UI as "skipped" but may not emit a commit-status that branch protection can read. + - Recommendation: The `gate` job design is robust regardless of this answer. Do not rely on skipped-job status for branch protection. + +3. **Should the `md:lint` script use the `globs` property in `.markdownlint-cli2.jsonc` instead of CLI args?** + - What we know: Both work. Config-file globs are more portable and don't require shell quoting. + - What's unclear: Whether having globs in the config file makes local developer runs (`npx markdownlint-cli2`) automatically scope correctly without extra args. + - Recommendation: Put globs and ignores in the config file; make the root script just `markdownlint-cli2` (no args). This means `pnpm md:lint` and direct `npx markdownlint-cli2` both use the same scope. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Gitea Actions runner | gate job execution | ✓ | ubuntu-latest (D-PROBE-01) | — | +| github.com resolution | `dorny/paths-filter@v4` | ✓ | Confirmed Phase 8 (D-PROBE-08) | — | +| `dorny/paths-filter@v4` | `changes` job | ✓ (inferred) | v4 | v3 (uses Node 20 instead of 24) | +| Node.js 22 | markdownlint-cli2 | ✓ | 22 LTS (via actions/setup-node) | — | +| markdownlint-cli2 | markdown lint step | ✓ | 0.22.1 (npm) | — | +| Gitea admin access | branch protection update | ✓ (operator) | — | tea CLI API | + +**Missing dependencies with no fallback:** None. + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| Framework | Gitea Actions CI (behavioral) | +| Config file | `.gitea/workflows/ci.yml` | +| Quick run command | Construct a doc-only PR; observe job statuses | +| Full suite command | Construct a code-touching PR; observe all jobs pass | + +### Phase Requirements → Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| SC-1 | Doc-only PR skips `api`/`harness`, runs `fast-checks` | CI behavioral | Open a PR with only `README.md` changed; observe Actions | ❌ Wave 0 (need test PR) | +| SC-2 | Code PR runs all three jobs; failure blocks merge | CI behavioral | Open a code PR; observe all three jobs run | ❌ Wave 0 (need test PR) | +| SC-3 | Branch protection requires `gate`, not `api`/`harness` directly | Manual verification | Check Gitea branch protection settings | Manual | +| SC-4 | markdownlint violation fails `fast-checks` | CI behavioral | Introduce a deliberate lint violation (bare code fence) in a PR | ❌ Wave 0 (need test) | +| SC-4b | Baseline passes (green gate) | Local | `pnpm md:lint` exits 0 after fixing 13 violations | ❌ Wave 0 (after baseline fix) | + +### Wave 0 Gaps + +- [ ] `.markdownlint-cli2.jsonc` — new file needed before lint step runs +- [ ] `package.json` root `md:lint` script — needed for CI step +- [ ] Fix 13 baseline violations — needed for `pnpm md:lint` to pass +- [ ] Gitea branch protection update — manual operator step, must be called out in the plan + +*(If tests pass after these gaps are addressed: "Gate is green before Phase 16 starts")* + +## Security Domain + +The security surface of this phase is entirely CI workflow configuration. No user data or auth paths are involved. + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | no | — | +| V3 Session Management | no | — | +| V4 Access Control | no | — | +| V5 Input Validation | no | CI YAML is not user input | +| V6 Cryptography | no | — | + +**CI security note:** `dorny/paths-filter@v4` is a third-party action that runs with `pull-requests: read` permission on the `GITHUB_TOKEN`. This is the minimal permission needed and does not grant write access to the repo or its secrets. The action does not receive any secrets beyond the token. This is standard for PR-triggered third-party actions and is consistent with the risk posture already accepted by `actions/checkout@v4` and `actions/setup-node@v4`. [ASSUMED — specific Gitea token permission model not verified; action's README confirms read-only usage] + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | `.pnpm-store/` does not contain `.md` files that would appear in the markdownlint glob | Pitfall 6 | markdownlint would scan store files; non-breaking but noisy | +| A2 | Gitea's default GITHUB_TOKEN has sufficient permission for dorny/paths-filter@v4 with explicit `permissions: pull-requests: read` declared | Pitfall 3 / Open Questions | If Gitea's token model ignores the permissions key, the action would need an explicit PAT — very unlikely given GitHub-compatibility goal | +| A3 | Updating required checks in branch protection requires a Gitea admin UI / API step (not automatable via workflow YAML) | Gitea-Specific Notes #4 | If Gitea has a workflow-driven protection update mechanism, the manual step could be automated — low risk | +| A4 | `dorny/paths-filter@v4` works on Gitea 1.26.2 (inferred from GitHub Actions compatibility + v3 confirmed mirrored on Gitea) | Standard Stack | If v4 has a GitHub-specific API call that Gitea 1.26.2 doesn't support, fall back to v3 | + +**If this table is empty:** All claims in this research were verified or cited — no user confirmation needed. (It is not empty — A1-A4 are low risk but flagged.) + +## Sources + +### Primary (HIGH confidence) + +- Phase 8 probe results (`08-01-SUMMARY.md`) — confirmed runner behavior: `ubuntu-latest`, Docker-executor, no mysql CLI, actions/cache unreliable, github.com action resolution works +- Gitea API `GET /api/v1/version` — confirmed Gitea 1.26.2 +- `npx markdownlint-cli2` run (2026-06-12) — confirmed 13 violations with recommended config on current repo + +### Secondary (MEDIUM confidence) + +- [github.com/dorny/paths-filter README](https://github.com/dorny/paths-filter/blob/master/README.md) — PR event uses REST API; `permissions: pull-requests: read` required; no fetch-depth needed; v4 is current +- [github.com/DavidAnson/markdownlint Prettier.md](https://github.com/DavidAnson/markdownlint/blob/main/doc/Prettier.md) — 23 rules disabled by `markdownlint/style/prettier` +- [github.com/DavidAnson/markdownlint-cli2 README](https://github.com/DavidAnson/markdownlint-cli2/blob/main/README.md) — config file formats; `#` glob negation prefix +- [devopsdirective.com gate job pattern](https://devopsdirective.com/posts/2025/08/github-actions-required-checks-for-conditional-jobs/) — always-running aggregate gate job pattern +- [github.com/go-gitea/gitea #27906](https://github.com/go-gitea/gitea/issues/27906) — `if: always()` deadlock fixed in 1.21.8 +- [github.com/go-gitea/gitea #31007](https://github.com/go-gitea/gitea/issues/31007) — `contains(needs.*.result, ...)` returns false on Gitea +- [github.com/go-gitea/gitea #36895](https://github.com/go-gitea/gitea/issues/36895) — skipped jobs via `on: paths` cause branch protection deadlock + +### Tertiary (LOW confidence) + +- General GitHub Actions `if: always()` gate pattern (training knowledge, confirmed by multiple secondary sources above) + +## Metadata + +**Confidence breakdown:** + +- Doc-only detection mechanism: HIGH — `dorny/paths-filter@v4` is confirmed working and mirrored on Gitea; the PR API mode avoids fetch-depth issues +- Gate job pattern: HIGH — `if: always()` fix confirmed in 1.21.8; individual `needs.X.result` checks are the safe Gitea workaround for the wildcard bug +- markdownlint config: HIGH — config tested against actual repo, 13 violations confirmed, all fixable +- Branch protection update: MEDIUM — confirmed manual step; exact UI path not re-verified in this research session + +**Research date:** 2026-06-12 +**Valid until:** 2027-01-01 (stable tooling; Gitea bug status should be re-checked if the instance is upgraded past 1.26.x before execution) -- 2.54.0 From 18cf62b702e95ce732f5cffdfb9f66fcd4b3258e Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:19:08 -0400 Subject: [PATCH 02/16] docs(phase-15): add validation strategy --- .../15-VALIDATION.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md new file mode 100644 index 0000000..b6653b5 --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md @@ -0,0 +1,76 @@ +--- +phase: 15 +slug: ci-skip-api-harness-jobs-for-doc-only-prs +status: draft +nyquist_compliant: false +wave_0_complete: false +created: 2026-06-12 +--- + +# Phase 15 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | +| **Config file** | {path or "none — Wave 0 installs"} | +| **Quick run command** | `{quick command}` | +| **Full suite command** | `{full command}` | +| **Estimated runtime** | ~{N} seconds | + +--- + +## Sampling Rate + +- **After every task commit:** Run `{quick run command}` +- **After every plan wave:** Run `{full suite command}` +- **Before `/gsd-verify-work`:** Full suite must be green +- **Max feedback latency:** {N} seconds + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} +- [ ] `{tests/conftest.py}` — shared fixtures +- [ ] `{framework install}` — if no framework detected + +*If none: "Existing infrastructure covers all phase requirements."* + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| {behavior} | REQ-{XX} | {reason} | {steps} | + +*If none: "All phase behaviors have automated verification."* + +--- + +## Validation Sign-Off + +- [ ] All tasks have `` verify or Wave 0 dependencies +- [ ] Sampling continuity: no 3 consecutive tasks without automated verify +- [ ] Wave 0 covers all MISSING references +- [ ] No watch-mode flags +- [ ] Feedback latency < {N}s +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** {pending / approved YYYY-MM-DD} -- 2.54.0 From 899528eeb3c7c5f15eebeda57e30bd96c6c2283e Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:22:37 -0400 Subject: [PATCH 03/16] docs(phase-15): add pattern map --- .../15-PATTERNS.md | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md new file mode 100644 index 0000000..14d4ef7 --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md @@ -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 -- 2.54.0 From e19d4736c78c41fb80eff702f9309b8862fced03 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:28:56 -0400 Subject: [PATCH 04/16] =?UTF-8?q?docs(15):=20create=20phase=20plan=20?= =?UTF-8?q?=E2=80=94=20doc-only=20CI=20skip=20+=20markdown=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 plans across 3 waves: - 15-01: markdownlint-cli2 config/script/step + fix 13 baseline violations (SC-4) - 15-02: ci.yml changes job + conditional api/harness + always-running gate (SC-1/2, SC-3 YAML) - 15-03: operator branch-protection checkpoint + publish.yml comment (SC-3) --- .planning/ROADMAP.md | 11 +- .../15-01-PLAN.md | 179 ++++++++++++++++++ .../15-02-PLAN.md | 167 ++++++++++++++++ .../15-03-PLAN.md | 144 ++++++++++++++ .../15-VALIDATION.md | 69 ++++--- 5 files changed, 539 insertions(+), 31 deletions(-) create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-PLAN.md create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-PLAN.md create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index b8e3f14..9c9a82b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -280,7 +280,14 @@ Plans: - **Prettier vs markdownlint overlap** — Prettier already owns markdown *formatting*; scope markdownlint to *content* rules (heading increments, no broken/duplicate link refs, list/code-fence conventions) and disable its purely-stylistic rules that fight Prettier (e.g. line-length, list-indent), so the two don't conflict on the same `.md`. - **`.planning/*` is push-direct, never linted** — planning bookkeeping bypasses CI via the Unprotected file pattern, so markdownlint never sees it; scope the lint glob to `docs/` + repo-root/app `*.md` and exclude `.planning/**` (and any generated markdown) to avoid a baseline cleanup of churny bookkeeping files. -**Plans**: TBD +**Plans**: 3 plans (3 waves) + +Plans: + +- [ ] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4) +- [ ] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML) +- [ ] 15-03-PLAN.md — operator branch-protection checkpoint (require `CI / fast-checks` + `CI / gate`, drop api/harness) + publish.yml comment update (SC-3) + **UI hint**: no ## Progress @@ -301,7 +308,7 @@ Plans: | 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - | | 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 | | 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 | -| 15. Doc-Only CI Skip + MD Lint | v1.1 | 0/? | Not started | - | +| 15. Doc-Only CI Skip + MD Lint | v1.1 | 0/3 | Planned | - | ## Backlog diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-PLAN.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-PLAN.md new file mode 100644 index 0000000..c615b82 --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-PLAN.md @@ -0,0 +1,179 @@ +--- +phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .markdownlint-cli2.jsonc + - package.json + - pnpm-lock.yaml + - .gitea/workflows/ci.yml + - README.md + - apps/api/README.md + - apps/pwa/README.md + - apps/pwa/e2e/README.md + - docs/API.md + - docs/ARCHITECTURE.md + - docs/DEVELOPMENT.md + - docs/GETTING-STARTED.md +autonomous: true +requirements: [] +must_haves: + truths: + - "pnpm md:lint exits 0 on the current repo (13 baseline violations fixed)" + - "Introducing a bare fenced code block (MD040) makes pnpm md:lint exit non-zero" + - "The fast-checks CI job runs a Markdown lint step between Format check and Typecheck" + - "pnpm format:check still exits 0 after the MD031 blank-line fixes (no Prettier conflict)" + - "markdownlint never scans .planning/** (excluded by config ignores)" + artifacts: + - path: ".markdownlint-cli2.jsonc" + provides: "markdownlint-cli2 config (extends prettier preset; content rules; globs + ignores)" + contains: "markdownlint/style/prettier" + - path: "package.json" + provides: "md:lint script + markdownlint-cli2 devDependency" + contains: "md:lint" + - path: ".gitea/workflows/ci.yml" + provides: "Markdown lint step in fast-checks job" + contains: "pnpm md:lint" + key_links: + - from: ".gitea/workflows/ci.yml" + to: "package.json md:lint script" + via: "fast-checks step runs pnpm md:lint" + pattern: "pnpm md:lint" + - from: "package.json md:lint script" + to: ".markdownlint-cli2.jsonc" + via: "markdownlint-cli2 auto-discovers root config (globs + ignores)" + pattern: "markdownlint-cli2" +--- + + +Add a real markdown lint gate to the existing `fast-checks` CI job: install `markdownlint-cli2`, create the root `.markdownlint-cli2.jsonc` config (Prettier-compatible preset + content rules + scoped globs that exclude `.planning/**`), add a root `md:lint` script, wire a "Markdown lint" step into `fast-checks`, and fix the 13 baseline violations so the gate starts GREEN. + +This delivers Success Criterion 4: `fast-checks` runs markdownlint over docs; an introduced violation fails the gate; the existing baseline passes. + +Purpose: Docs get a fast but real format+lint gate without a separate CI job, scoped so churny `.planning/**` bookkeeping is never linted. +Output: `.markdownlint-cli2.jsonc`, updated `package.json` (script + devDep), one new `fast-checks` step, 7 doc files fixed (13 violations), green `pnpm md:lint` + `pnpm format:check`. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md +@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md + + + + + + Task 1: Install markdownlint-cli2, add md:lint script, create .markdownlint-cli2.jsonc + + - package.json (root — current scripts block lines 6-16 and devDependencies lines 17-25; mirror the existing `format:check` script + `prettier` devDep placement) + - .prettierrc (root JSON tool-config structural analog: printWidth 100 — the reason MD013 must stay disabled) + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Pattern 5 + "Code Examples" → the exact `.markdownlint-cli2.jsonc` body and rule-decision table; Pitfall 5 `#` vs `!` negation; Pitfall 6 .pnpm-store) + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("`.markdownlint-cli2.jsonc` (new file)" and "package.json (root)" sections — exact insertion points) + + + Install `markdownlint-cli2` at version `0.22.1` as a root-workspace devDependency: run `pnpm add -D markdownlint-cli2@0.22.1 --workspace-root` (this updates root package.json devDependencies and pnpm-lock.yaml). Add a root script `"md:lint": "markdownlint-cli2"` immediately after the existing `"format:check"` script — NO glob args on the CLI; globs/ignores live in the config file so `pnpm md:lint` and a bare `npx markdownlint-cli2` resolve identically (RESEARCH Open Question 3 resolved). + + Create `.markdownlint-cli2.jsonc` at the repo root using the exact body from RESEARCH "Code Examples → .markdownlint-cli2.jsonc" / PATTERNS "`.markdownlint-cli2.jsonc` (new file)". Required keys: `config.extends` = `"markdownlint/style/prettier"`; ENABLE `MD001`, `MD024`, `MD040`, `MD031`, `MD051`, `MD052` (all `true`); DISABLE `MD041`, `MD034`, `MD036` (all `false`) — rationale per the rule-decision table (MD041 because CLAUDE.md starts with `## Project`; MD034 because the version table uses `pkg@version` syntax; MD036 because docs/API.md uses bold response labels). Set `globs` to exactly `["docs/**/*.md", "*.md", "apps/**/*.md"]` and `ignores` to exactly `[".planning/**", "node_modules/**", "**/node_modules/**", ".pnpm-store/**"]`. Do NOT add MD013 (line-length) — it is disabled by the prettier preset and re-enabling it produces 700+ false violations against `.prettierrc` printWidth 100. + + Do NOT run md:lint to green yet — the 13 baseline violations are fixed in Task 2; this task may leave `pnpm md:lint` red. + + + test -f .markdownlint-cli2.jsonc && node -e "const p=require('./package.json'); if(!p.scripts['md:lint']) process.exit(1); if(!p.devDependencies['markdownlint-cli2']) process.exit(1); console.log('script+dep present')" && grep -q 'markdownlint/style/prettier' .markdownlint-cli2.jsonc && grep -q '.planning/' .markdownlint-cli2.jsonc && echo OK + + + - package.json `scripts` contains `"md:lint": "markdownlint-cli2"` (no glob args) + - package.json `devDependencies` contains `markdownlint-cli2` at `0.22.1`; pnpm-lock.yaml updated + - `.markdownlint-cli2.jsonc` exists at repo root and `config.extends` is `"markdownlint/style/prettier"` + - `.markdownlint-cli2.jsonc` enables MD001/MD024/MD040/MD031/MD051/MD052 and disables MD041/MD034/MD036 + - `.markdownlint-cli2.jsonc` `ignores` array includes `".planning/**"`, `"node_modules/**"`, `"**/node_modules/**"`, `".pnpm-store/**"` + - `.markdownlint-cli2.jsonc` `globs` array is exactly `["docs/**/*.md", "*.md", "apps/**/*.md"]` + + markdownlint-cli2@0.22.1 is a root devDep, `pnpm md:lint` is wired to the root config, and the config scopes the lint to docs/repo-root/app markdown while excluding `.planning/**`. md:lint may still be red (fixed next task). + + + + Task 2: Fix the 13 baseline markdown violations; green md:lint + format:check; wire the fast-checks step + + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md ("Baseline Violation Assessment" table — exact file/rule/count; Pitfall 4 — verify MD031 fix does not break Prettier) + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("Docs content fixes" table + MD040/MD031 fix patterns; "Markdown lint step in fast-checks" insertion slot) + - .gitea/workflows/ci.yml (fast-checks job lines 8-38 — confirm the real step names: "Format check" line 31-32, "Typecheck" line 34; insert the new step between them) + - README.md (lines ~78, ~119 — the 2 bare fences) + - docs/API.md (lines ~517, ~544), docs/ARCHITECTURE.md (lines ~64, ~171), docs/DEVELOPMENT.md (lines ~9, ~22) — 2 bare fences each + - docs/GETTING-STARTED.md (lines ~52, ~54 — the MD031 fence pair needing blank lines) + - apps/api/README.md (line ~19), apps/pwa/README.md (line ~45), apps/pwa/e2e/README.md (line ~76) — 1 bare fence each + + + Fix all 13 violations confirmed at these exact locations (run `pnpm md:lint` first to re-confirm line numbers, then fix): + - MD040 (11 bare fences — add a language tag matching the fence content; use `bash` for shell, `text` for plain output, `json`/`yaml`/`ts` as appropriate): apps/api/README.md:19, apps/pwa/e2e/README.md:76, apps/pwa/README.md:45, docs/API.md:517, docs/API.md:544, docs/ARCHITECTURE.md:64, docs/ARCHITECTURE.md:171, docs/DEVELOPMENT.md:9, docs/DEVELOPMENT.md:22, README.md:78, README.md:119. + - MD031 (1 fence pair — add a blank line before the opening ```` ```bash ```` and after the closing ```` ``` ````): docs/GETTING-STARTED.md:52 and :54. + + Only change what each rule requires — add a language token after the opening backticks (MD040) or add surrounding blank lines (MD031); do NOT rewrite fence bodies or restructure docs. Do not "fix" any rule that the config disables (MD013/MD034/MD036/MD041) — those are intentionally off. + + After fixes, add the new step to the `fast-checks` job in ci.yml between the "Format check" step and the "Typecheck" step — a named step `Markdown lint` running `pnpm md:lint` (mirror the one-line `Format check` step pattern exactly). This is the only ci.yml change in this plan; do not touch the api/harness/changes/gate jobs (Plan 02 owns those). + + Then run BOTH `pnpm md:lint` (must exit 0) AND `pnpm format:check` (must exit 0) — per RESEARCH Pitfall 4, the MD031 blank-line additions must not introduce a Prettier conflict on docs/GETTING-STARTED.md. If format:check newly fails on a file you touched, run `pnpm format` on that file and re-confirm md:lint is still 0. + + + pnpm md:lint && pnpm format:check && grep -q 'pnpm md:lint' .gitea/workflows/ci.yml && echo OK + + + - `pnpm md:lint` exits 0 (13 baseline violations resolved) + - `pnpm format:check` exits 0 (no Prettier conflict introduced by the MD031 fix) + - `.gitea/workflows/ci.yml` fast-checks job contains a step named `Markdown lint` running `pnpm md:lint`, positioned after the `Format check` step and before the `Typecheck` step + - Re-running md:lint after temporarily inserting a bare ```` ``` ```` fence into any in-scope .md (then reverting) exits non-zero (gate can fail) — proven during execution, not left in the tree + - No api/harness/changes/gate job was modified by this plan + + The markdown baseline is clean, `fast-checks` runs `pnpm md:lint`, the gate can fail on a real violation, and Prettier and markdownlint do not conflict. SC-4 satisfied. + + + + + +This phase introduces the following new symbols (Plan 01 portion). The plan-review source-grounding pass must treat these as newly-created, not drift: + +- `.markdownlint-cli2.jsonc` — new root config file +- `md:lint` — new root `package.json` script +- `markdownlint-cli2` — new root devDependency (0.22.1) +- `Markdown lint` — new step name in the `fast-checks` job + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| npm registry → repo devDependencies | `markdownlint-cli2` is fetched and runs in CI and on developer machines | +| markdown content → lint tool | doc files are the input; markdownlint only reads, never executes content | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-15-01 | Tampering (supply chain) | new `markdownlint-cli2` devDependency | mitigate | Pin to exact `0.22.1` (no `^`/`~`); RESEARCH Package Legitimacy Audit verdict OK (~4 yrs, ~3M/wk, DavidAnson/markdownlint-cli2). Audit table present in RESEARCH; no `[ASSUMED]`/`[SUS]` packages so no blocking-human checkpoint required. | +| T-15-02 | Denial of Service (gate noise) | lint glob accidentally scanning `.planning/**` or `node_modules` | accept→mitigate | `ignores` excludes `.planning/**`, `node_modules/**`, `**/node_modules/**`, `.pnpm-store/**`; verified the glob lints exactly 12 in-scope files, not bookkeeping churn. | +| T-15-03 | Tampering (false-green) | a disabled content rule silently hides a real doc defect | accept | Rule-disable decisions (MD034/MD036/MD041) are scoped to non-author-controlled patterns documented in RESEARCH; MD040/MD031/MD001/MD024/MD051/MD052 stay enabled to catch broken fences/links. | + + + +- `pnpm md:lint` exits 0 on the clean tree; exits non-zero when a bare fence is introduced. +- `pnpm format:check` exits 0 (Prettier/markdownlint do not conflict). +- `.gitea/workflows/ci.yml` fast-checks job runs `pnpm md:lint` between Format check and Typecheck. +- markdownlint scans 12 in-scope files and never touches `.planning/**` (confirm "Finding:" line lists the negated ignores). + + + +Maps to Phase 15 Success Criterion 4: `fast-checks` runs markdownlint-cli2 over the docs glob; an introduced markdown-lint violation fails the gate; the existing markdown baseline passes (13 violations fixed, rules configured) so the gate starts green. + + + +Create `.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-SUMMARY.md` when done. + diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-PLAN.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-PLAN.md new file mode 100644 index 0000000..4ed561a --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-PLAN.md @@ -0,0 +1,167 @@ +--- +phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs +plan: 02 +type: execute +wave: 2 +depends_on: + - 15-01 +files_modified: + - .gitea/workflows/ci.yml +autonomous: true +requirements: [] +must_haves: + truths: + - "A doc-only PR to main skips the api and harness jobs but still runs fast-checks" + - "A PR touching code runs fast-checks, api, and harness; a failure in any blocks the gate" + - "An always-running gate job reports CI / gate and passes only when fast-checks succeeded and each heavy job is success OR skipped" + - "The gate fails (exit 1) when fast-checks fails or when api/harness fails or is cancelled" + artifacts: + - path: ".gitea/workflows/ci.yml" + provides: "changes job (paths-filter), conditional api/harness, always-running gate aggregate" + contains: "dorny/paths-filter@v4" + key_links: + - from: "ci.yml api/harness jobs" + to: "ci.yml changes job output" + via: "needs: [changes] + if needs.changes.outputs.code == 'true'" + pattern: "needs.changes.outputs.code" + - from: "ci.yml gate job" + to: "ci.yml fast-checks/api/harness results" + via: "needs: [fast-checks, changes, api, harness] + if: always() + per-job needs.X.result checks" + pattern: "needs.fast-checks.result" +--- + + +Restructure `.gitea/workflows/ci.yml` so doc-only PRs skip the slow `api` and `harness` jobs without deadlocking branch protection: add a `changes` job (`dorny/paths-filter@v4`) that emits a `code` output, gate `api`/`harness` on `needs.changes.outputs.code == 'true'`, and add an always-running `gate` aggregate job that branch protection can require in place of the heavy jobs directly. + +This delivers Success Criteria 1 and 2 fully, and lands the `CI / gate` status that Success Criterion 3 (Plan 03's branch-protection checkpoint) requires to exist first. + +Purpose: Doc-only PRs go from ~5 min to ~30s while a single always-reporting `gate` keeps the merge gated. The conditional skip and the gate are interdependent and ship together in one PR so the heavy jobs can actually be skipped while the gate still reports. +Output: One modified file — `ci.yml` with a new `changes` job, `needs`/`if` on `api` and `harness`, and a new `gate` job. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md +@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md + + + + + + Task 1: Add the changes job and gate api/harness on the code output + + - .gitea/workflows/ci.yml (FULL file — current job names `fast-checks`/`api`/`harness`; api header lines 40-44 with existing `if: github.event_name == 'pull_request'`; harness header lines 126-129; runner label `ubuntu-latest`; both heavy jobs run in parallel with no `needs:` today) + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Pattern 1 changes job + Pattern 2 conditional heavy jobs + "Code Examples → Complete changes job"; Pitfall 3 permissions; Gitea-Specific Notes #5/#6 action resolution + runner label) + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("`changes` job (new)", "`api` job modification", "`harness` job modification" — exact YAML and insertion ordering: changes BEFORE fast-checks) + + + Insert a new `changes` job (use the exact YAML from RESEARCH "Code Examples → Complete changes job" / PATTERNS "`changes` job (new)") positioned BEFORE `fast-checks` so the UI ordering reads changes → fast-checks/api/harness → gate. The job: `runs-on: ubuntu-latest`; `if: github.event_name == 'pull_request'`; `permissions: pull-requests: read` (job-scoped, required by paths-filter v4 — Pitfall 3); `outputs.code: ${{ steps.filter.outputs.code }}`; a single step `uses: dorny/paths-filter@v4` with `id: filter`. The `code` filter must list exactly the positive code patterns from RESEARCH: `**/*.ts`, `**/*.tsx`, `**/*.js`, `**/*.json`, `**/*.yaml`, `**/*.yml`, `apps/**`, `packages/**`, `pnpm-lock.yaml`, `Dockerfile`, `docker-compose*.yml`. Define the POSITIVE `code` filter (not a `docs` filter) so `code == 'false'` means doc-only and any new/ambiguous file type defaults to the full gate. No `actions/checkout` and no `fetch-depth` — paths-filter uses the PR REST API on `pull_request` events. + + Modify the `api` job header: add `needs: [changes]` and change its `if` to `github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'`. EXTEND the existing event guard with `&&` — do not replace it (replacing loses the event-type guard). Update the inline comment to say the job is skipped for doc-only PRs. Change nothing else in the api job body (services, env, steps, mariadb wait, migrate, test). + + Modify the `harness` job header identically: `needs: [changes]` + the same combined `if`. Update its comment; change nothing else in the harness body. + + Pin the third-party action to the `@v4` tag exactly as written (`dorny/paths-filter@v4`); do not float to a branch. + + + command -v yq >/dev/null 2>&1 && yq -e '.jobs.changes' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.changes.outputs.code' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.api.needs | contains(["changes"])' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.harness.needs | contains(["changes"])' .gitea/workflows/ci.yml >/dev/null && grep -q 'dorny/paths-filter@v4' .gitea/workflows/ci.yml && grep -q "needs.changes.outputs.code == 'true'" .gitea/workflows/ci.yml && echo OK + + + - ci.yml contains a job named `changes` using `dorny/paths-filter@v4` with `id: filter` + - the `changes` job declares `permissions: pull-requests: read` (job-scoped) and `outputs.code: ${{ steps.filter.outputs.code }}` + - the `changes` job has NO `actions/checkout` step + - the `code` filter lists the positive code patterns (`**/*.ts`, `apps/**`, `pnpm-lock.yaml`, `Dockerfile`, `docker-compose*.yml`, etc.) — not a `docs` filter + - the `api` job has `needs: [changes]` and its `if` is `github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'` + - the `harness` job has `needs: [changes]` and the same combined `if` + - the api/harness `services`, `env`, and step bodies are unchanged from the pre-edit file + - ci.yml parses as valid YAML (`yq` exits 0) + + The `changes` job classifies each PR; `api` and `harness` skip on doc-only PRs and run on code PRs. SC-1/SC-2 job-skip behavior is wired. + + + + Task 2: Add the always-running gate aggregate job (Gitea-safe per-job result checks) + + - .gitea/workflows/ci.yml (post-Task-1 state — confirm final job names `fast-checks`/`changes`/`api`/`harness`; gate goes at end of file) + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Pattern 3 gate job + "Code Examples → Gate job"; Pitfall 2 — `contains(needs.*.result, ...)` is BROKEN on Gitea 1.26.2 issue #31007; Gitea-Specific Notes #1 skipped-status quirk, #2 if:always() fixed in 1.21.8, #3 contains bug) + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("`gate` job (new)" — exact YAML and the publish.yml `if: always()` analog) + + + Append a new `gate` job at the end of ci.yml using the exact YAML from RESEARCH "Code Examples → Gate job" / PATTERNS "`gate` job (new)". The job: `runs-on: ubuntu-latest`; `needs: [fast-checks, changes, api, harness]`; `if: always()` (job-scoped — without it a skipped upstream skips the gate and branch protection on `CI / gate` deadlocks; the if:always() deadlock bug was fixed in Gitea 1.21.8 and this instance is 1.26.2). One step `Check all required jobs passed or were skipped` running a bash script that: (1) fails with exit 1 if `${{ needs.fast-checks.result }}` is not `success` (fast-checks always runs — never skipped); (2) loops over `${{ needs.api.result }}` and `${{ needs.harness.result }}` and fails with exit 1 if either is neither `success` NOR `skipped`; (3) echoes a pass message otherwise. + + CRITICAL Gitea 1.26.2 constraint: do NOT use `contains(needs.*.result, 'success')` or any `needs.*.result` wildcard — issue #31007 makes the wildcard return false even when jobs succeed. Reference each job result individually via `needs.fast-checks.result`, `needs.api.result`, `needs.harness.result`. Treat `skipped` as acceptable ONLY for `api`/`harness` (the conditionally-skippable jobs), never for `fast-checks`. Do not add `changes` to the pass/fail evaluation logic — it is in `needs` for ordering only; its result is not gated (a failure there already fails downstream `if` evaluation). + + This is the change-set that, once merged, makes Gitea start emitting a `CI / gate` commit-status — the prerequisite for Plan 03's branch-protection update. Do not touch branch protection here. + + + command -v yq >/dev/null 2>&1 && yq -e '.jobs.gate' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.gate.needs | contains(["fast-checks","changes","api","harness"])' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.gate.if == "always()"' .gitea/workflows/ci.yml >/dev/null && grep -q 'needs.fast-checks.result' .gitea/workflows/ci.yml && grep -q 'needs.api.result' .gitea/workflows/ci.yml && grep -q 'needs.harness.result' .gitea/workflows/ci.yml && ! grep -q 'needs.\*.result' .gitea/workflows/ci.yml && echo OK + + + - ci.yml contains a job named `gate` with `if: always()` and `needs: [fast-checks, changes, api, harness]` + - the gate references `needs.fast-checks.result`, `needs.api.result`, and `needs.harness.result` individually + - the gate does NOT contain `contains(needs.*.result` or any `needs.*.result` wildcard + - the gate fails (exit 1) when fast-checks != success + - the gate accepts `success` OR `skipped` for api and harness, and fails on any other result + - ci.yml parses as valid YAML (`yq` exits 0) + + An always-running `gate` job aggregates the four jobs using Gitea-safe per-job result checks, passing when fast-checks succeeds and each heavy job is success-or-skipped. Once merged it emits `CI / gate`, unblocking Plan 03. SC-3's gating surface exists in YAML. + + + + + +This phase introduces the following new symbols (Plan 02 portion). The plan-review source-grounding pass must treat these as newly-created, not drift: + +- `changes` — new ci.yml job name (dorny/paths-filter@v4) +- `code` — new paths-filter output name consumed by `api`/`harness` `if:` +- `gate` — new ci.yml aggregate job name (the always-running required surface) +- `CI / gate` — new commit-status context Gitea emits once this lands (required by Plan 03) +- `dorny/paths-filter@v4` — newly-referenced third-party action + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| PR author → CI gating decision | a PR's changed-file set decides whether the heavy code jobs run — the core security-relevant invariant of this phase | +| github.com → CI runner | `dorny/paths-filter@v4` is a third-party action resolved and executed on the runner with a scoped token | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-15-04 | Elevation of Privilege / bypass (CORE) | a misconfigured gate passes when fast-checks fails, letting unreviewed code merge | mitigate | gate fails (exit 1) on `fast-checks != success`; `skipped` accepted ONLY for api/harness, never fast-checks; uses individual `needs.X.result` (not the broken Gitea `contains(needs.*.result)`) so it cannot silently always-pass (Pitfall 2). | +| T-15-05 | Spoofing (skip-detection) | a PR that actually changes code is misclassified doc-only and skips api/harness | mitigate | `code` is the POSITIVE filter — any new/ambiguous file type matches `code` and runs the full gate; doc-only requires EVERY changed file to fall outside the code patterns; `code` includes `**/*.ts`, `apps/**`, lockfile, Dockerfile, compose. | +| T-15-06 | Tampering (supply chain) | `dorny/paths-filter@v4` third-party action | mitigate | Pinned to the `@v4` tag; RESEARCH Package Legitimacy Audit verdict OK (~5 yrs, widely used, github.com/dorny/paths-filter); runs with `pull-requests: read` only — no write, no secrets access (RESEARCH Security Domain). | +| T-15-07 | Denial of Service (deadlock) | a required check that never reports blocks the PR forever | mitigate | api/harness are NEVER added as required checks (Plan 03); the only gating surfaces are `fast-checks` (always runs) and `gate` (`if: always()`, always reports); no workflow-level `on: paths` filter on any required job. | +| T-15-SC | Tampering | npm/action installs | mitigate | No package-manager install task in this plan (paths-filter is a `uses:` action, not an npm dep; markdownlint-cli2 install is Plan 01). RESEARCH Package Legitimacy Audit present; no `[ASSUMED]`/`[SUS]` packages → no blocking-human checkpoint required. | + + + +Behavioral (observable in CI after this PR merges and on a follow-up test PR — see 15-VALIDATION.md): +- Doc-only PR: `changes` emits `code=false`; `api`/`harness` show skipped; `fast-checks` runs; `gate` passes. +- Code PR: `changes` emits `code=true`; all three run; `gate` passes when green, fails when any heavy job fails. +- Gate-fail path: a deliberately failing fast-checks (or api/harness) makes `gate` exit 1 → merge blocked. + +Static (pre-merge): +- `yq` parses ci.yml; `changes`/`gate` jobs present; api/harness carry `needs: [changes]` + combined `if`; gate uses individual `needs.X.result`, no wildcard. + + + +Maps to Phase 15 Success Criteria 1, 2, and the YAML half of 3: +- SC-1: doc-only PR skips api/harness, still runs fast-checks. +- SC-2: code PR runs all three; a failure blocks the merge (via the gate). +- SC-3 (YAML half): an always-running `CI / gate` aggregate exists that passes when each heavy job is success-or-skipped; the branch-protection required-check change is Plan 03. + + + +Create `.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-SUMMARY.md` when done. + diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-03-PLAN.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-03-PLAN.md new file mode 100644 index 0000000..583d44a --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-03-PLAN.md @@ -0,0 +1,144 @@ +--- +phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs +plan: 03 +type: execute +wave: 3 +depends_on: + - 15-02 +files_modified: + - .gitea/workflows/publish.yml +autonomous: false +requirements: [] +must_haves: + truths: + - "Branch protection on main requires CI / fast-checks + CI / gate" + - "Branch protection on main no longer requires CI / api or CI / harness directly" + - "A doc-only PR (which skips api/harness) is mergeable — no missing-required-check deadlock" + - "publish.yml's safety-gate comment names the new required checks (fast-checks + gate)" + artifacts: + - path: ".gitea/workflows/publish.yml" + provides: "Updated safety-gate comment naming the new required checks" + contains: "CI / gate" + key_links: + - from: "Gitea branch-protection (main)" + to: "ci.yml gate job" + via: "required status check 'CI / gate' (always-running aggregate)" + pattern: "CI / gate" +--- + + +Finalize the gating surface: update Gitea branch protection on `main` to require `CI / fast-checks` + `CI / gate` and DROP the now-skippable `CI / api` and `CI / harness` direct requirements, and update the `publish.yml` safety-gate comment to name the new required checks. + +This delivers Success Criterion 3 — the only step that makes doc-only PRs actually mergeable without a missing-required-check deadlock. + +Purpose: With the heavy jobs conditionally skipped (Plan 02), a doc-only PR no longer emits `CI / api` / `CI / harness` statuses; leaving them required would deadlock the merge. The always-running `CI / gate` is the correct gating surface. +Output: A Gitea branch-protection change (operator checkpoint — not automatable in YAML) + a one-comment edit to publish.yml. + +ORDERING HAZARD (read before executing): This plan MUST run only AFTER Plan 02's ci.yml change has merged to `main` and Gitea has emitted at least one `CI / gate` commit-status. Dropping `CI / api` + `CI / harness` before `CI / gate` exists would leave `main` with no valid heavy-job gate, and adding `CI / gate` as required before it has ever reported can itself block PRs. Confirm `CI / gate` has appeared on a recent run before changing branch protection. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md +@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md + + + + + + Task 1: Update the publish.yml safety-gate comment to name the new required checks + + - .gitea/workflows/publish.yml (lines 13-17 — the "Safety gate" comment block currently naming "the three required checks (CI / fast-checks, CI / api, CI / harness)") + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("publish.yml Comment Update (advisory)" section — exact current text and the required-check change) + + + Edit ONLY the safety-gate comment in publish.yml (lines ~14-17). Change the clause that names "the three required checks (CI / fast-checks, CI / api, CI / harness)" to name the new required checks: `CI / fast-checks` and `CI / gate`. Reflect that `api`/`harness` are now conditionally skipped and gated via the always-running `CI / gate` aggregate rather than being required directly. Do not change any publish.yml job, step, env, or trigger — comment text only. This is the documentation half; the actual protection change is Task 2 (operator). + + + grep -q 'CI / gate' .gitea/workflows/publish.yml && grep -q 'CI / fast-checks' .gitea/workflows/publish.yml && ! grep -qE 'three required checks \(CI / fast-checks, CI / api, CI / harness\)' .gitea/workflows/publish.yml && command -v yq >/dev/null 2>&1 && yq -e '.jobs.publish' .gitea/workflows/publish.yml >/dev/null && echo OK + + + - publish.yml safety-gate comment names `CI / fast-checks` and `CI / gate` as the required checks + - publish.yml safety-gate comment no longer asserts `CI / api` and `CI / harness` are required (the old "three required checks" line is replaced) + - no publish.yml job/step/env/trigger changed (only comment text); publish.yml still parses as valid YAML + + publish.yml's safety-gate rationale matches the new branch-protection reality. + + + + Task 2: Operator — update Gitea branch protection on main (drop api/harness, require gate) + + - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Gitea-Specific Notes #4 — branch-protection required-check update is a MANUAL Gitea admin step; Pitfall 1 — required-check deadlock from skipped jobs) + + + Plan 02 added an always-running `CI / gate` aggregate job to ci.yml and made `CI / api` / `CI / harness` conditionally skipped on doc-only PRs. Once Plan 02 merged, Gitea emits a `CI / gate` commit-status on every PR run. Branch protection still requires the old `CI / api` + `CI / harness` contexts, which a doc-only PR will never emit — so until you make this change, doc-only PRs deadlock on "missing required checks". This change is a Gitea admin action that cannot be done in a workflow file. + + + PRECONDITION — confirm first: open the most recent PR run in Gitea Actions for this repo and verify a `CI / gate` job ran and reported a status. Do NOT proceed until `CI / gate` has appeared at least once (Plan 02 must already be merged to main). If it has not appeared, stop — the protection change is premature. + + Then update branch protection: + 1. Gitea → this repo → Settings → Branches → edit the protection rule for `main`. + 2. Under "Status Check Patterns" / required status checks, set the required contexts to EXACTLY these two: + - `CI / fast-checks` + - `CI / gate` + 3. REMOVE these two from the required list (they are now conditionally skipped and gated via `CI / gate`): + - `CI / api` + - `CI / harness` + 4. Save the protection rule. + (Alternative: the `tea` CLI / Gitea API can set branch-protection `status_check_contexts` to `["CI / fast-checks", "CI / gate"]` — login Bergerhouse — if you prefer not to use the UI.) + + VERIFY the change end-to-end: + - Open a throwaway DOC-ONLY PR (edit only a `*.md` under docs/ or the repo root). Expect: `fast-checks` runs, `api`/`harness` show skipped, `CI / gate` passes, and the PR is MERGEABLE (no "missing required checks"). This proves SC-1 + SC-3. + - Open a throwaway CODE PR (touch a `*.ts` file). Expect: `fast-checks`, `api`, `harness`, and `CI / gate` all run; the PR is mergeable only when all are green. This proves SC-2. + - Close both throwaway PRs without merging. + + Type "approved" once branch protection requires exactly `CI / fast-checks` + `CI / gate`, `CI / api` and `CI / harness` are no longer required, and the doc-only + code throwaway PRs behaved as described. Or describe what differed. + + + + + +This phase introduces the following new configuration state (Plan 03 portion). The plan-review source-grounding pass must treat these as newly-created, not drift: + +- Branch-protection required checks on `main` changed to `CI / fast-checks` + `CI / gate` (Gitea admin state, not a repo file) +- publish.yml safety-gate comment updated to name the new required checks + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| operator → Gitea branch-protection config | the required-check set decides whether unreviewed/failing code can merge to main | +| skipped heavy job → branch-protection check list | a skipped job may emit no commit-status; a still-required skipped context would deadlock | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-15-08 | Denial of Service (deadlock) | leaving `CI / api`/`CI / harness` required after the skip logic ships | mitigate | This plan explicitly drops both from required checks and requires the always-running `CI / gate` instead (RESEARCH Pitfall 1 / Gitea-Specific Note #4). | +| T-15-09 | Elevation of Privilege (bypass window) | dropping the heavy-job requirements BEFORE `CI / gate` exists leaves main ungated for code | mitigate | Ordering hazard called out: this plan is wave 3, `depends_on: [15-02]`; the checkpoint's PRECONDITION requires confirming `CI / gate` has reported at least once before any required-check edit. | +| T-15-10 | Tampering (false sense of gating) | publish.yml comment still claims api/harness are required, misleading a future maintainer | mitigate | Task 1 updates the comment to name `CI / fast-checks` + `CI / gate`. | + + + +- Gitea branch protection for `main` requires exactly `CI / fast-checks` + `CI / gate`; `CI / api` and `CI / harness` are not required. +- Throwaway doc-only PR is mergeable with api/harness skipped (SC-1/SC-3). +- Throwaway code PR runs and gates on all jobs (SC-2). +- publish.yml safety-gate comment names the new required checks. + + + +Maps to Phase 15 Success Criterion 3: branch protection requires `CI / fast-checks` + an always-running `CI / gate` aggregate; the direct `api`/`harness` requirements are dropped so a skipped heavy job never deadlocks the merge. (SC-1 doc-only mergeability and SC-2 code-PR gating are confirmed end-to-end here against the live protection rule.) + + + +Create `.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-03-SUMMARY.md` when done. + diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md index b6653b5..dfd2486 100644 --- a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-VALIDATION.md @@ -2,7 +2,7 @@ phase: 15 slug: ci-skip-api-harness-jobs-for-doc-only-prs status: draft -nyquist_compliant: false +nyquist_compliant: true wave_0_complete: false created: 2026-06-12 --- @@ -11,34 +11,43 @@ created: 2026-06-12 > Per-phase validation contract for feedback sampling during execution. +This phase is CI workflow config + tool config + mechanical markdown fixes. There is +no application business logic; the gate-job result logic is validated by static YAML +checks plus real CI behavior on doc-only vs code PRs (not a unit-test framework). + --- ## Test Infrastructure | Property | Value | |----------|-------| -| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | -| **Config file** | {path or "none — Wave 0 installs"} | -| **Quick run command** | `{quick command}` | -| **Full suite command** | `{full command}` | -| **Estimated runtime** | ~{N} seconds | +| **Framework** | Static (markdownlint-cli2 + Prettier + `yq`/`grep`) for local checks; Gitea Actions CI (behavioral) for SC-1/2/3 | +| **Config file** | `.markdownlint-cli2.jsonc` (new, Plan 01); `.gitea/workflows/ci.yml` (modified, Plans 01/02) | +| **Quick run command** | `pnpm md:lint && pnpm format:check` | +| **Full suite command** | `pnpm md:lint && pnpm format:check` locally; then a doc-only PR + a code PR observed in Gitea Actions | +| **Estimated runtime** | local ~3s; CI doc-only PR ~30s; CI code PR ~5min | --- ## Sampling Rate -- **After every task commit:** Run `{quick run command}` -- **After every plan wave:** Run `{full suite command}` -- **Before `/gsd-verify-work`:** Full suite must be green -- **Max feedback latency:** {N} seconds +- **After every task commit:** Run `pnpm md:lint` (Plan 01) or `yq -e '.jobs.' .gitea/workflows/ci.yml` (Plan 02). +- **After every plan wave:** Run `pnpm md:lint && pnpm format:check`; for Plan 02, parse ci.yml with `yq` and grep the gate's per-job result checks. +- **Before `/gsd-verify-work`:** local checks green; CI behavioral checks observed on throwaway PRs (Plan 03 checkpoint). +- **Max feedback latency:** ~3s local; CI behavioral confirmation is the operator checkpoint in Plan 03. --- ## Per-Task Verification Map -| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | -|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | +| Task ID | Plan | Wave | Success Criterion | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------------|------------|-----------------|-----------|-------------------|-------------|--------| +| 15-01-01 | 01 | 1 | SC-4 (setup) | T-15-01/02/03 | Pinned devDep; `.planning/**` excluded from lint scope | static | `test -f .markdownlint-cli2.jsonc && node -e "require('./package.json').scripts['md:lint']"` | ❌ W0 (config new) | ⬜ pending | +| 15-01-02 | 01 | 1 | SC-4 | T-15-03 | Gate can fail on a real violation; baseline green; no Prettier conflict | static | `pnpm md:lint && pnpm format:check` | ✅ (after fixes) | ⬜ pending | +| 15-02-01 | 02 | 2 | SC-1, SC-2 | T-15-05/06 | Positive `code` filter — ambiguous files default to full gate; action pinned @v4 | static | `yq -e '.jobs.changes.outputs.code' ci.yml && grep "needs.changes.outputs.code == 'true'" ci.yml` | ❌ W0 (jobs new) | ⬜ pending | +| 15-02-02 | 02 | 2 | SC-1, SC-2, SC-3(YAML) | T-15-04/07 | Gate fails on fast-checks!=success; individual `needs.X.result` (no broken wildcard) | static | `yq -e '.jobs.gate.if == "always()"' ci.yml && grep 'needs.fast-checks.result' ci.yml && ! grep 'needs.\*.result' ci.yml` | ❌ W0 (gate new) | ⬜ pending | +| 15-03-01 | 03 | 3 | SC-3 (docs) | T-15-10 | publish.yml comment matches new required checks | static | `grep 'CI / gate' .gitea/workflows/publish.yml` | ✅ (publish.yml exists) | ⬜ pending | +| 15-03-02 | 03 | 3 | SC-1, SC-2, SC-3 | T-15-08/09 | Drop api/harness required; require gate; only after `CI / gate` reports | manual (operator) | Gitea branch-protection UI/API + throwaway doc-only & code PRs | Manual | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* @@ -46,31 +55,33 @@ created: 2026-06-12 ## Wave 0 Requirements -- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} -- [ ] `{tests/conftest.py}` — shared fixtures -- [ ] `{framework install}` — if no framework detected +These artifacts do not exist before this phase and are created within it (no separate test scaffold needed — the "tests" are the static checks above): -*If none: "Existing infrastructure covers all phase requirements."* +- [ ] `.markdownlint-cli2.jsonc` — new config (Plan 01 Task 1); required before `pnpm md:lint` runs +- [ ] root `md:lint` script + `markdownlint-cli2` devDep (Plan 01 Task 1) +- [ ] 13 baseline markdown violations fixed (Plan 01 Task 2) — needed for `pnpm md:lint` to pass +- [ ] `changes` + `gate` jobs in ci.yml (Plan 02) — needed before any `yq`/grep gate check passes +- [ ] Gitea branch-protection update — manual operator step (Plan 03 Task 2), only after `CI / gate` has reported at least once --- ## Manual-Only Verifications -| Behavior | Requirement | Why Manual | Test Instructions | -|----------|-------------|------------|-------------------| -| {behavior} | REQ-{XX} | {reason} | {steps} | - -*If none: "All phase behaviors have automated verification."* +| Behavior | Success Criterion | Why Manual | Test Instructions | +|----------|-------------------|------------|-------------------| +| Branch protection requires `CI / fast-checks` + `CI / gate`, not `CI / api`/`CI / harness` | SC-3 | Gitea admin config — cannot be set in a workflow YAML file (RESEARCH Gitea-Specific Note #4) | Gitea → Settings → Branches → edit `main` rule; set required contexts to exactly `CI / fast-checks` + `CI / gate`; remove `CI / api` + `CI / harness` (Plan 03 Task 2) | +| Doc-only PR is mergeable with api/harness skipped | SC-1, SC-3 | Requires a live PR run + the live protection rule | Open a throwaway `*.md`-only PR; confirm api/harness skipped, `CI / gate` passes, PR mergeable | +| Code PR runs and gates on all jobs | SC-2 | Requires a live PR run | Open a throwaway `*.ts` PR; confirm fast-checks/api/harness/gate all run; PR mergeable only when green | --- ## Validation Sign-Off -- [ ] All tasks have `` verify or Wave 0 dependencies -- [ ] Sampling continuity: no 3 consecutive tasks without automated verify -- [ ] Wave 0 covers all MISSING references -- [ ] No watch-mode flags -- [ ] Feedback latency < {N}s -- [ ] `nyquist_compliant: true` set in frontmatter +- [x] All tasks have ``/static verify or are the designated manual operator checkpoint +- [x] Sampling continuity: no 3 consecutive code tasks without automated verify (the only manual task is the unavoidable Gitea-admin checkpoint) +- [x] Wave 0 covers all newly-created artifacts +- [x] No watch-mode flags +- [x] Feedback latency ~3s local +- [x] `nyquist_compliant: true` set in frontmatter -**Approval:** {pending / approved YYYY-MM-DD} +**Approval:** pending -- 2.54.0 From c687efce27f4d06f4bc485b56a68aa30cd87d3f6 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:30:50 -0400 Subject: [PATCH 05/16] docs(15): record planning complete + roadmap wave annotations --- .planning/ROADMAP.md | 8 +++++++- .planning/STATE.md | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9c9a82b..8f4f626 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -281,11 +281,17 @@ Plans: - **`.planning/*` is push-direct, never linted** — planning bookkeeping bypasses CI via the Unprotected file pattern, so markdownlint never sees it; scope the lint glob to `docs/` + repo-root/app `*.md` and exclude `.planning/**` (and any generated markdown) to avoid a baseline cleanup of churny bookkeeping files. **Plans**: 3 plans (3 waves) - Plans: +**Wave 1** - [ ] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4) + +**Wave 2** *(blocked on Wave 1 completion)* + - [ ] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML) + +**Wave 3** *(blocked on Wave 2 completion)* + - [ ] 15-03-PLAN.md — operator branch-protection checkpoint (require `CI / fast-checks` + `CI / gate`, drop api/harness) + publish.yml comment update (SC-3) **UI hint**: no diff --git a/.planning/STATE.md b/.planning/STATE.md index bc4cddf..236d087 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,8 +4,8 @@ milestone: v1.1 milestone_name: Operability & Polish status: executing stopped_at: Phase 14 context gathered -last_updated: "2026-06-12T13:10:04.656Z" -last_activity: 2026-06-12 +last_updated: "2026-06-12T14:30:50.407Z" +last_activity: 2026-06-12 -- Phase 15 planning complete progress: total_phases: 18 completed_phases: 4 @@ -27,8 +27,8 @@ See: .planning/PROJECT.md (updated 2026-06-10) Phase: 999.1 Plan: Not started -Status: Executing Phase 14 -Last activity: 2026-06-12 +Status: Ready to execute +Last activity: 2026-06-12 -- Phase 15 planning complete ## Performance Metrics -- 2.54.0 From 4728c4153906bb7c5ff7879a3339650887d5cf56 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:40:07 -0400 Subject: [PATCH 06/16] docs(phase-15): mark phase execution started --- .planning/STATE.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 236d087..7cbbfb5 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,12 +4,12 @@ milestone: v1.1 milestone_name: Operability & Polish status: executing stopped_at: Phase 14 context gathered -last_updated: "2026-06-12T14:30:50.407Z" -last_activity: 2026-06-12 -- Phase 15 planning complete +last_updated: "2026-06-12T14:39:49.644Z" +last_activity: 2026-06-12 -- Phase 15 execution started progress: total_phases: 18 completed_phases: 4 - total_plans: 12 + total_plans: 15 completed_plans: 12 percent: 22 --- @@ -21,14 +21,14 @@ progress: See: .planning/PROJECT.md (updated 2026-06-10) **Core value:** One color-coded family calendar (shared + personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store -**Current focus:** Phase 14 — desktop-e2e-coverage +**Current focus:** Phase 15 — ci-skip-api-harness-jobs-for-doc-only-prs ## Current Position -Phase: 999.1 -Plan: Not started -Status: Ready to execute -Last activity: 2026-06-12 -- Phase 15 planning complete +Phase: 15 (ci-skip-api-harness-jobs-for-doc-only-prs) — EXECUTING +Plan: 1 of 3 +Status: Executing Phase 15 +Last activity: 2026-06-12 -- Phase 15 execution started ## Performance Metrics -- 2.54.0 From adb7641c195f643a0187f1cf119b461545eabbf3 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:42:52 -0400 Subject: [PATCH 07/16] feat(15-01): add markdownlint-cli2 + md:lint script + root config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install markdownlint-cli2@0.22.1 as root workspace devDependency - Add md:lint script (no glob args — globs/ignores live in config file) - Create .markdownlint-cli2.jsonc with prettier preset + content rules - Enable MD001/MD024/MD040/MD031/MD051/MD052; disable MD041/MD034/MD036 - Glob: docs/**/*.md, *.md, apps/**/*.md; ignores .planning/** and node_modules --- .markdownlint-cli2.jsonc | 23 ++ package.json | 4 +- pnpm-lock.yaml | 644 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 .markdownlint-cli2.jsonc diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..3ec0ab4 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,23 @@ +// .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/**"] +} diff --git a/package.json b/package.json index dbcb494..f8894e8 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "lint": "pnpm -r --if-present lint", "typecheck": "pnpm -r typecheck", "format": "prettier --write .", - "format:check": "prettier --check ." + "format:check": "prettier --check .", + "md:lint": "markdownlint-cli2" }, "devDependencies": { "@eslint/js": "9.39.4", @@ -20,6 +21,7 @@ "eslint-config-prettier": "10.1.8", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.1.1", + "markdownlint-cli2": "0.22.1", "prettier": "3.8.4", "typescript-eslint": "8.61.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b9ee3c..77cfa5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: eslint-plugin-react-hooks: specifier: 7.1.1 version: 7.1.1(eslint@9.39.4) + markdownlint-cli2: + specifier: 0.22.1 + version: 0.22.1 prettier: specifier: 3.8.4 version: 3.8.4 @@ -1337,6 +1340,18 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} @@ -1670,6 +1685,10 @@ packages: '@schedule-x/theme-default@4.6.0': resolution: {integrity: sha512-SM3bcvROJeG7ScH14lBIHZSTOBTR8h+oTswbpx1gpscddUfQfAnu7cFW4OJJI+ii+Si1IdTd4T3+rrnKd4HDDg==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1729,6 +1748,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1738,6 +1760,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.19.19': resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} @@ -1755,6 +1783,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/web-push@3.6.4': resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} @@ -1876,6 +1907,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1991,6 +2026,10 @@ packages: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2029,6 +2068,15 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2039,6 +2087,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -2102,6 +2154,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2129,6 +2184,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -2250,6 +2308,10 @@ packages: electron-to-chromium@1.5.366: resolution: {integrity: sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -2393,6 +2455,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2402,6 +2468,9 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2418,6 +2487,10 @@ packages: filelist@1.0.6: resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2476,6 +2549,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2494,6 +2571,10 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -2512,6 +2593,10 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globby@16.2.0: + resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} + engines: {node: '>=20'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2613,6 +2698,12 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2645,6 +2736,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2661,6 +2755,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2676,10 +2773,18 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-obj@1.0.1: resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} engines: {node: '>=0.10.0'} + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -2752,6 +2857,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + js-yaml@4.2.0: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true @@ -2787,6 +2896,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} @@ -2804,6 +2916,10 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2889,6 +3005,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + linkify-it@5.0.1: + resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2935,10 +3054,114 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + markdownlint-cli2-formatter-default@0.0.6: + resolution: {integrity: sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==} + peerDependencies: + markdownlint-cli2: '>=0.0.4' + + markdownlint-cli2@0.22.1: + resolution: {integrity: sha512-X14ZbytybDCXAViDmtN4DKLt9ZTrRn+oOrxTYlg3a65jS6QcYYbAkGPh/En2L/GDNbFYJ6lKaQSUNrrbN1bPrw==} + engines: {node: '>=20'} + hasBin: true + + markdownlint@0.40.0: + resolution: {integrity: sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==} + engines: {node: '>=20'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@4.0.0: + resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -3057,6 +3280,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -3081,6 +3307,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -3130,10 +3360,17 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: @@ -3214,6 +3451,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown@1.0.3: resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3227,6 +3468,9 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} @@ -3315,10 +3559,18 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + smob@1.6.2: resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} engines: {node: '>=20.0.0'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3349,6 +3601,10 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-width@8.1.0: + resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} + engines: {node: '>=20'} + string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -3372,6 +3628,10 @@ packages: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-comments@2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} @@ -3436,6 +3696,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -3501,6 +3765,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3524,6 +3791,10 @@ packages: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -4904,6 +5175,18 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@oxc-project/types@0.133.0': {} '@playwright/test@1.60.0': @@ -5112,6 +5395,8 @@ snapshots: '@schedule-x/theme-default@4.6.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/spec@1.1.0': {} '@tanstack/query-core@5.101.0': {} @@ -5191,12 +5476,20 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} + '@types/katex@0.16.8': {} + + '@types/ms@2.1.0': {} + '@types/node@22.19.19': dependencies: undici-types: 6.21.0 @@ -5213,6 +5506,8 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} + '@types/web-push@3.6.4': dependencies: '@types/node': 22.19.19 @@ -5385,6 +5680,8 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -5524,6 +5821,10 @@ snapshots: dependencies: balanced-match: 4.0.4 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.33 @@ -5564,6 +5865,12 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5572,6 +5879,8 @@ snapshots: commander@2.20.3: {} + commander@8.3.0: {} + common-tags@1.8.2: {} concat-map@0.0.1: {} @@ -5630,6 +5939,10 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -5652,6 +5965,10 @@ snapshots: detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -5687,6 +6004,8 @@ snapshots: electron-to-chromium@1.5.366: {} + entities@4.5.0: {} + entities@6.0.1: {} es-abstract@1.24.2: @@ -5996,12 +6315,24 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} fast-uri@3.1.2: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -6014,6 +6345,10 @@ snapshots: dependencies: minimatch: 5.1.9 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6071,6 +6406,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6101,6 +6438,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -6121,6 +6462,15 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globby@16.2.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -6208,6 +6558,13 @@ snapshots: hasown: 2.0.4 side-channel: 1.1.0 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -6248,6 +6605,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -6266,6 +6625,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} is-module@1.0.0: {} @@ -6277,8 +6638,12 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-number@7.0.0: {} + is-obj@1.0.1: {} + is-path-inside@4.0.0: {} + is-potential-custom-element-name@1.0.1: {} is-property@1.0.2: {} @@ -6351,6 +6716,10 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -6394,6 +6763,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@6.2.1: dependencies: universalify: 2.0.1 @@ -6420,6 +6791,10 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + katex@0.16.47: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6480,6 +6855,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + linkify-it@5.0.1: + dependencies: + uc.micro: 2.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -6516,8 +6895,230 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.1 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdownlint-cli2-formatter-default@0.0.6(markdownlint-cli2@0.22.1): + dependencies: + markdownlint-cli2: 0.22.1 + + markdownlint-cli2@0.22.1: + dependencies: + globby: 16.2.0 + js-yaml: 4.1.1 + jsonc-parser: 3.3.1 + jsonpointer: 5.0.1 + markdown-it: 14.1.1 + markdownlint: 0.40.0 + markdownlint-cli2-formatter-default: 0.0.6(markdownlint-cli2@0.22.1) + micromatch: 4.0.8 + smol-toml: 1.6.1 + transitivePeerDependencies: + - supports-color + + markdownlint@0.40.0: + dependencies: + micromark: 4.0.2 + micromark-core-commonmark: 2.0.3 + micromark-extension-directive: 4.0.0 + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-math: 3.1.0 + micromark-util-types: 2.0.2 + string-width: 8.1.0 + transitivePeerDependencies: + - supports-color + math-intrinsics@1.1.0: {} + mdurl@2.0.0: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@4.0.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.47 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + min-indent@1.0.1: {} minimalistic-assert@1.0.1: {} @@ -6642,6 +7243,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -6661,6 +7272,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} playwright-core@1.60.0: {} @@ -6701,8 +7314,12 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} + queue-microtask@1.2.3: {} + react-dom@19.2.7(react@19.2.7): dependencies: react: 19.2.7 @@ -6792,6 +7409,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} + rolldown@1.0.3: dependencies: '@oxc-project/types': 0.133.0 @@ -6846,6 +7465,10 @@ snapshots: rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-array-concat@1.1.4: dependencies: call-bind: 1.0.9 @@ -6945,8 +7568,12 @@ snapshots: signal-exit@4.1.0: {} + slash@5.1.0: {} + smob@1.6.2: {} + smol-toml@1.6.1: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -6971,6 +7598,11 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + string-width@8.1.0: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.9 @@ -7021,6 +7653,10 @@ snapshots: is-obj: 1.0.1 is-regexp: 1.0.0 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-comments@2.0.1: {} strip-indent@3.0.0: @@ -7076,6 +7712,10 @@ snapshots: dependencies: tldts-core: 6.1.86 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -7160,6 +7800,8 @@ snapshots: typescript@5.9.3: {} + uc.micro@2.1.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -7180,6 +7822,8 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} + unicorn-magic@0.4.0: {} + unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 -- 2.54.0 From 46bea03757843d16ece32d951a43de3365b97bb9 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:46:01 -0400 Subject: [PATCH 08/16] feat(15-01): fix 13 markdown violations + wire md:lint to fast-checks CI step - Fix MD040 (11 bare fences): add language tags (text/bash) across 7 files - Fix MD031 (2 violations): add blank lines around fence in GETTING-STARTED.md - Wire 'Markdown lint' step to fast-checks job (after Format check, before Typecheck) - Reformat .markdownlint-cli2.jsonc per Prettier (trailing commas in JSONC) - pnpm md:lint exits 0; pnpm format:check exits 0; gate can fail on bare fence (verified) --- .gitea/workflows/ci.yml | 3 +++ .markdownlint-cli2.jsonc | 4 ++-- README.md | 4 ++-- apps/api/README.md | 2 +- apps/pwa/README.md | 2 +- apps/pwa/e2e/README.md | 2 +- docs/API.md | 4 ++-- docs/ARCHITECTURE.md | 4 ++-- docs/DEVELOPMENT.md | 4 ++-- docs/GETTING-STARTED.md | 2 ++ 10 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a146c0a..63952cd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: - name: Format check run: pnpm format:check + - name: Markdown lint + run: pnpm md:lint + - name: Typecheck run: pnpm typecheck diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 3ec0ab4..877f908 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -16,8 +16,8 @@ // 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 + "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/**"] + "ignores": [".planning/**", "node_modules/**", "**/node_modules/**", ".pnpm-store/**"], } diff --git a/README.md b/README.md index f96ba68..8d4efe6 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ The API listens on port 3000. The PWA build is served separately (Vite `preview` ## Monorepo Structure -``` +```text apps/ api/ Hono backend — CalDAV sync, OIDC auth, lists API, push notifications pwa/ React 19 PWA — calendar view, lists UI, service worker @@ -116,7 +116,7 @@ docker-compose.dev.yml Dev overrides (bind-mount src/, expose DB/Redis ports) FamilySync reads and writes calendars via CalDAV against Fastmail — not JMAP (not available for Fastmail calendars). Configure your Fastmail app password under the "Mail, Contacts & Calendars" scope. The principal URL follows the pattern: -``` +```text https://caldav.fastmail.com/dav/principals/user// ``` diff --git a/apps/api/README.md b/apps/api/README.md index dec2081..6ac07dd 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -16,7 +16,7 @@ Part of the [FamilySync monorepo](../../README.md). ## Source layout -``` +```text src/ index.ts Hono app entrypoint; server startup; background worker initialization routes/ diff --git a/apps/pwa/README.md b/apps/pwa/README.md index 1b05479..55e4d19 100644 --- a/apps/pwa/README.md +++ b/apps/pwa/README.md @@ -42,7 +42,7 @@ The API backend must also be running for most features. See [GETTING-STARTED.md] ## Source layout -``` +```text src/ api/ # Typed fetch wrappers for @familysync/api (client.ts, listsClient.ts) components/ # Shared UI components co-located with their *.test.tsx files diff --git a/apps/pwa/e2e/README.md b/apps/pwa/e2e/README.md index 54d2509..18ec683 100644 --- a/apps/pwa/e2e/README.md +++ b/apps/pwa/e2e/README.md @@ -73,7 +73,7 @@ Set `DB_PASSWORD` (and other non-default values) via the shell or the repo root The API enforces this via `apps/api/src/auth/devBypass.ts`: -``` +```text if (process.env.NODE_ENV === 'production') → bypass is a no-op (always) if (process.env.DEV_AUTH_BYPASS !== 'true') → bypass is a no-op ``` diff --git a/docs/API.md b/docs/API.md index cc7ed11..199ef2d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -514,7 +514,7 @@ Streams `heartbeat` events every 10 seconds indefinitely. Used as a Pangolin tun **Event format** -``` +```text event: heartbeat id: 0 data: {"ts":"2025-06-10T14:00:00.000Z","id":0} @@ -541,7 +541,7 @@ A `heartbeat` event is sent every 30 seconds to keep the Pangolin connection ali **Event format example** -``` +```text event: item:added id: 1-1718020800000 data: {"type":"item:added","listId":1,"payload":{"id":11,"listId":1,"text":"Eggs"}} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3efd060..53f8505 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -61,7 +61,7 @@ graph TD ## Directory Structure -``` +```text familysync/ ├── apps/ │ ├── api/ # Hono backend (Node 22 + TypeScript) @@ -168,7 +168,7 @@ familysync/ ### Backend modules -``` +```text routes/events.ts ──→ broker/expand.ts (read: RRULE expansion) ──→ calendarOutbox (DB) (write: enqueue) ──→ broker/sync.ts (write-sync after outbox drain) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 9c4d5f4..4b3bf8d 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -6,7 +6,7 @@ Local development setup and workflows for FamilySync — a pnpm monorepo with tw ## Repo Layout -``` +```text familysync/ ├── apps/ │ ├── api/ # Hono API server — Node.js 22, TypeScript, Drizzle/MariaDB @@ -19,7 +19,7 @@ familysync/ Key paths inside `apps/api/src/`: -``` +```text src/ ├── db/ │ ├── schema.ts # Drizzle table definitions (source of truth for migrations) diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index af5bd7d..755f327 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -49,9 +49,11 @@ Open `.env` and fill in the required values. See [docs/CONFIGURATION.md](CONFIGU - `DB_PASSWORD` and `DB_ROOT_PASSWORD` — pick any local passwords - `APP_PASSWORD_ENCRYPTION_KEY` — 64 hex characters; generate with: + ```bash node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ``` + - `DEV_AUTH_BYPASS=true` — bypasses the live Authelia OIDC flow for local dev - `DB_HOST=localhost` — the dev Docker Compose exposes MariaDB on the host at `localhost:3306` -- 2.54.0 From 4c5efb06041054176432d7012bbfd26702bbdfac Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:47:17 -0400 Subject: [PATCH 09/16] =?UTF-8?q?docs(15-01):=20complete=20markdown=20lint?= =?UTF-8?q?=20gate=20plan=20=E2=80=94=20SUMMARY.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../15-01-SUMMARY.md | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-SUMMARY.md diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-SUMMARY.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-SUMMARY.md new file mode 100644 index 0000000..c7a5973 --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-SUMMARY.md @@ -0,0 +1,137 @@ +--- +phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs +plan: "01" +subsystem: ci +tags: [markdownlint, ci, docs, fast-checks] +dependency_graph: + requires: [] + provides: + - markdownlint-cli2 root devDependency (0.22.1) + - md:lint root package.json script + - .markdownlint-cli2.jsonc config (Prettier-compatible preset + content rules) + - Markdown lint step in fast-checks CI job + affects: + - .gitea/workflows/ci.yml (fast-checks job — new Markdown lint step) + - package.json (new script + devDependency) + - 7 doc files (13 baseline violations fixed) +tech_stack: + added: + - markdownlint-cli2@0.22.1 (root devDependency) + patterns: + - Config-file-driven markdownlint (globs/ignores in .markdownlint-cli2.jsonc, not CLI args) + - markdownlint/style/prettier preset to avoid Prettier/markdownlint rule conflicts +key_files: + created: + - .markdownlint-cli2.jsonc + modified: + - package.json + - pnpm-lock.yaml + - .gitea/workflows/ci.yml + - README.md + - apps/api/README.md + - apps/pwa/README.md + - apps/pwa/e2e/README.md + - docs/API.md + - docs/ARCHITECTURE.md + - docs/DEVELOPMENT.md + - docs/GETTING-STARTED.md +decisions: + - "md:lint script uses no glob args — globs and ignores live in .markdownlint-cli2.jsonc so pnpm md:lint and npx markdownlint-cli2 behave identically (RESEARCH Open Question 3)" + - "MD031 re-enabled despite prettier preset disabling it — GETTING-STARTED.md has a genuine structural fix (fence inside list item with no blank lines); verified MD031 fix does not conflict with Prettier (format:check still 0 after fix)" + - "MD040 bare fences tagged as 'text' for plain-output blocks (SSE event format, directory trees); 'bash' for shell commands" + - "Prettier reformatted .markdownlint-cli2.jsonc (added trailing commas per JSONC trailingComma:all rule) — committed in Task 2" +metrics: + duration_minutes: 4 + completed_date: "2026-06-12" + tasks_completed: 2 + files_changed: 12 +--- + +# Phase 15 Plan 01: Markdown Lint Gate Summary + +**One-liner:** markdownlint-cli2@0.22.1 with Prettier-compatible config wired into fast-checks CI; 13 baseline violations fixed across 7 doc files, gate starts green. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Install markdownlint-cli2, add md:lint script, create .markdownlint-cli2.jsonc | adb7641 | .markdownlint-cli2.jsonc, package.json, pnpm-lock.yaml | +| 2 | Fix 13 baseline violations; green md:lint + format:check; wire fast-checks step | 46bea03 | .gitea/workflows/ci.yml, .markdownlint-cli2.jsonc (Prettier reformat), README.md, apps/api/README.md, apps/pwa/README.md, apps/pwa/e2e/README.md, docs/API.md, docs/ARCHITECTURE.md, docs/DEVELOPMENT.md, docs/GETTING-STARTED.md | + +## What Was Built + +### Task 1: markdownlint-cli2 install + config + +- Installed `markdownlint-cli2@0.22.1` as a root workspace devDependency via `pnpm add -D markdownlint-cli2@0.22.1 --workspace-root` +- Added `"md:lint": "markdownlint-cli2"` script to root `package.json` after `format:check` +- Created `.markdownlint-cli2.jsonc` at repo root with: + - `extends: "markdownlint/style/prettier"` — disables the 23 rules Prettier owns (including MD013 line-length, which would produce 700+ false positives against printWidth 100) + - Content rules ENABLED: MD001, MD024, MD040, MD031, MD051, MD052 + - Rules DISABLED: MD041 (CLAUDE.md starts with `## Project`), MD034 (pkg@version syntax in version tables), MD036 (docs/API.md uses `**Response 200**` as semantic labels) + - `globs`: `["docs/**/*.md", "*.md", "apps/**/*.md"]` + - `ignores`: `[".planning/**", "node_modules/**", "**/node_modules/**", ".pnpm-store/**"]` + +### Task 2: Baseline fixes + CI wiring + +Fixed all 13 violations: +- **MD040 (11 bare fences):** Added language tags across 7 files: + - `apps/api/README.md:19` — directory tree → `text` + - `apps/pwa/e2e/README.md:76` — pseudo-code block → `text` + - `apps/pwa/README.md:45` — directory tree → `text` + - `docs/API.md:517` — SSE event format → `text` + - `docs/API.md:544` — SSE event format example → `text` + - `docs/ARCHITECTURE.md:64` — directory tree → `text` + - `docs/ARCHITECTURE.md:171` — backend module flow → `text` + - `docs/DEVELOPMENT.md:9` — directory tree → `text` + - `docs/DEVELOPMENT.md:22` — directory tree → `text` + - `README.md:78` — monorepo structure → `text` + - `README.md:119` — CalDAV principal URL → `text` +- **MD031 (2 violations):** `docs/GETTING-STARTED.md:52-54` — added blank lines before ```` ```bash ```` and after closing ```` ``` ```` surrounding the `node -e` command inside a list item + +Wired CI step in `.gitea/workflows/ci.yml`: +```yaml + - name: Markdown lint + run: pnpm md:lint +``` +Inserted after `Format check`, before `Typecheck` in the `fast-checks` job. + +**Verification performed:** +- `pnpm md:lint` exits 0 (Summary: 0 error(s), 12 files scanned) +- `pnpm format:check` exits 0 (no Prettier conflict from MD031 blank-line additions) +- Gate-can-fail test: appended a bare fence to README.md, confirmed `pnpm md:lint` exits non-zero (1 error), then reverted via `git checkout -- README.md` (NOTE: this reverted the Task 2 README.md fixes; they were re-applied before the Task 2 commit) + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Prettier reformatted .markdownlint-cli2.jsonc** +- **Found during:** Task 2 format:check run +- **Issue:** `.markdownlint-cli2.jsonc` created in Task 1 had no trailing commas; Prettier's `trailingComma: "all"` added trailing commas to the JSONC properties and arrays +- **Fix:** Ran `pnpm format -- .markdownlint-cli2.jsonc` to apply Prettier's preferred style; verified `pnpm md:lint` still passes after the reformat +- **Files modified:** `.markdownlint-cli2.jsonc` +- **Commit:** 46bea03 (included in Task 2 commit with the doc fixes) + +**2. [Rule 1 - Bug] gate-can-fail test reverted README.md MD040 fixes** +- **Found during:** Task 2 gate verification test +- **Issue:** Using `git checkout -- README.md` to revert the test bare fence also reverted the two MD040 fixes previously applied in Task 2 (README.md:78 and :119). The gate-can-fail test used `git checkout -- README.md` for cleanup which is a blanket revert. +- **Fix:** Re-read README.md, re-applied both MD040 fixes (`text` language tags at lines 78 and 119), confirmed `pnpm md:lint` exits 0 before committing +- **Files modified:** README.md +- **Commit:** 46bea03 + +## Known Stubs + +None — plan goal (markdownlint gate) is fully wired and green. + +## Threat Surface Scan + +No new network endpoints, auth paths, file access patterns, or schema changes introduced. The only surface is the `markdownlint-cli2` devDependency (supply chain) — addressed in the plan's threat model (T-15-01: pinned to exact 0.22.1, package legitimacy audit OK in RESEARCH.md). + +## Self-Check: PASSED + +- `.markdownlint-cli2.jsonc` exists at repo root: FOUND +- `package.json` contains `md:lint` script and `markdownlint-cli2` devDep: FOUND +- `.gitea/workflows/ci.yml` contains `pnpm md:lint` step: FOUND +- Commit adb7641 exists: FOUND +- Commit 46bea03 exists: FOUND +- `pnpm md:lint` exits 0: VERIFIED +- `pnpm format:check` exits 0: VERIFIED -- 2.54.0 From 20ebd90554dda6c2c51f0c65596d7944843a2744 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:49:12 -0400 Subject: [PATCH 10/16] docs(phase-15): update tracking after wave 1 --- .planning/ROADMAP.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 8f4f626..dfa26a4 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -284,7 +284,7 @@ Plans: Plans: **Wave 1** -- [ ] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4) +- [x] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4) **Wave 2** *(blocked on Wave 1 completion)* @@ -314,7 +314,7 @@ Plans: | 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - | | 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 | | 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 | -| 15. Doc-Only CI Skip + MD Lint | v1.1 | 0/3 | Planned | - | +| 15. Doc-Only CI Skip + MD Lint | v1.1 | 1/3 | In Progress| | ## Backlog @@ -322,7 +322,7 @@ Plans: **Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern. **Requirements:** TBD -**Plans:** 1/1 plans complete +**Plans:** 1/3 plans executed Plans: -- 2.54.0 From 72604385bc37399db1587fb209b62ef5e4058c45 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:51:20 -0400 Subject: [PATCH 11/16] feat(15-02): add changes job and gate api/harness on code output - insert changes job (dorny/paths-filter@v4) before fast-checks - changes job: permissions pull-requests:read, outputs code, no checkout - code filter lists positive patterns: **/*.ts, apps/**, pnpm-lock.yaml, Dockerfile, etc. - api job: needs [changes] + if combined with needs.changes.outputs.code == 'true' - harness job: same needs/if pattern as api - services, env, and step bodies unchanged in both heavy jobs --- .gitea/workflows/ci.yml | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 63952cd..09ebf71 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -5,6 +5,31 @@ on: branches: [main] jobs: + 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' + fast-checks: runs-on: ubuntu-latest if: github.event_name == 'pull_request' @@ -42,8 +67,9 @@ jobs: api: runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - # Runs in PARALLEL with fast-checks (D-03) — no needs: dependency. + 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: mariadb: image: mariadb:11 @@ -128,8 +154,9 @@ jobs: harness: runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - # Runs in PARALLEL with fast-checks + api (D-03) — no needs: dependency. + 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: mariadb: image: mariadb:11 -- 2.54.0 From 547b12ca4a2ee68e243f08ef97e875cb100e8d0c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:52:18 -0400 Subject: [PATCH 12/16] feat(15-02): add always-running gate aggregate job (Gitea-safe per-job result checks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gate job needs [fast-checks, changes, api, harness] with if: always() - gate fails (exit 1) when fast-checks != success - gate accepts success OR skipped for api and harness, fails on any other result - uses individual needs.X.result checks (not wildcard) — Gitea 1.26.2 bug #31007 - once merged, emits CI / gate commit-status required by Plan 03 branch-protection update --- .gitea/workflows/ci.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 09ebf71..ae3a5aa 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -341,3 +341,26 @@ jobs: apps/pwa/test-results/ apps/pwa/playwright-report/ retention-days: 14 + + 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 + # NOTE: uses individual needs.X.result checks (not the wildcard aggregate) due to + # Gitea 1.26.2 bug #31007 where the wildcard expression returns false even when jobs succeed. + 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." -- 2.54.0 From e153f7c355d4a2ba03485f702d2e1ffed8189e5c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:53:22 -0400 Subject: [PATCH 13/16] =?UTF-8?q?docs(15-02):=20complete=20plan=2002=20sum?= =?UTF-8?q?mary=20=E2=80=94=20changes=20job=20+=20gate=20aggregate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../15-02-SUMMARY.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-SUMMARY.md diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-SUMMARY.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-SUMMARY.md new file mode 100644 index 0000000..5eb7d75 --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-SUMMARY.md @@ -0,0 +1,119 @@ +--- +phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs +plan: "02" +subsystem: ci +tags: [ci, gitea-actions, paths-filter, gate, doc-only-skip] +dependency_graph: + requires: [15-01] + provides: [CI / gate commit-status, doc-only PR skip] + affects: [.gitea/workflows/ci.yml] +tech_stack: + added: + - dorny/paths-filter@v4 (Gitea action for PR diff detection) + patterns: + - changes job with paths-filter + - always-running gate aggregate job + - Gitea-safe individual needs.X.result checks +key_files: + modified: + - .gitea/workflows/ci.yml +decisions: + - D-15-02-CHANGES-JOB: dorny/paths-filter@v4 with positive code filter; code==false means doc-only; ambiguous files default to full gate + - D-15-02-GATE-INDIVIDUAL: individual needs.X.result checks (not wildcard) due to Gitea 1.26.2 bug #31007 + - D-15-02-GATE-ALWAYS: if:always() on gate prevents deadlock on skipped upstream jobs (fix in Gitea 1.21.8, instance is 1.26.2) +metrics: + duration_minutes: 5 + completed_date: "2026-06-12" + tasks_completed: 2 + tasks_total: 2 + files_modified: 1 +--- + +# Phase 15 Plan 02: CI doc-only skip + gate aggregate Summary + +**One-liner:** `changes` job (dorny/paths-filter@v4) classifies each PR; `api`/`harness` skip on `code==false`; always-running `gate` job aggregates all results using Gitea-safe individual `needs.X.result` checks, emitting the `CI / gate` status that Plan 03's branch-protection update requires. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Add changes job and gate api/harness on code output | 7260438 | .gitea/workflows/ci.yml | +| 2 | Add always-running gate aggregate job | 547b12c | .gitea/workflows/ci.yml | + +## What Was Built + +**Task 1 — changes job + conditional api/harness:** + +A new `changes` job using `dorny/paths-filter@v4` was inserted before `fast-checks` in `.gitea/workflows/ci.yml`. It: +- Runs on `ubuntu-latest` with `if: github.event_name == 'pull_request'` +- Declares `permissions: pull-requests: read` (job-scoped, required by paths-filter v4) +- Emits `outputs.code: ${{ steps.filter.outputs.code }}` +- Contains a single `uses: dorny/paths-filter@v4` step with `id: filter` and NO `actions/checkout` +- Defines a positive `code` filter covering `**/*.ts`, `**/*.tsx`, `**/*.js`, `**/*.json`, `**/*.yaml`, `**/*.yml`, `apps/**`, `packages/**`, `pnpm-lock.yaml`, `Dockerfile`, `docker-compose*.yml` + +The `api` and `harness` jobs were modified: +- Added `needs: [changes]` +- Changed `if` from `github.event_name == 'pull_request'` to `github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'` +- Updated inline comment to say "skipped for doc-only PRs" +- All services, env, and step bodies left unchanged + +**Task 2 — gate aggregate job:** + +A new `gate` job was appended at the end of `ci.yml`: +- `runs-on: ubuntu-latest` +- `needs: [fast-checks, changes, api, harness]` +- `if: always()` — ensures the job reports regardless of upstream outcome +- One step running a bash script that: + 1. Fails (exit 1) if `needs.fast-checks.result` is not `success` (fast-checks always runs) + 2. Iterates over `needs.api.result` and `needs.harness.result`, failing if either is not `success` or `skipped` + 3. Echoes "Gate passed." otherwise +- Uses individual `needs.X.result` references (not `contains(needs.*.result,...)`) due to Gitea 1.26.2 bug #31007 + +## Decisions Made + +- **D-15-02-CHANGES-JOB:** Positive `code` filter chosen (not a `docs` exclusion filter) so any new or ambiguous file type defaults to the full gate. A doc-only PR must have every changed file fall outside the code patterns. +- **D-15-02-GATE-INDIVIDUAL:** Individual `needs.X.result` checks used instead of `contains(needs.*.result, ...)` wildcard — Gitea 1.26.2 issue #31007 confirms the wildcard returns false even when jobs succeed. +- **D-15-02-GATE-ALWAYS:** `if: always()` on the `gate` job prevents deadlock when upstream jobs are skipped. The deadlock bug (Gitea #27906) was fixed in 1.21.8; this instance runs 1.26.2. + +## Deviations from Plan + +None — plan executed exactly as written. + +## Threat Model Coverage + +| Threat | Mitigation | Status | +|--------|-----------|--------| +| T-15-04: gate passes when fast-checks fails | gate exits 1 on fast-checks != success; skipped accepted only for api/harness | Implemented | +| T-15-05: code PR misclassified doc-only | positive code filter — any ambiguous file matches code and runs full gate | Implemented | +| T-15-06: supply chain via dorny/paths-filter@v4 | pinned to @v4 tag; pull-requests:read only; no secrets access | Implemented | +| T-15-07: required check deadlock | gate uses if:always(); api/harness NOT added as required checks (Plan 03 scope) | Implemented | + +## Known Stubs + +None — this plan produces only CI workflow YAML. No runtime state or UI involved. + +## Threat Flags + +None — no new network endpoints, auth paths, file access patterns, or schema changes. + +## Verification + +**Static (pre-merge):** +- Python yaml.safe_load parses ci.yml without error +- `changes` job present with `dorny/paths-filter@v4`, `id: filter`, `permissions: pull-requests: read`, `outputs.code` +- No `actions/checkout` step in `changes` job +- `api` and `harness` have `needs: [changes]` and combined `if` with `needs.changes.outputs.code == 'true'` +- `gate` job present with `if: always()` and `needs: [fast-checks, changes, api, harness]` +- `needs.fast-checks.result`, `needs.api.result`, `needs.harness.result` individually referenced in gate +- No `contains(needs.*.result` wildcard in file + +**Behavioral (post-merge, per 15-VALIDATION.md):** +- Doc-only PR: `changes` emits `code=false`; `api`/`harness` show skipped; `fast-checks` runs; `gate` passes +- Code PR: all three run; `gate` passes when green +- Gate-fail path: failing fast-checks causes `gate` to exit 1 + +## Self-Check: PASSED + +- `.gitea/workflows/ci.yml` modified: EXISTS +- Commit 7260438 (Task 1): FOUND +- Commit 547b12c (Task 2): FOUND -- 2.54.0 From 0278e2080aa44b1d43f3dfdff934667604bce462 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:54:26 -0400 Subject: [PATCH 14/16] docs(phase-15): update tracking after wave 2 --- .planning/ROADMAP.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index dfa26a4..51ce7c1 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -288,7 +288,7 @@ Plans: **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML) +- [x] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML) **Wave 3** *(blocked on Wave 2 completion)* @@ -314,7 +314,7 @@ Plans: | 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - | | 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 | | 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 | -| 15. Doc-Only CI Skip + MD Lint | v1.1 | 1/3 | In Progress| | +| 15. Doc-Only CI Skip + MD Lint | v1.1 | 2/3 | In Progress| | ## Backlog @@ -322,7 +322,7 @@ Plans: **Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern. **Requirements:** TBD -**Plans:** 1/3 plans executed +**Plans:** 2/3 plans executed Plans: -- 2.54.0 From da623ac234f061d9cdd1874387953be5c7233dfe Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:56:03 -0400 Subject: [PATCH 15/16] docs(15-03): update publish.yml safety-gate comment to name new required checks - Replace "three required checks (CI / fast-checks, CI / api, CI / harness)" with the new gating surface: CI / fast-checks + CI / gate - Note that CI / api and CI / harness are conditionally skipped on doc-only PRs and gated via the always-running CI / gate aggregate - Comment-only change; no job/step/env/trigger modified --- .gitea/workflows/publish.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 2e12f90..047f8bb 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -11,10 +11,12 @@ # GITEA_-prefixed names cannot be created. GITEA_TOKEN / GITHUB_TOKEN cannot push packages. # # Safety gate: branch protection on main, NOT a needs: dependency in this file. -# 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. +# The PR test jobs (fast-checks, api, harness, gate 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 two required +# checks (CI / fast-checks, CI / gate) must pass before merge. CI / api and CI / harness +# are conditionally skipped on doc-only PRs and are gated via the always-running CI / gate +# aggregate rather than being required directly. # # To bump the milestone tag at a milestone boundary: edit MILESTONE below. -- 2.54.0 From a6e2474379034057d9bc2914219edf67b88c225e Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 10:59:48 -0400 Subject: [PATCH 16/16] docs(phase-15): record deferred 15-03 operator checkpoint --- .planning/STATE.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 7cbbfb5..d402fae 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -25,10 +25,22 @@ See: .planning/PROJECT.md (updated 2026-06-10) ## Current Position -Phase: 15 (ci-skip-api-harness-jobs-for-doc-only-prs) — EXECUTING -Plan: 1 of 3 -Status: Executing Phase 15 -Last activity: 2026-06-12 -- Phase 15 execution started +Phase: 15 (ci-skip-api-harness-jobs-for-doc-only-prs) — PENDING (deferred checkpoint) +Plan: 15-03 (Task 2 of 2 — human-action checkpoint, deferred) +Status: Plans 15-01 + 15-02 complete; 15-03 Task 1 (publish.yml comment) committed (da623ac); 15-03 Task 2 awaiting operator after branch merges to main +Last activity: 2026-06-12 -- Phase 15 code complete; PR pushed; 15-03 operator checkpoint deferred to post-merge + +### Deferred Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action) + +Operator must update Gitea branch protection on `main` AFTER this branch merges and `CI / gate` has reported at least once: +- Set required status checks to EXACTLY: `CI / fast-checks` + `CI / gate` +- REMOVE: `CI / api` + `CI / harness` (now conditionally skipped on doc-only PRs, gated via the always-running `CI / gate`) +- CLI option (login Bergerhouse): `tea api --method PATCH repos/luckberg/familysync/branches/main/protection --data '{"status_check_contexts":["CI / fast-checks","CI / gate"]}'` +- Verify: throwaway doc-only PR is mergeable with api/harness skipped (SC-1/SC-3); throwaway code PR runs+gates all jobs (SC-2); close both. + +ORDERING HAZARD: do NOT make this change before `CI / gate` exists on `main` (premature drop leaves main ungated / can block PRs). + +Resume: after the operator completes the change, re-run `/gsd-execute-phase 15` (re-dispatches 15-03 → Task 1 already satisfied, confirm Task 2, write 15-03-SUMMARY) — then phase verification + completion run. ## Performance Metrics -- 2.54.0