Files
support_backend/specs/006-support-organization/research.md
T
saqib mirandClaude Sonnet 5 c18e203ad1 docs: plan and design artifacts for support organization feature
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>
2026-09-02 17:52:33 +05:30

8.8 KiB

Phase 0 Research: Support Organization

Decision: Module placement — reuse and supersede the existing scaffold stubs

  • Decision: Team/Agent/AgentSkill/AgentAvailability live in the existing src/modules/identity/agents/ and src/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 generic User/UserRole model). HierarchyNode lives in the existing src/modules/orchestration/hierarchy/ directory (currently an empty index.ts). This feature replaces the identity/agents stub's content wholesale (real Agent, not a User filter) and populates identity/teams and orchestration/hierarchy for 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 real Agent module elsewhere — rejected; User/UserRole is disconnected from this system's real architecture (no feature built since 002-saas-integration references it; CustomerReference is 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: AgentAvailability updates are plain upserts keyed on agentId (unique) — no expectedVersion field, unlike Ticket.status or KnowledgeEntry'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/409 pattern 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: AgentSkill has @@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 upsert on 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) validates parentId (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 changing parentId) 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 against catalog/products/categories/priorities — same loosely-typed-scope convention KnowledgeEntry.categoryScope already established in 004). Resolution:
    1. Find active HierarchyNodes whose productScope/categoryScope/priorityScope each 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").
    2. Union every matched node's own skills into the caller's requested skills set.
    3. Return active agents, on active teams, whose own AgentSkill tags are a superset of that combined skill set (FR-014) — presence only, proficiency level is not filtered here (doc 05 §4 SKILL_BASED is a later assignment-strategy concern that reads level for weighting, not an eligibility gate).
    4. Never filters or reorders by AgentAvailability (FR-015, doc 05 §3's explicit "capability before availability" ordering) — a busy/offline agent who has every required skill is still returned.
  • 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/strategies structure (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.ts exports a writeHierarchyAuditEvent(...) function, structurally identical to catalog/products' existing writeIntegrationAuditEvent — one AuditLog row 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 to AuditLog at 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's GET /knowledge/retrieve precedent 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.