Commit Graph
31 Commits
Author SHA1 Message Date
saqib mirandClaude Sonnet 5 8400a8db84 feat(016-load-concurrency-testing): US5 — autocannon load-test tooling
tests/load/autocannon.config.ts is a thin shared wrapper over autocannon's
programmatic API producing this feature's own report shape (throughput,
latency p50/p90/p99, non-2xx, rate-limited count) — printed and written to
tests/load/reports/ (gitignored) for every run. No pass/fail threshold is
applied (FR-009): throughput/latency targets are an OPEN BUSINESS DECISION
per the roadmap's own convention, never invented.

Three scripts cover the named critical endpoint groups: ticket-creation
(pure DB path), admin-reporting (015-reporting-dashboards, pure DB path),
and ai-support-flow (005-ai-support's real Anthropic API calls — clearly
flagged as real, billed cost, run only at a small bounded amount rather than
an open-ended duration). All three were run once at a small scale against the
real dev server to confirm the tooling works end-to-end and cleans up fully
after itself (verified via direct DB checks, not just script exit codes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 11:50:12 +05:30
saqib mirandClaude Sonnet 5 cd8e408d7e test(016-load-concurrency-testing): US4 — prove ticket status optimistic concurrency
tests/concurrency/ticket-status-race.test.ts fires 20 genuinely concurrent
TicketsRepository.updateStatus calls from the same starting version against
real Postgres. Passes on the first run, confirming (rather than assuming)
003-ticketing's existing atomic version-checked updateMany already holds
under real concurrency — no implementation change needed (research.md §4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 11:19:24 +05:30
saqib mirandClaude Sonnet 5 c7aac460b7 test(016-load-concurrency-testing): US3 — prove escalation idempotency holds
tests/concurrency/escalation-idempotency.test.ts fires the same escalation
trigger (escalationService.handleBreach) concurrently more than once for the
same ticket against real Postgres, asserting exactly one EscalationEvent and
one current Assignment result every time — verified across 10 repeated runs
(SC-003). The database-level unique-violation is visibly caught and absorbed
in the logs, confirming the fix (previous commit) actually engages under a
genuine race rather than being untested code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 11:17:31 +05:30
saqib mirandClaude Sonnet 5 93d6fe8b94 fix(016-load-concurrency-testing): US3 — finish escalation idempotency plumbing
Completes the escalation-event repository/service changes from the prior
commit: normalizes the exactOptionalPropertyTypes mismatch in the
findFirst fallback lookup, and updates every existing unit test
(sla-pause-resume, sla-breach-detection, sla-compliance-metric,
escalation-rule-match) to the new SlaRunRepository.updateWithVersion and
EscalationEventRepository.create({event, wasNewlyCreated}) signatures.
Full typecheck/lint/architecture-check clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 11:13:46 +05:30
saqib mirandClaude Sonnet 5 0e3543bb01 fix(016-load-concurrency-testing): US2 — version-guard SLA pause/resume/sweep
SlaRunRepository.updateWithVersion replaces the old unguarded update(),
mirroring TicketsRepository.updateStatus's exact atomic-updateMany pattern.
pause/resume/complete now retry (bounded) against fresh state on a version
conflict; the breach sweep skips a run that lost the race to a concurrent
pause/resume/complete rather than clobbering it, deferring to the next
scheduled pass. Verified against real Postgres: concurrent pause/resume/sweep
activity against the same run now always leaves it in one
internally-consistent state, across repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 17:17:09 +05:30
saqib mirandClaude Sonnet 5 58d0a98134 fix(016-load-concurrency-testing): US1 — retry assignment creation on race conflict
AssignmentRepository.createAssignment now catches the
assignments_one_current_per_ticket unique-violation and retries the whole
supersede-then-create transaction (bounded, with jitter) instead of
propagating a raw P2002 to the caller. Verified against real Postgres: before
this fix, 20 genuinely concurrent assignment attempts on the same ticket
reliably threw an unhandled unique-constraint error; after it, exactly one
current assignment results every time across 10 repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 17:10:10 +05:30
saqib mirandClaude Sonnet 5 7106753ed3 fix(015-reporting-dashboards): count humanEscalated by Assignment existence, not status
ManagementRepository/ProductReportRepository.countEverEscalatedToHuman checked
a list of terminal statuses that ticket-state-machine.ts's own transition
table shows are reachable from BOTH the AI-resolved path and the
human-escalation path once they converge (RESOLUTION_PENDING_CUSTOMER,
RESOLVED, CLOSED, REOPENED). Every AI-resolved ticket was being double-counted
as human-escalated too — confirmed live against real seeded dev data
(humanEscalated: 34 out of totalCases: 34, an impossible 100%).

Fixed by keying off assignments: { some: {} } instead, since
orchestrationService.handleHumanEscalation is the only code path that ever
creates an Assignment row. Updated management-dashboard.test.ts's own
human-resolved fixture to create a real Assignment row, since it previously
relied on the now-fixed buggy status-based signal without one.

Found via manual verification against a real running dev server while
building supporthub-web's 002-reporting-dashboards-ui, not by any existing
automated test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 16:31:13 +05:30
saqib mirandClaude Sonnet 5 d65683641a feat(015-reporting-dashboards): four real reporting/analytics endpoints
Wires the pre-scaffolded, unused platform/reports module (ReportsService
.generateSummaryReport previously returned {}) into four real, admin-
gated dashboards matching docs/09-testing-observability-cicd.md's own
table:

- GET /admin/reports/management: total cases, AI-resolved, human-
  escalated, resolved/open, SLA compliance/breaches, escalation count,
  average response/resolution time.
- GET /admin/reports/product/:externalProductId: support volume,
  problem-category breakdown, recurring problems, AI-resolution/human-
  escalation rate, top error codes.
- GET /admin/reports/support: current per-agent workload, SLA at-risk/
  breached counts, escalation count, response/resolution performance.
- GET /admin/reports/ai: AI resolution/human-handoff rate, failed-
  troubleshooting-then-escalated rate, knowledge-match rate, confidence
  distribution (reusing 005-ai-support's own decideConfidenceBand),
  tool invocation success/failure.

Every rate/average is number|null -- null means no qualifying data in
range, never a computed NaN or a misleading 0. Adds one new durable
table, ErrorCodeLookup, since 014-full-observability's own equivalent
metric is a process-lifetime Prometheus counter unusable for a
historical "top errors" report.

Verified end-to-end against real Postgres/Redis: every figure checked
against hand-computed expected values, including a no-activity range
(all-zero counts, all-null rates) and cross-product isolation.

Also fixes a real regression the new ErrorCodeLookup FK caused in the
pre-existing known-issues.test.ts (its afterAll deleted ErrorCode rows
before the now-referencing lookup rows).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 11:59:38 +05:30
saqib mirandClaude Sonnet 5 ea50e3596a test(014-full-observability): fix cross-file contamination + polish
business-metrics.test.ts's "human resolution" case drove a ticket
through a real HUMAN_ESCALATION transition via ticketsService.updateStatus,
which triggers the real orchestration subscriber's default ROUND_ROBIN
auto-assignment against every agent in the shared throwaway database —
reproduced deterministically landing on agent-ticket-queue.test.ts's own
dedicated agent. Fixed by driving the intermediate transitions directly
through ticketsRepository.updateStatus (no domain-event publish),
reserving the real, event-publishing call for only the final RESOLVED
transition the metric subscriber needs to observe.

Also documents (checklist Notes), without fixing, a separate pre-existing
issue confirmed unrelated to this feature via git checkout to the clean
013-auth-hardening tip: nearly every integration test file's product ID
collapses to the same 4-letter ticket-code prefix ("TEST"), so enough
concurrent TEST_*-prefixed files can exceed the fixed retry ceiling on
ticket-code generation and surface as a real 500 — a 003-ticketing
concern, out of scope here.

Marks all 30 tasks.md items complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:27:56 +05:30
saqib mirandClaude Sonnet 5 de5915a8c1 feat(014-full-observability): remaining business metrics (first response, resolution, SLA, errors, tools)
Completes User Story 4's eleven named metrics: first-response-time
(messages.service.ts's post(), guarded against double-counting a
ticket's second agent message), SLA compliance (sla.service.ts's
complete()/runBreachDetectionSweep(), with a guard so a run already
breached by the sweep is never also counted "met" when it later
resolves), most-common-errors (error-codes.service.ts, counted only
once a code is confirmed real), and tool-failure-rate/knowledge-
effectiveness (tools.service.ts's single executeTool call site).

Verified end-to-end against real Postgres/Redis by scraping the real
/metrics endpoint before and after driving each metric's actual
underlying event through the real service layer — including a genuine
tool-execution failure (a nonexistent ticket ID) rather than a
simulated one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 15:57:47 +05:30
saqib mirandClaude Sonnet 5 f75589d9ce feat(014-full-observability): real distributed tracing + first 5 business metrics
User Story 3: initializes a real OpenTelemetry TracerProvider (previously
inert — getTracer() returned a no-op tracer with nothing ever exported).
Adds ticket.create, ai.escalation, and orchestration.assignment spans
covering both FR-006 cross-module paths, verified via a real, in-memory
test exporter that confirms actual trace/parent-span nesting, not mocked.

Also registers an AsyncLocalStorageContextManager
(@opentelemetry/context-async-hooks) — without one, OTel's context API is
a no-op that doesn't propagate across the await boundaries this feature's
own event-bus subscribers rely on for span nesting; caught by the first
version of the tracing integration test actually failing on real
parent/child assertions, not assumed.

Graceful degradation (FR-007) verified against a real, deliberately
unreachable OTLP endpoint: the SDK's own background export path (what
production actually exercises) never produces an unhandled rejection.

Starts on the 11 named business-health metrics: AI session
resolved/escalated outcomes (session.repository.ts, the single choke
point every branch in session.service.ts funnels through), human-vs-AI
resolution + resolution-time (a new TICKET_UPDATED/RESOLVED subscriber),
escalation rate (a new subscriber on ESCALATION_TRIGGERED, published
unconditionally since 008 but never previously consumed), and recurring
problems (tickets.service.ts's existing problem-creation call site).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:32:14 +05:30
saqib mirandClaude Sonnet 5 acd3843aaf feat(014-full-observability): per-request access log + live request-duration metric
User Stories 1-2: every completed request (including 404s and early
replies from other hooks) now emits exactly one structured access-log
line, and every log line produced during that request's handling shares
its requestId/correlationId via a new AsyncLocalStorage-backed Pino mixin
— with zero changes to any existing log call site. The previously-dead
supporthub_http_request_duration_seconds histogram now actually receives
observations, so error rate and latency per route are computable from
/metrics alone.

Also bumps @opentelemetry/sdk-trace-base 1.x -> 2.x to align with the two
new tracing dependencies added in this same branch (exporter-trace-otlp-http,
resources) onto one consistent major version — npm had otherwise installed
two incompatible OTel core/resources majors side by side, which also
happened to resolve a moderate DoS advisory in @opentelemetry/core <2.8.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:12:14 +05:30
saqib mirandClaude Sonnet 5 79bc2ef25b feat(013-auth-hardening): password reset, password strength policy, login rate-limiting
Closes the two gaps 010-identity-auth explicitly deferred (password reset,
login rate-limiting), plus a shared password-strength validator both the
reset-consume endpoint and admin account creation now depend on.

- Password reset: single-use, paired-Redis-key tokens (never in Postgres),
  identical response regardless of account existence, stubbed delivery via
  a structured log line (no email infrastructure exists yet).
- Password strength: one validatePasswordStrength() call site, wired into
  both POST /admin/users and the reset-consume flow.
- Login rate-limiting: checkRateLimit keyed by submitted email, checked
  before any credential verification.

Also fixes tests/helpers/auth.ts's shared loginAs() helper, which reused
two fixed accounts across the whole integration suite via upsert — now
rate-limited per email, that collided across ~30 files sharing one budget.
Each call now gets a unique email; no call sites needed to change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 21:22:49 +05:30
saqib mirandClaude Sonnet 5 3bdccc901f test(011-agent-ticket-queue): harden afterAll against wildcard SLA policy contamination
Same class of cross-file test-isolation gap already fixed in
orchestration-flow.test.ts and sla-escalation-flow.test.ts (010's own
regression work): a wildcard (non-product-scoped) SLA policy from
another suite can match this file's own tickets too, leaving a real
sla_run row that RESTRICTs the ticket delete. Also cleaned up several
orphaned wildcard SLA policies that had accumulated in the shared
throwaway test database from earlier runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 17:33:52 +05:30
saqib mirandClaude Sonnet 5 249e7cd0ce feat(012-admin-list-views): GET /admin/business-calendars/:id now includes holidays
Follow-up to 012-admin-list-views, discovered while building
supporthub-web's own SLA/calendar admin screen (001-agent-admin-ui User
Story 4): holidays could only be added or removed, never read back -
GET /admin/business-calendars/:id returned the bare calendar with no
way to display what holidays were already on file. The repository
already had findByIdWithHolidays; it just wasn't wired to this route.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:40:00 +05:30
saqib mirandClaude Sonnet 5 7948182988 feat(012-admin-list-views): add GET /admin/products/:id/knowledge for governance
Follow-up to 012-admin-list-views, discovered while building supporthub-
web's own knowledge-governance screen (001-agent-admin-ui User Story 7):
GET /knowledge/retrieve only ever returns published entries (its own
AI-consumption purpose), so a governance screen that needs to see and
publish a draft entry had no endpoint to list it. Adds a small
admin-list-views-style read query scoped to the knowledge module itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:35:25 +05:30
saqib mirandClaude Sonnet 5 3bd068b031 feat(012-admin-list-views): SLA-run, escalation-event, and product-catalog list endpoints
Adds GET /admin/sla-runs (filterable by status), GET /admin/escalation-
events (capped, most-recent-first), and GET /admin/products (with
integration status joined in, never the full ProductIntegration row).
None of these existed as a single query before - only per-ticket or
per-integration-id lookups did.

Discovered while planning supporthub-web's 001-agent-admin-ui User
Stories 6-7 (SLA/escalation monitoring, product catalog), the same way
011-agent-ticket-queue was discovered for User Story 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:31:20 +05:30
saqib mirandClaude Sonnet 5 fb9606b6aa feat(011-agent-ticket-queue): link agents to accounts, list assigned tickets
Extends PATCH /admin/agents/:agentId with an optional userId to finally
wire Agent.userId (added in 010-identity-auth as schema-only, never
consumed by any workflow), with proactive role/duplicate-link checks
mirroring UsersService.create's own pre-check style.

Adds GET /agents/me/tickets and GET /admin/agents/:agentId/tickets,
sharing one TicketsService.listAssignedTo method, returning a dashboard-
ready summary (product, customer, priority, severity, status, SLA state)
of every ticket currently assigned to an agent — no such query existed
anywhere in the ticketing or orchestration modules before this. Backed by
a new Assignment @@index([agentId, isCurrent]).

Discovered while starting supporthub-web's 001-agent-admin-ui: its agent-
dashboard user story had no backend data source without this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 13:19:13 +05:30
saqib mirandClaude Sonnet 5 40687f68fa feat(010-identity-auth): real staff login, session verification, and role gating
Replaces the no-op fastify.authenticate stub and the never-implemented
identity/auth login with real bcrypt password verification, JWT session
issuance/verification (reusing the existing JWT_SECRET), and a Redis-backed
revocation denylist for logout. Adds requireRole('ADMIN') to admin-only
configuration writes across 002-009 that previously relied on a decorator
that never actually checked anything. Adds self-identity (GET /auth/me,
re-validated against live account state) and admin-provisioned accounts
(POST /admin/users).

Making the auth check genuinely reject invalid/missing tokens exposed that
~18 pre-existing integration test files called already-gated routes with no
Authorization header (safe against the old no-op stub, broken against a real
one) — fixed via a shared tests/helpers/auth.ts (loginAs/authHeader) and a
file-by-file pass, plus two related SLA-run cleanup races exposed once admin
setup calls in those files' own beforeAll blocks started actually succeeding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:45:37 +05:30
saqib mirandClaude Sonnet 5 16daf8d32d feat: implement problem resolution (009)
Populates the five real problem-management stubs (investigation,
root-causes, solutions, verification, resolutions -- problems is
confirmed dead/unwired scaffold and stays untouched) with doc04's
sequential workflow engine:

- investigation: version-row-per-attempt (never overwritten), with a
  customer-safe read path that always strips internalNotes.
- root-causes/solutions/verification: a strict existence chain
  (investigation -> root cause -> solution -> approval -> implementation
  -> verification), each step resolve-or-409 on its own precondition,
  matching doc06's schema field-for-field with no invented columns.
- resolutions: gated on a successfully verified solution (no stored
  solutionId FK, per doc06 -- resolved via a join at write time), moving
  the ticket to RESOLUTION_PENDING_CUSTOMER; explicit customer
  confirmation and a durable auto-close sweep (the previously-unregistered
  CLEANUP queue stub, mirroring 008's breach-detection job) both resolve
  it from there.
- reopen (ticketing/tickets): two real, separately-audited transitions
  (RESOLVED|CLOSED -> REOPENED -> IN_PROGRESS), touching no prior
  problem-resolution record and no SLARun -- closes the loop 008's own
  spec.md left open.

Verification-failure escalation reuses 003/007's existing
HUMAN_ESCALATION transition directly rather than adding an eleventh
trigger type to 008's already-shipped escalation rules.

Customer-facing confirm-resolution/reopen needed a body-shape variant of
002's inbound trust boundary that didn't previously exist:
fastify.authenticateProductIntegration hard-required a full
ticket-creation-shaped body. Extracted the shared token/scope/replay
verification into verifyIntegrationIdentity and added a narrower
authenticateProductIntegrationIdentity decorator + identityOnlyRequestSchema
on top of it -- purely additive, ticket creation's own behavior is
unchanged.

Also fixes a real test-data-hygiene bug surfaced by running this
feature's suite alongside 008's: a wildcard-scoped HierarchyNode and an
intentionally-global SLAPolicy in 008's own test fixtures were silently
affecting other test files' tickets sharing the same live Postgres.

Verified against throwaway Docker Postgres/Redis: typecheck, lint,
architecture-check all clean; full regression (tests/unit +
tests/integration together, 172 tests) passes except the 2 pre-existing
MinIO-dependent attachment failures, unrelated to this feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 15:09:29 +05:30
saqib mirandClaude Sonnet 5 9357f03e1d feat: implement SLA and escalation (008)
Populates platform/business-calendars, orchestration/sla, and
orchestration/escalation (all thin stubs until now) with the real engine:

- business-calendars: a luxon-based day-by-day calendar walk
  (addBusinessMinutes/isWithinWorkingHours) excluding non-working hours,
  weekends, and holidays — replacing the naive createdAt+hours stub FR-004
  explicitly forbids.
- sla: most-specific SLAPolicy resolution (product/category/problemType/
  priority, wildcard-or-exact-match, specificity-count + updatedAt
  tiebreak), SLARun creation on the first real publish of the
  long-unused TICKET_ASSIGNED domain event, durable pause/resume via an
  absolute-timestamp shift (no in-memory state, verified across a real
  buildApp() restart), and a repeatable BullMQ breach-detection sweep
  (src/jobs/sla, itself a previously-unregistered stub) that is directly
  callable for tests, not only reachable through a running worker.
- escalation: EscalationPolicy/Rule CRUD (all 10 doc05 trigger types
  storable, only resolution_breach/first_response_breach evaluated),
  breach-triggered and manual escalation both funnel through one EscalationEvent
  + scoped re-assignment path. AssignmentEngine (007) gains
  assignToSpecificNode — a new, explicitly node-scoped entry point,
  since escalation must never let 007's general resolution re-derive a
  different node than the one a rule or a caller targeted.

Two small pre-existing scaffold gaps were closed along the way:
CategoriesRepository had no findById, and TICKET_ASSIGNED/SLA_BREACHED/
ESCALATION_TRIGGERED were defined since earlier phases but never
published by any code.

Verified against throwaway Docker Postgres/Redis (typecheck, lint,
architecture-check all clean; 148/150 relevant tests pass — the 2
failures are pre-existing, MinIO-dependent, and unrelated to this
feature).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 13:02:05 +05:30
saqib mirandClaude Sonnet 5 77928c4878 feat: implement orchestration and assignment (007) — routing, strategies, history
Phase 7 of the roadmap. On a ticket's automatic transition to
HUMAN_ESCALATION, routing resolves 006's hierarchy nodes and
capability-eligibility lookup directly (never a second matching
algorithm), and a pluggable strategy (ROUND_ROBIN, concurrency-safe via
atomic Redis INCR; LEAST_LOADED; SKILL_BASED) selects exactly one
eligible agent, persisted as a version-row-per-period Assignment plus an
append-only AssignmentHistory event log. MANUAL/DIRECT are never
auto-selected — only an explicit admin-supplied agentId reaches them.
On success the ticket moves HUMAN_ESCALATION -> IN_PROGRESS through
003's existing state machine. A ticket's "required skill" comes from
its most recent AI diagnosis's problemType (005) when one exists,
unioned with any matching hierarchy node's skills (006); when neither
exists, there's no skill constraint (every active agent eligible),
never zero.

Found and fixed two real, latent bugs in the shared event-bus
infrastructure while building this feature's own tests: (1)
EventBus.publish was built on EventEmitter.emit(), which never awaits
async listeners, so a caller had no guarantee any subscriber (005's
AI-session-ending hook, now also this feature's orchestration hook) had
actually finished — rewritten to track subscribers directly and await
them via Promise.all, same per-handler error isolation as before. (2)
registerDomainEventHandlers() was only called from server.ts's
production startup path, never from buildApp() — meaning every
integration test in this codebase had zero domain-event subscribers
registered at all. Now called (idempotently) from buildApp() itself,
since domain-event wiring is synchronous application behavior, not a
background-worker concern like the queue.

Adds 8 unit tests (each strategy's pure selection/tie-break logic), a
dedicated round-robin concurrency test verifying no two concurrent
selections collide under real parallel load, and 2 integration test
files covering all five user stories. Full regression (every
pre-existing 002-006 integration test plus every new 007 test) run
together against real Postgres/Redis/MinIO: 124 passed, 9 skipped
(005's AI-key-gated tests, unrelated), 0 failed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 11:58:17 +05:30
saqib mirandClaude Sonnet 5 a0c1a9aa33 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>
2026-09-02 18:12:26 +05:30
saqib mirandClaude Sonnet 5 82d02bcdcd feat: implement AI support agent (005) — diagnosis, tools, runbooks, verification
Real Anthropic Claude integration per explicit product decision: a
ticket's AI session diagnoses the problem via a structured-output call,
applies a DB-configurable confidence-band policy (FR-005), and on
"proceed" reasons and acts through a small permission/risk-gated tool
system (FR-011/FR-012), optionally walking a matching runbook step by
step with the application — never the model — owning the step index
(FR-015/FR-016). Resolution requires real tool evidence, never customer
claims alone (FR-018) — verifyProductResolution is a documented
fail-closed placeholder mirroring the existing malware-scanner precedent,
since no real per-product operational signal exists yet.

AISupportSession.status mirrors onto Ticket.status through 003-ticketing's
existing AI_ANALYZING/AI_TROUBLESHOOTING/AI_VERIFYING/AI_RESOLVED/
HUMAN_ESCALATION state machine, discovered during planning to have been
built anticipating this exact feature. Two circular module dependencies
(escalation<->sessions, tools<->sessions) were designed around rather than
found as bugs: escalation is a pure summary formatter with no state
dependencies of its own, and tools stays a clean leaf module with zero
dependency on ai-support/sessions. Ticket creation enqueues the first
diagnosis turn via the existing queue infrastructure (off the hot path of
the inbound SaaS integration endpoint); a human actor changing ticket
status ends the AI session via the event-bus scaffold that existed in
this codebase but had never been wired to anything.

A real Prisma limitation was found and fixed before it reached tests:
compound-unique upsert rejects null for a nullable key column, so
AIConfidencePolicy uses find-then-update/create instead, same fix class
004 already used for the same underlying limitation.

Adds 9 unit tests (confidence-band, tool-policy-gate, runbook-step-
advance) and 6 integration test files, including the two constitution-
required standing E2E scenarios. AI-independent tests were run against
real Postgres/Redis/MinIO (88 passed, 0 failed across the full suite,
including every pre-existing 002/003/004 test). The AI-dependent tests
compile and skip cleanly via describe.skipIf but were not run against a
live model — no ANTHROPIC_API_KEY was available in this session; a real
key must be supplied before this feature can actually run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 17:44:52 +05:30
saqib mirandClaude Sonnet 5 e33d86081f feat: implement product knowledge management & retrieval (004)
Implements 26 of 28 tasks from specs/004-product-knowledge/tasks.md
across all three user stories -- Phase 3 of the roadmap. First
feature to populate src/modules/ai-support/ (doc 07 places
`knowledge` there; only that submodule is built, matching this
codebase's convention of not pre-building unneeded submodules).

Schema (prisma/schema.prisma + migration):
- KnowledgeEntry, ErrorCode, KnownIssue, Runbook per docs/06,
  refining its conceptual flat `version` field into an explicit
  version-history mechanism: each edit inserts a new row
  (isCurrentVersion flag, compound unique on (code, version) /
  (key, productId, version)) instead of overwriting in place -- the
  only way "prior versions remain retrievable" (FR-004/FR-009) is
  actually true rather than aspirational.

User Story 1 -- knowledge entry authoring/publish/version (P1, MVP):
- draft -> published -> unpublished lifecycle; publish only takes
  effect from its effectiveDate.
- Editing uses the same conditional-update-then-insert optimistic
  concurrency pattern as 003-ticketing's Ticket.version (409 on a
  stale expectedVersion).
- Full version history readable via GET .../versions.

User Story 2 -- error codes, known issues, runbooks (P2):
- ErrorCode + KnownIssue with direct lookup-by-error-code.
- Runbook steps stored as an ordered JSON array, preserved exactly;
  same version-on-edit mechanism as knowledge entries; inactive
  runbooks are indistinguishable from nonexistent ones on lookup.

User Story 3 -- filtered retrieval (P3):
- GET /knowledge/retrieve: product-scoped, excludes draft/
  unpublished/not-yet-effective entries, validated entries ranked
  ahead of unvalidated. Deliberately NOT semantic/vector search --
  doc 11 gap B1 explicitly defers embedding-model choice to the
  future AI-support feature; this is real, usable structured
  filtering a semantic layer can sit in front of later.

Found and fixed one real bug before it reached tests: the retrieval
endpoint initially queried by the raw external product id instead of
resolving it to the internal Product.id first (every other endpoint
in this feature does that resolution) -- would have silently
returned zero results for every caller. Fixed with a lenient
tryResolveProductId (empty array, not 404, for an unregistered
product -- matches the "no matches, never an error" contract).

Deliberately skipped (not forgotten, see checklist notes): the two
planned mock-repository unit-test tasks (T004, T019) -- unlike
003-ticketing's state machine, this feature has no pure-logic
surface to isolate from Prisma; coverage comes entirely from
integration tests instead.

All 13 integration test files in the repo (36 tests, spanning this
feature and every prior one) verified passing together against a
real Postgres/Redis/MinIO -- no regressions. Full quality gate
(typecheck/lint/format/architecture/unit tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:55:18 +05:30
saqib mirandClaude Sonnet 5 2edfbacf82 feat: implement ticket creation, messages & attachments (003-ticketing)
Implements all 39 tasks from specs/003-ticketing/tasks.md across all
three user stories -- Phase 5 of the roadmap.

Schema (prisma/schema.prisma + migration):
- Ticket (code, status, version for optimistic concurrency,
  idempotencyKey, customerId FK), Problem, TicketMessage,
  TicketAttachment per docs/06, with Product/Category/
  CustomerReference back-relations.

User Story 1 -- ticket/problem creation (P1, MVP):
- Explicit 12-state lifecycle adjacency table
  (ticket-state-machine.ts), not "any transition allowed."
- Ticket code generation (<PRODUCT_CODE>-<YEAR>-<SEQUENCE>) scoped
  by the actual code prefix, not productId -- see the collision bug
  fixed below.
- Idempotency-key enforcement via atomic create-then-catch-conflict
  (never a read-then-write race), completing the FR-012 placeholder
  from 002-saas-integration.
- Explicit-reference-only recurring-problem linking (no fuzzy
  matching -- that's a future AI-support concern).
- POST /v1/support/requests (002-saas-integration) now creates a
  real ticket instead of echoing context back.
- PATCH /tickets/:id/status with expectedVersion-based optimistic
  concurrency (409 on stale version, 400 on an invalid transition).

User Story 2 -- typed messages (P2):
- Message type -> visibleToCustomer mapping is a fixed constant map,
  never caller-supplied; customer-scoped reads filter at the query
  layer so an internal note is never fetched, not just hidden.
- POST/GET /tickets/:id/messages (customer-scoped) and
  GET /agent/tickets/:id/messages (agent-scoped).

User Story 3 -- attachment pipeline (P3):
- Presigned-PUT upload (new getPresignedUploadUrl on the existing
  storageService) -- file bytes never transit this API.
- A MalwareScanner interface with a fail-closed placeholder
  (UnimplementedPlaceholderScanner) since no scanner exists in this
  stack -- it always reports 'infected', never silently 'clean'.
- The existing attachments-queue job stub now actually calls the
  scanner and updates scanStatus; registerAttachmentWorker() is
  wired into bootstrapQueue() (previously defined but never called).
- Downloads are gated on scanStatus === 'clean' -- currently always
  refused until a real scanner replaces the placeholder.
- MinIO added to docker-compose.{test,development}.yml for local/CI
  S3-compatible storage, matching doc 04's explicit guidance.

Two real bugs found and fixed via integration testing against a
live Postgres/Redis/MinIO (not just typechecked):
- Ticket codes could collide across different products: the
  sequence counter was scoped by internal productId, but the code
  column's uniqueness is global, and deriveProductCode's 4-character
  truncation means different products can share a prefix. Fixed by
  counting against the actual code prefix instead.
- Three existing 002-saas-integration integration tests' cleanup
  started failing an FK RESTRICT check once ticket creation was
  wired in (deleting a Product before the Ticket/Problem that now
  reference it). Fixed their afterAll ordering.

All 9 integration test files (24 tests, spanning this feature and
the pre-existing suite) verified passing against real Postgres,
Redis, and MinIO, including a genuine presigned-PUT/GET round trip.
Full quality gate (typecheck/lint/format/architecture/unit tests)
passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:21:37 +05:30
saqib mirandClaude Sonnet 5 55253287b3 feat: per-integration and per-user rate limiting (US3) + polish
Implements tasks T026-T032 from specs/002-saas-integration/tasks.md
(User Story 3, P3 - the final piece of this feature) plus Polish.

- New bespoke Redis fixed-window counter (checkRateLimit,
  src/infrastructure/cache/rate-limiter.ts) rather than
  @fastify/rate-limit's default onRequest-stage hook -- that hook
  runs before this feature's preHandler-based auth resolves the
  integration/user identity the limit needs to key on. A second
  preHandler (checkIntegrationRateLimit) runs after
  authenticateProductIntegration on the inbound route, checking the
  integration-level limit then the per-user limit independently,
  each throwing the existing RateLimitError (429
  RATE_LIMIT_EXCEEDED) on breach.
- New integration test (inbound-rate-limit.test.ts) verifies both
  limits are enforced independently against a real Postgres/Redis:
  a throttled user doesn't affect others, and the integration cap
  throttles even when no individual user has hit their own limit.
- Docs: contracts/quickstart updated from the placeholder
  "RATE_LIMITED" code to the actual reused RATE_LIMIT_EXCEEDED code;
  cleaned up a duplicated paragraph in the admin endpoints section;
  added a "SaaS Integration" section to README.md documenting the
  inbound contract, admin routes (and their known auth-stub
  limitation), and how rate limits are configured.

All 32 tasks in tasks.md are now complete -- all three user stories
(P1 trust boundary, P2 admin lifecycle, P3 rate limiting) are
implemented and covered by integration tests verified against a
live database, in addition to unit tests for the crypto/token
primitives. Full quality gate (typecheck/lint/format/architecture/
unit tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 14:42:20 +05:30
saqib mirandClaude Sonnet 5 3e2a5b97a3 feat: admin lifecycle endpoints for product integrations (US2)
Implements tasks T021-T025 from specs/002-saas-integration/tasks.md
(User Story 2, P2): an admin can register, rotate, revoke, and change
the status of a ProductIntegration, and retrieve its audit trail.

- ProductIntegrationsService: register (finds-or-creates the Product
  by external id), rotate (dual-credential transition window per
  research.md), revoke, updateStatus, getAuditTrail -- each writes
  its own AuditLog entry via a new shared
  integration-audit-log.repository.ts (extracted from the auth
  plugin, which now reuses it instead of writing to Prisma directly).
- Routes: POST /admin/products/:externalProductId/integration,
  POST/admin/integrations/:id/rotate|revoke, PATCH .../status,
  GET .../audit-trail -- gated by the existing fastify.authenticate
  (human/admin JWT) decorator.
- New integration test (product-integrations-admin.test.ts) covers
  Quickstart Scenarios 5-7 end-to-end against a real Postgres/Redis:
  register+rotate+audit-trail, and revoke-takes-effect-immediately.
  Verified passing against a live database.

Known, pre-existing limitation flagged (not fixed here, out of
scope): fastify.authenticate is currently a no-op stub with no real
JWT verification, so these admin endpoints aren't actually
access-controlled yet -- that depends on the unimplemented
identity/auth module. Documented in the contract and checklist notes
so it isn't mistaken for done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 18:57:07 +05:30
saqib mirandClaude Sonnet 5 8d5731340d feat: implement SaaS product integration trust boundary (US1 MVP)
Implements tasks T001-T020 from specs/002-saas-integration/tasks.md
(Setup, Foundational, and User Story 1 - the P1 MVP: every inbound
request is authenticated and trusted before anything happens).
User Story 2 (admin onboarding/rotation/revocation) and User Story 3
(rate limiting) are not yet implemented (T021-T032 remain).

Schema (prisma/schema.prisma + initial migration):
- Replace the placeholder Product model (leftover starter-template
  scaffolding: code/description/ProductStatus enum) with the real
  docs/06-database-schema.md shape (externalProductId,
  supportEnabled, status).
- Add ProductIntegration (credential ref, rotation/revocation state,
  allowed scope, per-integration/per-user rate limits) and
  CustomerReference models.
- Align AuditLog to docs/06's shape (actor/actorType/entityType/
  entityId/reason/metadata) -- the placeholder shape had no fields
  to satisfy this feature's audit requirements.

Auth:
- HMAC-signed short-lived tokens (issue/verify) with jti-based replay
  defense via Redis and a bounded clock-skew tolerance.
- Credential secrets are AES-256-GCM encrypted at rest (new required
  INTEGRATION_CREDENTIAL_ENCRYPTION_KEY env var) since no secret
  manager exists in this stack yet -- see research.md "Credential
  storage".
- New product-integration-auth.plugin.ts Fastify plugin runs the
  validation order in contracts/inbound-request-contract.md and
  populates request.reqContext only on full success; every attempt
  (success or failure) is audit-logged without ever persisting the
  raw token/credential. Unregistered product and invalid credential
  return an identical response (FR-010).
- New POST /v1/support/requests endpoint exercises the boundary
  end-to-end (ticket creation itself is a future feature).

Also:
- Fix docker-compose.test.yml's container_name collisions --
  discovered while testing this change concurrently is now covered
  by an app-level regression test (separate commit).
- Fix test:unit to scope to tests/unit only (it was running the
  entire tests/** glob including integration tests) -- this feature's
  new integration test makes real Prisma/Redis calls, unlike the
  prior instantiation-only checks, so the existing glob-scoping gap
  became actually harmful.
- Update Jenkinsfile with the new required credential.

Verified: full quality gate (typecheck/lint/format/architecture/
unit tests) passes; all of User Story 1's quickstart scenarios
manually verified end-to-end against a live server + Postgres +
Redis; the new integration test suite verified against a live
database (not run as part of `npm test`, matches existing
test:integration convention).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 18:40:43 +05:30
saqib mirandClaude Sonnet 5 5444fb7ef3 fix: setErrorHandler must be registered before route modules
Fastify resolves each encapsulated child context's error handler at
the time that context is registered. app.ts called
app.setErrorHandler()/setNotFoundHandler() AFTER bootstrapRoutes()
had already registered every domain module's routes (each
app.register(someRoutes) call creates its own child context, since
none of the route modules use fastify-plugin). A handler set on the
parent afterwards does not retroactively apply to already-registered
children, so every module's routes were silently falling back to
Fastify's default {statusCode, error, message} error shape instead
of this app's {success:false, error:{code,message,details},
requestId} envelope, for any thrown error -- not specific to any one
feature. Discovered while building and manually verifying the
002-saas-integration feature's inbound endpoint.

Also fixed the generic error-handler branch to preserve a framework
error's own client-facing statusCode (e.g. 400 for malformed JSON)
instead of always reporting 500.

Added a regression test in tests/unit/app.test.ts that fails without
this fix and passes with it (verified both ways).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 18:40:20 +05:30
saqib mir 5e4ed9d64a first commit 2026-08-19 16:29:17 +05:30