From 8d993bc75e4c9f3d58819d23c68d54b49dc91b05 Mon Sep 17 00:00:00 2001 From: martyy-code Date: Mon, 13 Jul 2026 11:26:52 +0200 Subject: [PATCH] docs: add internal learnings and agent skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add researcher agent README update (English-only queries) - Add agent skills: triage (backlog triage) and create-issue (GitHub issue creation) - Add agent memory for tech-lead (GitHub Issue API, create-issue skill refs) - Add internal learnings docs covering: - changesets v3 + Trusted Publishing - ESLint flat config (v9→v10) - Dependabot in pnpm monorepo - GitHub security/quality (API + features) - TS library monorepo patterns - pnpm workspaces + catalogs - Turborepo patterns + Transit Node Co-Authored-By: Claude --- .claude/agent-memory/tech-lead/MEMORY.md | 5 + .../tech-lead/github-issue-api-2026.md | 24 ++ .claude/agents/researcher/README.md | 10 +- .claude/skills/create-issue/SKILL.md | 159 +++++++++ .claude/skills/triage/SKILL.md | 300 ++++++++++++++++ .../learnings/changesets/changesets-2026.md | 111 ++++++ .../learnings/eslint/eslint-monorepo-2026.md | 77 ++++ .../github/dependabot-pnpm-monorepo-2026.md | 169 +++++++++ .../github/security-quality-api-2026.md | 336 ++++++++++++++++++ .../github/security-quality-features-2026.md | 193 ++++++++++ .../monorepo/ts-library-patterns-2026.md | 83 +++++ .../learnings/pnpm/pnpm-workspaces-2026.md | 77 ++++ .../turborepo/turborepo-patterns-2026.md | 73 ++++ 13 files changed, 1615 insertions(+), 2 deletions(-) create mode 100644 .claude/agent-memory/tech-lead/MEMORY.md create mode 100644 .claude/agent-memory/tech-lead/github-issue-api-2026.md create mode 100644 .claude/skills/create-issue/SKILL.md create mode 100644 .claude/skills/triage/SKILL.md create mode 100644 docs/internal/learnings/changesets/changesets-2026.md create mode 100644 docs/internal/learnings/eslint/eslint-monorepo-2026.md create mode 100644 docs/internal/learnings/github/dependabot-pnpm-monorepo-2026.md create mode 100644 docs/internal/learnings/github/security-quality-api-2026.md create mode 100644 docs/internal/learnings/github/security-quality-features-2026.md create mode 100644 docs/internal/learnings/monorepo/ts-library-patterns-2026.md create mode 100644 docs/internal/learnings/pnpm/pnpm-workspaces-2026.md create mode 100644 docs/internal/learnings/turborepo/turborepo-patterns-2026.md diff --git a/.claude/agent-memory/tech-lead/MEMORY.md b/.claude/agent-memory/tech-lead/MEMORY.md new file mode 100644 index 0000000..ca88e80 --- /dev/null +++ b/.claude/agent-memory/tech-lead/MEMORY.md @@ -0,0 +1,5 @@ +- [GitHub Issue API (types + fields) — 2026](github-issue-api-2026.md) — REST endpoints to apply `type` + `issue_field_values` via `gh api`; header `X-GitHub-Api-Version: 2026-03-10` required. +- [create-issue skill](../skills/create-issue/SKILL.md) — **use this** when asked to create/file/open an issue. Contains the full org mapping (labels, issue types, issue fields IDs for `deessejs`), all templates, CODEOWNERS, and the correct `gh api --input -` workaround. +- **Repo labels** (see skill): `area:*`, `status:*`, `breaking-change`, `dependencies`; GitHub defaults preserved. +- **Org issue types**: Task / Bug / Feature. +- **Org issue fields**: Priority (43676415), Effort (43676418) + Start/Target date. Option IDs in skill file. diff --git a/.claude/agent-memory/tech-lead/github-issue-api-2026.md b/.claude/agent-memory/tech-lead/github-issue-api-2026.md new file mode 100644 index 0000000..84fcfcf --- /dev/null +++ b/.claude/agent-memory/tech-lead/github-issue-api-2026.md @@ -0,0 +1,24 @@ +--- +name: github-issue-api-2026 +description: How to set issue_type and issue_field_values via the GitHub API (REST + GraphQL), mid-2026. Used to programmatically enrich issues with org-level metadata when `gh issue create` doesn't expose those fields. +metadata: + type: reference +--- + +GitHub Issue **types** (org-level) and **fields** (org-level structured metadata) are NOT exposed by `gh issue create`. To add them after the fact (or in a script), use the REST API directly with `gh api` (or any HTTP client). + +**Why:** I created 9 issues on `deessejs/type-testing` via the CLI and they came out without `type` or `issue_field_values`. The researcher agent confirmed this and gave the exact endpoints. + +**How to apply:** When asked to apply issue types/fields to existing or new issues programmatically: + +1. Required header on every REST call: `X-GitHub-Api-Version: 2026-03-10`. +2. Discover IDs first (they're not constant across orgs): + ```bash + gh api -H "X-GitHub-Api-Version: 2026-03-10" /orgs/{org}/issue-types + gh api -H "X-GitHub-Api-Version: 2026-03-10" /orgs/{org}/issue-fields + ``` +3. PATCH /repos/{owner}/{repo}/issues/{n} accepts **both** `type` (string, the type NAME not ID) and `issue_field_values` (array of `{field_id, value}`) in one call. `value` is typed by `data_type` (string for text/single_select option name, number, ISO date, or array of option names for multi_select). +4. GraphQL alternative: `createIssue(input)` accepts `issueTypeId: ID` + `issueFields: [IssueFieldCreateOrUpdateInput!]`. `updateIssue(input)` does NOT accept field values — use `updateIssueFieldValue` / `createIssueFieldValue` mutations. +5. Permissions: `admin:org` for defining types/fields; push access on the repo for applying them. +6. Default fields when Issue Fields is enabled on an org: `Priority` (single_select Urgent/High/Medium/Low), `Effort` (single_select High/Medium/Low), `Start date` (date), `Target date` (date). Match by `name`, not ID. +7. Watch out for the secondary rate limit on POST/PATCH (these endpoints send notifications). Honor `Retry-After`. diff --git a/.claude/agents/researcher/README.md b/.claude/agents/researcher/README.md index 78e9d39..7faf526 100644 --- a/.claude/agents/researcher/README.md +++ b/.claude/agents/researcher/README.md @@ -55,8 +55,14 @@ Do not attempt to authenticate yourself. ## 2. Decompose State 1–3 sub-questions internally (don't output them) to drive your searches. -## 3. Search -Start broad, then iterate **wording** before adding query count. Prefer `-l 5`. For this project, favor authoritative sources: the **TypeScript handbook / release notes / GitHub issues**, the **npm page or repo** of the peer library, official **turbo / changesets / pnpm** docs. Treat random blog posts as medium/low confidence. +## 3. Search — always in English + +**Always formulate your queries and search in English.** GitHub, npm, and the authoritative docs for TypeScript, Turbo, pnpm, ESLint, and all peer libraries are English-language. English queries return the most authoritative and complete results. Mixing languages degrades source quality. + +- Prefer `-l 5`. +- Start broad, then iterate wording before adding query count. +- Favor authoritative sources: **TypeScript handbook / release notes / GitHub issues**, the **npm page or repo** of the peer library, official **turbo / changesets / pnpm / ESLint / GitHub docs**. Treat random blog posts as medium/low confidence. +- If a source is in another language, note it explicitly as lower confidence. ## 4. Fetch (verification only) `fresh fetch` is for confirmation, not exploration. Fetch when a snippet is partial, a claim is load-bearing (e.g. "TS changed this in version X"), or sources disagree. Always pass a targeted `-p` prompt for the specific fact, not "summarize this page." Don't fetch the same URL twice. diff --git a/.claude/skills/create-issue/SKILL.md b/.claude/skills/create-issue/SKILL.md new file mode 100644 index 0000000..cdb7487 --- /dev/null +++ b/.claude/skills/create-issue/SKILL.md @@ -0,0 +1,159 @@ +--- +name: create-issue +description: Create GitHub issues with type, Priority, Effort, and custom fields via the REST API. Use when asked to "create an issue", "file an issue", or "open an issue" for the project. +--- + +# `create-issue` Skill + +Create GitHub issues with org-level `issue_type` and `issue_field_values` (Priority, Effort, etc.) via the REST API. Falls back to plain `gh issue create` if no structured fields are needed. + +## When to use + +Use this skill whenever the user asks to create an issue, file an issue, or open an issue. The skill decides whether a simple `gh issue create` suffices or whether `gh api` with structured fields is needed. + +**Trigger phrases:** "create an issue", "file an issue", "open an issue", "add an issue", "create a ticket". + +## How it works + +### Step 1 — Resolve field IDs (once per org, cached in memory) + +The org's `issue_field` IDs are not constant. On first use in a session: + +```bash +gh api -H "X-GitHub-Api-Version: 2026-03-10" \ + "https://api.github.com/orgs/{org}/issue-fields" +``` + +Parse the response to build a map of `{ fieldName → { id, options: { optionName → id } } }`. + +**Known gotcha (Windows/Git Bash):** URLs starting with `/` are rewritten as filesystem paths. **Always use the full URL** `https://api.github.com/…`. + +### Step 2 — Create the issue + +Use `gh issue create` for title, body, labels, and milestone (it handles Markdown rendering, @mention parsing, and label validation). + +Then enrich it with `type` + `issue_field_values` via `PATCH`. + +### Step 3 — Apply type + fields + +`gh api` via `--input -` with a JSON body: + +```bash +gh api -X PATCH "https://api.github.com/repos/{owner}/{repo}/issues/{n}" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + --input - <"))] | length' +``` + +If result > 0, skip the comment. If 0, post it. + +### Step 4f — Comment (only if step 4e returned 0) + +All comments start with ``. + +**`status: ready`:** +```markdown + +## Triage complete + +- **Type:** {type} +- **Area:** {area} +- **Status:** ready +- **Priority:** {priority} +- **Effort:** {effort} + +**Verification:** {CONFIRMED / FALSE / PARTIAL — one line: what you found in the code} + +**Next steps:** This issue is ready to be picked up. I've reacted 👀 to signal I'm looking at it. + +{related: See also #N for context.} + +_Triage by @{actor}._ +``` + +**`status: needs-repro`:** +```markdown + +## Triage: needs reproduction + +Thank you for the report. To move forward, could you please provide: + +- The minimal code snippet that reproduces the issue +- Your TypeScript version (`tsc --version`) +- Your package version of `@deessejs/type-testing` +- A `tsc --noEmit` output showing the error (if applicable) + +**Verification so far:** {what the code check found} + +Without a minimal reproduction, it's difficult to confirm and fix the issue. I've reacted 🤔 to signal we need more from your side — we'll revisit when the repro is added. + +{related: e.g. "See #42 for a similar case."} + +_Triage by @{actor}._ +``` + +**`status: blocked`:** +```markdown + +## Triage: blocked + +This issue is currently blocked by: **{reason}**. See also #{n} for the upstream tracking. + +**Verification:** {what the code check found} + +I've reacted 👎 to signal the blocker. We'll revisit once it's resolved. + +_Triage by @{actor}._ +``` + +**Close as duplicate:** +```markdown + +## Closed as duplicate + +This issue is a duplicate of #{n}. Follow the discussion there. + +**Verification:** {what the code check found} + +I've reacted 🚀 to point to the right place. + +_Triage by @{actor}._ +``` + +**Close as not planned:** +```markdown + +## Closed + +**Verification:** {what the code check found} + +{reason: e.g. "this is expected behavior", "the described API already exists at src/api/check.ts", "outside the scope of the library"} + +I've reacted 👎 to signal this isn't the right fit right now — happy to revisit if you can share more context. + +{related: e.g. "See #42 for the recommended approach."} + +_Triage by @{actor}._ +``` + +In GitHub, `#N` is automatically a link to issue/PR #N. + +## §5 — Org field IDs + +| Field | `field_id` | Options (string names) | +|---|---|---| +| Priority | `43676415` | `Urgent`, `High`, `Medium`, `Low` | +| Effort | `43676418` | `High`, `Medium`, `Low` | + +## Output + +After the run: "Triaged N issues: M set to ready, K closed, J skipped (already triaged)." + +## Constraints + +- **Verify every issue** against the codebase before deciding — not optional. +- **Filter out PRs** on every fetch with `--jq '.[] | select(.pull_request == null)'`. +- **Idempotency check** before every comment** — skip if `` already present. +- **Always react** after PATCH (Step 4c) — reactions are the warm signal before the comment. +- Do NOT use `-f`/`-F` for `issue_field_values` — use `--input -`. +- Never create new labels. +- Never close without a reason. +- Process one issue at a time — never parallelize. +- Assign only for `ready` / `in-progress` — never for external contributors. diff --git a/docs/internal/learnings/changesets/changesets-2026.md b/docs/internal/learnings/changesets/changesets-2026.md new file mode 100644 index 0000000..a0d9554 --- /dev/null +++ b/docs/internal/learnings/changesets/changesets-2026.md @@ -0,0 +1,111 @@ +# Changesets — Status, Versions, and Migration Path (mid-2026) + +> Learning note captured 2026-07-13 via the `researcher` agent. Sources: GitHub changelogs, releases, issues, and docs. +> Purpose: current state of the changesets ecosystem for `@deessejs/type-testing`, what to upgrade, what to defer, and what to prepare for. + +## TL;DR + +**v2 stable + `changesets/action@v1` = production safe today.** v3 CLI and Action v2 are in prerelease (`3.0.0-next.*` / `2.0.0-next.*`) — do not use in prod unless explicitly testing. The current setup (`@changesets/cli@2.27.0`) is outdated in the v2 series but functional. The immediate action is: bump `@changesets/cli` to latest v2, keep `changesets/action@v1`, configure npm Trusted Publishing via the workflow, and plan the pnpm 10 → v3 migration for later. + +## 1. Current version status + +| Package | Current in this repo | Latest stable | Status | +|---|---|---|---| +| `@changesets/cli` | `2.27.0` | v2 latest patch | **Outdated in v2** — v3 is prerelease only | +| `changesets/action` | (in `release.yml`) | `v1.9.0` | **v1 is stable; v2 is prerelease** | + +**v3 CLI is NOT GA.** Published as `3.0.0-next.*` (latest observed: `3.0.0-next.6`). The v3 plan ([#1945](https://github.com/changesets/changesets/issues/1945)) still has open tasks. Do not use `3.0.0-next.*` in production without explicit testing. + +**Action v2 is NOT GA.** The README on `main` shows `changesets/action@v2` examples but the Latest release is still `v1.9.0`. `v2.0.0-next.*` is prerelease. Stay on `v1` until both v3 CLI and v2 action are marked stable. + +## 2. What changed recently (v2 patch releases) + +### `@changesets/cli` v2 recent + +The changelog is large; key recent additions in the v2 series: +- `changeset version --snapshot [tag]` syntax confirmed stable (unchanged in v3 from v2) +- `changeset publish-plan` (new in `3.0.0-next.*`, not yet in v2) + +### `changesets/action` v1 recent (v1.6.0 → v1.9.0) + +| Version | Notable changes | +|---|---| +| `v1.9.0` | Added sub-actions `pr-comment` and `pr-status`; fixed GitHub Release creation when some packages fail to publish; improved custom version/publish command handling; better force-push behavior in `github-api` mode | +| `v1.8.0` | Version PRs now created as drafts | +| `v1.7.0` | Fixed `.npmrc` generation to not write an `undefined` token when using npm Trusted Publishing; `GITHUB_TOKEN` handling improvements | +| `v1.6.0` | Internal runtime migrated to Node 24 | + +**Immediate action**: pin `changesets/action` to `v1.9.0` (or its SHA) rather than `v1` floating — the `v1.7.0` fix for Trusted Publishing is directly relevant here. + +## 3. npm Trusted Publishing / OIDC + +**How it works with changesets**: not a changesets-native feature — it is configured in npm (register the GitHub Actions workflow as a trusted publisher) and in the workflow file (`permissions: id-token: write`). Changesets does not have a `--provenance` flag; the `--provenance` flag goes to `npm publish` directly. + +**Setup for this repo** (`release.yml`): + +1. Register the GitHub Actions workflow as a trusted publisher on npm for `@deessejs/type-testing` +2. In the release job: `permissions: { id-token: write, contents: write }` + `NPM_CONFIG_PROVENANCE: true` +3. Remove `NPM_TOKEN` from secrets once validated +4. `v1.7.0` of the action fixed the `.npmrc` generation bug that wrote `undefined` tokens for OIDC — ensure the action is at `v1.7.0+` + +**Note**: npm CLI ≥ 11.5.1 + Node ≥ 22.14 in the release job is required for OIDC. Verify the current `release.yml` uses a compatible runtime. + +## 4. Snapshot releases + +Syntax unchanged from docs: +```bash +changeset version --snapshot [tag] # e.g. --snapshot canary +changeset publish --tag canary # --no-git-tag for CI-only without tags +``` + +## 5. v3 CLI breaking changes (preparation, not action) + +When v3 CLI goes GA, these are the breaking changes to handle: + +- **pnpm minimum: `>=10.0.0`** — current repo uses `pnpm@9.0.0`. Must bump first before touching CLI v3. +- **npm minimum: `>=10.9.0`** +- **Yarn minimum: `>=4.5.2`** +- **New interactive CLI**: `@clack/prompts`-based UI (non-breaking for CI-only usage) +- **`@manypkg/get-packages` v3** — internal dep upgrade +- **Prettier decoupled**: if you run `changeset version` with a custom Prettier setup, check the output +- **Default exports deprecated**: prefer named exports in programmatic usage +- **CLI parsing refactored**: any scripts that call `changesets` programmatically may need updates + +**Migration steps when v3 goes GA**: +1. Bump pnpm to 10+ (separate PR, test build/test first) +2. Pin `changesets/action` to `v2.x` stable +3. Check `cwd` usage in the workflow (removed in Action v2) +4. Rename action inputs: `publish` → `publish-script`, `version` → `version-script` (kebab-case in Action v2) +5. Check custom `version`/`publish` scripts for named-export compatibility + +## 6. `changeset publish-plan` (v3, not yet in v2) + +New in `3.0.0-next.6`: `changeset publish-plan` outputs a JSON plan of what would be published or tagged. Useful for: +- Auditing that only `packages/type-testing/` would be published (excluding `apps/web` which is `"private": true`) +- CI gating: fail if unexpected packages appear in the plan + +This is a v3 feature. Once v3 is stable, it would be a good addition to the release workflow. + +## 7. Relevance to this repo (priorities) + +1. **High (now)**: Bump `@changesets/cli` to latest v2 patch — low risk, keeps you current in the stable series. +2. **High (now)**: Pin `changesets/action` to `v1.9.0` in `release.yml`. +3. **High (now)**: Verify `release.yml` runtime uses Node 22+ for OIDC support; add `id-token: write` + `NPM_CONFIG_PROVENANCE` if not present; remove `NPM_TOKEN` once OIDC is validated. +4. **Medium (now)**: Check if the action still uses `cwd` (deprecated in v2) — currently v1, so OK, but note the deprecation. +5. **Deferred**: Plan pnpm 9 → 10 upgrade before v3 migration. +6. **Deferred**: Track v3 GA and action v2 stabilization; prepare migration branch. + +## Gaps / caveats + +- Latest v2 patch version number not isolated from the changelog — recommend checking `npm view @changesets/cli version` to confirm. +- No official v2 → v3 migration guide exists yet — track [#1945](https://github.com/changesets/changesets/issues/1945) for when it drops. +- OIDC provenance may become automatic in future npm versions without the `--provenance` flag; monitor. + +## Sources + +- — v3 plan and status +- +- — action releases (v1.6.0 through v1.9.0) +- +- +- — OIDC / Trusted Publishing diff --git a/docs/internal/learnings/eslint/eslint-monorepo-2026.md b/docs/internal/learnings/eslint/eslint-monorepo-2026.md new file mode 100644 index 0000000..58ad16a --- /dev/null +++ b/docs/internal/learnings/eslint/eslint-monorepo-2026.md @@ -0,0 +1,77 @@ +# ESLint in a TS Monorepo — State of the Art (mid-2026) + +> Learning note captured 2026-07-10 via the `researcher` agent. Sources: eslint.org, typescript-eslint.io, nextjs.org, eslint.style. +> Snapshot: ESLint v10.6.0 (Jun 2026); **v9.x EOL 2026-08-06**. +> Purpose: know the modern flat-config setup and what to migrate in `deessejs/type-testing` (no code change yet). + +## TL;DR + +Flat config is the only game now. Start from `defineConfig()` + `globalIgnores()` (from `eslint/config`), use `typescript-eslint` v8 with `parserOptions.projectService: true`, share config as a `@repo/eslint-config` package. **Delete the leftover `.eslintrc.cjs`** — ESLint v10 removed eslintrc entirely, and v9 is EOL in August 2026. + +## Flat config essentials + +- File: `eslint.config.js|mjs|ts` (v10 supports `.ts` by default). Linter looks for `eslint.config.*`, not `.eslintrc.*`. +- Official helpers from `eslint/config` (Mar 2025, in v9.18+/v10): + - `defineConfig([...])` — type-safe, flattens nested arrays, supports a per-block `extends` field. + - `globalIgnores([...])` — unambiguous global ignores (vs a lone `{ ignores: [...] }`). + - Backport: `@eslint/config-helpers`. +- typescript-eslint v8: `tseslint.configs.recommended | recommendedTypeChecked | strict | strictTypeChecked | stylistic | stylisticTypeChecked`. +- **`parserOptions.projectService: true`** is the recommended default: uses the TS Language Service, no need to list `tsconfig`s or a `tsconfig.eslint.json`, faster, matches the IDE. + - If sticking with `parserOptions.project`: avoid broad globs `**/tsconfig.json` (perf); use `packages/*/tsconfig.json`. >~10 packages → documented OOM; the fix is `projectService`. + +## eslintrc legacy — status + +- ESLint v9 (Apr 2024): flat config default, eslintrc deprecated (`ESLINT_USE_FLAT_CONFIG=false` still worked with a warning). +- **ESLint v10 (Feb 2026): eslintrc removed entirely** — `LegacyESLint`/`FlatESLint` gone, `ESLINT_USE_FLAT_CONFIG` ignored, several `context.*` methods removed (use `context.languageOptions`, `context.sourceCode`, `context.filename`). +- Config lookup in v10 is **per linted file's directory** (not cwd) → multiple configs in one run. +- **Consequence for this repo**: the root `.eslintrc.cjs` is obsolete. Under v10 it's simply ignored. Remove it and migrate to `eslint.config.*`. + +## Monorepo patterns + +- Turborepo-recommended: a `@repo/eslint-config` package exporting `base.js`, `next.js`, etc.; each package's `eslint.config.js` imports and spreads it. Declare all ESLint deps inside that package. +- `extends` at block level (via `defineConfig`) resolves the historical "config shape" mess of plugins. +- Typed-linting perf: `projectService: true` reuses the TS service; the default single-run heuristic saves 10–20% in CI. + +## Next.js integration + +- **`next lint` removed in Next.js 16** — use `eslint .` directly (Next ships a codemod). The `eslint` option in `next.config` no longer has effect. +- `eslint-config-next` is flat-config-ready: `eslint-config-next/core-web-vitals` + `eslint-config-next/typescript`. +- Monorepo: `@next/eslint-plugin-next` accepts `settings.next.rootDir` when the app isn't at the repo root. + +## 2025-2026 timeline + +- v9.0.0 (Apr 2024): flat config default, dropped Node <18.18. +- v9.18+ (Mar 2025): `defineConfig`, `globalIgnores`, `extends` reintroduced. +- v10.0.0 (Feb 2026): eslintrc removed, per-file lookup, Node ≥20.19, bundled TS types (no more `@types/eslint`). +- typescript-eslint v8 (May 2024→): unified `tseslint.configs.*`, `projectService` happy path. +- Stylistic rules deprecated in core (v8.53, Oct 2023) and in typescript-eslint v8 → moved to `@stylistic/*` (`@stylistic/eslint-plugin` v4+, ESM + flat-config only). Keep `eslint-config-prettier/flat` **after** presets if using Prettier. + +## Best practices / pitfalls + +- Always start with `defineConfig(...)`; always use `globalIgnores(...)` for `dist`, `.next`, `node_modules`, `coverage`, `.turbo`. +- Prefer `projectService: true`; reserve `parserOptions.project` for edge cases; never use `**` globs there. +- Turbo: give the `lint` task `dependsOn: ["^lint"]` so it invalidates when `@repo/eslint-config` changes. +- v10 requires Node ≥20.19 / 22.13 / 24+ — check CI. + +## Relevance to this repo (priorities) + +1. **High**: remove the residual root `.eslintrc.cjs`, migrate to `eslint.config.*`. +2. **High**: plan the ESLint 9 → 10 upgrade (v9 EOL Aug 2026). +3. Extract a shared `packages/eslint-config` with `defineConfig`. +4. Use `parserOptions.projectService: true` for typed linting. +5. If Next is bumped to 16 in `apps/web`, drop `next lint`, switch to `eslint .`. + +## Open questions + +- Exact `eslint-config-next` compatibility with ESLint v10 (may need a bump). +- Next.js 16 release date / the app's current Next version (verify in `apps/web/package.json`). + +## Sources + +- +- +- +- +- +- +- diff --git a/docs/internal/learnings/github/dependabot-pnpm-monorepo-2026.md b/docs/internal/learnings/github/dependabot-pnpm-monorepo-2026.md new file mode 100644 index 0000000..76c195f --- /dev/null +++ b/docs/internal/learnings/github/dependabot-pnpm-monorepo-2026.md @@ -0,0 +1,169 @@ +# Dependabot in a pnpm Monorepo — Behavior, Pitfalls, Patterns (mid-2026) + +> Learning note captured 2026-07-13 via the `researcher` agent. Sources: docs.github.com (dependabot options reference + how-tos). +> Purpose: what Dependabot actually does (and doesn't) in a pnpm-workspace monorepo, with the `.github/dependabot.yml` patterns that work here and the ones that silently generate duplicate PRs. No code change yet — config proposal only. + +## TL;DR + +In a pnpm monorepo, Dependabot's `npm` ecosystem **does not recursively walk `pnpm-workspace.yaml`** — it treats each entry of `directories` as the literal location of a `package.json`. So you **must** list each workspace (or use globs). The real cause of "I keep getting duplicate PRs for the same dependency" is **not** the `directories` list — it's that `groups` lack `group-by: dependency-name`. Without that flag, even a `production-dependencies` group opens **one PR per workspace** for each bumped dep. Add `group-by: dependency-name` to collapse them. + +## 1. The mental model Dependabot uses + +- Each `directory` (or matched glob under `directories`) is a root. Dependabot parses the `package.json` it finds there and updates the lockfile it finds there. +- The `npm` ecosystem is **not** pnpm-workspace-aware. It will not auto-discover `packages/*/package.json` from a single `directories: ["/"]` even though `pnpm-lock.yaml` lists every workspace. +- `directories` supports globs: `*` (single segment) and `**` (recursive). The singular `directory:` field does **not** accept globs. +- Glob expansion produces N roots — but `group-by: dependency-name` (inside `groups`) collapses them back into one PR per dep. + +## 2. The duplication trap + +### Symptom +For a dep that exists in both root and a workspace (e.g. `typescript` declared in `apps/web/package.json` and used transitively via the root lockfile), Dependabot opens: +- one PR scoped to `apps/web` that updates `apps/web/package.json` + `pnpm-lock.yaml` +- one PR from the `/` root that touches only `pnpm-lock.yaml` (or a root `package.json` it doesn't really need to touch) + +Result: 2× PR per bump, lockfile conflicts between the two branches. + +### Root cause +The config has `groups` (good — bundles minor/patch) but **no `group-by`** inside them: + +```yaml +# ❌ generates dup PRs +production-dependencies: + dependency-type: "production" + update-types: ["minor", "patch"] +``` + +Per the docs, "if directories have incompatible version constraints for a dependency, Dependabot will create separate pull requests" — and absent `group-by`, the default is per-directory. + +### Fix (one-line) +```yaml +# ✅ collapses to one PR per dep across workspaces +groups: + production-dependencies: + dependency-type: "production" + update-types: ["minor", "patch"] + group-by: dependency-name # <-- the magic line + development-dependencies: + dependency-type: "development" + update-types: ["minor", "patch"] + group-by: dependency-name +``` + +## 3. Major-version strategy + +The current `.github/dependabot.yml` left majors **ungrouped by design** ("Major bumps are intentionally left ungrouped so each gets its own reviewable PR"). That is the correct choice for a type-testing library — each TS/Vitest major is a reviewable event, not a churn item. Don't change it. + +## 4. `versioning-strategy` — the hidden risk for **published** libs + +`versioning-strategy: "increase"` rewrites `^1.0.0 → ^1.2.0` (caret stays, lower bound lifted). It is: +- **Fine for apps** (internal consumers, no published interface) +- **Risky for published libraries** — every bump narrows what `npm install` will resolve to, and `^1.2.0` published means downstream consumers see a narrower peer range over time + +**For `packages/type-testing/` (the published `@deessejs/type-testing`) consider `auto`** (default), which widens ranges. The current "global `increase`" config is fine for `apps/web` but suspicious for the lib. Options to fix: +- Split the entry by `directory` (different `versioning-strategy` per workspace) +- Or rely on `ignore` patterns per published package + +## 5. Auto-merge for the safe bumps + +To avoid re-piling-up of grouped minor/patch PRs once `group-by` is in place: + +**Repo pre-requisites** +- Settings → General → Pull Requests → **Allow auto-merge** = enabled +- Branch protection on `main` → **Require status checks to pass before merging** + +**Workflow** (`.github/workflows/dependabot-auto-merge.yml`) +```yaml +name: Dependabot auto-merge +on: pull_request +permissions: + contents: write + pull-requests: write +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 + id: metadata + - run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: steps.metadata.outputs.update-type != 'version-update:semver-major' +``` + +Caveat from the docs: if `main` uses a **merge queue**, the built-in `GITHUB_TOKEN` cannot enqueue — use a PAT. This repo is not on a merge queue today, so the recipe applies as-is. + +## 6. Recommended corrected `dependabot.yml` for this repo + +```yaml +version: 2 +updates: + - package-ecosystem: "npm" + directories: + - "/" + - "/packages/*" + - "/apps/*" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Europe/Paris" + open-pull-requests-limit: 10 + versioning-strategy: "increase" # OK for the app; revisit for the lib (see §4) + commit-message: + prefix: "chore(deps)" + prefix-development: "chore(deps-dev)" + include: "scope" + groups: + production-dependencies: + dependency-type: "production" + update-types: ["minor", "patch"] + group-by: dependency-name + development-dependencies: + dependency-type: "development" + update-types: ["minor", "patch"] + group-by: dependency-name + # Majors left ungrouped by design — reviewable one at a time + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Europe/Paris" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(ci)" + groups: + github-actions: + patterns: ["*"] +``` + +## 7. Open-pulls-limit mechanics + +`open-pull-requests-limit` (default 5) caps how many Dependabot PRs are open at once. If we hit the cap, Dependabot pauses opening new ones until some close — silently. So reducing noise via `group-by` matters not just for review hygiene but to stay under the cap. + +## 8. Relevance to this repo (priorities) + +1. **Immediate**: add `group-by: dependency-name` to both `groups` entries → next weekly run collapses the 17 duplicate "root" PRs already open into ~8. +2. **Immediate**: dry-run by commenting `@dependabot rebase` on one open dup PR (or merging the config change alone) to verify behavior before applying auto-merge. +3. **Soon**: enable `Allow auto-merge` + add the auto-merge workflow above, gated on `update-type != 'version-update:semver-major'`. +4. **Soon**: clean up current pile manually — `apps/web` PRs first (the actual bumps), then close the root dup PRs since the workspace PRs regenerate the lockfile correctly. +5. **Higher leverage for the lib** (`packages/type-testing/`): + - For majors there (TypeScript 5→7, Vitest 2→4), do **not** just merge the Dependabot PR — apply the diff on a feature branch, run `pnpm test` + `pnpm typecheck` + `pnpm build`, then merge only if the type-level tests pass. + - Switch the published lib to `versioning-strategy: "auto"` (or per-workspace override) so the declared ranges don't shrink over time. + +## Gaps / caveats + +- **Confidence: medium** — the interaction between `group-by: dependency-name` and `dependabot/fetch-metadata`'s `update-type` output for PRs that span multiple directories is not explicitly documented. Worth one real-world observation round before relying on auto-merge to gate majors out. +- **Confidence: high** — no canonical "GitHub-recommended pnpm monorepo" recipe exists in the docs; the patterns above come from general "Defining multiple locations for manifest files" examples. +- **Confidence: high** — `@types/*` and pure type packages are handled identically to runtime deps by Dependabot; `versioning-strategy` affects them the same way. + +## Sources + +- (sections: `directories`, `groups`, `group-by`, `versioning-strategy`) +- ("Defining multiple locations for manifest files", `ignore`/`allow`) +- (group syntax) +- ("Enabling automerge on a pull request" with `dependabot/fetch-metadata` + `gh pr merge --auto` recipe, plus the merge-queue caveat) diff --git a/docs/internal/learnings/github/security-quality-api-2026.md b/docs/internal/learnings/github/security-quality-api-2026.md new file mode 100644 index 0000000..09f7da5 --- /dev/null +++ b/docs/internal/learnings/github/security-quality-api-2026.md @@ -0,0 +1,336 @@ +# GitHub Security & Quality — REST API Reference (mid-2026) + +> Learning note captured 2026-07-13 via the `researcher` agent. Sources: docs.github.com/rest (code-scanning, secret-scanning, dependabot, dependency-graph). +> Purpose: complete REST API reference for querying and managing security findings in `deessejs/type-testing`. For all endpoints: header `X-GitHub-Api-Version: 2026-03-10`. + +## Global notes + +- **GHAS license NOT required** for public repos. All endpoints work on public repos without Advanced Security. +- **Header** required: `X-GitHub-Api-Version: 2026-03-10` +- **Permissions (fine-grained PAT / GitHub App)**: + - Code scanning → "Code scanning alerts": read / write + - Secret scanning → "Secret scanning alerts": read / write + - Dependabot alerts → "Dependabot alerts": read / write + - SBOM / dependency graph → "Contents": read (repo read access sufficient) +- **Token classic scopes**: `security_events` (all three), or `public_repo` for public-only access. +- **Pagination**: `per_page` (max 100, default 30) + `page`, OR cursor-based via `Link` header (`rel="next"`). Loop while `Link` header contains `rel="next"`. + +--- + +## 1. Code scanning / CodeQL + +Docs: `docs.github.com/rest/code-scanning/code-scanning` + +### List alerts + +``` +GET /repos/{owner}/{repo}/code-scanning/alerts +``` + +**Query params:** +| Param | Values | Notes | +|---|---|---| +| `state` | `open`, `closed`, `dismissed`, `fixed` | | +| `severity` | `critical`, `high`, `medium`, `low`, `warning`, `note`, `error` | | +| `tool_name` | e.g. `CodeQL` | Mutually exclusive with `tool_guid` | +| `tool_guid` | GUID string | | +| `ref` | e.g. `refs/heads/main` | | +| `sort` | `created`, `updated` | | +| `direction` | `asc`, `desc` | | +| `assignees` | GitHub login | | + +**Note:** No server-side `rule_id` filter — filter client-side on `rule.id` in response. + +**Key response fields:** +```json +{ + "number": 42, + "state": "open", + "rule": { "id": "js/insecure-randomness", "severity": "warning", "description": "..." }, + "tool": { "name": "CodeQL", "guid": "..." }, + "most_recent_instance": { + "location": { "path": "src/crypto.ts", "start_line": 7, "end_line": 7 }, + "message": "..." + }, + "dismissed_by": "martyy-code", + "dismissed_reason": "false positive", + "dismissed_comment": "...", + "created_at": "2026-01-15T10:00:00Z", + "updated_at": "2026-01-15T10:00:00Z", + "fixed_at": null, + "html_url": "https://github.com/deessejs/type-testing/security/code-scanning/42" +} +``` + +### Get alert detail + +``` +GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number} +``` + +Returns the same structure as the list item, with full rule + tool detail. + +### Get instances (code snippets + locations) + +``` +GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}/instances +``` + +Each instance has `location` (path, start/end line/column) and `message`. + +### Dismiss / reopen alert + +``` +PATCH /repos/{owner}/{repo}/code-scanning/alerts/{alert_number} +``` + +```json +{ + "state": "dismissed", + "dismissed_reason": "false positive", + "dismissed_comment": "This is a test-only function" +} +``` + +Valid `dismissed_reason`: `false positive`, `won't fix`, `used in tests`. + +Note: `fixed` is set **automatically** when a rescanned commit removes the vulnerable code. You cannot force `fixed` via API. + +### List analysis history (SARIF uploads) + +``` +GET /repos/{owner}/{repo}/code-scanning/analyses +GET /repos/{owner}/{repo}/code-scanning/analyses/{analysis_id} +``` + +--- + +## 2. Secret scanning + +Docs: `docs.github.com/rest/secret-scanning/secret-scanning` + +### List alerts + +``` +GET /repos/{owner}/{repo}/secret-scanning/alerts +``` + +**Query params:** +| Param | Values | Notes | +|---|---|---| +| `state` | `open`, `resolved` | | +| `secret_type` | CSV string | e.g. `npm_token,aws_access_key` | +| `resolution` | `false_positive`, `wont_fix`, `revoked`, `pattern_edited`, `pattern_deleted`, `used_in_tests` | Only when `state=resolved` | +| `validity` | `active`, `inactive`, `unknown` | | +| `is_publicly_leaked` | `true`, `false` | | +| `is_bypassed` | `true`, `false` | | +| `sort` | `created`, `updated` | | +| `direction` | `asc`, `desc` | | + +**Key response fields:** +```json +{ + "number": 7, + "state": "open", + "secret_type": "npm_token", + "secret_type_display_name": "npm Publishing Token", + "secret": "**************************", + "resolution": null, + "resolved_by": null, + "validity": "active", + "push_protection_bypassed": false, + "created_at": "2026-07-10T08:00:00Z", + "html_url": "https://github.com/deessejs/type-testing/security/secret-scanning/7" +} +``` + +Set `hide_secret: false` in the request to see the unmasked value (requires write access). + +### Get alert detail + +``` +GET /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number} +``` + +### Get secret locations (in git history) + +``` +GET /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number}/locations +``` + +Returns each commit + location where the secret appears. + +### Resolve / reopen alert + +``` +PATCH /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number} +``` + +```json +{ + "state": "resolved", + "resolution": "revoked", + "resolution_comment": "Token rotated immediately after detection" +} +``` + +--- + +## 3. Dependabot alerts + +Docs: `docs.github.com/rest/dependabot/dependabot` + +> **Status:** Public preview (stable schema, subject to change). Works on public repos. + +### List alerts + +``` +GET /repos/{owner}/{repo}/dependabot/alerts +``` + +**Query params:** +| Param | Values | Notes | +|---|---|---| +| `state` | CSV: `auto_dismissed`, `dismissed`, `fixed`, `open` | | +| `severity` | `low`, `medium`, `high`, `critical` | | +| `ecosystem` | `npm`, `pip`, `go`, `maven`, `composer`, `nuget`, `pub`, `rubygems`, `rust` | | +| `package` | CSV package names | | +| `scope` | `development`, `runtime` | | +| `epss_percentage` | 0–100 | Exploitability score threshold | +| `has` | `patch` | Alert has a patch available | +| `sort` | `created`, `updated`, `epss_percentage` | | +| `direction` | `asc`, `desc` | | + +**Key response fields:** +```json +{ + "number": 12, + "state": "open", + "dependency": { + "package": { "ecosystem": "npm", "name": "lodash" }, + "manifest_path": "packages/type-testing/package.json", + "scope": "runtime" + }, + "security_advisory": { + "ghsa_id": "GHSA-xxxx", + "cve_id": "CVE-2026-12345", + "severity": "high", + "cvss": { "score": 7.5, "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" } + }, + "security_vulnerability": { + "package": { "name": "lodash", "ecosystem": "npm" }, + "vulnerable_version_range": ">= 0.0.0 < 4.17.21", + "first_patched_version": { "identifier": "4.17.21" } + }, + "dismissed_reason": null, + "dismissed_by": null, + "auto_dismissed_at": null, + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-06-01T00:00:00Z", + "html_url": "https://github.com/deessejs/type-testing/security/dependabot/12" +} +``` + +### `auto_dismissed` vs `dismissed` — how to distinguish + +| State | Distinguishing field | +|---|---| +| `dismissed` | `dismissed_by`, `dismissed_reason`, `dismissed_comment` are populated | +| `auto_dismissed` | `state: "auto_dismissed"` + `auto_dismissed_at` is set | + +Auto-dismissal happens when GitHub's rule determines the dependency is not reachable in production (e.g., dev-only scope, unreachable in the lockfile). + +### Get alert detail + +``` +GET /repos/{owner}/{repo}/dependabot/alerts/{alert_number} +``` + +### Dismiss / reopen alert + +``` +PATCH /repos/{owner}/{repo}/dependabot/alerts/{alert_number} +``` + +```json +{ + "state": "dismissed", + "dismissed_reason": "not_used", + "dismissed_comment": "lodash is only imported in test helpers not shipped to consumers" +} +``` + +Valid `dismissed_reason`: `fix_started`, `inaccurate`, `no_bandwidth`, `not_used`, `tolerable_risk`. + +--- + +## 4. Dependency graph / SBOM / Dependency review + +Docs: `docs.github.com/rest/dependency-graph` + +### Export SBOM (SPDX JSON) + +``` +GET /repos/{owner}/{repo}/dependency-graph/sbom +``` + +Returns SPDX 2.3 JSON. Requires only repo read access (no GHAS). No query params. + +```json +{ + "sbom": { + "SPDXID": "SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.3", + "packages": [ + { + "name": "lodash", + "versionInfo": "4.17.21", + "licenseConcluded": "MIT" + } + ], + "relationships": [...] + } +} +``` + +### Compare dependencies between two refs (dependency review) + +``` +GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead} +``` + +`basehead` format: `{before}...{after}`, e.g. `main...feat-new-dep`. + +Returns the diff of dependencies between two commits, including `vulnerabilities` per changed package. This is the API powering the inline "Dependency review" check on PRs. + +**Note:** There is **no separate API** for the inline PR comments. The `compare/{basehead}` endpoint is the source; GitHub's UI renders it inline. + +--- + +## 5. Quick reference — all endpoints + +| Feature | List | Detail | Update | +|---|---|---|---| +| **Code scanning** | `GET /repos/{owner}/{repo}/code-scanning/alerts` | `GET /repos/{owner}/{repo}/code-scanning/alerts/{n}` | `PATCH /repos/{owner}/{repo}/code-scanning/alerts/{n}` | +| **Secret scanning** | `GET /repos/{owner}/{repo}/secret-scanning/alerts` | `GET /repos/{owner}/{repo}/secret-scanning/alerts/{n}` | `PATCH /repos/{owner}/{repo}/secret-scanning/alerts/{n}` | +| **Dependabot alerts** | `GET /repos/{owner}/{repo}/dependabot/alerts` | `GET /repos/{owner}/{repo}/dependabot/alerts/{n}` | `PATCH /repos/{owner}/{repo}/dependabot/alerts/{n}` | +| **SBOM export** | `GET /repos/{owner}/{repo}/dependency-graph/sbom` | — | — | +| **Dependency review diff** | `GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}` | — | — | + +--- + +## Gaps / caveats + +- No server-side `rule_id` filter on code scanning list — filter on `rule.id` in response client-side. +- `dependency-graph/summary` endpoint does not exist under that name — use SBOM or `compare/{basehead}`. +- No dedicated API for inline dependency review comments — rendered by GitHub UI from `compare/{basehead}`. +- Dependabot alerts API is public preview (marked subject to change). +- `auto_dismissed_at` field semantics not double-confirmed on a second source (enum `state` confirmed with high confidence). + +## Sources + +- (list, detail, dismiss, analysis history) +- (list, detail, locations, resolve) +- (list, detail, dismiss, auto_dismissed vs dismissed) +- (SBOM SPDX export) +- (compare/basehead for dependency review diff) diff --git a/docs/internal/learnings/github/security-quality-features-2026.md b/docs/internal/learnings/github/security-quality-features-2026.md new file mode 100644 index 0000000..12521c3 --- /dev/null +++ b/docs/internal/learnings/github/security-quality-features-2026.md @@ -0,0 +1,193 @@ +# GitHub Security & Quality Features — Feature Overview + REST API (mid-2026) + +> Learning note captured 2026-07-13 via the `researcher` agent. Sources: docs.github.com (code-security, billing, advanced-security, repository-rules, merge-queue, dependency-graph, secret-scanning, code-scanning, dependabot). +> Purpose: what GitHub security/quality features are available for a public OSS TypeScript monorepo, which are active by default vs require setup, and how to query them via REST API. + +## TL;DR + +For a public repo, everything is **free**. CodeQL, secret scanning, Dependabot alerts, dependency graph — no GitHub Advanced Security license needed. High ROI actions: enable CodeQL default setup, push protection, private vulnerability reporting, and move branch protection to rulesets. + +## 1. Feature overview — what's active by default + +| Feature | Default | Plan | Key action needed | +|---|---|---|---| +| **Dependency graph** | ✅ Auto | Free | None — verify in Insights | +| **Secret scanning (alerts)** | ✅ Auto | Free | Enable push protection in Settings | +| **Dependency review (inline PR)** | ✅ Auto (via graph) | Free | None | +| **CodeQL code scanning** | ❌ Manual | Free (public) | One-click Setup in Settings | +| **Push protection** | ❌ Manual | Free (public) | Toggle in Settings | +| **Dependabot security updates** | ❌ Manual | Free | Toggle in Settings + `dependabot.yml` | +| **Private vulnerability reporting** | ❌ Manual | Free | Toggle in Settings | +| **Rulesets** | N/A | Free (public) | Create in Settings → Rules | +| **Merge queue** | N/A | Team+ | Skip for now | + +**GHAS (Advanced Security license) is NOT required** for public repositories. It is only needed for private/internal repos. + +## 2. Code scanning (CodeQL) + +**"Default setup"** is the right entry point for a TypeScript repo: +- Settings → Security → Advanced Security → CodeQL analysis → **Set up → Default** +- No YAML file written to `.github/workflows/`. No `query-filters.yml` or `extraction-js.yml` needed. +- GitHub picks languages automatically (JS/TS detected via `package.json`, `tsconfig.json`, `.ts` files). +- `autobuild` mode handles the build — no special config for TypeScript. +- Runs: weekly on schedule + on every PR. Free on public repos. +- Cost: **$0 — unlimited minutes on public repos with standard GitHub-hosted runners.** + +### Advanced setup (optional) +Only if default setup produces too many false positives. Requires writing `.github/workflows/codeql.yml`. Skip for now. + +## 3. Secret scanning + +**Two distinct features:** + +### Secret scanning alerts (always-on) +- Scans entire git history, issues, PRs, discussions, gists. +- Free on public repos. No setup. +- List via: `GET /repos/{owner}/{repo}/secret-scanning/alerts` +- Dismiss via: `PATCH .../secret-scanning/alerts/{alert_number}` + +### Push protection (opt-in) +- **Free on public repos** since May 2023. +- Blocks a push if a secret matching a partner pattern (AWS keys, npm tokens, etc.) is detected. +- Enable: Settings → Code Security → **Push protection** → Enable. +- One-time toggle. No YAML or workflow needed. + +### Custom patterns + validity checks (optional) +`.github/secret-scanning.yml` enables: +- **Custom patterns** (e.g., internal token formats) +- **Validity checks** — pings the issuer to confirm the token is still active. For an OSS lib publishing to npm, this is high ROI. + +## 4. Dependabot — security updates vs version updates + +They are **independent and complementary:** + +| Feature | Trigger | Config needed | +|---|---|---| +| **Version updates** | `dependabot.yml` with `package-ecosystem` + `schedule` | `.github/dependabot.yml` (already present) | +| **Security updates** | GitHub Advisory Database flags a vuln | Settings → Dependabot → Enable (no `dependabot.yml` required) | + +Both can be active simultaneously. Security updates create targeted PRs that fix the specific vulnerability; version updates handle all bumps. + +### Grouped security updates +`dependabot.yml` can group multiple security updates into one PR: +```yaml +groups: + security-updates: + dependency-type: "all" + update-types: ["security"] +``` + +### Compatibility scores +Heuristic based on past CI runs. Available in Dependabot settings. + +## 5. Dependency graph + +**Automatically enabled on public repos.** pnpm is supported via `pnpm-lock.yaml`. + +Provides: +- Transitive dependency path display +- "Used by" badge (threshold: 100 dependents) +- SBOM export (SPDX CycloneDX) via API +- Powers Dependabot + dependency review inline comments + +Verify: Insights → Dependency graph. + +## 6. Dependency review (inline PR diff) + +Powered by the dependency graph. **No separate API** — the same data is available via `GET /repos/{owner}/{repo}/dependency-graph/compare/{base...head}` (the diff between two commits). This endpoint is what GitHub uses to render the inline "dependency review" check on PRs. + +## 7. Private vulnerability reporting + +- Adds a "Report a vulnerability" button on the Security Advisories tab. +- Reports arrive as private draft security advisories — only visible to maintainers. +- **Free on public repos.** Enable: Settings → Security → Advanced Security → **Private vulnerability reporting** → Enable. +- For an OSS library, this is a standard expectation from security-conscious consumers. + +## 8. Rulesets (replacing branch protection) + +**Rulesets are the modern replacement for branch protection rules** (deprecated in favor of rulesets over time). + +Available on free public repos for per-repo rulesets. Org-level rulesets require Team or Enterprise. + +### Recommended rulesets for this repo + +**Ruleset 1 — `main`:** +- ✅ Require a pull request before merging (1 approval + CODEOWNERS) +- ✅ Require status checks to pass (lint, typecheck, test) +- ✅ Require linear history (prevents merge commits on main) +- ✅ Block force pushes + +**Ruleset 2 — `v*` tags (release tags):** +- ✅ Restrict tag updates (prevent overwriting a release tag) +- ✅ Restrict tag creations (prevent accidental `git push --tags`) +- ✅ Restrict deletions + +Note: Tag protection via the classic "Protected tags" UI is deprecated since Oct 2023. Use tag rulesets instead. + +### Rulesets vs branch protection rules +- Rulesets support **layering** (multiple rulesets can target the same branch; most-restrictive wins). +- Rulesets expose rules to read-only viewers. +- Rulesets support **metadata restrictions** unavailable in classic branch protection. + +## 9. GitHub Actions cache for pnpm (native) + +`actions/setup-node@v6` supports `cache: 'pnpm'` out of the box: + +```yaml +- uses: actions/setup-node@v6 + with: + node-version: "22" + cache: "pnpm" # uses pnpm-lock.yaml automatically +``` + +Even better: if `package.json` has `"packageManager": "pnpm@9.0.0"` (Corepack), `setup-node` enables pnpm cache **without** the `cache:` input: + +```yaml +# In package.json: +"packageManager": "pnpm@9.0.0" +``` + +This repo already has `"packageManager": "pnpm@9.0.0"` — the native cache works with zero config. + +Turbo `.turbo/` cache is independent — combine both. + +## 10. Actions Performance Metrics (CI insights) + +Native since March 2025 (GA). Available at **Insights → Actions Usage Metrics** and **Actions Performance Metrics**. + +Provides: +- Success rate, p50/p95 duration, queue time, failure rate per workflow +- Exportable to CSV +- No config needed — data is collected automatically + +## 11. Merge queue — skip for now + +Designed for repos with **high PR volume from many contributors**. Requires handling the `merge_group` event in CI. For a small OSS monorepo with low PR throughput, the ruleset option "Require branches to be up to date before merging" achieves the same correctness without the complexity. + +## 12. HIGH ROI summary for this repo + +| # | Action | Effort | Where | +|---|---|---|---| +| 1 | **Enable push protection** (secret scanning) | 1 toggle | Settings → Code Security | +| 2 | **Enable CodeQL default setup** | 1 toggle | Settings → Security → Advanced Security | +| 3 | **Enable private vulnerability reporting** | 1 toggle | Settings → Security → Advanced Security | +| 4 | **Migrate to rulesets** (`main` + `v*` tags) | ~15 min | Settings → Rules | +| 5 | **Enable Dependabot security updates** | 1 toggle | Settings → Dependabot | +| 6 | **Verify dependency graph populated** | 2 min | Insights → Dependency graph | +| 7 | **Check Actions Performance Metrics** | 2 min | Insights → Actions Performance | + +## Sources + +- (CodeQL default setup) +- (secret scanning) +- (push protection) +- (version vs security updates) +- (security updates + grouped) +- (private vulnerability reporting) +- (rulesets) +- (tag rulesets) +- (rulesets vs branch protection) +- (dependency graph + SBOM) +- (pnpm cache native) +- (Actions Performance Metrics GA) +- (push protection free) diff --git a/docs/internal/learnings/monorepo/ts-library-patterns-2026.md b/docs/internal/learnings/monorepo/ts-library-patterns-2026.md new file mode 100644 index 0000000..681b19a --- /dev/null +++ b/docs/internal/learnings/monorepo/ts-library-patterns-2026.md @@ -0,0 +1,83 @@ +# TS Library Monorepo Architecture — Senior Patterns (mid-2026) + +> Learning note captured 2026-07-10 via the `researcher` agent. Sources: TypeScript handbook, docs.npmjs.com, changesets, tool docs. +> Purpose: cross-cutting build/publish/release/CI/hygiene patterns for `@deessejs/type-testing` beyond turbo/eslint/pnpm (no code change yet). + +## TL;DR + +For a "pure types + a few runtime guards" library, **`tsc -b` is enough** — no bundler needed. The high-ROI additions are: validate packaging with **`publint` + `attw`**, adopt **npm Trusted Publishing (OIDC)** to drop `NPM_TOKEN`, and add **`knip` + `sherif`** for hygiene. Keep `moduleResolution: nodenext` on the lib, `bundler` on the Next app. + +## 1. Library build + +- **`tsc` alone** suffices when output is ~1:1 JS (typical for type libs); it loses tree-shaking/bundling/minification. +- Bundlers (only if the lib grows assets/deps to tree-shake): **tsdown** (Oxc + Rolldown, integrates `publint`+`attw` as lint), **bunup** (Bun-native, easy dual ESM/CJS + `--exports`), **unbuild** (unjs), **tsup** (now "legacy"; official tsup→tsdown migration exists). +- **ESM-only** is the 2025-2026 default for new libs (Node ≥20, Bun, Deno, edge). Dual ESM/CJS only if a CJS bundler must consume it. +- **`exports` map**: conditioned form with `"types"` **before** `"import"`/`"require"` (attw flags inversions). +- **`publint`** validates `package.json`/`exports`/`files`/`main`/`types`. **`@arethetypeswrong/cli` (attw)** resolves types against node16/nodenext/bundler and catches "types-only exports vanishing in CJS", missing defaults, bad module syntax. → **Add both as CI jobs** to guard the existing packaging (would have caught the missing `.js` import in `special.ts`). + +## 2. TypeScript / tsconfig in a monorepo + +- **Project references** (`composite: true`, `references`, `tsc -b`): incremental builds, load `.d.ts` of referenced projects instead of re-reading `.ts`. Solution `tsconfig.json` at root with `files: []` + `references`, shared `compilerOptions` via `extends`. +- **`moduleResolution`**: `nodenext` (or the new stable `node20` from TS 5.9) for the **lib** (matches the runtime); `bundler` only for the **bundled app**. +- Shared base tsconfig as a private `@repo/tsconfig` workspace package (`workspace:*`). +- **`verbatimModuleSyntax: true`** (default in TS 5.9 `tsc --init`): forces `import type`, avoids tree-shaking bugs, preps ESM output. +- For this repo: `tsc -b` + one composite tsconfig per package is enough; no bundler until there are assets/deps to bundle. + +## 3. Release with changesets + +- `@changesets/cli` v3 + `changesets/action` (v1.9.0 stable on `maintenance/v1`; v2 in dev on main). +- **npm Trusted Publishing (OIDC)** — GA 2025-07-31: no `NPM_TOKEN`, short-lived OIDC tokens. Workflow needs `permissions: { id-token: write, contents: write }` + `NPM_CONFIG_PROVENANCE: true`. Requires npm CLI ≥11.5.1 + Node ≥22.14 in the release job; **self-hosted runners unsupported**; 1 trusted publisher per package. +- **`--provenance`** automatic when publishing via trusted publishing from GitHub Actions on a public repo. +- **Snapshot releases**: `changeset version --snapshot ` → `0.0.0--`; publish with `changeset publish --tag ` (never plain — would hit `latest`). Don't merge the snapshot commit. +- **Pre-releases**: `changeset pre enter/exit` for alpha/beta/rc trains. +- `createGithubReleases: true` (default) generates GitHub Releases on publish. + +## 4. CI (GitHub Actions) + +- pnpm cache via `actions/setup-node` (`cache: 'pnpm'`) + `pnpm/action-setup`. +- Turbo remote cache: `TURBO_TOKEN` (secret) + `TURBO_TEAM` (variable). Fallback: `actions/cache@v4` on `.turbo/`. +- `turbo run build --affected` on PRs (needs deep git history); or rely on a warm remote cache. +- Split jobs for Node matrix (20/22/24) + billing; `concurrency: ${{ github.workflow }}-${{ github.ref }}`; `fetch-depth: 2` so changesets/turbo see the previous commit. + +## 5. Hygiene / quality + +- **`knip`** — dead files/exports/deps via static analysis, native pnpm-workspace + Next/Vitest/Turbo plugins. Very high ROI on a monorepo. (Adopted by Vercel, TanStack, Microsoft.) +- **`sherif`** — zero-dep version-consistency check (misplaced `@types/*`, out-of-sync `next`/`eslint-config-next`, inconsistent versions), `--fix`. Prefer over `syncpack` (Rust rewrite in progress). +- **`publint` + `attw`** — see §1. +- **commitlint + conventional commits** (`@commitlint/config-conventional` + Husky `commit-msg`) as a layer over changesets. +- **Husky + lint-staged** (`eslint --fix` + `prettier --write` on staged files). +- **`.editorconfig`** — expected of any serious OSS lib. +- Renovate (config-as-code, monorepo-aware, groups) vs Dependabot (already set up here). + +## 6. Notable 2025-2026 + +- **TS 5.9** (Jul 2025): stable `--module node20`, minimal prescriptive `tsc --init` (`verbatimModuleSyntax`, `noUncheckedSideEffectImports`, `exactOptionalPropertyTypes`), instantiation cache (big wins on type-heavy libs). +- **npm Trusted Publishing OIDC** GA (Jul 2025), "allowed actions" + staged publish (May 2026). +- **tsdown/bunup** redefined lib bundling ahead of tsup. +- **knip** v5+ framework auto-detection. + +## Relevance to this repo (priorities) + +1. **High**: add `publint` + `attw` CI jobs to validate the existing `exports` map (`.`/`./vitest`/`./vitest/setup`). +2. **High**: migrate `release.yml` to npm Trusted Publishing OIDC (drop `NPM_TOKEN`, auto provenance; bump the release job to Node 22). +3. **Medium**: add `knip` (audit `apps/web` bloat) and `sherif` (version consistency) as fast CI checks. +4. Ensure `verbatimModuleSyntax` is on in the lib tsconfig; keep `nodenext` on the lib, `bundler` on the app. +5. **Do NOT** swap `tsc` for a bundler yet — unnecessary for a pure-types + guards lib. + +## Gaps / caveats + +- Bundler benchmarks are vendor-declared (low confidence on the real perf hierarchy). +- `changesets/action` v2 GA date unclear (v1.9.0 is prod). +- syncpack Rust-rewrite stable date unknown. + +## Sources + +- +- +- +- +- +- +- +- +- diff --git a/docs/internal/learnings/pnpm/pnpm-workspaces-2026.md b/docs/internal/learnings/pnpm/pnpm-workspaces-2026.md new file mode 100644 index 0000000..3280a70 --- /dev/null +++ b/docs/internal/learnings/pnpm/pnpm-workspaces-2026.md @@ -0,0 +1,77 @@ +# pnpm Workspaces — Senior Patterns (mid-2026) + +> Learning note captured 2026-07-10 via the `researcher` agent. Sources: pnpm.io docs/blog, pnpm GitHub releases, syncpack.dev. +> Purpose: know modern pnpm workspace patterns and the upgrade landscape for `deessejs/type-testing` (currently `pnpm@9`, no code change yet). + +## TL;DR + +Keep `packages` as the only required workspace field; use **catalogs** to centralize shared external versions and `workspace:` for internal deps. **pnpm 10 (Jan 2025) is the mid-2026 sweet spot** for a Node-20 repo (security-by-default, `minimumReleaseAge`, `trustPolicy`); **pnpm 11 (Apr 2026) requires Node 22**. Clean up the phantom `examples` workspace entry. + +## `pnpm-workspace.yaml` + +- `packages` globs with `!` exclusions; root package always included. Prefer `packages/*` (single level) over `packages/**` (captures `dist/`, fixtures). +- **Non-existent entries don't fail install** — a glob matching nothing is silent noise. Lint in CI that each entry points to a real dir. (Our `examples` entry is exactly this.) +- Catalogs, settings and overrides live in their own top-level keys. + +## Catalogs + +- Purpose: single source of truth for versions → dedupe + fewer git conflicts across packages. +- Syntax: top-level `catalog:` (default) or `catalogs:` (named). Reference in `package.json` as `"react": "catalog:"` or `"react": "catalog:react18"`. Works for deps/devDeps/peerDeps/optionalDeps + `overrides`. +- **GA** since pnpm 9.x (named catalogs finalized in 9.5). At publish, `catalog:` is replaced by the real semver range (like `workspace:`). +- Settings (v10.12+): `catalogMode: manual | strict | prefer`; `cleanupUnusedCatalogs` (v10.15). +- `workspace:*` / `workspace:^` for internal deps; `catalog:` for external versions to keep consistent. + +## pnpm 10 vs 11 (mid-2026) + +- **pnpm 10.0 (Jan 7 2025)** — "Security by Default": lifecycle scripts (`preinstall`/`postinstall`) no longer run by default → allowlist via `pnpm.onlyBuiltDependencies`. SHA256 store, empty `public-hoist-pattern`. + - `minimumReleaseAge` (v10.16, minutes) — block just-published versions; `minimumReleaseAgeExclude` for scopes/pins. + - `trustPolicy: no-downgrade` (v10.21); `blockExoticSubdeps` (v10.26); `allowBuilds` map (v10.26) replaces `onlyBuiltDependencies`. +- **pnpm 11.0 (Apr 28 2026)** — Node 22+ required, pure ESM. SQLite store, `pnpm audit` → bulk advisories (`ignoreCves` → `ignoreGhsas`), `pnpm ci`, `pnpm sbom`, `pnpm peers check`, `registries`/`namedRegistries` in `pnpm-workspace.yaml` (replaces `.npmrc` `@scope:registry`), `packageConfigs` (per-package `.npmrc` replacement), `allowBuilds` mandatory. Codemod: `pnpx codemod run pnpm-v10-to-v11`. +- **Migration for this repo**: 9 → 10 keeps Node 20 and buys the security features cheaply. 9 → 11 forces Node 22 + `onlyBuiltDependencies`→`allowBuilds` + `.npmrc`→`pnpm-workspace.yaml` moves + `ignoreCves`→`ignoreGhsas`. + +## Monorepo best practices + +- Internal deps via `workspace:` protocol (`workspace:*`/`^`/`~`/relative). Keep `saveWorkspaceProtocol` at its rolling default — turning it off silently breaks the workspace link (you edit a package but the old one is used). +- `sharedWorkspaceLockfile: true` (default) → single root `pnpm-lock.yaml`. +- `publishConfig` (npm-standard) to override `registry`/`access` at publish. +- `-r` / `--filter ` / `--filter ...` (deps) / `--filter ...^` (dependents); `-F` short alias in v11. Use `failIfNoMatch: true` in CI. +- `node-linker`: keep `isolated` (default). `hoisted`/`shamefully-hoist` disable phantom-dep detection — use only for RN/Lambda/`bundledDependencies`. +- `.npmrc` in v11 only reads auth/registry — everything else (hoist, save-exact, node-linker) moves to `pnpm-workspace.yaml`. + +## Dependency-hygiene tools + +- `pnpm dedupe`, `pnpm outdated -r`, `pnpm audit` (`--fix=update` in v11). +- **`syncpack` v14 (Jun 2026)**: native pnpm-catalog support (auto-migrate literals → `catalog:`). (Note: a Rust rewrite is underway — check stability.) +- **`@manypkg/cli`**: `manypkg check` / `fix` for package.json consistency. +- `pnpm dlx` (ex `pnpx`) for one-off tools. + +## Anti-patterns + +- Overly broad `packages/**` globs. +- `shamefully-hoist: true` globally (masks phantom/peer bugs). +- Migrating 9 → 10 without listing build deps (breaks `postinstall`-dependent packages: esbuild, node-gyp). +- `saveWorkspaceProtocol: false` (breaks internal linking). +- `nodeLinker: hoisted` without a hard reason. +- Forgetting `.npmrc` settings must move to `pnpm-workspace.yaml` in v11; `npm_config_*` env → `pnpm_config_*`. + +## Relevance to this repo + +- On `pnpm@9` today → **target pnpm 10** (keeps Node 20; gains `minimumReleaseAge`, `blockExoticSubdeps`, `trustPolicy`, `allowBuilds`). Hold off on 11 until you move to Node 22. +- **Clean up the `examples` entry** in `pnpm-workspace.yaml` (phantom glob) — add a `.gitkeep`'d dir or remove the line. +- **Adopt `catalog:`** for shared versions (`typescript`, `vitest`, `react`) across lib + Next app; keep `workspace:^` for internal deps. `sherif`/`syncpack` can auto-migrate. + +## Open questions / known bugs + +- `dedupePeerDependents: true` cyclic peer bug (#11834, open mid-2026). +- No pnpm 12 / post-v11 roadmap found. + +## Sources + +- +- +- +- +- +- +- +- diff --git a/docs/internal/learnings/turborepo/turborepo-patterns-2026.md b/docs/internal/learnings/turborepo/turborepo-patterns-2026.md new file mode 100644 index 0000000..dd3a2d3 --- /dev/null +++ b/docs/internal/learnings/turborepo/turborepo-patterns-2026.md @@ -0,0 +1,73 @@ +# Turborepo — Senior Patterns (mid-2026) + +> Learning note captured 2026-07-10 via the `researcher` agent. Sources: turborepo.dev docs, Vercel/Turborepo blog, vercel/turborepo GitHub. +> Version window: Turborepo 2.3 (Nov 2024) → 2.10.x (Jul 2026). +> Purpose: know what a modern `turbo.json` looks like and what we could adopt in `deessejs/type-testing` (no code change yet). + +## TL;DR + +`tasks` (not `pipeline`), split every atomic action into its own task, use a **Transit Node** so `typecheck` runs in parallel with `build` while keeping cache invalidation correct, declare `outputs`/`env` properly, keep `.turbo/` gitignored, and use `turbo run` (not `turbo`) in CI. Boundaries, `turbo watch`, `--affected`, and Package Configurations are the newer senior tools. + +## `turbo.json` schema (modern) + +- Top-level: `tasks` (ex-`pipeline`, still back-compat but deprecated), `extends`, `globalDependencies`, `globalEnv`, `globalPassThroughEnv`. +- `$schema` → prefer `./node_modules/turbo/schema.json` (versioned with the installed turbo). `turbo.jsonc` supports comments (2.5, Apr 2025). +- Per task: `dependsOn`, `inputs`, `outputs`, `cache`, `persistent`, `interruptible`, `interactive`, `env`, `passThroughEnv`, `with`, `outputLogs`, `dotEnv`, `extends: false`. +- `dependsOn` microsyntax: `^task` (deps), `task` (same package), `pkg#task`, `//#task` (root). +- `inputs`: use `$TURBO_DEFAULT$` to keep git-tracked defaults; `$TURBO_ROOT$` (2.5) replaces `../../` to reach workspace-root files. +- `outputs`: e.g. `dist/**`, `.next/**` with negations `!.next/cache/**`, `!.next/dev/**`. +- `env` affects the hash; `passThroughEnv` is runtime-only (no hash impact). Wildcards `PREFIX_*` and negations `!FOO_*` supported. Framework Inference auto-adds `NEXT_PUBLIC_*`, `VITE_*`, etc. +- `outputLogs`: `full` (default), `hash-only`, `new-only`, `errors-only`, `none`. + +## What changed 2025-2026 + +- **Boundaries** (experimental, 2.4 Jan 2025): flags imports outside a package dir + undeclared deps; optional tags with `allow`/`deny` rules. A senior way to lock down that `type-testing` isn't imported by unauthorized packages. +- **Package Configurations**: a `turbo.json` per package with `{ "extends": ["//"] }`. Scalars inherited; arrays replaced by default — use `$TURBO_EXTENDS$` first in a list to append. `extends: false` to exclude a task. +- **`turbo.jsonc`**, **`$TURBO_ROOT$`**, **`with`** (sidecar persistent tasks) — all 2.5 (Apr 2025). +- **Watch Mode** (`turbo watch`, experimental cache behind `--experimental-write-cache`); `interruptible: true` for restartable persistent tasks. +- **`--affected`**: only packages changed vs the branch base; auto-detects `GITHUB_BASE_REF` on PRs. **Requires deep git history** in CI (shallow clone → everything looks affected). +- **`turbo query` / `turbo query affected`**: JSON output for CI scripting. +- **Microfrontends (zones)** GA 2.6 (Oct 2025); **Bun** stable 2.6; **Cargo workspaces** in progress (2.10 canary). +- Roadmap 3.0 removes `--no-cache`, `--remote-only`, `--parallel`, `--daemon`. + +## Caching + +- Signature = global hash (root `package.json`, lockfile, `globalDependencies`, `globalEnv`, `turbo.json`) + task hash (`inputs` + task `env` + script + `outputs`). +- Local cache in `.turbo/cache` — **must be gitignored**. +- Remote Cache: Vercel (free) via `TURBO_TOKEN` + `TURBO_TEAM`; self-hostable (open OpenAPI spec, 2.5). `--cache=local:rw,remote:r` replaces `--no-cache`/`--remote-only`. +- CI: install `turbo` pinned to the major in `package.json`; call `turbo run …` (not `turbo …`). +- `--summarize` writes `.turbo/runs/.json` — diff two runs to debug cache misses. + +## Anti-patterns (from the official gotchas) + +- Root scripts that bypass `turbo run` (no cache/parallelism); chaining with `&&` instead of a task graph. +- `globalDependencies` too broad (invalidates everything). +- Forgetting `^` in `dependsOn` (looks in the same package). +- Forgetting `outputs` on file-producing tasks (nothing cached) — omitting is only OK for stdout-only tasks. +- Forgetting `persistent: true` on `dev`/`start` (dependents block). +- Forgetting `cache: false` on side-effecting tasks (`deploy`, `publish`). +- Overriding default `inputs` without `$TURBO_DEFAULT$` (false cache hits). +- Shallow clone + `--affected` in CI. + +## Relevance to this repo + +- Scripts already align (build/dev/lint/typecheck/test/test:coverage/clean). +- **Adopt a Transit Node for `typecheck`** so it parallelizes with `build` yet stays sensitive to dependency source changes. +- **Verify `.turbo/` is gitignored** and CI installs a pinned `turbo` + uses `turbo run`. +- **Consider Remote Cache** (`TURBO_TOKEN`/`TURBO_TEAM`) + `--affected` on PRs. +- **Boundaries** later to enforce import rules once more packages exist. +- Strict env mode matters for the published lib (an undeclared `PUBLISH_TOKEN`/registry var would cause a bad cache hit). + +## Open questions + +- Exact inheritance precedence across chained Package Configurations. +- Vercel Remote Cache pricing/limits in 2026 (not verified). + +## Sources + +- +- +- +- +- +-