Skip to content

feat: render API timestamps as RFC3339 via self-describing types#20

Merged
ysyneu merged 3 commits into
mainfrom
feat/timestamp-type
May 29, 2026
Merged

feat: render API timestamps as RFC3339 via self-describing types#20
ysyneu merged 3 commits into
mainfrom
feat/timestamp-type

Conversation

@ysyneu
Copy link
Copy Markdown
Contributor

@ysyneu ysyneu commented May 29, 2026

What

Flashduty API responses encode time as Unix integers — opaque to an LLM (and a human) reading CLI / MCP output. This puts the unit/instant knowledge on the field type so structured output renders RFC3339 with zero downstream guessing.

  • New Timestamp (Unix seconds) and TimestampMilli (Unix milliseconds) types. MarshalJSON → RFC3339 in the local timezone; zero → 0 (unset sentinel, never a 1970 date; omitempty still drops it); UnmarshalJSON accepts epoch or RFC3339 (symmetric round-trip). TimestampMilli uses RFC3339Nano so sub-second precision survives.
  • Re-typed every absolute-instant field in response structs (59 seconds, 4 millis). The 4 ms fields are the ES-backed feed/audit endpoints (AuditLogRecord.CreatedAt, RawTimelineItem.CreatedAt/UpdatedAt, TimelineEvent.Timestamp).
  • Kept int64 (NOT instants): durations / cyclic-window offsets / counts — ScheduleLayer.{RotationDuration,RotationValue,HandoffTime,RestrictStart,RestrictEnd}, ScheduleNotify.AdvanceInTime — and all request-input fields (the LLM never reads request bodies).
  • Typed the two previously-any create methods → *CreateIncidentOutput / *CreateStatusIncidentOutput, retiring the last untyped return.
  • Added CLAUDE.md (+ AGENTS.md symlink) with the conventions.

⚠️ Breaking (pre-1.0)

Response time fields change from int64Timestamp/TimestampMilli; the two create methods now return typed structs instead of any. Consumers do .Time() / .Unix() / int64(...). The two first-party consumers (CLI, MCP) are updated in follow-up PRs that pin this branch.

Reviewer attention

  • Docs-silent fields migrated as seconds (AccountInfo.CreatedAt, MemberInfo.CreatedAt, StatusChange.CreatedAt/UpdatedAt, ChangeTimeline.At): the OpenAPI lacks these read schemas, but they are created_at/updated_at/at and the existing CLI already renders them as seconds — corroborating. Flagging in case you know otherwise.
  • War-room created_at classified seconds per docs (创建时间戳(秒)); the old CLI helper hedged with a magnitude guess. Confirm against a live response if convenient.

Classification source

Every ambiguous schedule field resolved against flashduty-docs on-call OpenAPI (zh, source of truth): migrate-bucket fields say Unix 秒; kept fields say 偏移/周期/提前通知时间 (no "Unix").

Tests / gate

go build ./... && go vet ./... && go test ./... && gofmt -l . — all green. New tests cover both types (marshal/unmarshal/zero/omitempty/round-trip incl. fractional-ms), a migration golden (sec + ms render RFC3339; trap fields stay numeric), and both create-method decodes.

ysyneu added 3 commits May 29, 2026 11:20
Add `Timestamp` (Unix seconds) and `TimestampMilli` (Unix milliseconds) types
whose MarshalJSON emits RFC3339 in the local timezone, so structured output is
human- and LLM-readable instead of opaque integers. The zero value marshals to
0 (an unset sentinel, never a 1970 date) and is dropped by `omitempty`;
UnmarshalJSON accepts a numeric epoch or an RFC3339 string. TimestampMilli uses
RFC3339Nano so sub-second precision survives a round-trip.

Re-type every absolute-instant field in RESPONSE structs to these (59 seconds,
4 millis). Durations, cyclic-window offsets, counts, ids, and all request-input
fields stay int64 — the unit/instant knowledge now lives on the field type, so
consumers (CLI, MCP) get readable timestamps with no downstream guessing.

Type the two previously-`any` create methods: CreateIncident ->
*CreateIncidentOutput, CreateStatusIncident -> *CreateStatusIncidentOutput,
retiring the last untyped return in the SDK.

Add CLAUDE.md (+ AGENTS.md symlink) documenting the conventions.

BREAKING (pre-1.0): response time fields change from int64 to
Timestamp/TimestampMilli; the two create methods now return typed structs.
toon-go renders named scalar types via fmt.Stringer (its normalize() has no
case for reflect.Int64 on a named type, so a bare Timestamp errors with
"unsupported value of type"). Add String() to both types returning the same
RFC3339 representation MarshalJSON uses, so the CLI/MCP TOON output path — a
primary LLM format — renders readable timestamps instead of erroring.

MarshalJSON now reuses String() for the non-zero case (JSON still goes through
MarshalJSON, never Stringer, so JSON output is unchanged).
@ysyneu ysyneu merged commit f5b93ab into main May 29, 2026
12 checks passed
@ysyneu ysyneu deleted the feat/timestamp-type branch May 29, 2026 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant