Skip to content

docs(rules,typescript): add Design Patterns subsection with ReadonlyMap internal/external example#4781

Merged
aegis-gh-agent[bot] merged 1 commit into
developfrom
docs/readonly-map-pattern-4779
Jun 21, 2026
Merged

docs(rules,typescript): add Design Patterns subsection with ReadonlyMap internal/external example#4781
aegis-gh-agent[bot] merged 1 commit into
developfrom
docs/readonly-map-pattern-4779

Conversation

@OneStepAt4time

Copy link
Copy Markdown
Owner

Summary

Add a new ## Design Patterns subsection to .claude/rules/typescript.md documenting the internal-mutable / external-readonly Map pattern. This is the contributor-side TS convention introduced in #4779 (PR #4780) for the acp backend's pendingHandshakes: a private mutable Map (producer) plus a public ReadonlyMap view (consumer), enforced at compile time via Vitest's expectTypeOf.

What's changed

  • File: .claude/rules/typescript.md (+42, one canonical example).
  • New section: ## Design Patterns with one subsection (Internal-mutable / external-readonly Map (issue #4779)).
  • Pattern shape: code block showing the producer field (pendingHandshakesInternal) + the readonly public view, plus the rationale for why two fields (a single readonly ReadonlyMap field still has a mutable identity — the producer needs the real Map to call .set/.delete; the structural alias is what TypeScript narrows, not the runtime object).
  • Test-the-contract snippet: expectTypeOf assertions for missing .set/.delete/.clear and present .get/.has/.size.
  • Canonical example references: src/services/acp/backend/types.ts (PendingHandshake named type) and src/__tests__/acp-pendinghandshakes-readonly-4779.test.ts (the expectTypeOf regression test).

Why this home

  • .claude/rules/typescript.md is the agent-facing TS conventions doc — the right home for contributor-side type patterns, not user-facing docs/ (zero matches for PromptDeps / pendingHandshakes / expectTypeOf in docs/).
  • The pattern is contributor-side (how to structure a TS service-layer state holder), not user-facing architecture.

Scope discipline

  • One canonical example only. No expansion to other patterns in this PR — that's scope creep. Future patterns can be added as separate entries using the format this PR establishes.

Verification

  • npm run gate passed locally: 6291 passed / 10 skipped / 0 failed (446 test files, 263s wall).
  • ✅ Conventional commit title: docs(rules,typescript):.
  • ✅ Scope: 1 file, +42/-0 (within Boss's ~30 lines ballpark; the extra ~12 lines are the rationale for the two-field split, which is the bit that makes the pattern non-obvious from the shape alone).

Aegis version

develop (post-#4780 merge, 665db2c8).

Reviewers

  • @hephaestus — please confirm the example reflects your actual implementation choices (expectTypeOf test, internal/external naming, producer/consumer contract).
  • @ag-argus — 9-gate audit on the diff.
  • @ag-manudis — flagged this lane; standing by.

Out of scope

  • User-facing docs (docs/architecture.md, docs/contributing.md, docs/internal/) — left alone per Boss's direction.
  • No cross-link to user-facing docs added (the pattern is contributor-side; cross-link would be premature).

Resolves

@OneStepAt4time OneStepAt4time added the documentation Improvements or additions to documentation label Jun 21, 2026
aegis-gh-agent[bot]
aegis-gh-agent Bot previously approved these changes Jun 21, 2026

@aegis-gh-agent aegis-gh-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️ Argus 9-gate re-review — APPROVED

Verdict: ✅ APPROVED. Content LGTM, scope discipline intact, conventional commit, gate clean.

9-gate audit (docs-focused per MEMORY 2026-06-09):

Content quality (vs Hep's audit criteria from 05:22 GMT+2):

  • ✅ Internal/external split matches src/services/acp/backend.ts:97-100 pattern: pendingHandshakesInternal (mutable Map<string, PendingHandshake>) + pendingHandshakes (typed as ReadonlyMap<string, PendingHandshake>)
  • expectTypeOf regression test assertion matches src/__tests__/acp-pendinghandshakes-readonly-4779.test.ts (no .set/ .delete/ .clear, presence of .get/ .has)
  • ✅ Producer/consumer boundary correctly framed: pendingHandshakesInternal for mutations, PromptDeps.pendingHandshakes as consumer surface
  • ✅ Pointer to src/services/acp/backend/types.ts and acp-pendinghandshakes-readonly-4779.test.ts at the end of the section

Sharp additions beyond the strict criteria:

  • 'Why two fields, not a single readonly?' — explains the TypeScript readonly modifier (compile-time type narrowing only) vs the runtime mutability of the underlying object. This is the most valuable teaching point in the entry — many readers would default to 'just declare readonly' without understanding why that doesn't work.
  • 'Test the contract, not the runtime' — sharp conceptual framing of expectTypeOf as a compile-time check vs runtime check.
  • Regression-test benefit at tsc time, not code-review or runtime — explains why the test is worth its weight.

Scope discipline:

  • ✅ One canonical example only (ReadonlyMap internal/external split). No scope creep to other patterns.
  • ✅ Single file: .claude/rules/typescript.md
  • ⚠️ +42 lines vs the ~30 line target from Boss's acceptance criteria. Slight overage, but the extra lines are explanatory text (the 'why two fields' rationale, the regression-test benefit paragraph), not scope creep. Acceptable.

PR hygiene:

  • ✅ Conventional commit title: docs(rules,typescript): add Design Patterns subsection with ReadonlyMap internal/external example
  • ✅ Owner-authored (OneStepAt4time / OWNER association) → no App self-approval blocker
  • ✅ Targets develop (base_sha = 665db2c, the #4780 squash)
  • npm run gate clean: 6291 pass / 10 skip / 0 fail (per Scribe)
  • ✅ No secrets / gitignored files (docs change in tracked file)
  • ✅ Gate 5 (unit tests): N/A — docs change, content is the test
  • ✅ Gate 6 (E2E/UAT): N/A — same
  • ✅ Gate 8 (security clean): no secrets, no .env, no internal paths

Docs hygiene:

  • ✅ No internal plans/specs leaked
  • ✅ New section follows existing .claude/rules/typescript.md structure (## / ### / code blocks)
  • ✅ Cross-references to actual implementation files

Next in the review chain (per Boss's 05:09 criteria):

  • Hep's technical review — pending. He'll confirm the example reflects his actual implementation choices (internal/external naming, producer/consumer boundary, expectTypeOf test). If he flags drift from src/services/acp/backend/types.ts or the test file, ping him for clarification before merging.
  • Ema sign-off — pending. Owner-authored PR, so no App self-approval blocker dance, but per the lane convention Ema gives final sign-off.

Once Hep's tech review is in + Ema's sign-off, I squash-merge via gh api .../merge -X PUT with JSON body, conventional commit title preserved, no extra metadata (docs PR — no linked issue to Closes).

…ap internal/external example

Add a new Design Patterns subsection to .claude/rules/typescript.md that
documents the internal-mutable / external-readonly Map pattern introduced
in #4779 (PR #4780) for the acp backend's pendingHandshakes. The pattern
splits a private mutable Map (producer) from a public ReadonlyMap view
(consumer), enforced at compile time via Vitest's expectTypeOf.

Canonical example: src/services/acp/backend/types.ts (PendingHandshake
named type) and src/__tests__/acp-pendinghandshakes-readonly-4779.test.ts
(the expectTypeOf regression test that pins the contract).

Refs #4779.

@aegis-gh-agent aegis-gh-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical review — example accurately reflects implementation:

  • Naming: pendingHandshakesInternal (mutable Map) + pendingHandshakes (ReadonlyMap view) matches backend.ts:97-100 exactly.
  • Producer boundary: launchBackgroundHandshake is the sole .set/.delete callsite, verified by grep.
  • Consumer boundary: PromptDeps.pendingHandshakes in prompts.ts uses only .get/.has, no mutators.
  • expectTypeOf regression test: src/tests/acp-pendinghandshakes-readonly-4779.test.ts proves the contract at tsc time.
  • Why-two-fields rationale is correct: TypeScript readonly narrows declared type, not runtime identity. The split makes the producer the only compilable mutator path.

LGTM.

@aegis-gh-agent aegis-gh-agent Bot merged commit 5ef9f12 into develop Jun 21, 2026
17 checks passed
@aegis-gh-agent aegis-gh-agent Bot deleted the docs/readonly-map-pattern-4779 branch June 21, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant