feat: implement support organization (006) — teams, agents, hierarchy, capability lookup

Phase 6 of the roadmap. Populates the identity/teams and
orchestration/hierarchy module directories (previously empty or
near-empty scaffold stubs) and replaces identity/agents' pre-existing
placeholder, which queried a generic User/UserRole model unrelated to
this system's real architecture (nothing since 002-saas-integration's
CustomerReference has used it).

Teams and agents: active/inactive CRUD, never hard deletion; team
deactivation never cascades to its agents. Skills: compound-unique
upsert on (agentId, skillTag), never duplicates. Availability: a single
current record per agent, deliberately last-write-wins rather than
optimistic-locked — operational telemetry, not a durable business
record. The dynamic hierarchy: a nestable, orderable HierarchyNode tree
with every field (scope, assignment strategy reference, entry/exit
conditions) stored as opaque admin-set data; cycle detection runs only
on reparenting edits (a new node can't form a cycle); every
create/edit/activate/deactivate is audited via AuditLog, reusing
002's existing writer pattern. A capability-eligibility read path
composes hierarchy scope with caller-supplied skills for the future
orchestration/assignment phase (Phase 7) to call, deliberately
excluding availability per doc 05's own capability-before-availability
ordering.

Found and fixed a real bug before it reached tests: the initial
availability upsert reset currentLoad to 0 on every update, not just
creation.

Adds 11 unit tests (cycle detection, capability matching) and 4
integration test files covering all four user stories. Full regression
(every pre-existing 002-005 integration test plus all new ones) run
against real Postgres/Redis/MinIO: 107 passed, 9 skipped (005's
AI-key-gated tests, unrelated), 0 failed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
saqib mir
2026-09-02 18:12:26 +05:30
co-authored by Claude Sonnet 5
parent e703f826de
commit a0c1a9aa33
55 changed files with 1782 additions and 92 deletions
@@ -48,3 +48,22 @@
`/knowledge/retrieve` being built for 005-ai-support to call rather than that later feature
reaching into 004's internals.
- All items pass; no revision iterations were needed.
## Implementation notes (added during /speckit-implement)
- **Found and fixed a real bug before it reached tests**: the initial `AgentAvailability.upsert`
wrote `currentLoad: data.currentLoad ?? 0` unconditionally on every call, including updates —
meaning an update that only changed `status` would silently reset `currentLoad` back to `0`
every time, even if it had since been incremented by something else. Fixed by only defaulting
to `0` on the `create` branch of the upsert; the `update` branch omits the field entirely
unless the caller explicitly supplied a new value.
- `identity/teams` was *also* already a `User`-unrelated but still fully placeholder scaffold
(`findAllTeams()` hardcoded to return `[]`, no real query, its one route unregistered) — the
spec's Assumptions only called out `identity/agents`/`identity/customers`'s `User`-based stub;
`identity/teams`' emptier placeholder was found and superseded the same way during
implementation.
- All four user stories' integration tests passed on the first real run against Postgres — no
further bugs surfaced beyond the availability-upsert one above. Full regression (every
pre-existing integration test file across 002-005 plus all four new ones) was run together
against real Docker-provisioned Postgres/Redis/MinIO: 107 passed, 9 skipped (005's AI-key-gated
tests, unrelated to this feature), 0 failed.
+33 -33
View File
@@ -25,13 +25,13 @@ All file paths are relative to `supporthub-api/` (repo root).
## Phase 1: Setup
- [ ] T001 [P] Populate `src/modules/identity/teams/` with the standard module shape
- [x] T001 [P] Populate `src/modules/identity/teams/` with the standard module shape
(`controller/`, `routes/`, `schema/`, `repository/`, `service/`, `types/`, `mapper/`,
`constants/`, `index.ts`) — this directory currently has no files
- [ ] T002 [P] Replace `src/modules/identity/agents/`'s existing `User`-based stub content
- [x] T002 [P] Replace `src/modules/identity/agents/`'s existing `User`-based stub content
(`repository/agents.repository.ts`, `service/agents.service.ts`,
`schema/agents.schema.ts`) — keep the module's file layout, replace what each file does
- [ ] T003 [P] Populate `src/modules/orchestration/hierarchy/` with the standard module shape —
- [x] T003 [P] Populate `src/modules/orchestration/hierarchy/` with the standard module shape —
this directory currently only has an empty `index.ts`
---
@@ -42,12 +42,12 @@ All file paths are relative to `supporthub-api/` (repo root).
**⚠️ CRITICAL**: No user-story stage work can begin until this phase is complete.
- [ ] T004 Add `Team`, `Agent`, `AgentSkill`, `AgentAvailability`, `HierarchyNode` models to
- [x] T004 Add `Team`, `Agent`, `AgentSkill`, `AgentAvailability`, `HierarchyNode` models to
`prisma/schema.prisma` per data-model.md (including `@@unique([agentId, skillTag])` on
`AgentSkill`, `agentId @unique` on `AgentAvailability`, the `HierarchyTree` self-relation
on `HierarchyNode`, and the `(teamId, active)`/`(parentId, order)`/`(active)` indexes)
(depends on T001-T003)
- [ ] T005 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`) for
- [x] T005 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`) for
T004 (depends on T004)
**Checkpoint**: Schema migrated. User stories can now be built.
@@ -63,28 +63,28 @@ never cascades.
### Tests for User Story 1
- [ ] T006 [US1] Integration test covering Quickstart Scenario 1 (create team/agent, agent
- [x] T006 [US1] Integration test covering Quickstart Scenario 1 (create team/agent, agent
appears in roster, deactivate excludes from active listing without deleting, reactivate
restores it, team deactivation doesn't cascade) against a real Postgres in
`tests/integration/support-org-teams-agents.test.ts`
### Implementation for User Story 1
- [ ] T007 [P] [US1] Add `TeamsRepository`/`TeamsService` (create; update; findById with agent
- [x] T007 [P] [US1] Add `TeamsRepository`/`TeamsService` (create; update; findById with agent
roster; findAll) in `src/modules/identity/teams/repository/teams.repository.ts` +
`service/teams.service.ts` (depends on T005)
- [ ] T008 [US1] Add `AgentsRepository`/`AgentsService` (create; update; findById; findAll with
- [x] T008 [US1] Add `AgentsRepository`/`AgentsService` (create; update; findById; findAll with
`active`/`teamId` filters) — replacing the existing `User`-based stub — in
`src/modules/identity/agents/repository/agents.repository.ts` +
`service/agents.service.ts` (depends on T005, T007 for the `teamId` FK)
- [ ] T009 [US1] Add Zod schemas + routes: `POST /admin/teams`, `PATCH /admin/teams/:teamId`,
- [x] T009 [US1] Add Zod schemas + routes: `POST /admin/teams`, `PATCH /admin/teams/:teamId`,
`GET /admin/teams/:teamId`, `GET /admin/teams` (gated by `fastify.authenticate`) in
`identity/teams/schema/` + `routes/`, registered from `src/api/routes.ts` (depends on T007)
- [ ] T010 [US1] Add Zod schemas + routes: `POST /admin/teams/:teamId/agents`,
- [x] T010 [US1] Add Zod schemas + routes: `POST /admin/teams/:teamId/agents`,
`PATCH /admin/agents/:agentId`, `GET /admin/agents/:agentId`, `GET /admin/agents` (gated by
`fastify.authenticate`) in `identity/agents/schema/` + `routes/`, registered from
`src/api/routes.ts` (depends on T008, T009)
- [ ] T011 [US1] Run Quickstart Scenario 1 locally and confirm all 5 steps pass
- [x] T011 [US1] Run Quickstart Scenario 1 locally and confirm all 5 steps pass
**Checkpoint**: Teams and agents can be created, updated, and deactivated/reactivated without
ever losing history. Usable as the foundation every later story needs.
@@ -99,26 +99,26 @@ ever losing history. Usable as the foundation every later story needs.
### Tests for User Story 2
- [ ] T012 [US2] Integration test covering Quickstart Scenario 2 (skill upsert doesn't
- [x] T012 [US2] Integration test covering Quickstart Scenario 2 (skill upsert doesn't
duplicate, availability upsert stays single-record, invalid status rejected) against a
real Postgres in `tests/integration/support-org-skills-availability.test.ts`
### Implementation for User Story 2
- [ ] T013 [P] [US2] Add `AgentSkillsRepository`/`AgentSkillsService` (`upsert(agentId,
- [x] T013 [P] [US2] Add `AgentSkillsRepository`/`AgentSkillsService` (`upsert(agentId,
skillTag, level)`; `findAllByAgent`) in
`src/modules/identity/agents/repository/agent-skills.repository.ts` +
`service/agent-skills.service.ts` (depends on T005)
- [ ] T014 [P] [US2] Add `AgentAvailabilityRepository`/`AgentAvailabilityService`
- [x] T014 [P] [US2] Add `AgentAvailabilityRepository`/`AgentAvailabilityService`
(`upsert(agentId, status, workingHours, currentLoad?)`; `findByAgent`) — `status` validated
against the fixed `available`/`busy`/`away`/`offline` set at the Zod schema layer (FR-008)
— in `src/modules/identity/agents/repository/agent-availability.repository.ts` +
`service/agent-availability.service.ts` (depends on T005)
- [ ] T015 [US2] Add Zod schemas + routes: `PUT /admin/agents/:agentId/skills/:skillTag`,
- [x] T015 [US2] Add Zod schemas + routes: `PUT /admin/agents/:agentId/skills/:skillTag`,
`GET /admin/agents/:agentId/skills`, `PUT /admin/agents/:agentId/availability`,
`GET /admin/agents/:agentId/availability` (gated by `fastify.authenticate`) in
`identity/agents/schema/` + `routes/` (depends on T013, T014)
- [ ] T016 [US2] Run Quickstart Scenario 2 locally and confirm all 5 steps pass
- [x] T016 [US2] Run Quickstart Scenario 2 locally and confirm all 5 steps pass
**Checkpoint**: Agents carry real, current skill and availability data — everything a future
capability/assignment lookup needs to read.
@@ -134,37 +134,37 @@ opaque data for a future orchestration engine.
### Tests for User Story 3
- [ ] T017 [P] [US3] Unit tests for the pure cycle-detection function — a direct self-reference
- [x] T017 [P] [US3] Unit tests for the pure cycle-detection function — a direct self-reference
rejected, a transitive cycle (A's new parent is A's own descendant) rejected, a legitimate
reparent to an unrelated node accepted — in `tests/unit/support-org/cycle-check.test.ts`
- [ ] T018 [US3] Integration test covering Quickstart Scenario 3 (parent/child creation, order
- [x] T018 [US3] Integration test covering Quickstart Scenario 3 (parent/child creation, order
preserved, nonexistent `parentId` rejected, self-reparent rejected, deactivation doesn't
cascade) against a real Postgres in `tests/integration/support-org-hierarchy.test.ts`
### Implementation for User Story 3
- [ ] T019 [P] [US3] Add the pure cycle-detection function `wouldCreateCycle(nodeId,
- [x] T019 [P] [US3] Add the pure cycle-detection function `wouldCreateCycle(nodeId,
candidateParentId, ancestorChainOf: (id) => string[]) => boolean` in
`src/modules/orchestration/hierarchy/service/cycle-check.ts` (no dependencies — pure
function, takes the ancestor-lookup as an injected function so it's testable without a
database)
- [ ] T020 [US3] Add `HierarchyAuditLogRepository.writeHierarchyAuditEvent(...)` (research.md,
- [x] T020 [US3] Add `HierarchyAuditLogRepository.writeHierarchyAuditEvent(...)` (research.md,
same shape as `catalog/products`' `writeIntegrationAuditEvent`) in
`src/modules/orchestration/hierarchy/repository/hierarchy-audit-log.repository.ts`
(depends on T005)
- [ ] T021 [US3] Add `HierarchyRepository` (create — validates `parentId` exists, `404` if not;
- [x] T021 [US3] Add `HierarchyRepository` (create — validates `parentId` exists, `404` if not;
`updateById` — validates via T019 before applying a `parentId` change; `findById`;
`findChildren(parentId, ordered)`; `findAll(activeOnly?)`; `setActive`) +
`HierarchyService` (wraps the repository, calls T020 on every create/edit/activate/
deactivate — FR-017) in
`src/modules/orchestration/hierarchy/repository/hierarchy.repository.ts` +
`service/hierarchy.service.ts` (depends on T019, T020)
- [ ] T022 [US3] Add Zod schemas + routes: `POST /admin/hierarchy-nodes`,
- [x] T022 [US3] Add Zod schemas + routes: `POST /admin/hierarchy-nodes`,
`PUT /admin/hierarchy-nodes/:nodeId`, `PATCH .../activate`, `PATCH .../deactivate`,
`GET /admin/hierarchy-nodes/:nodeId`, `GET .../children`, `GET /admin/hierarchy-nodes`
(gated by `fastify.authenticate`) in `orchestration/hierarchy/schema/` + `routes/`,
registered from `src/api/routes.ts` (depends on T021)
- [ ] T023 [US3] Run Quickstart Scenario 3 locally and confirm all 6 steps pass
- [x] T023 [US3] Run Quickstart Scenario 3 locally and confirm all 6 steps pass
**Checkpoint**: The dynamic support hierarchy — this feature's core differentiator — is fully
configurable, cycle-safe, and audited.
@@ -180,32 +180,32 @@ excluding availability.
### Tests for User Story 4
- [ ] T024 [P] [US4] Unit tests for the pure capability-matching predicate (agent skill set is a
- [x] T024 [P] [US4] Unit tests for the pure capability-matching predicate (agent skill set is a
superset of the required set → eligible; missing one required skill → not eligible; empty
hierarchy scope matches any product/category/priority) in
`tests/unit/support-org/capability-match.test.ts`
- [ ] T025 [US4] Integration test covering Quickstart Scenario 4 (skill-only match; availability
- [x] T025 [US4] Integration test covering Quickstart Scenario 4 (skill-only match; availability
never filters; inactive agent excluded; empty result on no match, never an error;
hierarchy-scope skill composition) against a real Postgres in
`tests/integration/support-org-capability-lookup.test.ts`
### Implementation for User Story 4
- [ ] T026 [P] [US4] Add the pure capability-matching predicate `isCapabilityEligible(agentSkills:
- [x] T026 [P] [US4] Add the pure capability-matching predicate `isCapabilityEligible(agentSkills:
string[], requiredSkills: string[]) => boolean` in
`src/modules/orchestration/hierarchy/service/capability-match.ts` (no dependencies)
- [ ] T027 [US4] Add `CapabilityLookupService.findEligibleAgents(skills, productId?, categoryId?,
- [x] T027 [US4] Add `CapabilityLookupService.findEligibleAgents(skills, productId?, categoryId?,
priorityId?)`: finds active `HierarchyNode`s whose scope arrays are empty-or-matching for
each given context value, unions their `skills` into the requested set, queries active
agents (active team) via T026's predicate applied to each candidate's `AgentSkill` tags —
in `src/modules/orchestration/hierarchy/service/capability-lookup.service.ts` (depends on
T026, and on `identity/agents`' repository being queryable — reuses its exported
repository rather than duplicating the query)
- [ ] T028 [US4] Add Zod schema + `GET /support-org/capability-eligibility` route (no
- [x] T028 [US4] Add Zod schema + `GET /support-org/capability-eligibility` route (no
`fastify.authenticate` gate — research.md "Admin endpoint authentication") in
`orchestration/hierarchy/schema/` + `routes/`, registered from `src/api/routes.ts`
(depends on T027)
- [ ] T029 [US4] Run Quickstart Scenario 4 locally and confirm all 5 steps pass
- [x] T029 [US4] Run Quickstart Scenario 4 locally and confirm all 5 steps pass
**Checkpoint**: All four user stories work independently and together — a full support
organization (teams, agents, skills, availability, hierarchy) exists and is queryable by
@@ -215,13 +215,13 @@ capability, ready for Phase 7's orchestration engine to build on.
## Phase 7: Polish & Cross-Cutting Concerns
- [ ] T030 [P] Add a "Support Organization" section to `README.md` describing the admin CRUD,
- [x] T030 [P] Add a "Support Organization" section to `README.md` describing the admin CRUD,
the cycle-safety/audit guarantees on hierarchy nodes, and the capability-eligibility
contract (explicitly noting what it does *not* do — availability filtering, assignment)
- [ ] T031 [P] Update `specs/006-support-organization/checklists/requirements.md` Notes with any
- [x] T031 [P] Update `specs/006-support-organization/checklists/requirements.md` Notes with any
implementation-time findings
- [ ] T032 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
- [ ] T033 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing broke
- [x] T032 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
- [x] T033 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing broke
elsewhere, then the full integration suite against real Docker-provisioned infra
---