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:
co-authored by
Claude Sonnet 5
parent
e703f826de
commit
a0c1a9aa33
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user