📅

RES

P3 · Resource planning Planned · P3 design phase Owner: COO / CHRO seat (interim CEO)

Capacity versus forecast for every Member — an allocation Gantt that answers "who has bandwidth for which project when", a hiring forecast that surfaces the senior-X gap before it bites, and a skills-coverage heatmap that ties LEARN mastery to project demand.

RES is the consultancy resource-planning surface. It is the place a COO opens at 09:00 ICT on Monday and answers four questions in one glance: (1) is anyone over-booked this week? (2) does any project on the books have insufficient staffing two weeks out? (3) are we short on any skill that the current pipeline needs? and (4) when do we need to hire to keep growing without dropping deliverables? Every column on the Gantt is sourced from another module — HR owns the Member roster and contractual capacity (40h / week default, sabbatical-aware); PROJ owns project timelines and per-issue effort estimates; TIME owns actual logged hours that calibrate the model; LEARN owns skill mastery levels that determine who is staffable for what. RES is the integrator — it does not own primary data, it owns the joins, the forecasts, and the rebalance recommendations. The CUO surface is the CSO/COO-skill: it reads the gap, drafts a hiring memo, or suggests "move Linh from Project A to Project B for the next two weeks." Vietnamese labour-law overtime caps (Labor Code 2019 Art. 107 — 200 h/year for ordinary sectors, 300 h/year for permitted sectors with prior MoLISA registration) are hard-enforced: the allocation engine refuses to suggest a plan that would breach them and surfaces the violation in red. PRD §9.18 locks the FRs; this page documents the planned implementation at cyberos/services/res/.

RES is CyberOS's resource-planning surface for a services org. It composes four data streams — HR's Member roster + contracts, PROJ's project timelines + estimates, TIME's actual hours logged, LEARN's per-Member skill mastery — into one read-canonical view: an Allocation matrix indexed by (Member, week). On top of that matrix sits four AI-assisted lenses: capacity-vs-forecast (where are we over- or under-booked?), allocation Gantt (drag-drop reshuffle with conflict feedback), hiring forecast (when do we need to add headcount of which level / skill?), and skills-gap heatmap (which skills are demanded vs supplied for the next 6 months?). The CSO/COO-skill emits Notify cards into Genie when an allocation crosses 110% (over-allocation) or drops below 60% (under-allocation for > 2 weeks). Vietnamese Labor Code 2019 Art. 107 overtime caps are enforced at the allocation-write boundary — a plan that would breach the cap is refused, not warned.

Status
Planned
P3 · design phase
Data sources
4 modules
HR · PROJ · TIME · LEARN
Default capacity
40h / week
configurable per Member
OT cap (VN)
200h / year
300h with MoLISA registration
Over-alloc flag
> 110%
(FR pending)
Under-alloc flag
< 60%
(FR pending)
Depends on
HR · PROJ · TIME · LEARN
+ AUTH · BRAIN · OKR (P3) · OBS
Est. LoC
~8,500
Rust + TS Gantt SPA
1

Why RES exists

Consultancies live and die on staffing decisions. Over-allocate Linh by 20% and she ships late on two projects; under-allocate Minh by 50% and you are paying for capacity that is not generating revenue. The traditional toolchain is a spreadsheet that goes stale within a week, a Gantt tool that does not know about timesheets, and a hiring plan that exists only in the CEO's head. RES collapses the three into one — an allocation matrix that is always sourced from current data, a Gantt that respects timesheets and skill mastery, and a hiring forecast that is generated, not narrated.

📊
Capacity is computed, not typed

Per-Member weekly capacity is read from HR (contract + sabbatical state + public-holiday calendar), not a spreadsheet column anyone can edit.

🎯
Allocation is bound to skills

"Staff this project with someone who can do X" is a query, not a Slack ping. LEARN mastery levels filter the candidate list.

⚖️
Overtime cap is enforced

Labor Code 2019 Art. 107 — 200 h / year (300 with MoLISA registration). The engine refuses a plan that would breach the cap; the violation row is auditable.

The bet is that a services org with a clean allocation matrix can answer staffing questions in seconds rather than days — and that the same matrix, augmented with forecast curves from CRM (pipeline win-rate × project size) and OKR (Q3 hiring objective), becomes the substrate of a hiring forecast that is concrete, dated, and signable. RES turns "we should probably hire a senior Rust person at some point" into "we will be capacity-short by 60 h / week starting 2026-08-12 on backend skills; backfilling requires hire by 2026-06-25 given 90-day average ramp."

2

What it does — 5W1H2C5M

A structured decomposition of RES's scope. Every cell traces back to PRD §9.18 + SRS §7.18.

AxisQuestionAnswer
5W · WhatWhat is RES?A resource-planning service. Inputs: HR Member roster, PROJ project timelines + estimates, TIME logged hours, LEARN skill mastery. Outputs: Allocation matrix, capacity-vs-forecast view, allocation Gantt, hiring forecast, skills-gap heatmap. Rust service with a TS SPA Gantt frontend.
5W · WhoWho uses it?COO / CHRO: primary user — owns staffing decisions. Account Manager: reads the per-Engagement allocation; flags when a project is at risk of being under-staffed. Members: read their own per-week Allocation timeline. Agents: COO-skill (allocation recommendations, hiring forecast); CHRO-skill (over-allocation Notify).
5W · WhenWhen does it run?Continuous read-path. Allocation matrix recomputed on every upstream-data change (event-driven). Hiring forecast batch-refreshed nightly. Skills-gap heatmap refreshed weekly. Notify cards fire at the moment an allocation is written.
5W · WhereWhere does it run?P3: SG-1 region with VN-residency partition for VN tenants. Read replica behind PgBouncer; allocation matrix materialised view refreshed via Postgres LISTEN/NOTIFY.
5W · WhyWhy a separate module?Because the same allocation matrix is read by PROJ (project staffing dashboard), CRM (pipeline-vs-capacity), OKR (hiring objective progress), and the COO daily flow. Owning it once lets every consumer trust the same source-of-truth.
1H · HowHow does it work?Materialised view res.allocation_matrix indexed by (member_id, week_start). Backfill rule: capacity = HR contract minus PTO minus public holidays minus sabbatical. Forecast = sum(PROJ issue.estimated_hours over the week × probability) per assigned Member. Actual = sum(TIME entry.hours over the week). Over/under flags computed from forecast / capacity.
2C · CostCost budget?P3: ~$45 / month (Postgres read replica + small Fargate + materialised-view refresh). Per-tenant: ~$8 / month at 50-Member scale.
2C · ConstraintsConstraints?(a) Vietnamese Labor Code 2019 Art. 107 overtime caps — 200 h / year ordinary, 300 h / year with prior MoLISA registration. (b) EU AI Act Art. 22 — staffing recommendations are advisory, never auto-applied. (c) Members can read their own row but not others' compensation context. (d) Forecast probability inputs come from CRM win-rate, not from gut feel.
5M · MaterialsStack?Rust 1.81 · axum · sqlx · PostgreSQL 16 (materialised views + LISTEN/NOTIFY) · DuckDB embedded for ad-hoc analytics · TypeScript + React + visx for Gantt · Yjs CRDT for collaborative Gantt edits · OpenTelemetry.
5M · MethodsMethod choices?Event-driven matrix refresh (PROJ + TIME + HR + LEARN emit NATS events; RES subscribes). Materialised view rebuilds incrementally. Hiring forecast: simple convex optimisation (LP via Gurobi or HiGHS) — minimise hiring cost subject to forecast coverage. Skills gap: bipartite matching scored by mastery × demand.
5M · MachinesDeployment?Fargate task in SG-1 (P3). Read replica for the materialised-view-heavy read path. DuckDB embedded for the dashboards.
5M · ManpowerWho maintains?0.25 FTE at P3; data-platform engineer for the matrix-refresh pipeline. COO seat owns product direction.
5M · MeasurementHow measured?N(FR pending) (Gantt drag-drop p95 ≤ 200 ms), N(FR pending) (matrix freshness ≤ 5 s after upstream event), N(FR pending) (zero plans accepted that breach Art. 107). KPIs: allocation utilisation, hiring-forecast accuracy, skills-gap remediation time.
3

Architecture

RES is a Rust service that consumes events from four upstream modules, maintains a materialised-view-backed allocation matrix, and exposes both a federated GraphQL subgraph and a thin gRPC API for the COO-skill. The Gantt UI is a TypeScript SPA with Yjs CRDT for multi-user drag-drop. Every write to res.allocation emits an audit row to BRAIN and triggers downstream materialised-view refresh.

graph TB subgraph UPSTREAM ["Upstream data sources"] HR["👥 HR
member · contract · PTO · sabbatical"] PROJ["📋 PROJ
project · issue · estimated_hours"] TIME["⏱ TIME
actual hours logged"] LEARN["📚 LEARN
skill · mastery_level"] CRM["🏢 CRM
pipeline · win_probability"] end subgraph RES ["RES service (Rust · axum)"] INGEST["event_ingest.rs
NATS subscriber"] MATRIX["matrix.rs
allocation matrix builder"] FORECAST["forecast.rs
demand projection"] HIRING["hiring_forecast.rs
LP-solver wrapper"] GAP["skills_gap.rs
bipartite matching"] OT["overtime_guard.rs
VN Art. 107 enforcer"] GQL["gql_subgraph.rs
federated read API"] AGENT["agent_bridge.rs
COO/CHRO-skill RPC"] end subgraph STORES ["Stores"] PG[("PostgreSQL 16
res.allocation · res.forecast
materialised views
RLS by tenant_id")] DUCK[("DuckDB embedded
ad-hoc analytics")] NATS[("NATS JetStream
upstream events")] end subgraph CONSUMERS ["Consumers"] SPA["Gantt SPA
(React + visx + Yjs)"] COO_SK["🤖 COO-skill
allocation recommendations"] CHRO_SK["🤖 CHRO-skill
over-allocation Notify"] OKR_M["🎯 OKR
hiring objective progress"] DASH["📊 OBS dashboard"] end subgraph SINKS ["Audit"] BRAIN["🧠 BRAIN
allocation.* rows"] OBS["👁 OBS
traces + metrics"] end HR --> NATS PROJ --> NATS TIME --> NATS LEARN --> NATS CRM --> NATS NATS --> INGEST INGEST --> MATRIX MATRIX --> PG MATRIX --> FORECAST FORECAST --> HIRING FORECAST --> GAP MATRIX --> OT OT --> PG PG --> GQL PG --> DUCK DUCK --> AGENT GQL --> SPA AGENT --> COO_SK AGENT --> CHRO_SK GQL --> OKR_M GQL --> DASH MATRIX --> BRAIN RES --> OBS classDef planned fill:#cffafe,stroke:#155e75 classDef store fill:#f5f3ff,stroke:#7c3aed classDef sink fill:#f5ede6,stroke:#45210e class INGEST,MATRIX,FORECAST,HIRING,GAP,OT,GQL,AGENT,SPA,COO_SK,CHRO_SK,OKR_M,DASH planned class PG,DUCK,NATS store class BRAIN,OBS sink

Internal components

ComponentPath (planned)Responsibility
event_ingest.rsservices/res/src/event_ingest.rsSubscribe to NATS subjects hr.member.*, proj.issue.*, time.entry.*, learn.mastery.*, crm.deal.*. Normalise to internal UpstreamEvent enum.
matrix.rsservices/res/src/matrix.rsIncremental rebuild of res.allocation_matrix materialised view on event. Window: rolling 26-week horizon. Idempotent under replay.
forecast.rsservices/res/src/forecast.rsProject demand forecast = Σ(PROJ.issue.estimated_hours × project.staffing_probability) per (week, skill). Joined with CRM pipeline for committed-but-unsigned demand.
hiring_forecast.rsservices/res/src/hiring_forecast.rsLinear-program solver (HiGHS via rust binding). Inputs: forecast curve, ramp time per role, hiring cost. Output: dated hire requisitions.
skills_gap.rsservices/res/src/skills_gap.rsBipartite-matching: demand-side (PROJ skills × hours) vs supply-side (LEARN mastery × Member capacity). Output: gap heatmap by skill × week.
overtime_guard.rsservices/res/src/overtime_guard.rsHard predicate on every allocation write. Rejects writes that would push a Member past Labor Code 2019 Art. 107 OT cap. Reads HR Member.country to apply correct cap.
gantt_ws.rsservices/res/src/gantt_ws.rsWebSocket fan-out for collaborative Gantt drag-drop. Yjs Y-doc per (project, quarter).
conflict.rsservices/res/src/conflict.rsDetect over-allocation (> 110% per (FR pending)) and under-allocation (< 60% sustained > 2 weeks). Emits ConflictRecord rows.
gql_subgraph.rsservices/res/src/gql_subgraph.rsApollo federation subgraph. Types: Allocation, Capacity, ForecastedNeed, SkillGap, HiringPlan, ConflictRecord.
agent_bridge.rsservices/res/src/agent_bridge.rsRPC surface for COO-skill / CHRO-skill — returns ranked recommendations with deterministic ordering for replay.
audit_bridge.rsservices/res/src/audit_bridge.rsWrite every allocation / conflict / reallocation event to BRAIN canonical writer.
migrations/services/res/migrations/sqlx migrations. RLS by tenant_id on every table. Composite indexes on (member_id, week_start) and (project_id, week_start).
RES-INV-001 · Overtime cap is non-negotiable

Allocation writes that would breach Vietnamese Labor Code 2019 Art. 107 (200 h / year ordinary; 300 h with prior MoLISA registration) are rejected at the predicate boundary, not warned. The COO has no override path — the cap is parameterised per-Member from the HR contract, which records both the country and the MoLISA-registration state. A failing write returns HTTP 422 with code: "RES-OT-CAP" and the projected annual OT total at the point of breach. Verification: property-based test in services/res/tests/overtime_invariant.rs.

4

Data model

The core RES table is res.allocation — an append-only record of "Member X is allocated H hours to Project P during week W." A materialised view res.allocation_matrix rolls these up by (member, week) for the hot-path read. Capacity, forecast, skill-gap, hiring-plan, and conflict are derived tables driven by event-time refresh.

erDiagram TENANT ||--o{ ALLOCATION : "owns" TENANT ||--o{ CAPACITY : "tracks" MEMBER ||--o{ ALLOCATION : "is assigned" MEMBER ||--o{ CAPACITY : "has" MEMBER ||--o{ MEMBER_SKILL : "knows" PROJECT ||--o{ ALLOCATION : "consumes" PROJECT ||--o{ FORECASTED_NEED : "demands" SKILL ||--o{ FORECASTED_NEED : "scoped by" SKILL ||--o{ MEMBER_SKILL : "rated for" SKILL ||--o{ SKILL_GAP : "shows shortfall" FORECASTED_NEED ||--o{ SKILL_GAP : "feeds" CAPACITY ||--o{ SKILL_GAP : "feeds" ALLOCATION ||--o{ CONFLICT_RECORD : "may flag" FORECASTED_NEED ||--o{ HIRING_PLAN : "drives" SKILL_GAP ||--o{ HIRING_PLAN : "drives" TENANT { uuid id PK string slug string country "VN | SG | …" string molisa_ot_registered "300h | null" } MEMBER { uuid id PK uuid tenant_id FK string display_name string country string role_tier "T1 · T2 · T3 · T4" decimal contract_hours_per_week "default 40" timestamp hire_date bool active } ALLOCATION { uuid id PK uuid tenant_id FK uuid member_id FK uuid project_id FK date week_start "Monday-anchored" decimal planned_hours string allocation_type "billable | non_billable | training | leave" uuid created_by FK timestamp ts string brain_chain } CAPACITY { uuid id PK uuid member_id FK date week_start decimal available_hours string status "active | sabbatical | leave | holiday" decimal ytd_overtime_hours "rolling Art. 107 counter" } PROJECT { uuid id PK uuid tenant_id FK string name string status "active | paused | closed" decimal staffing_probability "0..1" } SKILL { uuid id PK string code "rust | react | vn-tax | …" string family "engineering | tax | design | …" } MEMBER_SKILL { uuid member_id FK uuid skill_id FK int mastery_level "1..5 from LEARN" timestamp updated_at } FORECASTED_NEED { uuid id PK uuid project_id FK uuid skill_id FK date week_start decimal demanded_hours decimal weighted_hours "× staffing_probability" } SKILL_GAP { uuid id PK uuid tenant_id FK uuid skill_id FK date week_start decimal demand_hours decimal supply_hours decimal gap_hours "demand - supply (positive = shortfall)" } HIRING_PLAN { uuid id PK uuid tenant_id FK uuid skill_id FK string role_tier int headcount_to_add date target_hire_date decimal coverage_hours_per_week string status "draft | approved | sourcing | filled" } CONFLICT_RECORD { uuid id PK uuid tenant_id FK uuid member_id FK date week_start string kind "over_alloc | under_alloc | ot_cap_breach | skill_mismatch" decimal planned decimal capacity string severity "warn | block" string status "open | acknowledged | resolved" timestamp ts }

Allocation matrix view (materialised)

CREATE MATERIALIZED VIEW res.allocation_matrix AS
SELECT
  m.tenant_id,
  m.id AS member_id,
  m.display_name,
  c.week_start,
  c.available_hours,
  COALESCE(SUM(a.planned_hours) FILTER (WHERE a.allocation_type = 'billable'), 0) AS billable_h,
  COALESCE(SUM(a.planned_hours) FILTER (WHERE a.allocation_type = 'non_billable'), 0) AS non_billable_h,
  COALESCE(SUM(a.planned_hours), 0) AS total_planned_h,
  CASE
    WHEN c.available_hours = 0 THEN NULL
    ELSE COALESCE(SUM(a.planned_hours), 0) / c.available_hours
  END AS utilisation
FROM res.member m
JOIN res.capacity c ON c.member_id = m.id
LEFT JOIN res.allocation a ON a.member_id = m.id AND a.week_start = c.week_start
WHERE m.active
  AND c.week_start BETWEEN now()::date - interval '4 weeks'
                       AND now()::date + interval '26 weeks'
GROUP BY m.tenant_id, m.id, m.display_name, c.week_start, c.available_hours;

CREATE UNIQUE INDEX ON res.allocation_matrix (tenant_id, member_id, week_start);

-- Refresh trigger: LISTEN/NOTIFY from event_ingest.rs
-- Each upstream event narrows the refresh window to affected (member, week) keys.
5

API surface

Three surfaces: a federated GraphQL subgraph for the Gantt SPA + cross-module reads, a thin gRPC API for agent-skill recommendations, and an MCP tool catalogue for natural-language allocation editing through Genie.

GraphQL subgraph (federated)

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@external", "@shareable", "@requiresScopes"])

type Allocation @key(fields: "id") {
  id: ID!
  tenantId: ID!
  member: Member!
  project: Project!
  weekStart: Date!
  plannedHours: Float!
  allocationType: AllocationType!
  createdAt: DateTime!
}

type Capacity @key(fields: "memberId weekStart") {
  memberId: ID!
  weekStart: Date!
  availableHours: Float!
  status: CapacityStatus!
  ytdOvertimeHours: Float!
}

type ForecastedNeed {
  projectId: ID!
  skillId: ID!
  weekStart: Date!
  demandedHours: Float!
  weightedHours: Float!
}

type SkillGap {
  skillId: ID!
  weekStart: Date!
  demandHours: Float!
  supplyHours: Float!
  gapHours: Float!
}

type HiringPlan @key(fields: "id") {
  id: ID!
  skill: Skill!
  roleTier: String!
  headcountToAdd: Int!
  targetHireDate: Date!
  coverageHoursPerWeek: Float!
  status: HiringPlanStatus!
}

type ConflictRecord @key(fields: "id") {
  id: ID!
  memberId: ID!
  weekStart: Date!
  kind: ConflictKind!
  severity: Severity!
  status: ConflictStatus!
  detectedAt: DateTime!
}

enum AllocationType { BILLABLE NON_BILLABLE TRAINING LEAVE }
enum CapacityStatus { ACTIVE SABBATICAL LEAVE HOLIDAY }
enum ConflictKind { OVER_ALLOC UNDER_ALLOC OT_CAP_BREACH SKILL_MISMATCH }
enum Severity { WARN BLOCK }
enum ConflictStatus { OPEN ACKNOWLEDGED RESOLVED }
enum HiringPlanStatus { DRAFT APPROVED SOURCING FILLED }

extend type Member @key(fields: "id") {
  id: ID! @external
  allocations(from: Date, to: Date): [Allocation!]! @requiresScopes(scopes: [["res.read"]])
  capacityByWeek(weekStart: Date!): Capacity @requiresScopes(scopes: [["res.read"]])
}

extend type Project @key(fields: "id") {
  id: ID! @external
  allocations(from: Date, to: Date): [Allocation!]! @requiresScopes(scopes: [["res.read"]])
  forecastedNeeds(from: Date, to: Date): [ForecastedNeed!]! @requiresScopes(scopes: [["res.read"]])
}

type Query {
  allocationMatrix(memberIds: [ID!], from: Date!, to: Date!): [Allocation!]!
    @requiresScopes(scopes: [["res.read"]])
  conflicts(memberId: ID, status: ConflictStatus, kind: ConflictKind): [ConflictRecord!]!
    @requiresScopes(scopes: [["res.read"]])
  skillsGap(from: Date!, to: Date!, family: String): [SkillGap!]!
    @requiresScopes(scopes: [["res.read"]])
  hiringPlans(status: HiringPlanStatus): [HiringPlan!]!
    @requiresScopes(scopes: [["res.read"]])
}

type Mutation {
  upsertAllocation(input: AllocationInput!): Allocation!
    @requiresScopes(scopes: [["res.write_allocation"]])
  deleteAllocation(id: ID!): Boolean!
    @requiresScopes(scopes: [["res.write_allocation"]])
  approveHiringPlan(id: ID!): HiringPlan!
    @requiresScopes(scopes: [["res.approve_hiring"]])
  acknowledgeConflict(id: ID!, note: String): ConflictRecord!
    @requiresScopes(scopes: [["res.write_allocation"]])
}

input AllocationInput {
  memberId: ID!
  projectId: ID!
  weekStart: Date!
  plannedHours: Float!
  allocationType: AllocationType!
}

REST endpoints (admin + report)

MethodPathPurpose
GET/res/matrix?from=YYYY-MM-DD&to=YYYY-MM-DDPull the allocation matrix for a date range. CSV / JSON.
GET/res/conflicts?status=openList unresolved over/under/OT-cap conflicts.
POST/res/allocations/bulkBulk allocation upsert (CSV import).
POST/res/recompute?week=YYYY-MM-DDForce-refresh the materialised view for a week. Admin scope.
GET/res/forecast.csv?weeks=2626-week capacity-vs-forecast spreadsheet export.
POST/res/hiring-plans/draftGenerate a new draft hiring plan from current forecast.
GET/res/skills-gap.csv?family=engineeringSkills-gap heatmap export for a skill family.
GET/res/member/{id}/timelinePer-Member 26-week allocation timeline.
POST/res/reallocateApply COO-skill suggested reallocation in batch.

MCP tool catalogue

Tool nameInputsOutputsAnnotations
cyberos.res.matrixfrom, to, member_ids?AllocationRow[]readonly · scope=res.read
cyberos.res.forecastweeks=26, skill_family?ForecastRow[]readonly · scope=res.read
cyberos.res.find_staffableproject_id, skills, weeksRankedMember[]readonly · scope=res.read
cyberos.res.upsert_allocationAllocationInput{allocation, conflicts}destructive · scope=res.write_allocation · human-confirm
cyberos.res.suggest_reallocationconflict_idRankedSuggestion[]readonly
cyberos.res.draft_hiring_planhorizon_weeks=26HiringPlanDraftreadonly · COO-confirm before persist
cyberos.res.skills_gapfamily?, from, toSkillGap[]readonly · scope=res.read
cyberos.res.acknowledge_conflictconflict_id, note{ok}scope=res.write_allocation
6

Key flows

Flow 1 — Allocate a Member to a project (drag-drop)

sequenceDiagram autonumber participant U as COO (browser) participant SPA as Gantt SPA participant AR as Apollo Router participant R as RES upsertAllocation participant OT as overtime_guard.rs participant PG as PostgreSQL participant NATS as NATS JetStream participant B as 🧠 BRAIN U->>SPA: drag Linh's row to "Project Atlas · week 2026-W23 · 18h" SPA->>SPA: optimistic local mutation; Yjs broadcast SPA->>AR: mutation upsertAllocation(input) AR->>R: forward with JWT R->>PG: SELECT capacity + ytd_overtime for (linh, 2026) PG-->>R: capacity=40h/wk, ytd_ot=178h, country=VN R->>OT: check (planned=18, existing=22, capacity=40, ytd_ot=178, cap=200) alt within OT cap OT-->>R: allow R->>PG: INSERT res.allocation (...) R->>B: append allocation.created row R->>NATS: publish res.allocation.created R-->>AR: Allocation AR-->>SPA: success; subscribers receive WS update SPA-->>U: drop confirmed (green flash) else would breach Art. 107 cap OT-->>R: deny (RES-OT-CAP, projected_ot=210) R-->>AR: 422 RES-OT-CAP AR-->>SPA: error SPA-->>U: drop rejected (red flash + cap explanation) R->>B: append allocation.rejected row {reason:"ot_cap"} end

The Gantt SPA shows a red shaded zone for any week where the next allocation would push the Member past the cap. The drag-drop refuses to land there; pinch-to-zoom and keyboard nudge are similarly bounded.

Flow 2 — Detect over-allocation (background)

sequenceDiagram autonumber participant E as event_ingest.rs participant M as matrix.rs (incremental refresh) participant C as conflict.rs participant CH as 🤖 CHRO-skill participant G as 💬 Genie / Notify participant B as 🧠 BRAIN Note over E: PROJ emits issue.estimated_hours changed E->>M: refresh matrix window (member_id, week_start..week_end) M->>M: recompute utilisation = total_planned / available alt utilisation > 1.10 OR utilisation < 0.60 (sustained 2+ wks) M->>C: create ConflictRecord{kind, severity} C->>B: append conflict.detected row C->>CH: notify(conflict_id) CH->>CH: rank with COO-skill recommendation CH->>G: post Notify card "Linh is at 124% for 2026-W23 — reassign 8h?" G-->>CHRO: card visible; one-click "open suggestion" else within bounds M-->>E: no action end

Flow 3 — Generate hiring forecast (nightly)

sequenceDiagram autonumber participant CR as cron (02:00 ICT) participant F as forecast.rs participant H as hiring_forecast.rs participant LP as HiGHS LP solver participant PG as PostgreSQL participant CO as 🤖 COO-skill participant B as 🧠 BRAIN CR->>F: refresh 26-week forecast F->>PG: pull PROJ.issue × CRM.deal_probability × LEARN.mastery PG-->>F: rows F->>F: compute weighted demand per (skill, week) F->>H: solve LP — minimise hire_cost subject to coverage_constraint H->>LP: model + constraints LP-->>H: optimal hire schedule H->>PG: INSERT res.hiring_plan (status='draft') H->>B: append hiring_plan.drafted row H->>CO: notify("draft plan ready for review") CO->>CO: produce ranked Notify card with rationale CO->>B: append agent.suggestion row {scope='res.hiring'}

The LP minimises (hiring + idle-capacity) cost subject to (Σ supply ≥ Σ demand × coverage_target) per skill family per quarter. Output is a list of dated hire requisitions; COO can approve, edit, or discard.

Flow 4 — Skills-gap analysis

sequenceDiagram autonumber participant U as COO participant SPA as RES skills-gap heatmap participant AR as Apollo Router participant G as gql_subgraph.rs (skillsGap) participant S as skills_gap.rs participant L as 📚 LEARN participant P as 📋 PROJ participant PG as PostgreSQL U->>SPA: open Skills Gap · "engineering" family · next 12 weeks SPA->>AR: query skillsGap(from, to, family) AR->>G: forward G->>S: compute bipartite matching S->>L: pull MemberSkill mastery × capacity hours L-->>S: supply rows S->>P: pull forecast (skill × weeks) P-->>S: demand rows S->>S: matching = solve maximum-weighted bipartite S->>PG: write derived res.skill_gap rows S-->>G: result G-->>SPA: heatmap data SPA-->>U: render — red cells where gap > 8h/week sustained

Flow 5 — Reallocation (project priorities shift)

sequenceDiagram autonumber participant CEO as CEO participant CHAT as 💬 CHAT participant CUO as 🧠 CUO participant COS as 🤖 COO-skill participant R as RES suggest_reallocation participant LP as HiGHS LP solver participant U as COO participant SPA as Gantt SPA participant B as 🧠 BRAIN CEO->>CHAT: "Project Atlas became P0 — reshuffle to ship in 4 wks" CHAT->>CUO: route CUO->>COS: invoke COO-skill with new priority COS->>R: suggest_reallocation(project=atlas, target_done=2026-06-30) R->>LP: re-solve allocation LP with atlas weight=10x LP-->>R: candidate plan (pull 12h/wk from Project Cygnus for 4 wks) R->>COS: ranked suggestion COS->>CUO: draft "Reshuffle plan: move Linh +12h, Minh +8h, pause Cygnus M2" CUO->>U: present Notify card to COO U->>SPA: review diff overlay on Gantt U->>R: approve (writes via upsertAllocation batch) R->>B: append reallocation.applied row R-->>SPA: WS broadcast updates

The reallocation is never auto-applied. The LP produces a candidate plan, the COO-skill drafts a one-paragraph rationale, the human approves. EU AI Act Art. 22 — no automated employment decision without human in the loop.

7

Allocation lifecycle

An allocation row traverses six states from draft to completed; conflict states branch off at any point. Every transition writes a chained audit row to BRAIN.

stateDiagram-v2 [*] --> Draft: COO drags onto Gantt (Yjs local) Draft --> Proposed: server accepts (OT-cap clears) Draft --> Rejected: OT-cap breach OR skill-mismatch BLOCK Proposed --> Confirmed: Member acknowledges OR auto-confirm at week_start - 7d Confirmed --> Active: week_start reached Active --> Completed: week_end + TIME.entries ingested Active --> Reallocated: re-shuffle replaces this row (chain to new id) Proposed --> Cancelled: project cancelled OR Member departed Confirmed --> Cancelled: project cancelled OR Member departed Reallocated --> [*] Completed --> [*] Cancelled --> [*] Rejected --> [*]

Per-state actions

StateTriggerSide-effects
DraftYjs local-write on GanttOptimistic UI; no audit yet.
ProposedServer upsert succeeds; OT-cap clearsBRAIN allocation.proposed; Member notified.
ConfirmedMember acknowledges, OR week_start - 7d auto-confirmBRAIN allocation.confirmed; TIME pre-fills the timesheet template.
Activeweek_start reachedMaterialised view shows in "current week" column.
Completedweek_end reached + TIME entries postedVariance computed; estimate-calibration feedback to PROJ.
ReallocatedCOO approves reshuffleNew row created with link to old (audit chain preserves history).
CancelledProject closed OR Member departedBRAIN allocation.cancelled; downstream invoices recalculated.
RejectedOT-cap breach OR skill-mismatch (severity=block)BRAIN allocation.rejected with reason; UI red-flash.
8

Functional Requirements

The CyberOS FR catalogue is being rebuilt one feature at a time via the open fr-author Agent Skill.

Previous FR enumerations were archived 2026-05-14 and are no longer reflected on this page. PRD/SRS narrative remains authoritative for the spec; specific FRs land here as they are re-authored.

9

Non-Functional Requirements

RES NFRs cover Gantt interaction latency, matrix freshness, OT-cap correctness, and reallocation-suggestion accuracy.

NFR IDConcernTargetMeasurement
N(FR pending)Gantt drag-drop optimistic-write p95≤ 200 msk6 load + browser instrumentation
N(FR pending)upsertAllocation server-canonical p95≤ 400 msk6 + Apollo Router latency histogram
N(FR pending)allocationMatrix query p95 (4-week × 50 members)≤ 250 msk6
N(FR pending)Materialised-view freshness after upstream event≤ 5 s p99NATS event → matrix-row diff probe
N(FR pending)Allocation plans accepted that breach Art. 107 cap= 0property-based test + chaos replay
N(FR pending)Hiring-forecast accuracy (drift between forecast and actual hire date)≤ 14 d MAEmonthly backtest
N(FR pending)RES availability (28-day)≥ 99.9%SLO monitor
N(FR pending)Time to first Gantt-paint, 26 weeks × 50 members≤ 1.5 s p95Lighthouse + RUM
N(FR pending)Yjs CRDT merge correctness under offline-edit conflict= 0 lost updates / 10kfuzz test in CI
N(FR pending)Cross-tenant matrix read= 0 leaksRLS verification harness
N(FR pending)Per-tenant infra cost at 50 Members≤ $10 / momonthly billing review
10

Dependencies

RES is downstream of four data-producing modules (HR, PROJ, TIME, LEARN) plus CRM for pipeline probabilities, and is consumed by OKR (hiring objective) and the COO daily flow.

graph LR subgraph upstream ["RES depends on"] AUTH["🔐 AUTH
RBAC + scope"] BRAIN["🧠 BRAIN
audit rows"] OBS["👁 OBS
traces + metrics"] HR["👥 HR
Member roster + contract"] PROJ["📋 PROJ
project + issue estimates"] TIME["⏱ TIME
actual hours"] LEARN["📚 LEARN
skill mastery"] CRM["🏢 CRM
pipeline + win prob"] NATS["📡 NATS JetStream"] end RES["📅 RES"] subgraph downstream ["Used by"] OKR["🎯 OKR
hiring objective"] COO["🤖 COO-skill"] CHRO["🤖 CHRO-skill"] PROJ_D["📋 PROJ staffing dash"] CUO["🧠 CUO daily"] DASH["📊 OBS dashboards"] end AUTH --> RES BRAIN --> RES OBS --> RES HR --> NATS PROJ --> NATS TIME --> NATS LEARN --> NATS CRM --> NATS NATS --> RES RES --> OKR RES --> COO RES --> CHRO RES --> PROJ_D RES --> CUO RES --> DASH classDef planned fill:#cffafe,stroke:#155e75 classDef shipped fill:#f5ede6,stroke:#45210e class AUTH,BRAIN,OBS,HR,PROJ,TIME,LEARN,CRM,NATS,RES,OKR,COO,CHRO,PROJ_D,CUO,DASH planned
11

Compliance scope

RES touches employment data — staffing decisions, overtime computation, hiring forecasts — placing it in Annex III §4 (employment) territory under the EU AI Act and squarely inside Vietnamese Labor Code compliance.

Regulation / standardArticle / clauseRES feature that satisfies it
Vietnam Labor Code 2019Art. 107 — overtime caps (200h / yr ordinary; 300h with MoLISA)overtime_guard.rs rejects allocations that would breach the cap; HR Member.molisa_ot_registered drives which cap applies.
Vietnam Labor Code 2019Art. 105 — daily / weekly working hoursCapacity model defaults to 40h / week × 8h / day; allocations cannot exceed daily cap.
Vietnam Labor Code 2019Art. 111 — weekly restCapacity respects weekly rest day; allocations cannot land on rest days.
Vietnam PDPL (Law 91/2025)Art. 14 — DSARPer-Member allocation timeline exportable via cyberos-res dsar-export.
EU AI Act (Reg. 2024/1689)Annex III §4 — Employment-related AIHiring-forecast LP is a decision-support tool, not auto-act; (FR pending) mandates human-in-the-loop.
EU AI ActArt. 14 — Human oversightCOO-skill suggestions are Notify cards; mutation requires explicit COO action.
EU AI ActArt. 22 — No automated decision-makingReallocation never auto-applied; explicit approval predicate.
GDPR (EU 2016/679)Art. 22 — Automated decision-makingSame as EU AI Act; explicit human approval before any allocation write.
ISO/IEC 27001:2022A.8.24 — Use of cryptographyAllocation chain hashed in BRAIN; tamper detection on replay.
SOC 2 Type IICC7.2 — System monitoringConflict-detection events emit OBS metrics; over-alloc rate is a dashboard KPI.
12

Risk entries

RES-specific risks tracked in the risk register.

IDRiskLikelihoodImpactOwnerMitigation
R-RES-001OT-cap predicate bypass (allocation written directly via SQL)LowHighCSORLS plus row-level CHECK constraints; CI test verifies cap; admin SQL access requires CSO + CTO co-sign.
R-RES-002Materialised view drift (stale matrix after event-bus outage)MediumMediumCTOIdempotent full-rebuild job every 6h; freshness probe alerts at p99 > 60s.
R-RES-003Hiring forecast over/under-shoots due to bad CRM probability inputsMediumMediumCOOForecast is advisory; monthly backtest publishes MAE; COO approves before procurement.
R-RES-004Yjs CRDT merge produces inconsistent state under network partitionLowMediumCTOServer-canonical rebase; conflict resolution returns deterministic order; fuzz harness.
R-RES-005EU AI Act Annex III interpretation drift (hiring tool reclassified high-risk)MediumHighCLOConservative classification today (high-risk); annual legal review post-AI-Act enforcement-act publication.
R-RES-006Cross-tenant allocation leak via predicate-elision in GraphQLLowCatastrophicCSORLS at Postgres + scope-required directive on every field; test gate on cross-tenant.
R-RES-007LP solver returns infeasible plan when demand > supply over horizonMediumLowCTOInfeasibility detected and surfaced as Notify card "demand unbookable through 2026-Q4"; never silent.
R-RES-008Member privacy: peer allocation visibility leakLowMediumCSO(FR pending) enforced — Member can only read own row; cross-Member read requires res.read_all scope.
R-RES-009Sabbatical / parental leave miscount inflates capacityMediumMediumCHROHR sabbatical event drives capacity event; integration test on each HR schema change.
13

KPIs

RES health rolls up into 10 KPIs covering utilisation, conflict rate, forecast accuracy, and OT-cap compliance.

KPIFormulaSourceTarget
Average utilisation (org-wide)Σ planned / Σ capacityallocation_matrix70–85% (sweet-spot band)
Over-allocation incidents / weekcount(util > 110%)conflict_record≤ 3 / 50 Members
Under-allocation incidents / weekcount(util < 60% for 2+ wks)conflict_record≤ 2 / 50 Members
OT-cap-rejected writes / weekcount(allocation.rejected reason=ot_cap)BRAINtracked; expect < 1 / week
Hiring-forecast MAE (days)monthly backtestres.hiring_plan≤ 14 d
Skills-gap remediation lead timemedian(filled - opened)hiring_plan≤ 90 d
Forecast accuracy (estimated vs actual hrs)1 - MAPEmatrix vs TIME≥ 80%
Matrix freshness p99histogramOBS≤ 5 s
Gantt drag-drop p95histogramOBS≤ 200 ms
Member self-service usage ratemembers reading own timeline / totalOBS≥ 70% / month
14

RACI matrix

RES is owned by the COO seat with CHRO consulted on allocation policy. Today (COO vacant), CEO is interim accountable.

ActivityCEOCOOCHROCTOCFOCLO
Service design + specARCCIC
ImplementationICIA/RII
Allocation policy (utilisation bands)ARRICI
OT-cap configuration + MoLISA regICA/RIIR
Hiring-forecast reviewARCIRI
Reallocation approval (P0 priority shift)ARCIII
Conflict-card triage dailyIRRIII
EU AI Act / Annex III conformityCICIIA/R

R Responsible · A Accountable · C Consulted · I Informed.

15

Planned CLI surface

Admin CLI cyberos-res for COO / CHRO seat operators. Every destructive command writes a chained BRAIN audit row before exit.

1. Show capacity vs forecast for the next 12 weeks

$ cyberos-res matrix --from 2026-W21 --weeks 12 --format table

WEEK    MEMBER       CAP    PLANNED  BILLABLE  ACTUAL  UTIL
W21     Linh         40h    44h      36h       —       110%  ⚠
W21     Minh         40h    18h      18h       —        45%  ⚠
W21     Khanh        40h    32h      28h       —        80%
W22     Linh         40h    38h      30h       —        95%
…

2. Find staffable Members for a project

$ cyberos-res find-staffable \
    --project proj-atlas \
    --skills rust,postgres,react \
    --weeks 2026-W23..2026-W30 \
    --hours-per-week 16

[ranked candidates]
  1. Linh    mastery: rust=4 postgres=4 react=3   slack=22h/wk avg   score=92
  2. Khanh   mastery: rust=3 postgres=5 react=4   slack=18h/wk avg   score=86
  3. Anh     mastery: rust=2 postgres=3 react=5   slack=24h/wk avg   score=74

3. Generate a hiring forecast

$ cyberos-res hiring-forecast --horizon 26w --output draft

[hiring plan · draft]
  skill=rust     tier=T3   need=1   target_hire=2026-08-25   coverage=24h/wk
  skill=design   tier=T2   need=1   target_hire=2026-09-15   coverage=18h/wk

[rationale]
  rust: forecast 96h/wk by 2026-W36; supply 60h/wk after Linh's parental leave
        gap opens 2026-W34; with 8wk ramp + 4wk hire cycle → target 2026-08-25
  design: forecast 36h/wk by 2026-W39; supply 18h/wk; gap opens 2026-W37
        → target 2026-09-15

[audit]  brain seq=88412 chain=a3f2…9d8e

4. Suggest reallocation for a conflict

$ cyberos-res reallocate --conflict CR-001834

[conflict] Linh @ W23: planned=44h capacity=40h util=110%

[suggestion 1]  move 4h "atlas-api-12" → Khanh (mastery=4, slack=12h)
[suggestion 2]  defer 4h "cygnus-m2"   → W24 (project allows; PM approved)
[suggestion 3]  trim 4h "atlas-api-12" (issue overestimated; TIME calibration says -25%)

→ apply [1] [2] [3] or [n]one ? _

5. List unresolved conflicts

$ cyberos-res conflicts list --status open --severity block

CR-001834  2026-05-13  Linh    W23  over_alloc       util=110%  severity=warn
CR-001841  2026-05-14  Khanh   W24  ot_cap_breach    proj_ot=212h cap=200h severity=block ⛔

6. Per-Member timeline (DSAR-style)

$ cyberos-res member-timeline --subject linh@cyberskill.com --weeks 13

WEEK     PLANNED  ACTUAL  PROJECTS                     STATUS
2026-W23 44h      —       atlas(28) · cygnus(16)        proposed
2026-W22 38h      36h     atlas(24) · cygnus(14)        completed
2026-W21 40h      41h     atlas(20) · cygnus(16) · L(4) completed
…

7. DSAR export for a Member

$ cyberos-res dsar-export --subject linh@cyberskill.com --output dsar.zip

[dsar] allocations:  624 rows (104 weeks)
[dsar] capacity:     104 rows
[dsar] conflicts:    18 rows
[dsar] member_skill: 24 rows
[dsar] written:      dsar.zip (188 KB)
16

Phase status & estimates

Status
Planned
P3 design phase
Est. LoC (Rust)
~5,800
services/res + sqlx migrations
Est. LoC (TS)
~2,700
Gantt SPA + Yjs binding
Planned tests
95+
incl. OT-cap property tests
External libs
~14
HiGHS · DuckDB · visx · Yjs
P3 budget
~$45/mo
PG replica + Fargate + DuckDB
CapabilityStatus
Capacity model + Member roster ingest from HRplanned · P3
Allocation matrix materialised viewplanned · P3
Gantt SPA with drag-drop + Yjsplanned · P3
Over/under-allocation conflict detectionplanned · P3
Vietnamese OT-cap enforcement (Art. 107)planned · P3
Skills-gap bipartite matchingplanned · P3
Hiring-forecast LP solverplanned · P3
COO-skill / CHRO-skill recommendation bridgeplanned · P3
Mobile-friendly capacity viewplanned · P3
OKR integration: hiring-objective auto-progressplanned · P3
Multi-region active-activeplanned · P4+
External client visibility (PORTAL surface)planned · P4+
17

References

  • PRD §9.18 — RES — Resource planning. FRs 001..005.
  • PRD §11.2.x — Performance / availability / usability NFRs that RES inherits.
  • SRS §7.18 — RES — Revenue sharing & resource planning subjects, allocation primitive, OT-cap policy.
  • Vietnam Labor Code 2019 (Law 45/2019/QH14) — Art. 105 (working hours), Art. 107 (overtime caps), Art. 111 (weekly rest).
  • MoLISA Decree 145/2020/NĐ-CP — Detailed regulations on labour standards.
  • EU AI Act (Reg. 2024/1689) — Annex III §4 (employment), Art. 14 (human oversight), Art. 22 (automated decision-making).
  • GDPR (EU 2016/679) — Art. 22 (automated decision-making with significant effects).
  • HiGHS (High Performance Optimization Software) — open-source LP / MIP solver used for the hiring-forecast LP.
  • Yjs CRDT — collaborative-editing data structure for the Gantt.
  • Architecture context: infrastructure.html#res.
  • HR modulehr.html · supplies the Member roster + sabbatical events.
  • PROJ moduleproj.html · supplies project timelines + estimated_hours.
  • TIME moduletime.html · supplies actual hours for calibration.
  • LEARN modulelearn.html · supplies skill mastery levels.
  • OKR moduleokr.html · consumes hiring-plan progress for the hiring objective.