Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 0 additions & 149 deletions .codacy/cli.sh

This file was deleted.

11 changes: 0 additions & 11 deletions .codacy/codacy.yaml

This file was deleted.

75 changes: 75 additions & 0 deletions .kiro/steering/brightdate-precision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
inclusion: fileMatch
fileMatchPattern: '**/{schemas,documents,services,stores}/**/*.ts'
---

# BrightDate Precision Discipline (BrightChain Workspace)

## When to relax `.toBe(timestamp)` to `.toBeCloseTo(timestamp, 6)`

Several BrightChain test suites assert that a BrightDate timestamp
round-trips exactly through a hydrate/dehydrate cycle:

```ts
const dehydrated = schema.dehydrate(original);
const rehydrated = schema.hydrate(dehydrated);
expect(rehydrated.dateCreated).toBe(original.dateCreated); // ❌ flaky
```

Per BrightDate spec §4.1, this is **not guaranteed** for arbitrary
inputs — Float64 BrightDate has a `~2⁻⁵² × magnitude ≈ 120 ns` round-trip
tax for off-day-anchor inputs. In practice, the `BrightDate ↔ ISO 8601
string ↔ BrightDate` path used by BrightChain schemas loses sub-ms
precision because ISO 8601 itself is ms-precision.

**Use `.toBeCloseTo(timestamp, 6)`** (≈ 86 ms tolerance) for these
assertions — far above the round-trip tax, far below any meaningful
wall-clock precision.

```ts
expect(rehydrated.dateCreated).toBeCloseTo(
original.dateCreated as number,
6,
);
```

## Bit-exact round-trip is achievable but not free

If a future requirement needs a bit-exact `hydrate ∘ dehydrate` (for
example, signing the hydrated form of a profile), the right move is to
change the **storage format**, not the in-memory BrightDate type.

The bottleneck is ISO 8601 ms-precision, not Float64. Switching the
runtime type to `ExactBrightDate` (BigInt picoseconds) without also
changing the persisted format is silently lossy at the ISO boundary.

Two viable paths if the requirement arrives:

1. **Sign the dehydrated bytes** — standard cryptographic discipline.
The hydrated form's round-trip tax becomes irrelevant because
signatures cover the storage form, not the in-memory form. No
schema change required.

2. **Persist a higher-precision representation** — store the timestamp
as a numeric BigInt-picosecond field (or an `ExactBrightDate`
sortable-string per BrightDate spec §6.2) instead of an ISO string.
This requires schema migration and persisted-data migration.

Path (1) is preferred and lower-cost. Path (2) is only justified when
the signed quantity is the in-memory shape and the storage format is
not under the signer's control.

## Where this applies

The pattern shows up wherever BrightChain hydrates a stored document
to runtime objects:

- `brightchain-lib/src/lib/schemas/member/memberSchema.ts`
- `brightchain-lib/src/lib/schemas/messaging/messageMetadataSchema.ts`
- `brightchain-lib/src/lib/documents/member/memberProfileHydration.ts`
- `brightchain-lib/src/lib/documents/member/memberHydration.ts`
- `brightchain-api-lib/src/lib/stores/diskBlockMetadataStore.ts`
- `brightchain-api-lib/src/lib/stores/diskMessageMetadataStore.ts`

Property tests that exercise `hydrate ∘ dehydrate` round-trips on
timestamp fields in these schemas should use `.toBeCloseTo(_, 6)`.
40 changes: 40 additions & 0 deletions .kiro/steering/shell-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Shell Conventions

## Use `bsh` for all shell invocations

This workspace uses **bsh** (a zsh clone, source tree at `/Volumes/Code/bsh/`)
as its shell. Every shell command must be invoked through `bsh -c "..."`,
never `bash` and never the bare `sh` interpreter.

### Why

The `bsh` binary is what the user runs interactively, what most workspace
scripts target, and what the BrightDate Rust workspace's `bsh` consumer
links against. Shell-specific behaviour (parameter expansion, builtins,
glob handling) must match what the user actually sees.

### How

When using the `execute_bash` tool, wrap the command in `bsh -c "..."`:

```
bsh -c "your command here"
```

For multi-line work, write a script file and execute it through bsh
rather than chaining commands inline.

### Compatibility notes

- bsh's `ls` builtin rejects some BSD `ls` flags. Use `/bin/ls` directly
when you need flags like `-i` for inode display.
- macOS `stat` syntax uses `-f` rather than GNU `--format`; bsh inherits
this from the underlying system.
- Standard POSIX-compatible commands (`grep`, `find`, `wc`, `sed`, `awk`,
`curl`, etc.) work as expected.

### Long-running commands

Tool framework messages occasionally surface a "please use bsh" prompt
on long-running commands. This is a framework artifact, not a real
shell-choice issue. Continue using `bsh -c "..."`; do not switch to bash.
Binary file modified .yarn/install-state.gz
Binary file not shown.
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<!-- nx configuration start-->
<!-- Leave the start & end comments to automatically receive updates. -->

- Use /opt/homebrew/bin/bsh (zsh clone) not bash for running commands, noting that dates will be in BrightDate (https://brightdate.org)
- See ./docs/papers/brightdate-specification.md

# General Guidelines for working with Nx

- For navigating/exploring the workspace, invoke the `nx-workspace` skill first - it has patterns for querying projects, targets, and dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
BlockManifest,
ReconciliationConfig,
} from '@brightchain/brightchain-lib';
import { toUnixMs } from '@brightchain/brightdate';
import fc from 'fast-check';
import { existsSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';
Expand Down Expand Up @@ -47,10 +48,12 @@ const arbValidBrightDate = fc
max: new Date('2025-12-31').getTime(),
})
.map((timestamp) => {
// Convert Unix ms to BrightDateTimestamp (days since J2000.0)
// J2000.0 = 2000-01-01T12:00:00Z = 946728000000 ms
const J2000_MS = 946728000000;
return (timestamp - J2000_MS) / 86400000;
// Convert Unix ms to BrightDateTimestamp (days since J2000.0).
// The J2000.0 anchor in Unix ms is the UTC label `946_727_935_816`
// (≡ `2000-01-01T11:58:55.816Z`), per BrightDate spec §2.0 — NOT UTC
// noon (`946_728_000_000`), which is TT noon and 64.184 s later.
const J2000_MS = 946_727_935_816;
return (timestamp - J2000_MS) / 86_400_000;
});

/**
Expand Down Expand Up @@ -355,11 +358,13 @@ describe('Sync Vector Property Tests', () => {
const updatedVector = service.getSyncVector(peerId);
expect(updatedVector).not.toBeNull();

// Timestamp should be updated to approximately now (as BrightDateTimestamp)
// Timestamp should be updated to approximately now (as BrightDateTimestamp).
// Use the library's toUnixMs to convert: this correctly accounts
// for the TAI substrate, leap seconds, and the J2000.0 UTC label
// anchor. Manual `J2000_MS + bd * 86_400_000` would understate
// by ~5 s in the current era because it ignores leap seconds.
const updatedTime = updatedVector!.lastSyncTimestamp;
// Convert to Unix ms for comparison
const J2000_MS = 946728000000;
const updatedMs = J2000_MS + updatedTime * 86400000;
const updatedMs = toUnixMs(updatedTime as number);
expect(updatedMs).toBeGreaterThanOrEqual(beforeReconcile);
expect(updatedMs).toBeLessThanOrEqual(afterReconcile + 1000); // 1s tolerance

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ describe('DiskBlockMetadataStore Persistence Property Tests', () => {
// Verify all fields are correct
expect(retrieved).not.toBeNull();
expect(retrieved!.blockId).toBe(blockId);
expect(retrieved!.createdAt).toBe(now);
// BrightDate timestamps incur a ~120 ns round-trip tax for
// arbitrary Unix-ms inputs (BrightDate spec §4.1). 6-dp
// closeness ≈ 86 ms — far above the tax, far under any
// meaningful clock resolution.
expect(retrieved!.createdAt).toBeCloseTo(now as number, 6);
expect(retrieved!.durabilityLevel).toBe(options.durabilityLevel);
expect(retrieved!.targetReplicationFactor).toBe(
options.targetReplicationFactor,
Expand All @@ -145,8 +149,9 @@ describe('DiskBlockMetadataStore Persistence Property Tests', () => {
expect(retrieved!.expiresAt).toBeNull();
} else {
expect(retrieved!.expiresAt).not.toBeNull();
expect(retrieved!.expiresAt!).toBe(
options.expiresAt,
expect(retrieved!.expiresAt!).toBeCloseTo(
options.expiresAt as number,
6,
);
}
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,12 @@ describe('DiskMessageMetadataStore - Persistence Property Tests', () => {

const found = retrieved.find((m) => m.blockId === metadata.blockId);
expect(found?.acknowledgments.has(recipientIds[0])).toBe(true);
expect(found?.acknowledgments.get(recipientIds[0])).toBe(
ackTime,
// BrightDate timestamps incur a ~120 ns round-trip tax for
// arbitrary Unix-ms inputs (BrightDate spec §4.1). 6-dp
// closeness ≈ 86 ms.
expect(found?.acknowledgments.get(recipientIds[0])).toBeCloseTo(
ackTime as number,
6,
);
} finally {
rmSync(iterTempDir, { recursive: true, force: true });
Expand Down
Loading
Loading