Reuses the existing identity/teams, identity/agents, and orchestration/hierarchy scaffold directories per doc 07's documented module placement. Key decisions: last-write-wins availability (not optimistic locking — operational telemetry, not a durable record), compound-unique skill upsert, cycle detection only on reparenting edits (not creation, which can't form a cycle), and a capability-eligibility read path that composes hierarchy scope with caller-supplied skills while deliberately excluding availability per doc 05's own ordering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.8 KiB
8.8 KiB
Phase 0 Research: Support Organization
Decision: Module placement — reuse and supersede the existing scaffold stubs
- Decision:
Team/Agent/AgentSkill/AgentAvailabilitylive in the existingsrc/modules/identity/agents/andsrc/modules/identity/teams/directories (doc 07's documented location) — both already exist as thin scaffold stubs from the original project setup (agentsRepository.findAllAgents()querying the genericUser/UserRolemodel).HierarchyNodelives in the existingsrc/modules/orchestration/hierarchy/directory (currently an emptyindex.ts). This feature replaces theidentity/agentsstub's content wholesale (realAgent, not aUserfilter) and populatesidentity/teamsandorchestration/hierarchyfor the first time. - Rationale: Doc 07 already places these entities in exactly these module paths — this is the documented, correct location, not an open design choice, and reusing the existing directories (rather than creating new ones) is the literal instruction the scaffold's own placeholder stubs were left there for.
- Alternatives considered: Leaving
identity/agents'User-based stub alone and adding a parallel realAgentmodule elsewhere — rejected;User/UserRoleis disconnected from this system's real architecture (no feature built since 002-saas-integration references it;CustomerReferenceis the real customer-identity mechanism), so the stub was never a foundation to build on, only a placeholder to replace.identity/customers(the sibling stub) is explicitly left untouched — out of scope for this feature (spec.md Assumptions).
Decision: Availability concurrency — last-write-wins, not optimistic locking
- Decision:
AgentAvailabilityupdates are plainupserts keyed onagentId(unique) — noexpectedVersionfield, unlikeTicket.statusorKnowledgeEntry's version-on-edit. - Rationale: Availability is frequently-changing operational telemetry (an agent's current status/load), not a durable business record whose history needs to survive a lost race — spec.md's Assumptions section states this explicitly. Constitution Principle VII's concurrency requirement is scoped to SLA/assignment-class correctness problems (double-assignment, missed breaches); a slightly stale availability read that gets overwritten a moment later by the correct current value isn't that class of problem.
- Alternatives considered: Reusing the
expectedVersion/409pattern from every other mutable-shared-state model in this codebase — rejected for the reason above; would add caller friction (every availability update needs to know the current version first) for a field that's supposed to be cheap and frequent to update.
Decision: Skill tags — unique on (agentId, skillTag), upsert on set
- Decision:
AgentSkillhas@@unique([agentId, skillTag]); "add or update a skill" is one upsert keyed on that compound uniqueness, never a raw insert. - Rationale: FR-006 requires updating an existing tag's level to replace it, not duplicate it — a compound unique constraint makes that a DB-enforced guarantee, not just an application-level convention that could drift.
- Alternatives considered: A plain non-unique table with "find-then-update-or-create" in the
service layer only — rejected; the DB-level constraint is strictly stronger and no harder to
implement (Prisma's
upserton a compound unique key is the same amount of code either way).
Decision: Hierarchy node — parent existence checked on create, cycle checked on reparent
- Decision:
POST(create) validatesparentId(if given) resolves to an existing node — no cycle check is needed here, since a brand-new node has no descendants yet.PUT(edit, including changingparentId) additionally walks the new parent's own ancestor chain up to the root; if the node being edited appears in that chain, the edit is rejected (FR-011). - Rationale: A newly created node literally cannot form a cycle (nothing points to it yet) — checking for one there would be dead code. A reparenting edit is the only operation that can introduce a cycle, so that's the only place the check needs to run. Walking the ancestor chain (bounded by the tree's actual depth, never unbounded) is the standard, simplest correct algorithm for this — no need for a more sophisticated cycle-detection structure at this scale.
- Alternatives considered: A materialized-path or nested-set model to make cycle detection O(1) — rejected as unwarranted complexity for an admin-configuration tree with no performance requirement in spec.md; the adjacency-list model doc 06 already specifies is sufficient, and matches this codebase's general preference for the simplest structure that satisfies the actual requirement.
Decision: Capability-eligibility lookup — skill union via hierarchy scope, no availability filtering
- Decision:
GET /support-org/capability-eligibility?skills=a,b&productId=&categoryId=&priorityId=(all query params free-text/external-reference strings, no FK validation againstcatalog/products/categories/priorities— same loosely-typed-scope conventionKnowledgeEntry.categoryScopealready established in 004). Resolution:- Find active
HierarchyNodes whoseproductScope/categoryScope/priorityScopeeach either is empty (matches anything) or contains the given value (spec.md Edge Cases: an unconfigured scope is "match on skill alone," not "match nothing"). - Union every matched node's own
skillsinto the caller's requestedskillsset. - Return active agents, on active teams, whose own
AgentSkilltags are a superset of that combined skill set (FR-014) — presence only, proficiencylevelis not filtered here (doc 05 §4 SKILL_BASED is a later assignment-strategy concern that readslevelfor weighting, not an eligibility gate). - Never filters or reorders by
AgentAvailability(FR-015, doc 05 §3's explicit "capability before availability" ordering) — abusy/offlineagent who has every required skill is still returned.
- Find active
- Rationale: This is FR-014/FR-015/Acceptance Scenario 5 (US4) made concrete: hierarchy scope
composes with, rather than replaces, the caller-supplied requirement, and the lookup stops at
"who is capability-eligible" — never touching availability or making an assignment decision,
which doc 05 §8 reserves for orchestration's own
engine/rules/strategiesstructure (a later, genuinely different feature). - Alternatives considered: Requiring the caller to pre-resolve which hierarchy node applies and pass its id directly — rejected; FR-014's own wording ("composing... when the context resolves to one") makes resolving the applicable node this lookup's job, not the caller's, matching how 004's retrieval resolves product/feature/category itself rather than making the caller pre-filter.
Decision: Hierarchy changes are audited via a dedicated per-module writer, reusing AuditLog
- Decision:
src/modules/orchestration/hierarchy/repository/hierarchy-audit-log.repository.tsexports awriteHierarchyAuditEvent(...)function, structurally identical tocatalog/products' existingwriteIntegrationAuditEvent— oneAuditLogrow per create/edit/ activate/deactivate,entityType: 'HierarchyNode'. - Rationale: FR-017/doc 07 explicitly require hierarchy changes to be audited, and this
codebase already has exactly one precedent for writing to
AuditLog(002-saas-integration's integration lifecycle events) — reusing that same shape (a small, module-owned writer function, not a shared generic "auditable" abstraction nothing has asked for) is the least surprising choice. - Alternatives considered: A shared cross-module
AuditService— rejected; only one other module in this codebase writes toAuditLogat all, and duplicating a ~15-line writer function a second time is simpler than introducing a new shared abstraction two call sites don't yet justify.
Decision: Admin endpoint authentication — reuse the existing stub
- Decision: Every admin CRUD endpoint in this feature (teams, agents, skills, availability,
hierarchy nodes) is gated by
fastify.authenticate, the same known-limitation stub every prior feature's admin surface uses. The capability-eligibility read endpoint is not gated — it's a read path a future internal caller (orchestration) will use, matching 004'sGET /knowledge/retrieveprecedent of leaving its own future-caller read path ungated while admin CRUD stays behind the stub. - Rationale: Consistency with established precedent; this feature does not build real agent
authentication (spec.md Assumptions) or touch
identity/auth. - Alternatives considered: None — direct reuse of existing, already-accepted conventions.