TIME

P1 · Internal productivity Planned · P1 design phase Owner: CFO seat

Time tracking, leave, and expense capture — the spine that converts Member effort into billable hours, reimbursable expense, and audit-grade records of who did what when.

TIME is the only module where a single missed entry can break a Client invoice; consequently it is the module with the most paranoid integrity model. Entries are append-only at the audit layer; mutations write new rows that supersede prior rows via correction_to links. Approval is a three-tier flow (Member → Account Manager → CFO/CEO). Expense capture is camera-first: snap a receipt, the OCR pipeline pulls amount + vendor + VAT (Vietnamese hóa đơn-aware), the categoriser picks a code from a per-tenant chart of accounts, and the entry queues for Member confirm. Multi-currency at every layer (VND primary, USD secondary). Vietnamese labour-law caps (overtime, max hours per week) enforced by hard rules. INV pulls billable hours weekly. PRD §9.10 + §19.5.

TIME is CyberOS's time-entry, leave-management, and expense-capture spine. The basic primitives are simple: a TimeEntry records minutes worked against an Engagement / Project / Issue with a billable flag; an Expense records VND or USD spent with vendor, VAT, and receipt image; a LeaveRequest records absence with a kind and approver flow. What is hard is the integrity model: entries are append-only at the audit layer, every mutation writes a fresh row with a correction_to link, the weekly approval flow goes Member → Account Manager → CFO/CEO visibility, and Vietnamese labour-law caps (40 h regular / week, 300 h overtime / year) are enforced as hard rules. Expense receipts run through a Vietnamese-hóa-đơn-aware OCR pipeline (MST extraction, line-item parsing, VAT split). Multi-currency at every layer. Feeds INV (weekly billable summary), feeds REW (members' total hours fold into compensation context), feeds OBS (audit replay).

Status
Planned
P1 · design phase
Primitives
3
TimeEntry · Expense · LeaveRequest
Audit
Append-only
correction_to for mutation
Currency
VND + USD
multi-currency at every layer
Approval
3 tiers
Member → AM → CFO
Receipt OCR
VN hóa đơn
MST + VAT split
Depends on
AUTH · BRAIN · PROJ
+ AI · OBS · INV
Est. LoC
~7,500
Rust + TS + OCR pipeline
1

Why TIME exists

Three reasons TIME is its own module rather than a feature in PROJ: (1) audit integrity — time entries drive Client invoices and Member compensation, so the storage model has to be append-only with chained audit rows, not the optimistic-mutation pattern PROJ uses for issues; (2) regulator obligations — Vietnamese Labour Code Art. 105 caps weekly hours and overtime, requires written approval flows, and demands a per-Member time record auditable on inspection; (3) expense capture is a different UX (camera + OCR) than time entry (timer + form) and warrants a separate workflow. Folding all of this into PROJ would corrupt PROJ's data model and bury the regulator-visible flows.

📜
Append-only audit

Every entry write produces a chained BRAIN row. Corrections write a new row with correction_to; the original is never edited. Invoice-grade integrity.

📷
Receipt-first expense

Snap a hóa đơn; the OCR pipeline pulls MST, line items, VAT split, and queues for Member confirm. No form-filling for the 80% case.

⚖️
VN labour-law caps

Hard rules enforce 40 h regular / week and 300 h overtime / year (Labour Code 2019 Art. 107). Violations block save and page CHRO.

The bet is that the integrity properties travel best when they live next to the timer; the labour-law caps travel best when they sit between the timer and the database; and the expense flow travels best when it shares the integrity model. Putting all three in one module keeps the audit chain coherent and lets INV pull from a single source of truth.

2

What it does — 5W1H2C5M

A structured decomposition of TIME's scope. Every cell traces back to PRD §9.10 and §19.5.

AxisQuestionAnswer
5W · WhatWhat is TIME?Three sub-modules: time tracking (start/stop, retroactive, billable flag), leave management (annual, sick, sabbatical, unpaid, other), and expense capture (camera + OCR + categorise). All three share a Postgres canonical store, an append-only audit chain into BRAIN, and a weekly approval workflow.
5W · WhoWho uses it?Members: log time + expense + leave daily; submit weekly. Account Managers: approve weekly per Engagement. CFO: view consolidated dashboards, approve overtime exceptions, sign off on reimbursement batches. CHRO: own leave policy + labour-law cap enforcement. Agents: CUO/COO-skill nudges Members who haven't logged time in 2 days ((FR pending)).
5W · WhenWhen does it run?Continuous: SPA timer, mobile capture. Weekly: batch approval cycle (Sunday 23:59 → Monday 09:00 AM review). Nightly: leave accrual recompute, overtime cap check, expense reimbursement batch. Monthly: hours summary to REW.
5W · WhereWhere does it run?P1: single region (SG-1) with VN-residency S3 for receipt images. P3+: multi-region. The OCR pipeline runs as a Fargate task per OCR job; bursty workloads scale to 50 parallel tasks via SQS.
5W · WhyWhy a separate module?Audit integrity, labour-law caps, and receipt OCR are three concerns that do not belong in PROJ. Splitting them out keeps PROJ's sync-engine simple and gives the regulator a single place to look.
1H · HowHow does it work?Timer: SPA writes start_time; on stop, end_time + duration computed server-side as a generated column. Retroactive: Member enters duration + date; same path. Expense: camera capture → S3 → OCR Lambda → categoriser → Member confirm → entry. Leave: form submit → approver Notify → decision → calendar sync.
2C · CostCost budget?P1: ~$70 / month for SG-1 single-tenant pilot. OCR cost averages $0.002 per receipt (AWS Textract); ~$50 / month for 25k receipts at 50 Members. 50-tenant: ~$280 / month.
2C · ConstraintsConstraints?(a) Audit append-only — non-negotiable. (b) Labour-law caps enforced as hard rules. (c) Multi-currency at every layer. (d) Member time-log annotations are private namespace ((FR pending)) — not visible to AM. (e) Receipt OCR must handle hóa đơn (Vietnamese VAT invoice) format.
5M · MaterialsStack?Rust 1.81 · axum 0.7 · sqlx · PostgreSQL 16 · Redis 7 · S3 + KMS · AWS Textract (or PaddleOCR self-hosted at P2+) · vn-mst-validate skill · OpenTelemetry SDK · React + Zustand SPA · React Native (P3+).
5M · MethodsMethod choices?Append-only with correction_to (not in-place edit). Generated column for duration_minutes. Per-Engagement billable_default + per-entry override. Weekly batch approval (not per-entry). Camera-first expense capture. Multi-currency stored as canonical-VND + spot-FX-rate snapshot.
5M · MachinesDeployment?Fargate axum service. RDS Postgres Multi-AZ. Redis hot cache. S3 + KMS for receipt images. SQS-driven OCR pool. Calendar sync via OAuth (Google / Outlook).
5M · ManpowerWho maintains?0.5 FTE (CFO seat) at P1 launch + 0.25 FTE (CHRO for leave policy). CTO owns the engine.
5M · MeasurementHow measured?Mobile capture p95 ≤ 250 ms (start/stop button). OCR turn-around p95 ≤ 8 s. Weekly approval completion ≥ 95% by Friday EOB. Labour-law cap violations = 0. INV billable-hours feed durability = 100%.
3

Architecture

TIME is a single axum service exposing four surfaces (GraphQL subgraph, REST admin, mobile-friendly REST, MCP). It writes to PostgreSQL canonical, drives an OCR pipeline via SQS + Fargate workers, and writes chained audit rows to BRAIN. Outputs flow downstream to INV (billable summary), REW (hours context), and Calendar (leave events).

graph TB subgraph CLIENT ["Clients"] SPA["SPA
timer + form"] MOB["Mobile capture (P3)"] AGENT["🎯 CUO
via MCP"] end subgraph EDGE ["Edge"] GQL["GraphQL subgraph"] REST["REST admin"] MCP["MCP tools"] end subgraph CORE ["TIME service (Rust)"] TE["TimeEntry handler"] EX["Expense handler"] LR["Leave handler"] LAW["VN labour-law
cap enforcer"] APPR["Approval workflow"] FX["FX rate snapshotter"] CAL_SYNC["Calendar sync
(Google / Outlook)"] AUDIT["BRAIN audit bridge"] end subgraph OCR ["OCR pipeline"] SQS["SQS queue"] OW["Fargate OCR worker"] TX["AWS Textract
(or PaddleOCR)"] VN["vn-mst-validate
skill"] CAT["Categoriser"] end subgraph STORES ["Stores"] PG[("PostgreSQL
time_entry · expense
leave_request
append-only audit-side")] RED[("Redis 7
active timers · FX cache")] S3[("S3 + KMS
receipt images")] end subgraph SINKS ["Sinks"] BRAIN["🧠 BRAIN
audit + summary"] INV["🧾 INV
billable summary"] REW["💎 REW
hours context"] OBS["👁 OBS"] end SPA --> GQL MOB --> REST AGENT --> MCP GQL --> TE GQL --> EX GQL --> LR REST --> TE REST --> EX REST --> LR MCP --> TE TE --> LAW EX --> SQS SQS --> OW OW --> TX TX --> VN VN --> CAT CAT --> EX TE --> FX EX --> FX LR --> APPR LR --> CAL_SYNC TE --> APPR EX --> APPR APPR --> AUDIT TE --> PG EX --> PG LR --> PG TE --> RED EX --> S3 AUDIT --> BRAIN TE --> INV TE --> REW TE --> OBS classDef planned fill:#ccfbf1,stroke:#115e59 classDef store fill:#f5f3ff,stroke:#7c3aed classDef sink fill:#f5ede6,stroke:#45210e class SPA,MOB,AGENT,GQL,REST,MCP,TE,EX,LR,LAW,APPR,FX,CAL_SYNC,AUDIT,SQS,OW,TX,VN,CAT planned class PG,RED,S3 store class BRAIN,INV,REW,OBS sink

Internal components

ComponentPath (planned)Responsibility
time_entry.rsservices/time/src/time_entry.rsTimeEntry CRUD. Generated column for duration_minutes. Per-entry billable override defaults from Engagement.
expense.rsservices/time/src/expense.rsExpense CRUD. Camera-capture → S3 → OCR queue. Categorisation via per-tenant chart of accounts.
leave_request.rsservices/time/src/leave_request.rsLeaveRequest CRUD. Kinds: annual · sick · sabbatical · unpaid · other. Approver flow via Notify.
law_caps.rsservices/time/src/law_caps.rsVietnamese Labour Code Art. 105 / 107 enforcement. 40 h regular / week, 300 h overtime / year. Hard-blocks save on violation; pages CHRO.
approval.rsservices/time/src/approval.rsWeekly approval workflow. Member submit → AM approve → CFO visibility. Self-approval rejected ((FR pending)).
fx.rsservices/time/src/fx.rsFX rate snapshotter. Pulls Vietcombank rate daily; stores snapshot per entry for invoice-time fidelity.
ocr_pipeline.rsservices/time/src/ocr_pipeline.rsSQS-driven OCR worker. Calls AWS Textract (P1) or PaddleOCR self-hosted (P2+). Parses Vietnamese hóa đơn format.
categoriser.rsservices/time/src/categoriser.rsExpense categoriser via AI Gateway. Picks from per-tenant chart of accounts; confidence threshold for auto-apply.
calendar_sync.rsservices/time/src/calendar_sync.rsTwo-way Google / Outlook sync for leave events ((FR pending)).
nudge.rsservices/time/src/nudge.rsCUO/COO-skill nudge for Members who haven't logged time in 2 days ((FR pending)).
inconsistency.rsservices/time/src/inconsistency.rsDetect overlapping entries, > 12 h continuous, and other anomalies ((FR pending) SRS).
inv_export.rsservices/time/src/inv_export.rsWeekly batched billable summary export to INV.
dsar_export.rsservices/time/src/dsar_export.rsDSAR bundle for a Member.
migrations/services/time/migrations/sqlx migrations. All tables RLS by tenant_id; time_entry.member_id additionally enforced.
4

Data model

Three primitives (TimeEntry, Expense, LeaveRequest) plus supporting tables for approvals, FX snapshots, and OCR results. Mutations are encoded as a fresh row with correction_to pointing at the superseded row.

erDiagram TENANT ||--o{ MEMBER : "employs" TENANT ||--o{ ENGAGEMENT : "delivers" ENGAGEMENT ||--o{ BILLABLE_RULE : "carries" MEMBER ||--o{ TIME_ENTRY : "logs" MEMBER ||--o{ EXPENSE : "incurs" MEMBER ||--o{ LEAVE_REQUEST : "files" TIME_ENTRY ||--o| TIME_ENTRY : "correction_to" TIME_ENTRY ||--o{ APPROVAL : "approved by" EXPENSE ||--o| OCR_RESULT : "produced" EXPENSE ||--o{ APPROVAL : "approved by" EXPENSE ||--o| CHART_OF_ACCOUNTS : "categorised" LEAVE_REQUEST ||--o{ APPROVAL : "approved by" LEAVE_REQUEST ||--o| CALENDAR_EVENT : "synced to" TIME_ENTRY ||--o| FX_SNAPSHOT : "valued at" EXPENSE ||--o| FX_SNAPSHOT : "valued at" TENANT { uuid id PK string slug string base_currency "VND" } MEMBER { uuid id PK uuid tenant_id FK string email string display_name int weekly_capacity_hours int annual_leave_quota_days int sabbatical_accrued_days } ENGAGEMENT { uuid id PK uuid tenant_id FK string name string client_account_id FK "→ CRM" } BILLABLE_RULE { uuid id PK uuid engagement_id FK string role bool billable_default int rate_vnd_per_hour int rate_usd_per_hour } TIME_ENTRY { uuid id PK uuid tenant_id FK uuid member_id FK uuid engagement_id FK uuid project_id FK "nullable" uuid issue_id FK "nullable" date entry_date timestamp start_time "nullable for retroactive" timestamp end_time "nullable for retroactive" int duration_minutes "generated column" bool billable string description_short string description_private "Member's private namespace" string status "draft | submitted | approved | rejected | corrected" uuid correction_to FK "nullable; chain to superseded row" timestamp created_at timestamp submitted_at string brain_chain } EXPENSE { uuid id PK uuid tenant_id FK uuid member_id FK uuid engagement_id FK "nullable; non-billable expenses" date expense_date string vendor string vn_mst "nullable; vendor tax code" int amount_minor_unit string currency "VND | USD | other" int vat_amount_minor_unit "nullable" string category_code "from chart_of_accounts" string receipt_s3_key string status "draft | submitted | approved | reimbursed | rejected" uuid correction_to FK "nullable" timestamp created_at string brain_chain } LEAVE_REQUEST { uuid id PK uuid tenant_id FK uuid member_id FK string kind "annual | sick | sabbatical | unpaid | other" date start_date date end_date int duration_days string reason string status "submitted | approved | rejected | cancelled" uuid approver_id FK timestamp decided_at string brain_chain } APPROVAL { uuid id PK uuid tenant_id FK string target_kind "time_entry | expense | leave_request" uuid target_id uuid approver_id FK string tier "AM | CFO | CEO" string decision "approved | rejected | revise" string note timestamp decided_at } OCR_RESULT { uuid expense_id PK string raw_text string parsed_vendor string parsed_vn_mst int parsed_amount_minor_unit string parsed_currency int parsed_vat_minor_unit float confidence string engine "textract | paddleocr" timestamp processed_at } CHART_OF_ACCOUNTS { string code PK "T&E.travel.taxi | T&E.meals | …" uuid tenant_id FK string display_name_vi string display_name_en bool reimbursable_default } FX_SNAPSHOT { uuid id PK date snap_date string from_currency string to_currency int rate_minor_unit "x10000 precision" string source "vcb-buy | vcb-sell | fixed" } CALENDAR_EVENT { uuid id PK uuid leave_request_id FK string provider "google | outlook" string provider_event_id timestamp synced_at }

Append-only audit pattern

A mutation never edits an existing row. The mutator inserts a new row with the new values and sets correction_to to the prior row's id. The prior row's status transitions to corrected. INV and downstream consumers read the latest row in a correction chain via WHERE correction_to IS NULL or by walking the chain forward from the original.

StepActionDB effectBRAIN row
1Original entryINSERT row A (status=submitted)time.entry_create
2Member discovers error, corrects durationINSERT row B (correction_to=A); UPDATE A status=correctedtime.entry_correct
3AM approvesUPDATE B status=approved + APPROVAL rowtime.entry_approved
4INV readsSELECT * FROM time_entry WHERE correction_to IS NULL AND status='approved'
5

API surface

Four surfaces: a federated GraphQL subgraph, a mobile-friendly REST surface for the timer / receipt-capture, an admin REST for OCR pipeline introspection, and an MCP tool catalogue for CUO.

GraphQL subgraph (federated)

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

type TimeEntry @key(fields: "id") {
  id: ID!
  member: Subject!
  engagement: Engagement!
  project: Project
  issue: Issue
  entryDate: Date!
  durationMinutes: Int!
  billable: Boolean!
  description: String!
  status: TimeEntryStatus!
  correctionTo: ID
  approvals: [Approval!]!
}

type Expense @key(fields: "id") {
  id: ID!
  member: Subject!
  engagement: Engagement
  expenseDate: Date!
  vendor: String!
  vnMst: String
  amountMinor: Int!
  currency: String!
  vatAmountMinor: Int
  categoryCode: String!
  receiptUrl: String!     # presigned 5-min
  status: ExpenseStatus!
  ocr: OcrResult
}

type LeaveRequest @key(fields: "id") {
  id: ID!
  member: Subject!
  kind: LeaveKind!
  startDate: Date!
  endDate: Date!
  durationDays: Int!
  status: LeaveStatus!
  approver: Subject
}

enum TimeEntryStatus { DRAFT SUBMITTED APPROVED REJECTED CORRECTED }
enum ExpenseStatus  { DRAFT SUBMITTED APPROVED REIMBURSED REJECTED CORRECTED }
enum LeaveStatus    { SUBMITTED APPROVED REJECTED CANCELLED }
enum LeaveKind      { ANNUAL SICK SABBATICAL UNPAID OTHER }

type Mutation {
  startTimer(engagementId: ID!, projectId: ID, issueId: ID, billable: Boolean): TimeEntry!
  stopTimer(entryId: ID!, description: String!): TimeEntry!
  logRetroactive(input: LogRetroactiveInput!): TimeEntry!
  correctTimeEntry(id: ID!, patch: TimeEntryPatch!): TimeEntry!
  submitWeek(weekOf: Date!): SubmitWeekResult!
    @requiresScopes(scopes: [["time.submit"]])
  approveTimeEntry(id: ID!): TimeEntry!
    @requiresScopes(scopes: [["time.approve"]])
  captureExpense(receiptBase64: String!, hint: ExpenseHint): Expense!
  fileLeave(input: LeaveInput!): LeaveRequest!
  approveLeave(id: ID!): LeaveRequest!
    @requiresScopes(scopes: [["time.leave.approve"]])
}

Mobile-friendly REST

MethodPathPurpose
POST/time/timer/startStart timer with engagement / project / issue context.
POST/time/timer/stopStop active timer.
GET/time/timer/activeReturn active timer for the caller, if any.
POST/time/expense/captureMultipart upload of receipt; returns expense id + OCR job handle.
GET/time/expense/{id}/ocrPoll OCR result.
POST/time/leaveFile leave request.
GET/time/week/{week_of}Get weekly summary for caller.
POST/time/week/submitSubmit week.
GET/admin/approvals/pendingList pending approvals for AM / CFO.
POST/admin/approvals/{id}/decideApprove or reject.
GET/admin/inv/billable-summary?week_of=…Billable summary export for INV.

MCP tool catalogue

Tool nameInputsOutputsAnnotations
cyberos.time.start_timerengagement_id, project_id?, issue_id?TimeEntryscope=time.write
cyberos.time.stop_timerdescriptionTimeEntryscope=time.write
cyberos.time.log_retroactivedate, minutes, descriptionTimeEntryscope=time.write
cyberos.time.weekly_summarymember_id?, week_ofWeeklySummaryreadonly · scope=time.read
cyberos.time.capture_expensereceipt_image_urlExpense + OcrResultscope=time.write
cyberos.time.file_leavekind, start, end, reasonLeaveRequestscope=time.leave.file
cyberos.time.approvetarget_id, tierApprovaldestructive · human-confirm · scope=time.approve
cyberos.time.nudge_membermember_id{ok}scope=time.nudge
6

Key flows

Flow 1 — Log a time entry (timer)

sequenceDiagram autonumber participant U as Member SPA participant API as TIME REST / GraphQL participant LAW as VN labour-law cap enforcer participant FX as FX snapshotter participant PG as PostgreSQL participant R as Redis (active timers) participant B as BRAIN audit U->>API: POST /time/timer/start {engagement, project, issue} API->>LAW: check weekly cap (current + projected) alt within cap LAW-->>API: ok else over 40h regular LAW-->>API: overtime warning (still saves) else over 300h annual overtime LAW-->>API: 422 hard-block · pages CHRO end API->>R: SET active_timer: = {entry_id, start_time} API->>FX: snapshot rate for date FX-->>API: rate API->>PG: INSERT time_entry (status=draft, start_time, …) API->>B: time.entry_start (chain audit row) API-->>U: 200 {entry_id, started_at} Note over U: ... work happens ... U->>API: POST /time/timer/stop {description} API->>R: GET active_timer: R-->>API: {entry_id, start_time} API->>PG: UPDATE time_entry SET end_time=now(), status=draft, description=… API->>B: time.entry_stop {duration_minutes} API-->>U: 200 {entry, duration_minutes}

Flow 2 — Weekly approval workflow

sequenceDiagram autonumber participant M as Member participant API as TIME participant AM as Account Manager participant CFO as CFO dashboard participant N as Notify participant B as BRAIN audit M->>API: submit week (Sunday 23:59 or earlier) API->>API: validate no overlapping entries · no > 12h continuous API->>API: lock entries · status=submitted API->>N: ping AM "Member X submitted week N" API->>B: time.week_submitted AM->>API: review entries · approve / revise / reject per entry alt approved API->>API: status=approved API->>B: time.entry_approved else revise API->>N: ping Member "AM requested revision" API->>B: time.entry_revise_requested else rejected API->>API: status=rejected API->>B: time.entry_rejected end API->>CFO: visibility on dashboard (no decision needed at AM-approved tier) Note over CFO: CFO may flag for personal review
overtime exceptions require explicit CFO sign-off

(FR pending) — a Member MUST NOT be able to approve their own entries; self-approval attempts rejected at the API.

Flow 3 — Expense capture with OCR

sequenceDiagram autonumber participant U as Member (camera) participant API as TIME REST participant S3 as S3 + KMS participant SQS as SQS participant OW as OCR worker participant TX as AWS Textract participant VN as vn-mst-validate skill participant CAT as Categoriser (AI Gateway) participant PG as PostgreSQL participant B as BRAIN audit U->>API: POST /time/expense/capture (image) API->>S3: PUT receipt.jpg (KMS-encrypted) API->>PG: INSERT expense (status=draft, receipt_s3_key) API->>SQS: enqueue OCR job {expense_id, s3_key} API-->>U: 202 {expense_id, ocr_status:"pending"} SQS->>OW: deliver job OW->>TX: detect_document_text(s3_key) TX-->>OW: lines + words OW->>OW: parse hóa đơn structure (header, MST, line items, VAT) OW->>VN: validate MST (vn-mst-validate skill) VN-->>OW: {valid:true, vendor_name_canonical:…} OW->>CAT: classify category (chart_of_accounts) CAT-->>OW: {category_code, confidence} OW->>PG: UPDATE expense + INSERT ocr_result OW->>B: time.expense_ocr_complete Note over OW,U: SPA polls /time/expense/{id}/ocr
or receives push via WebSocket U->>API: confirm / correct + submit API->>PG: status=submitted API->>B: time.expense_submit

Flow 4 — Sync to INV (weekly billable summary)

sequenceDiagram autonumber participant CR as Weekly cron (Mon 10:00) participant API as TIME participant PG as PostgreSQL participant FX as FX rate participant INV as 🧾 INV participant B as BRAIN audit CR->>API: build billable summary for week N API->>PG: SELECT time_entry WHERE billable AND status='approved' AND correction_to IS NULL PG-->>API: rows API->>FX: pull snapshot rates per row FX-->>API: rates API->>API: aggregate per Engagement: hours × rate API->>INV: POST /inv/billable-summary {week_of, items:[…]} INV-->>API: 200 {batch_id} API->>B: time.inv_summary_pushed {batch_id, rows:N, vnd_total:…}

Flow 5 — Leave request with calendar sync

sequenceDiagram autonumber participant M as Member participant API as TIME participant CHRO as CHRO leave-policy check participant APPR as Approver (manager) participant CAL as Google / Outlook participant N as Notify participant B as BRAIN audit M->>API: POST /time/leave {kind:annual, start, end, reason} API->>CHRO: check quota + blackout dates alt within quota + valid CHRO-->>API: ok else quota exceeded CHRO-->>API: 422 + remaining quota end API->>N: ping manager "Member X requested leave" API->>B: time.leave_requested APPR->>API: approve API->>API: status=approved · decided_at=now() API->>CAL: create event (two-way sync, (FR pending)) CAL-->>API: provider_event_id API->>B: time.leave_approved API->>N: ping Member "approved"
7

Entry lifecycle

A time entry traverses six states. Correction is encoded as a new row with correction_to pointing at the superseded row; the original is never edited in place.

stateDiagram-v2 [*] --> Draft: timer start / retroactive create Draft --> Submitted: Member submits week Submitted --> Approved: AM approves Submitted --> Rejected: AM rejects (with note) Submitted --> ReviseRequested: AM asks Member to revise ReviseRequested --> Draft: Member edits Approved --> Corrected: Member or AM files correction Rejected --> Corrected: Member files correction Corrected --> Submitted: chained correction row submitted Approved --> [*]: feeds INV / REW Rejected --> [*]

Expense lifecycle (variant)

StatusTriggerBRAIN row
draftcapture (image uploaded)time.expense_capture
ocr_pendingqueued to OCR worker
ocr_completeOCR done; Member to confirmtime.expense_ocr_complete
submittedMember confirms + submitstime.expense_submit
approvedAM approvestime.expense_approved
reimbursedCFO marks batch paidtime.expense_reimbursed
rejectedAM rejectstime.expense_rejected
correctedsuperseded by correction rowtime.expense_correct
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

NFRs from PRD §11.2 that TIME must satisfy.

NFR IDConcernTargetMeasurement
N(FR pending)Timer start / stop APIp95 ≤ 250 msk6 + RUM
N(FR pending)Weekly summary view (5 weeks)p95 ≤ 400 msSPA RUM
N(FR pending)OCR turn-around (receipt → result)p95 ≤ 8 sSQS + worker histogram
N(FR pending)Mobile capture viable on 3G≤ 3 s upload at 150 KB receiptsimulated network test
N(FR pending)OCR accuracy on hóa đơn corpus≥ 90% field-levelquarterly eval on 200-receipt corpus
N(FR pending)Append-only audit integrity0 in-place editsschema constraint + CI gate
N(FR pending)Receipt image at restKMS-encrypted, RLS by tenantpolicy audit
N(FR pending)Service availability≥ 99.9% (28-day)OBS SLO
N(FR pending)VN Labour Code cap violations= 0 (hard rule)law_caps.rs runtime + CI test
N(FR pending)INV billable summary durability100% deliveredretry + dead-letter monitor
N(FR pending)Approval decisions persisted0 lost under crashchaos test
N(FR pending)Correction-chain integritycycles forbidden; max depth 50migration constraint + CI
10

Dependencies

TIME depends on AUTH, BRAIN, PROJ (for Engagement / Project / Issue references), AI (categoriser + OCR pipeline), MCP (CUO tools), and OBS. It is depended on by INV (billable summary), REW (hours context), HR (leave accruals), and PORTAL (Client-visible billable view at P2+).

graph LR subgraph upstream ["TIME depends on"] AUTH["🔐 AUTH"] BRAIN["🧠 BRAIN"] PROJ["📋 PROJ"] AI["⚡ AI Gateway"] OCR["AWS Textract
(or PaddleOCR)"] MCP["🔌 MCP"] OBS["👁 OBS"] VN["vn-mst-validate skill"] end TIME["⏱ TIME"] subgraph downstream ["TIME is depended on by"] INV["🧾 INV"] REW["💎 REW"] HR["👥 HR"] PORTAL["Portal · P2"] end AUTH --> TIME BRAIN --> TIME PROJ --> TIME AI --> TIME OCR --> TIME MCP --> TIME OBS --> TIME VN --> TIME TIME --> INV TIME --> REW TIME --> HR TIME --> PORTAL classDef shipped fill:#f5ede6,stroke:#45210e classDef planned fill:#fef6e0,stroke:#9c750a class BRAIN,VN shipped class TIME,AUTH,PROJ,AI,OCR,MCP,OBS,INV,REW,HR,PORTAL planned
11

Compliance scope

Time records drive payroll and tax exposures; TIME must satisfy Vietnamese labour law, tax obligations, and the standard PDPL / GDPR set.

Regulation / standardArticle / clauseTIME feature that satisfies it
Vietnam Labour Code 2019 (Law 45/2019)Art. 105 — Normal working hours (≤ 48 h/week, ≤ 10 h/day)law_caps.rs enforces 40 h regular / 48 h with OT cap.
Vietnam Labour Code 2019Art. 107 — Overtime caps (≤ 200 h / 300 h annually with consent)Hard-block at 300 h annual; CHRO override required.
Vietnam Labour Code 2019Art. 113 — Annual leave entitlementLEAVE_REQUEST + quota tracking + auto-accrual.
Vietnam Decree 13/2023Art. 17 — Personal data processing logEvery time entry → audit row.
Vietnam Decree 53/2022Art. 26 — Data residencyVN-tenant receipt images on hanoi-1 S3.
Vietnam Circular 88/2020 (Ministry of Finance)Hóa đơn electronic invoice formatOCR pipeline parses canonical hóa đơn fields (MST, line items, VAT).
Vietnam PDPL (Law 91/2025)Art. 14 — DSARDSAR export includes time entries, expenses, leave.
GDPR (EU 2016/679)Art. 32 — Security of processingKMS-encrypted receipt images, RLS, audit chain.
ISO/IEC 27001:2022A.5.36 — Compliance with policiesApproval workflow enforces policy on every entry.
SOC 2 Type IICC6.1 — Logical accessRBAC + per-Member private namespace ((FR pending)).
SOC 2 Type IICC7.1 — Detection / monitoringInconsistency detection (overlap / > 12 h).
12

Risk entries

TIME-specific risks tracked in the risk register.

IDRiskLikelihoodImpactOwnerMitigation
R-TIME-001Audit chain breakage via direct DB editLowCatastrophicCSOschema constraint forbids UPDATE on canonical columns; in-place change rejected at trigger.
R-TIME-002Overtime cap bypass via timer-then-retroactive trickMediumHighCHROCap enforcer evaluates total per ISO-week regardless of entry mode.
R-TIME-003OCR mis-parse of MST → wrong vendor on invoiceMediumMediumCFOvn-mst-validate skill checks; Member confirm step required before submit.
R-TIME-004Self-approval via direct API callLowHighCFOAPI rejects approver_id == member_id; CI test on every PR.
R-TIME-005INV pulls double after correctionLowHighCFOINV reads WHERE correction_to IS NULL; chain walk asserted in test.
R-TIME-006Receipt image leaks via presigned URL replayLowMediumCSO5-min presign TTL; one-time use; audit row on every presign.
R-TIME-007FX rate snapshot stale → wrong invoice valueMediumMediumCFOSnapshot per entry; weekly Vietcombank fetch; INV uses entry-time snapshot.
R-TIME-008Calendar sync clobbers user-created eventsLowMediumCTOSync only events created by TIME; mark with X-CYBEROS-SOURCE property.
R-TIME-009OCR cost spike from receipt spamLowLowCTOPer-Member rate limit (50 captures / day); CFO dashboard alert at 80%.
R-TIME-010Member time-log private annotations leaked in DSARLowMediumDPODSAR for the Member themselves includes private namespace; cross-Member DSAR excludes.
13

KPIs

TIME health rolls up into 9 KPIs covering usage, integrity, and OCR quality.

KPIFormulaSourceTarget
Weekly submission ratesubmitted / expectedBRAIN≥ 95% by Friday EOB
Days-since-last-entry distributionhistogramBRAINmedian ≤ 1 day
OCR field-level accuracyfields_correct / fields_totalquarterly eval≥ 90%
OCR turn-around p95histogramSQS + worker≤ 8 s
Approval cycle time p50submit → approve, business hoursBRAIN≤ 24 h
Self-approval attempts blockedcount / monthBRAINtracked; expect 0
Overtime cap violationscount / monthlaw_caps= 0
INV summary deliverydelivered / weeksBRAIN100%
Correction-rate per Membercorrections / entriesBRAINtracked; alert > 10%
14

RACI matrix

TIME is owned by CFO seat with CHRO co-ownership on the leave / labour-law facets.

ActivityCEOCFOCHROCTOCSOAM
Service design + specARCCCI
ImplementationICIA/RCI
Approval-flow policyCA/RCIIR
VN labour-law cap policyICA/RRII
OCR engine procurementICIA/RCI
INV billable summary reviewIA/RICIC
Audit-chain integrity reviewCCIRAI
DSAR fulfilmentICCCII
Incident responseARCRRI

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

15

Planned CLI surface

A single admin CLI cyberos-time. Member-facing actions live in the SPA and mobile clients; the CLI is for tenant operators and CFO reporting.

1. Bulk-import historical entries (CSV)

$ cyberos-time import --csv historical-2026-q1.csv --tenant cyberskill

[import]   parsed 4,217 rows · 6 errors
[import]   creating entries (status=approved, source=migration)
[import]   ✓ 4,211 created · ✗ 6 in errors.csv
[audit]    brain seq=15203 chain=…

2. Weekly billable summary preview

$ cyberos-time billable-summary --week-of 2026-08-04 --tenant cyberskill

week of 2026-08-04 (5 working days)
engagement                     hours   billable_vnd   rate_card
ACME Q3 platform               142.5   285,000,000    senior-mix
BetaCo retainer                 40.0    72,000,000    retainer-flat
GamaCorp T&M                    12.0    24,000,000    senior
─────────────────────────────────────────────────────────────
total                          194.5   381,000,000

→ push to INV?  [y/N]

3. Push to INV

$ cyberos-time inv push --week-of 2026-08-04

[push]   194.5 hours · 381 mil VND · 3 engagements
[inv]    batch_id: 01HZK5… created (status=draft on INV)
[audit]  brain seq=15211 chain=…

4. Overtime cap report

$ cyberos-time overtime-report --year 2026

member                  ot_hours  cap   pct_used  risk
linh@cyberskill.world   142       300   47.3%     ▬ ok
tu@cyberskill.world     203       300   67.7%     ▲ approaching
hai@cyberskill.world    287       300   95.7%     ▼ critical → CHRO sign-off required

5. Replay OCR for a receipt

$ cyberos-time ocr replay --expense 01HZK6…

[replay]   re-running OCR pipeline (engine=textract)
[replay]   parsed: vendor=Highlands Coffee, MST=0304556677, amount=178,000 VND, VAT=16,182 VND
[replay]   vn-mst-validate: ✓ matches GDT registry
[replay]   categoriser: T&E.meals (confidence=0.93)
[audit]    brain seq=15218 chain=…

6. DSAR export for a Member

$ cyberos-time dsar-export --subject linh@cyberskill.world --output linh-time.zip

[dsar]   subject:   linh@cyberskill.world
[dsar]   time_entries:   1,247 (including 18 corrected chains)
[dsar]   expenses:        287 (with receipts)
[dsar]   leave_requests:   12
[dsar]   approvals:        842
[dsar]   written:   linh-time.zip (62 MB)
[audit]  brain seq=15221 chain=…

7. Calendar reconcile (re-sync drift)

$ cyberos-time calendar reconcile --provider google --member linh@cyberskill.world

[reconcile]  fetching Google calendar events created by CyberOS …
[reconcile]  3 leave requests · 3 calendar events found · 0 drift
[reconcile]  done
16

Phase status & estimates

Status
Planned
P1 · design phase
Est. LoC
~7,500
Rust + TS + OCR worker
Planned tests
90+
incl. OCR golden corpus
External libs
~10
axum · sqlx · Textract SDK
CLI subcommands
~18 planned
cyberos-time
P1 budget
~$70/mo
Fargate + RDS + S3 + OCR
CapabilityStatus
Timer start/stop + retroactive entryplanned · P1
Per-Engagement billable rulesplanned · P1
Append-only audit + correction_toplanned · P1
Weekly approval workflowplanned · P1
VN Labour Code cap enforcementplanned · P1
Multi-currency + FX snapshotplanned · P1
Camera receipt captureplanned · P1
Vietnamese hóa đơn OCR + MST validateplanned · P1
Expense categoriser (chart of accounts)planned · P1
Leave request + approver flowplanned · P1
Calendar two-way syncplanned · P1
INV weekly billable summary pushplanned · P1
CUO nudge for missing entriesplanned · P1
Inconsistency detectionplanned · P1
Self-hosted PaddleOCR (cost optimisation)planned · P2+
Native mobile (offline-first capture)planned · P3+
17

References

  • PRD §9.10 — TIME product FRs.
  • PRD §19.5 — Schedule, Leave, Attendance SRS-tier FRs.
  • PRD §11.2 — NFRs that TIME must satisfy.
  • SRS §4.10 — Formal (FR pending) through (FR pending) with verification methods.
  • Vietnam Labour Code (Law 45/2019/QH14) — Art. 105 (working hours), Art. 107 (overtime), Art. 113 (annual leave).
  • Vietnam Decree 13/2023/NĐ-CP — Personal data processing.
  • Vietnam Decree 53/2022/NĐ-CP — Data residency.
  • Vietnam Law 91/2025/QH15 (PDPL) — Personal Data Protection Law.
  • Circular 88/2020/TT-BTC — Hóa đơn (Vietnamese VAT e-invoice) format.
  • AWS Textract — OCR engine (P1).
  • PaddleOCR — open-source alternative for P2+ self-hosting.
  • vn-mst-validate — CyberOS skill for MST validation against GDT.
  • Architecture context: infrastructure.html#time.