key-cape/workplans/KEY-WP-0018-export-completeness-evidence.md
tegwick 7d0f6d67a6 chore(consistency): register KEY-WP-0018 and refresh work records [auto]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WAsfsfQmDu4vcBhiMcmQp

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 867844@bnt-lap001
Assistant-Session: 3d45905e-0016-4b49-b828-231406881f7b
2026-09-07 08:47:00 +02:00

4.6 KiB

id type title domain repo status owner topic_slug created updated state_hub_workstream_id
KEY-WP-0018 workplan Make the LLDAP export report its own completeness infotech key-cape finished claude export-completeness-evidence 2026-09-07 2026-09-07 82ff99a1-65cc-5e1f-93fc-5bc8911ce77a

Closes gap G05 of history/2026-09-05-011726-scope-intent-assessment.md.

src/internal/migration/lldapexport/exporter.go discovers groups by walking each user's memberships, so a group nobody belongs to is absent from the snapshot and nothing says so. A LookupGroups failure is skipped by a continue under a comment claiming it is recorded in the incompatibility report — it is not. The run then emits result: "success" and the CLI reports a clean export. Group order comes from map iteration, so two exports of an unchanged directory differ.

A second defect falls out of reading the adapter: LDAPAdapter.LookupGroups never populates Group.Members, and the exporter builds every membership from that field. Against a real LLDAP directory the memberships block is therefore always empty, while the fixture-based tests pass because the mock fills it in.

The unit of work is the evidence, not the enumeration: an export that cannot prove it saw the whole directory must say which one it is.

Enumerate groups independently of membership

id: KEY-WP-0018-T01
status: done
priority: medium
state_hub_task_id: "abb11f82-06e9-58ee-8be2-6d20491e9524"

Add an optional domain.GroupLister capability (ListGroups) and implement it on LDAPAdapter with a direct group-subtree search that reads member and uniqueMember, so Group.Members is populated from the directory rather than left empty. Keep it optional rather than widening UserRepository: the OIDC layer has no use for it, and every existing implementation and test double would otherwise have to grow a method it never calls.

Added domain.GroupLister and LDAPAdapter.ListGroups, searching the group subtree for groupOfNames/groupOfUniqueNames and reading member with uniqueMember as the fallback attribute. Compile-time assertions on the adapter now state both contracts it satisfies. The adapter tests assert the filter is not a membership filter and that a member-less group survives the mapping, which is the property the whole task exists for.

Report or fail on incomplete reads

id: KEY-WP-0018-T02
status: done
priority: medium
state_hub_task_id: "d08f62dd-42e8-5c22-ba50-15b53f7473fd"

Record on the result which enumeration actually ran — a complete directory enumeration or the membership-derived fallback — and treat a failed group enumeration as fatal rather than as a silent gap. In the fallback path, record each skipped user in the incompatibility report instead of discarding the error, and derive memberships from the discovered user/group pairs. Emit partial telemetry, and have the CLI say which enumeration produced the snapshot, so a degraded export is legible without reading the YAML.

ExportResult gained groupEnumeration — written unconditionally, so nobody has to infer completeness from an absent field — and a Complete() predicate that requires both a directory enumeration and an empty report. The fallback path records its own incompleteness as a report entry before it starts, so the snapshot says so even when every user lookup succeeds.

The fallback also had to stop deriving memberships from Group.Members: the LLDAP adapter never populated that field, so against a real directory the memberships block was always empty while the fixture-backed tests passed. It now derives each membership from the user/group pair actually observed.

Define ordering and test the failure modes

id: KEY-WP-0018-T03
status: done
priority: medium
state_hub_task_id: "3cd47458-9469-5cd9-a0b1-f2894402e1f8"

Sort users, groups and memberships on stable keys so an unchanged directory exports byte-identically. Cover the cases the assessment names and the current suite does not: an empty group, a group enumeration failure, a per-user lookup failure in the fallback path, and repeat-run determinism.

Sorted users and groups by ID and memberships by group then user. Four exporter tests and two adapter tests cover the named cases; the determinism test runs the export three times over deliberately unsorted input and compares the full ordering, rather than asserting on a single sorted field.

Recorded in SCOPE.md and in G05's status what this does and does not establish: completeness evidence for the user/group/membership surface, not for credential migration, which stays under G03.