check-pr
無料Check an externally created PR for coding rule violations against memory/INDEX.md atoms and docs/architecture/code-review-rules.md.
日本語の概要は準備中です。原文の説明を表示しています。
Introduce the cross-section read interface boundary (I<Section>ServiceRead) for one section's service per memory/architecture/section-read-write-split.md. Audits the surface, evaluates which methods belong on the read interface, sets up the workspace, dispatches a subagent that introduces the interface and migrates non-section callers, opens a PR. Use when the user says 'read split for X', 'split <Section>Service', 'add I<Section>ServiceRead boundary', 'apply the section-read-write-split rule to <Section>', or any variation of carving the cross-section read surface out of a section's full service interface. Reference implementation is Teams (PR 678). Operates on one section per invocation.
インストール方法を見るインストールする前に、エージェントに与えられる指示の中身を確認できます。
Apply the read/write interface split — defined in memory/architecture/section-read-write-split.md — to one section's service. External sections that only read end up depending on the narrow I<Section>ServiceRead; writes, cache hooks, and section-internal reads stay on the full I<Section>Service : I<Section>ServiceRead. Active mode: skill sets up the workspace, dispatches a subagent that implements and opens the PR, reports the URL.
Every cross-section-consumed service has a narrow read interface that returns only its own projections — *Info, *Dto or whatever that section already uses. External sections never see EF entities of another section, never see write methods that aren't theirs, never accidentally invalidate caches they don't own. Today the boundary is advisory; a future Roslyn analyzer enforces. Each invocation of this skill closes the gap for one more section.
Teams is the reference. PR #678 introduced ITeamServiceRead with 4 methods, migrated 23 production files, and folded in 3 audit-driven surface reductions. The skill operationalizes that pattern.
<SectionName> — section to split (e.g., Users, Camps, Calendar). Names the section only. Its service interface, that interface's project, and its projection types are all discovered in Phase 0, never derived from the section name — see 0.0.<empty> — ask which section.Run sequentially. If any check fails or surfaces ambiguity, stop and ask the user. Don't proceed to worktree creation until Phase 0 is clean.
Notation: I<Section>Service, <Section>Info and I<Section>ServiceRead below are placeholders for the types Phase 0 resolves — not literal names to construct from <SectionName>. For Expenses they read IExpenseReportService, ExpenseReportDto and IExpenseReportServiceRead: the service name drops the plural, and the projection carries neither the section name nor the Info suffix. Substitute the resolved names throughout.
Nothing in this skill may assert a path or a type name. Discover both, once, here. A section's layout varies along one axis that no naming convention predicts: whether its contracts sit in a sibling .Contracts project or an in-project Contracts/ folder. Its service and projection type names vary too. Establish $ROOTS first; every step below searches it.
SECTION=<SectionName>
ROOTS=$(ls -d src/Sections/Humans.$SECTION \
src/Sections/Humans.$SECTION.Contracts 2>/dev/null)
[ -n "$ROOTS" ] || { echo "no such section"; exit 1; }
# Sections don't all have the same folders, and `git grep pat a/ b/` (no `--`)
# aborts entirely if any path is missing. Filter every multi-path list.
exist() { for p in "$@"; do [ -e "$p" ] && printf '%s ' "$p"; done; }
A section without a .Contracts sibling keeps its contracts in an in-project Contracts/ folder instead — $ROOTS still yields just the one project root; Phase B.2 resolves which shape applies.
Resolve the doc and test homes here too, and carry these variables into the later phases — Phase B.6 and Phase D consume them:
DOC=src/Sections/Humans.$SECTION/Docs/$SECTION.md
TESTS=tests/Humans.$SECTION.Tests
For Events that is src/Sections/Humans.Events/Docs/Events.md and tests/Humans.Events.Tests — there is no docs/sections/Events.md. The same is true of Teams and every other section.
The service type name is not the section name. Expenses exposes IExpenseReportService, Events exposes IEventService, Tickets exposes ITicketService — a synthesized I<Section>Service misses all three. Find the declaration rather than a filename; the type is often declared inside another file, so a find -name cannot see it either:
grep -rnE '^[[:space:]]*(public|internal) interface I[A-Za-z]+Service[[:space:]]*[:{]' \
$ROOTS --include='*.cs' | grep -v '/obj/'
Both modifiers matter: a section-internal interface is internal, while one promoted to a cross-section contract is public and lives in the section's contracts (IHoldedService is public in Humans.Holded.Contracts; only IHoldedAdminService is internal under Services/).
Then count external callers of the resolved name:
reforge callers <IResolvedService> --format json
Filter out callers inside $OWNED, resolved below. $ROOTS already covers the section's own controllers, views, models, services and repository — all in-project — so $OWNED only needs to add anything a section keeps outside its own tree under a non-obvious name (e.g. an authorization handler filed under a different name than the section).
OWNED="$ROOTS"
# Add any of the section's own files that live outside $ROOTS under a
# non-obvious name (e.g. an authorization handler), if found.
If zero external callers remain after excluding $OWNED, the read interface buys nothing — tell the user and stop.
Check before creating one. A section may already have an I<Service>ServiceRead, and it is not always in contracts: ICalendarServiceRead is declared internal inside src/Sections/Humans.Calendar/Services/ICalendarService.cs, with ICalendarService : ICalendarServiceRead already in place. Because it is declared in the same file as the full interface, a filename search never finds it — search declarations, the same way 0.1 and 0.2 do:
grep -rnE '^[[:space:]]*(public|internal) interface I[A-Za-z]+ServiceRead\b' \
$ROOTS --include='*.cs' | grep -v '/obj/'
public, move it to the section's contracts, re-point external callers) or to widen its method set. Do not run B.2 — creating a second I<Section>ServiceRead in the contracts namespace leaves the existing same-namespace internal type winning name resolution inside the section, so the full interface keeps inheriting the old one, DI keeps registering the old one, and the new public contract is inert.The architectural rule requires the read interface to return projections, never entities.
Neither the location nor the name of a projection is predictable. There is no Services/Models/ convention in this repo — EventInfo is under Humans.Events/Services/Dtos/, LegalDocumentInfo sits directly in Humans.Consent/Services/, TicketStubInfo in Humans.Tickets.Contracts/, and TeamInfo is not a file at all: it is declared inside Humans.Teams.Contracts/ITeamService.cs. The *Info suffix is not the convention either — across src/Sections the projection records run Dto (52), Result (51), Row (41), Snapshot (35), Model (33), Info (26), Summary (18). Expenses has no *Info type at all; its read surface returns ExpenseReportDto. Filtering on a suffix is what produces the false "section has no projection" stop.
So search for the shape — a record declared outside the entity folder — and read the results:
grep -rnE '^[[:space:]]*(public|internal)([[:space:]]+sealed)? record([[:space:]]+struct)?[[:space:]]+[A-Za-z]+' \
$ROOTS --include='*.cs' | grep -v '/obj/' | grep -v '/Domain/'
Records are the projection idiom here; entities are classes under Domain/, which the last filter drops. Searching declarations rather than filenames is also what finds TeamInfo, and it avoids matching every section's Properties/AssemblyInfo.cs.
Confirm at least one result is actually returned by the service's read methods — a record that exists but is only used as a request/command payload (AdminLegalDocumentUpsertRequest) is not a projection. If the section genuinely has no projection type:
TeamInfo as the shape template. Do not attempt to invent the projection inside this PR — it's a separate concern with its own callsite migration.test -f memory/architecture/section-read-write-split.md
grep -q "Cross-section read interface" docs/sections/SECTION-TEMPLATE.md
Both must exist (created in PR 678). If either is missing, surface and ask whether to recreate.
Invoke the audit-surface skill on I<Section>Service. Capture its output. The audit gives you:
Audit findings are starting points, not directives. Tier 1A "delete" recommendations have shipped wrong recommendations before — PR 678 caught one (a repo-level method had two live internal callers the audit missed). Before acting on any deletion, the subagent must verify by reading the body, grepping for callers in the impl + decorator + repo + tests, and only deleting if zero remain.
Filter audit candidates by both criteria:
Task<<Section>Info?>, Task<IReadOnlyDictionary<Guid, <Section>Info>>, Task<IReadOnlyList<<Section>SearchHit>> — yes. Task<<EntityType>?>, Task<IReadOnlyList<<EntityType>>> — no.Detect known patterns and resolve before proposing:
| Pattern | Resolution |
|---|---|
Naming collision (e.g. GetBySlugAsync exists returning entity AND we want a *Info-returning version) | Rename the entity-returning method to Get<Section>EntityBy<Key>Async, keep on full interface only. The new *Info-returning version takes the canonical name on the read interface. |
GetUser<X>Async returning <Section>Member[] or similar | Defer. Per-user projection (<Section>UserInfo) is a separate follow-up PR. Method stays on full I<Section>Service; do not migrate user-teams-style callers. |
Invalidate*Cache / RemoveMemberFromAll*Cache | Stay on full interface — writes against cache state, not reads. |
Method returns a value-type aggregate (Task<int> counts, Task<bool> predicates) called cross-section | Include on read interface — it's a read. Predicates like IsUserCoordinatorOfTeamAsync were borderline in Teams; rule of thumb: if a non-section caller would otherwise reimplement the logic, expose it. |
Present the proposed read surface to the user:
Proposed I<Section>ServiceRead (N methods):
- Method1(...) -> ProjectionType?
- Method2(...) -> IReadOnlyDictionary<..., ProjectionType>
- ...
Known skip cases (stay on full I<Section>Service):
- <method>: returns entity
- <method>: per-user projection deferred
- <method>: write/cache hook
Audit tier 1A/1B fold-in (if any):
- <method>: Tier 1A (delete) — N external + N internal callers verified zero
- <method>: Tier 1B (make private) — N internal callers in impl
If the user is happy, proceed. Otherwise iterate the proposal until they greenlight.
Branch off origin/main (per memory/process/worktrees-off-origin-main.md), in a worktree locally and in the repo root in a cloud run (per memory/process/always-use-worktree.md):
git fetch origin --quiet
if [ "$CLAUDE_CODE_REMOTE" = "true" ]; then # ephemeral single-session container — no worktree
git checkout -b feat/<lower-section>-service-read-split origin/main
else
git worktree add .claude/worktrees/section-read-split-<lower-section> -b feat/<lower-section>-service-read-split origin/main
fi
Dispatch a single subagent. Locally, use isolation: "worktree" pointing at the worktree path; in a cloud run drop the flag — the subagent works in the repo root on the branch just created. The subagent prompt embeds the approved plan from Phase 0 — the read surface, skip cases, naming renames, Tier 1A/1B verifications. The skill does not parallelize phases inside the subagent (each phase depends on the previous: interface must exist before callers can swap).
Subagent model: Sonnet (mechanical refactor work; complexity is in the surface design which the main session already settled).
The subagent receives this plan, with <Section> / <section> / method names substituted from the Phase 0 proposal.
feat/<lower-section>-service-read-split is checked out off origin/main.dotnet build Humans.slnx -v quiet && dotnet test Humans.slnx -v quiet. If not green, stop and report.*-surface.json, *-downstream.json, *-classified.json) so they don't end up in the diff.For each Tier 1A "delete":
For each Tier 1B "make private":
I<Section>Service.private on the impl class.Build + test green between commits.
Commit: chore(<section>): drop dead/internal interface surface per audit
I<Section>ServiceReadFor each rename case identified in Phase 0 (entity-returning method colliding with new projection-returning name):
Get<Section>EntityBy<Key>Async on I<Section>Service, impl, caching decorator, and all callers.Commit (or fold into B.2): refactor(<section>): rename entity-returning <method> to <newName>
Confirm 0.1b found no existing read interface before creating one.
A read interface that external sections consume is a cross-section contract, so it goes with the section's other contracts. That is where the ones serving external callers live — ITeamServiceRead, IEventServiceRead, ITicketServiceRead and the rest. It is not a universal rule, though: ICalendarServiceRead is internal, declared inside Humans.Calendar/Services/ICalendarService.cs, because it draws the read boundary within the section rather than for outside consumers. Placement follows who consumes it. Match whichever contracts shape the section already uses:
| Section shape | New file | Namespace |
|---|---|---|
Sibling contracts project (Humans.Teams.Contracts, most sections) | src/Sections/Humans.<Section>.Contracts/I<Section>ServiceRead.cs | Humans.<Section>.Contracts |
In-project contracts folder (Humans.Store, Humans.Feedback) | src/Sections/Humans.<Section>/Contracts/I<Section>ServiceRead.cs | Humans.<Section>.Contracts |
Both shapes use the same namespace, so only the file path differs. If the section has neither a sibling .Contracts project nor a Contracts/ folder, create the folder in-project — that's the lighter of the two and needs no new .csproj or reference edits.
namespace Humans.<Section>.Contracts;
/// <summary>
/// Cross-section read surface for the <Section> section. External sections inject
/// this interface; only <Section>Info / <Section>SearchHit projections, no EF entities.
/// See memory/architecture/section-read-write-split.md.
/// </summary>
public interface I<Section>ServiceRead
{
// Methods from Phase 0 proposal
}
I<Section>Service may live in a different project than the read interface — the full service interface is often under Services/ while the read interface is in contracts. Make sure the project owning I<Section>Service references the contracts project before B.3.
I<Section>Service.cspublic interface I<Section>Service : I<Section>ServiceRead.Get<Section>EntityBy<Key>Async.GetBySlugAsync(slug) → <Section>Info?). On the caching decorator, these are typically a one-line Values.FirstOrDefault(x => x.Slug == slug) against TrackedCache.Values — no repo hit on warm cache.In the section's DI registration — src/Sections/Humans.<Section>/Section.cs (ISection.Register):
services.AddSingleton<Caching<Section>Service>();
services.AddSingleton<I<Section>Service>(sp => sp.GetRequiredService<Caching<Section>Service>());
services.AddSingleton<I<Section>ServiceRead>(sp => sp.GetRequiredService<Caching<Section>Service>());
services.AddHostedService(sp => sp.GetRequiredService<Caching<Section>Service>());
Both interfaces resolve to the same singleton.
In the section's architecture test file under $TESTS — $TESTS/<Section>ArchitectureTests.cs at the project root (e.g. tests/Humans.Events.Tests/EventsArchitectureTests.cs) — or a new file there if missing, assert:
Do not put a section's test in any other test project. No other project references the section assembly, and a section grants InternalsVisibleTo only to Humans.<Section>.Tests and Humans.Integration.Tests. Since section service interfaces are internal, a test placed elsewhere cannot see the type under test and will not compile — and even if it did, it could not build the section's production DI registration to check the resolution below.
I<Section>Service inherits from I<Section>ServiceRead.Build + test green.
Commit: feat(<section>): introduce I<Section>ServiceRead boundary
grep -rnE 'I<Section>Service\b' --include='*.cs' src/ tests/ | grep -v 'I<Section>ServiceRead'
For each file outside the section's own tree — exclude everything under $ROOTS as resolved in 0.0, the same scope 0.1 used for the caller count, plus the section's auth handler if it's filed under a different name:
I<Section>Service usages.I<Section>ServiceRead, swap the field/ctor parameter type from I<Section>Service → I<Section>ServiceRead. Update field name if it follows a _section/section convention.Many sections have architecture tests that reference I<Section>Service for dependency checks. These are spread across per-section test projects, so grep all of tests/:
grep -rnE 'I<Section>Service\b' --include='*.cs' tests/
For each match, evaluate: is the test asserting "this section depends on I<Section>Service" (write-bearing dependency, keep) or "this section reads from I<Section>Service" (read-only, swap to I<Section>ServiceRead)?
If the repo has tests/Humans.Web.Tests/Architecture/Baselines/*.txt files (these are repo-wide, not per-section) that enumerate methods or interface names that changed (renames, deletions, additions), update them. Common pattern: a baseline lists "entity-returning reads in Application services" — the rename in B.1 may update it.
Batch by directory or section, ≤10 files per commit. After each batch:
dotnet build Humans.slnx -v quiet
dotnet test Humans.slnx -v quiet
git push
Commit pattern: refactor(<consumer-section>): consume I<Section>ServiceRead
If this is the first section being split (artifacts didn't exist in Phase 0.3), create them. Otherwise just add the section reference.
$DOC — the invariant doc resolved in 0.0 at src/Sections/Humans.<Section>/Docs/<Section>.md, NOT docs/sections/. Add under "Architecture":
- **Read/write interface split.** `I<Section>ServiceRead` (N methods: ...) is the cross-section read surface — only `<Section>Info` projections, no EF entities. `I<Section>Service : I<Section>ServiceRead` adds writes, cache invalidation, and <Section>-internal reads. External sections inject `I<Section>ServiceRead`. See `memory/architecture/section-read-write-split.md`.
If memory atom is missing, recreate from PR 678's content. If section-template addendum is missing, recreate from PR 678's content. Both should already exist — this is a defensive fallback only.
docs/architecture/maintenance-log.md — update this section's Section Refactor History row: Last Lane (date + PR) and Post-Lane Score (the section's built reforge surface-score after the final commit).
Commit: docs(<section>): note read/write split + reference impl
gh pr create --title "feat(<section>): introduce I<Section>ServiceRead boundary" --body "$(cat <<'EOF'
## Summary
Introduces `I<Section>ServiceRead` as the cross-section read boundary for the <Section> section. External sections that only read inject the narrow interface (N `<Section>Info` / `<Section>SearchHit`-returning methods); writes and cache hooks stay on `I<Section>Service : I<Section>ServiceRead`.
[If Tier 1A/1B folded in:]
Also folds in <count> audit-driven surface reductions.
Enforcement is advisory for now — a future Roslyn analyzer will enforce. See `memory/architecture/section-read-write-split.md` and Teams' PR #678 for the reference implementation.
### Phase C migration counts
- **Migrated to `I<Section>ServiceRead`:** <N> production files
- Application services (<count>): <list>
- Infrastructure (<count>): <list>
- Web (<count>): <list>
- **Skipped (still inject `I<Section>Service`):** <N>+ files that call writes, cache hooks, entity-returning reads, deferred user-projection methods, or other full-interface members. A separate audit will sweep these.
### Audit deviations (if any)
[For each Tier 1A recommendation kept against the audit's advice:]
- `<method>` (Tier 1A "delete"): kept — <N> live internal callers in <files>.
## Test plan
- [x] `dotnet build Humans.slnx -v quiet`
- [x] `dotnet test Humans.slnx -v quiet`
- [x] New unit test: `<NewMethod>` returns same data as `<EntityVersion>` for a known row
- [x] New architecture tests: `I<Section>Service` inherits from `I<Section>ServiceRead`, both DI-resolve to same singleton
- [ ] Manual: load representative pages, confirm no regression
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
The PR description footer must include both the migration count breakdown and any audit deviations — these are the running tally the next-section-split skill (this one, run again) reads to track progress.
origin/feat/<lower-section>-service-read-split.peterdrier/Humans:main.*Info projection — needs a separate projection PR first.I<Section>Service — split buys nothing.Surface as: "I hit X. Decision needed: [A] / [B] / [C]."
These have shaped the skill above; called out here so the subagent doesn't relearn them:
ITeamRepository.GetPendingCountsByTeamIdsAsync; it had two live internal callers the audit missed. The subagent verified, kept it, noted the deviation in the PR footer. The skill's verify-before-deleting step came from this.Calendar, Campaigns, Feedback, TicketQuery, CityPlanning architecture tests because they referenced ITeamService. The Phase C.2 sweep came from this.Baselines/ApplicationServiceEntityReadReturns.baseline.txt needed an update from the slug rename. Phase C.3 came from this.CanUserApproveRequestsForTeamAsync private removed 110 lines from TeamServiceTests.cs (tests now flow through the public callers that exercise it). Don't be surprised by it.*Info) to enable a caller migration. That's a different PR.memory/architecture/section-read-write-split.md — the durable rule.docs/sections/SECTION-TEMPLATE.md — "Cross-section read interface" block.src/Sections/Humans.Teams/Docs/Teams.md — reference implementation. Its doc lives in the project; there is no docs/sections/Teams.md..claude/skills/audit-surface/ — invoked in Phase 0.4 (lives in ~/.claude/skills/audit-surface/, not this repo)..claude/skills/reforge/SKILL.md — invoked in Phase 0.1 for caller enumeration..claude/skills/section-align/SKILL.md — broader sibling that also touches cross-section boundaries; this skill is narrower.まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Check an externally created PR for coding rule violations against memory/INDEX.md atoms and docs/architecture/code-review-rules.md.
日本語の概要は準備中です。原文の説明を表示しています。
Two-phase memory hygiene. Phase 1: scan external Claude Code memory (~/.claude/projects/<slug>/memory/) for durable rules to migrate into the repo and entries already duplicated by repo atoms. Phase 2: audit in-repo memory/, CLAUDE.md, docs/architecture/, and docs/sections/ for dead links, duplication, drift, and bloat.
日本語の概要は準備中です。原文の説明を表示しています。
Review, repair and steward the nightly Codex debt-sweep PR (branch codex/daily-debt/<date>). Judges every commit GOOD / REPAIR / REVERT, triages the bot review findings through /fix, lands one round-1 commit, files follow-up issues, then stewards the PR itself with a 3-round ceiling. Run by the 08:00 UTC cloud routine; also '/debt-review', '/debt-review 1789'.
日本語の概要は準備中です。原文の説明を表示しています。
Autonomous themed tech-debt cleanup. Reads docs/architecture/debt-ledger.yml, rotates to the least-recently-served debt theme, works it for a time budget (default 2h) or until drained, opens one PR, and asks judgment questions inline at the end. Use for daily debt burndown without Peter pointing at a target: grandfathered analyzer rules, architecture-test baselines, obsolete-field reads, cross-section stitching.
日本語の概要は準備中です。原文の説明を表示しています。
Scrap and regenerate the in-flight EF migrations on the current branch as one consolidated migration. Use when migrations have accumulated mid-development cruft (added/removed columns, stacked add-column-to-existing-table fixes, hand-edited SQL that needs to come out, schema changes that should have been one migration but ended up as five), or when the branch has merged main and its migrations are now stuck mid-chain so `dotnet ef migrations remove` is unsafe. Triggers on phrases like 'regen the migrations', 'redo these migrations', 'scrap and regen migrations', 'consolidate the in-flight migrations', 'redo the migration stack', or any time an agent hits the mid-chain situation in `memory/architecture/migration-regen-after-rebase.md`.
日本語の概要は準備中です。原文の説明を表示しています。
Use when a sprint tracking issue (label `sprint`) on peterdrier/Humans needs working — routine-fired cloud runs, or any unattended session told to execute the sprint queue.
日本語の概要は準備中です。原文の説明を表示しています。