OFSClock

OFS-2.1 SDK

Production-ready, zero-dependency reference implementations. Copy-paste into your project. Integer millisecond arithmetic avoids float drift.

Civil display uses an 86,400-second mean-day interface baseline; ISO/UTC remains authoritative.

Quick Start

  1. Copy ofs-core.ts into your project (zero dependencies).
  2. Optional: add ofs-tz.ts if you need timezone construction via Intl.DateTimeFormat.

Source Reference

Zero dependencies. All arithmetic uses integer milliseconds (msPerPulse = 864) to avoid float drift.

lib/ofs/ofs-core.ts
/**
 * OFS SDK (Core) — OFS-2.1
 * Production-ready, copy-paste friendly, zero dependencies.
 *
 * Civil Display token: Φ.ψψψψψ
 * - Φ: 0..9 (phase)
 * - ψ: 5 digits "00000".."09999" (pulse within phase)
 * - "." is a separator, NOT a decimal point
 *
 * Design baseline:
 * - mean civil day = 86,400 seconds = 86,400,000 ms
 * - pulsesPerDay = 100,000
 * - msPerPulse = 864 ms  (since 0.864 s)
 */

export const OFS = {
  version: "2.1",
  phasesPerDay: 10,
  pulsesPerPhase: 10_000,
  pulsesPerDay: 100_000,
  secondsPerDay: 86_400,
  msPerDay: 86_400_000,
  secondsPerPhase: 8_640,
  msPerPhase: 8_640_000,
  secondsPerPulse: 0.864,
  msPerPulse: 864,
} as const;

export type Phase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
export type CivilToken = `${Phase}.${string}`;

export class OFSError extends Error {
  readonly code: string;
  constructor(code: string, message: string) {
    super(message);
    this.name = "OFSError";
    this.code = code;
  }
}

function clampInt(n: number, min: number, max: number): number {
  if (!Number.isFinite(n)) return min;
  const x = Math.trunc(n);
  return x < min ? min : x > max ? max : x;
}

function pad5(n: number): string {
  const s = String(n);
  return s.length >= 5 ? s : "0".repeat(5 - s.length) + s;
}

function isDigits(s: string): boolean {
  return /^[0-9]+$/.test(s);
}

/** Convert ms-of-day (local) → pulses-of-day (0..99999) */
export function pulsesOfDayFromMs(msOfDay: number): number {
  if (!Number.isFinite(msOfDay)) {
    throw new OFSError("E_MS_NAN", "msOfDay must be a finite number.");
  }
  const ms = Math.max(0, Math.min(OFS.msPerDay - 1, Math.trunc(msOfDay)));
  const pulses = Math.floor(ms / OFS.msPerPulse);
  return clampInt(pulses, 0, OFS.pulsesPerDay - 1);
}

/** Convert pulses-of-day → Civil token Φ.ψψψψψ */
export function civilFromPulsesOfDay(pulsesOfDay: number): CivilToken {
  const p = clampInt(pulsesOfDay, 0, OFS.pulsesPerDay - 1);
  const phase = Math.floor(p / OFS.pulsesPerPhase) as Phase;
  const within = p % OFS.pulsesPerPhase;
  return `${phase}.${pad5(within)}` as CivilToken;
}

/** Convert ms-of-day (local) → Civil token */
export function civilFromMsOfDay(msOfDay: number): CivilToken {
  return civilFromPulsesOfDay(pulsesOfDayFromMs(msOfDay));
}

/** Convert local time parts (0..23 etc) → Civil token */
export function civilFromTimeParts(input: {
  hour: number;
  minute?: number;
  second?: number;
  millisecond?: number;
}): CivilToken {
  const h = clampInt(input.hour, 0, 23);
  const m = clampInt(input.minute ?? 0, 0, 59);
  const s = clampInt(input.second ?? 0, 0, 59);
  const ms = clampInt(input.millisecond ?? 0, 0, 999);
  const msOfDay = h * 3_600_000 + m * 60_000 + s * 1_000 + ms;
  return civilFromMsOfDay(msOfDay);
}

/** Strict parse + validate Civil token "Φ.ψψψψψ" */
export function parseCivil(token: string): {
  phase: Phase; pulse: number; pulsesOfDay: number
} {
  if (typeof token !== "string") {
    throw new OFSError("E_CIVIL_TYPE", "Civil token must be a string.");
  }
  const parts = token.split(".");
  if (parts.length !== 2) {
    throw new OFSError("E_CIVIL_FORMAT",
      'Civil token must be "Φ.ψψψψψ" with exactly one dot separator.');
  }
  const [phiStr, psiStr] = parts;
  if (phiStr.length !== 1 || !isDigits(phiStr)) {
    throw new OFSError("E_PHASE_FORMAT", "Φ must be a single digit 0–9.");
  }
  const phaseNum = Number(phiStr);
  if (psiStr.length !== 5 || !isDigits(psiStr)) {
    throw new OFSError("E_PULSE_FORMAT",
      "ψ must be exactly 5 digits 00000–09999.");
  }
  const pulseNum = Number(psiStr);
  const pulsesOfDay = phaseNum * OFS.pulsesPerPhase + pulseNum;
  return { phase: phaseNum as Phase, pulse: pulseNum, pulsesOfDay };
}

/** Flow-Time: monotonic integer pulses. FT = floor(unixMs / 864) */
export function toFT(unixMs: number): number {
  if (!Number.isFinite(unixMs))
    throw new OFSError("E_UNIXMS_NAN", "unixMs must be a finite number.");
  return Math.floor(unixMs / OFS.msPerPulse);
}

export function fromFT(ft: number): Date {
  if (!Number.isFinite(ft))
    throw new OFSError("E_FT_NAN", "ft must be a finite number.");
  return new Date(Math.trunc(ft) * OFS.msPerPulse);
}

Test Vectors

Deterministic outputs for civilFromTimeParts. All implementations must match these values exactly.

Local TimeCivil Token
00:000.00000
02:000.08333
04:001.06666
06:002.05000
08:003.03333
10:004.01666
12:005.00000
14:005.08333
16:006.06666
18:007.05000
20:008.03333
21:008.07500
22:009.01666

GMT+8 Quick Reference mappings are identical to the above when the local timezone offset is applied.

Security and Misuse Prevention

No tracking. OFS pulses are coordination units, not surveillance timestamps. Never use pulse-level resolution to track individual behavior or location.

No scoring. OFS must not be used for productivity scoring, behavioral profiling, or performance measurement of individuals.

Privacy-first defaults. Meaning-Time events default to privacy: "private" and require explicit consent: { mode: "explicit" } when handling personal data.

Implementations that violate these normative safeguards are non-compliant with OFS-2.1.

Civil display uses an 86,400-second mean-day interface baseline. ISO/UTC remains authoritative for all legal and precision contexts. Leap-second handling follows a smear strategy to maintain monotonic flow.