Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
ed56fbf
:sparkles: ci(workflow): update security workflow and release plan
abmmhasan Oct 6, 2026
0e437eb
chore(release): establish UID 6.0 runtime and QA baseline
abmmhasan Oct 6, 2026
00e31cf
fix(uid): correct ULID state and identifier ordering
abmmhasan Oct 6, 2026
0148019
fix(uid): enforce UUID and numeric boundary contracts
abmmhasan Oct 6, 2026
6a4b7df
fix(sequence): harden filesystem allocation state
abmmhasan Oct 6, 2026
70ed0bc
fix(state): fail closed on allocation regression
abmmhasan Oct 6, 2026
78e6238
style(uid): align constants with PHP 8.4 rules
abmmhasan Oct 6, 2026
c417593
style(uid): complete PHP 8.4 constant typing
abmmhasan Oct 6, 2026
3e0c999
fix(qa): resolve UID regression and class ordering
abmmhasan Oct 6, 2026
b20c673
feat(runtime): add bounded generation context
abmmhasan Oct 6, 2026
df11487
fix(runtime): allow shared Runwire scope probe
abmmhasan Oct 6, 2026
559cb28
feat(config): pass generation runtime explicitly
abmmhasan Oct 6, 2026
e3915d7
feat(runtime): bound coordinated millisecond waits
abmmhasan Oct 6, 2026
8df127b
feat(runtime): bound TBSL and Randflake timing
abmmhasan Oct 6, 2026
c171f3c
feat(runtime): make sequence locks cooperative
abmmhasan Oct 6, 2026
84c8dfe
test(runtime): cover bounded waits and cancellation
abmmhasan Oct 6, 2026
b2ab7c8
feat(sonyflake): add explicit upstream wire format
abmmhasan Oct 6, 2026
5a799d7
feat(randflake): add upstream SPARX64 primitives
abmmhasan Oct 6, 2026
eb38ad4
feat(randflake): define explicit format and lease mode
abmmhasan Oct 6, 2026
3e82c2f
feat(randflake): implement upstream SPARX64 format
abmmhasan Oct 6, 2026
bb3c377
test(randflake): verify pinned upstream vectors
abmmhasan Oct 6, 2026
839d966
fix(qa): resolve analyzer findings in coordinated ID paths
abmmhasan Oct 6, 2026
04f8e7e
fix(analysis): resolve runtime type diagnostics
abmmhasan Oct 6, 2026
cfb0496
fix(qa): clean diagnostics and test teardown
abmmhasan Oct 6, 2026
a6e5f28
style(uid): normalize optional runtime calls
abmmhasan Oct 6, 2026
a7d0f83
style(uid): order runtime and allocation members
abmmhasan Oct 6, 2026
7c57ea1
style(uid): order coordinated generator members
abmmhasan Oct 6, 2026
37692a7
style(uid): order support members
abmmhasan Oct 6, 2026
d65b847
style(config): align UID 6 configuration layout
abmmhasan Oct 6, 2026
7cc7936
style(uid): finish member and import ordering
abmmhasan Oct 6, 2026
14b61d5
style(uid): restore reordered member indentation
abmmhasan Oct 6, 2026
ee3da26
style(uid): restore generator member indentation
abmmhasan Oct 6, 2026
c05f9a0
style(uid): restore support member indentation
abmmhasan Oct 6, 2026
0287d6f
style(tbsl): restore member indentation
abmmhasan Oct 6, 2026
64b03db
docs(uid): document runtime, coordination, and 6.0 migration
abmmhasan Oct 6, 2026
6b99ee2
docs(uid): correct UUID and random ID contracts
abmmhasan Oct 6, 2026
4999d25
docs(uid): document explicit compatibility formats
abmmhasan Oct 6, 2026
2fd8041
fix(qa): normalize generator and lock quality
abmmhasan Oct 7, 2026
b1e3fec
style(uid): normalize coordinated generator layout
abmmhasan Oct 7, 2026
f71cd16
style(uid): normalize support and configuration layout
abmmhasan Oct 7, 2026
c924450
fix(sequence): bound persistent provider domains
abmmhasan Oct 7, 2026
9e65b13
fix(runtime): bound generator safety domains
abmmhasan Oct 7, 2026
25e503d
fix(qa): clear persistent-domain quality gates
abmmhasan Oct 7, 2026
dbeddea
test(uid): lock runtime and v5 compatibility contracts
abmmhasan Oct 7, 2026
c8f61a8
docs(uid): pin upstream compatibility revisions
abmmhasan Oct 7, 2026
0a11157
fix(qa): align formatter and byte-range contracts
abmmhasan Oct 7, 2026
f59c2ad
style(uid): match PHPForge PER formatting
abmmhasan Oct 7, 2026
6b89b29
:art: style(randflake): format sensitive parameter attribute annotations
abmmhasan Oct 7, 2026
2e5859b
fix(uid): preserve format and runtime forwarding metadata
abmmhasan Oct 7, 2026
764baf9
docs(plan): sync completed UID 6 implementation batches
abmmhasan Oct 7, 2026
3415db4
refactor(codec): fold signed 64-bit conversion into DecimalBytes
abmmhasan Oct 7, 2026
5795ea1
fix(value): restore readonly value-object compatibility
abmmhasan Oct 7, 2026
4683d10
bench(uid): expand CUID2 and codec profiling
abmmhasan Oct 7, 2026
554d839
refactor(value): fold single-use comparable trait into base value
abmmhasan Oct 7, 2026
60f05ff
style(uid): align benchmark and value member ordering
abmmhasan Oct 7, 2026
e68b002
ci(release): add UID 6 host performance and soak acceptance
abmmhasan Oct 7, 2026
c678fc5
fix(ci): harden release acceptance scripts
abmmhasan Oct 7, 2026
e91720d
fix(ci): correct release harness output and cleanup
abmmhasan Oct 7, 2026
54d3755
ci(release): gate final tracker commit with release guard
abmmhasan Oct 7, 2026
95dc2d8
fix(ci): isolate candidate release guard from baseline fixtures
abmmhasan Oct 7, 2026
21475ed
perf(codec): remove radix loop allocation overhead
abmmhasan Oct 7, 2026
82c7423
fix(analysis): preserve radix byte-list type
abmmhasan Oct 7, 2026
246b32c
fix(analysis): widen private radix mutation contract
abmmhasan Oct 7, 2026
46876f5
fix(analysis): declare radix mutation output type
abmmhasan Oct 7, 2026
c3b86ac
fix(analysis): preserve mutable radix list contract
abmmhasan Oct 7, 2026
a970afc
fix(analysis): widen radix mutation and normalize once
abmmhasan Oct 7, 2026
d37ea1f
perf(codec): restore direct radix hot path
abmmhasan Oct 7, 2026
d7eb244
perf(codec): keep radix loops direct within complexity budget
abmmhasan Oct 7, 2026
3ab18a1
perf(lock): collapse secure-open warning handling
abmmhasan Oct 7, 2026
14dd0c4
fix(analysis): make runtime timeout branch explicit
abmmhasan Oct 7, 2026
7e526f3
fix(qa): remove redundant lock exception catch
abmmhasan Oct 7, 2026
4dda9ae
perf(codec): inline cheap hex boundary checks
abmmhasan Oct 7, 2026
d6c62b9
bench(host): use sustained fixed-duration trials
abmmhasan Oct 7, 2026
060b41f
ci(release): run sustained paired host acceptance
abmmhasan Oct 7, 2026
30dc7e1
fix(bench): bootstrap Runwire profiling
abmmhasan Oct 7, 2026
2fa02dd
fix(ci): retain hidden release evidence
abmmhasan Oct 7, 2026
3d144c9
fix(bench): separate reproduction inputs from measured results
abmmhasan Oct 7, 2026
1f338a6
bench(host): use fixed comparable warmup operations
abmmhasan Oct 7, 2026
e247dd1
perf(codec): restore inline base encoding hot path
abmmhasan Oct 7, 2026
8e7b0b4
perf(lock): fast-path uncontended verified locks
abmmhasan Oct 7, 2026
1fceadb
style(codec): normalize class element spacing
abmmhasan Oct 7, 2026
c95936b
perf(codec): simplify inline radix preparation
abmmhasan Oct 7, 2026
2612fe5
refactor(codec): centralize byte unpack typing
abmmhasan Oct 7, 2026
7ece434
refactor(codec): keep inline radix loop under complexity gate
abmmhasan Oct 7, 2026
b53b4b7
bench(host): right-size strict warmup by concurrency
abmmhasan Oct 7, 2026
c3e34a1
bench(host): interleave paired baseline and candidate trials
abmmhasan Oct 7, 2026
6a302ef
bench(host): run balanced paired trials on one runner
abmmhasan Oct 7, 2026
5f62244
:sparkles: refactor(benchmarks): optimize radix encoding and fix host…
abmmhasan Oct 7, 2026
d3858dc
:sparkles: refactor(sequence): improve runtime context forwarding and…
abmmhasan Oct 7, 2026
7721003
:sparkles: refactor(sequence): optimize file locking and optimize seq…
abmmhasan Oct 7, 2026
b90fb23
:art: refactor(sequence): simplify filesystem locking and snowflake i…
abmmhasan Oct 7, 2026
dfe2370
:fire: chore(ci): remove release performance harnesses and add produc…
abmmhasan Oct 7, 2026
141af40
docs(uid): align encoding and format references with UID 6
abmmhasan Oct 7, 2026
61a2e2d
docs(uid): sync value and storage semantics
abmmhasan Oct 7, 2026
729c4cf
docs(uid): sync value and storage semantics
abmmhasan Oct 7, 2026
348bd2d
docs(uid): sync value and storage semantics
abmmhasan Oct 7, 2026
06fd579
docs(uid): sync value and storage semantics
abmmhasan Oct 7, 2026
64bd29d
docs(tbsl): align specification with 60-bit payload
abmmhasan Oct 7, 2026
b0cbf11
docs(runtime): align optional integration requirements
abmmhasan Oct 7, 2026
a47a8c5
docs(runtime): align optional integration requirements
abmmhasan Oct 7, 2026
000fe74
docs(runtime): align optional integration requirements
abmmhasan Oct 7, 2026
b5055ff
docs(runtime): align optional integration requirements
abmmhasan Oct 7, 2026
35e3ff6
docs(uid): finish public capability synchronization
abmmhasan Oct 7, 2026
ab3b263
docs(uid): finish public capability synchronization
abmmhasan Oct 7, 2026
4e509b4
docs(uid): finish public capability synchronization
abmmhasan Oct 7, 2026
0db4caf
docs(uid): finish structural documentation audit
abmmhasan Oct 7, 2026
57fc4ef
docs(uid): finish structural documentation audit
abmmhasan Oct 7, 2026
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
50 changes: 50 additions & 0 deletions .github/workflows/release-acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: UID 6 Release Acceptance

on:
workflow_dispatch:
push:
branches: ["main", "master"]
paths:
- "src/**"
- "tests/**"
- "composer.json"
- ".github/workflows/release-acceptance.yml"
pull_request:
branches: ["main", "master"]
paths:
- "src/**"
- "tests/**"
- "composer.json"
- ".github/workflows/release-acceptance.yml"

permissions:
contents: read

jobs:
production-smoke:
name: Production smoke (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
php: ["8.4", "8.5"]
steps:
- name: Checkout exact candidate
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
extensions: ctype
coverage: none

- name: Install production dependencies
run: composer install --no-dev --no-interaction --prefer-dist --no-progress --classmap-authoritative

- name: Check every generator and format in one 100-cycle pass
run: php tests/smoke.php
12 changes: 11 additions & 1 deletion .github/workflows/security-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,29 @@ on:
- cron: "0 0 * * 0"
push:
branches: [ "main", "master" ]
tags: [ "v*", "[0-9]*" ]
pull_request:
branches: [ "main", "master", "develop", "development" ]

jobs:
phpforge:
if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
uses: infocyph/phpforge/.github/workflows/security-standards.yml@main
with:
fail_on_skipped_tests: true
integration_services: '[]'
service_topologies: '{}'
php_extensions: '["bcmath"]'
php_extensions: '["ctype", "pcntl"]'
permissions:
security-events: write
actions: read
contents: read

release:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
uses: infocyph/phpforge/.github/workflows/release.yml@main
permissions:
contents: write
secrets:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}

20 changes: 10 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
.idea
.psalm-cache
.phpunit.cache
.vscode
.windsurf
.codex
/.idea
/.psalm-cache
/.phpunit.cache
/.vscode
/.windsurf
/.codex
*~
*.patch
*.txt
!docs/requirements.txt
AI_CONTEXT.md
composer.lock
example
example.php
git-story_media
/git-story_media
patch.php
test.php
var
vendor
/var
/vendor
d2utmp*
/graphify-out
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ All-in-one unique ID toolkit for PHP.
- TypeID, ObjectID, NanoID, RandomId, CUID2, KSUID, XID
- Opaque and deterministic IDs
- Value objects and comparator utilities
- Binary conversion and base encoders (`16`, `32`, `36`, `58`, `62`)
- Binary conversion and base encoders (`10`, `16`, `32`, `36`, `58`, `62`)
- Pluggable sequence providers (filesystem, memory, PSR-16 cache, callback)
- Explicit legacy/upstream Sonyflake and Randflake compatibility modes
- Optional PSR-20 clocks and Runwire 2.1.1+ cooperative wait integration

## Requirements

- PHP `>=8.2`
- PHP `>=8.4`
- A 64-bit PHP runtime
- PHP ctype extension

## Installation

Expand Down Expand Up @@ -75,7 +78,7 @@ $decoded = UUID::fromBase($base58, 58);
```

The shared byte-level encoder is available as
`Infocyph\UID\Support\BaseEncoder` for bases `16`, `32`, `36`, `58`, and `62`.
`Infocyph\UID\Support\BaseEncoder` for bases `10`, `16`, `32`, `36`, `58`, and `62`.

## References

Expand All @@ -98,6 +101,10 @@ UID is protected by [PHPForge](https://github.com/infocyph/PHPForge), an automat
tests, static and taint analysis, dependency auditing, architecture checks, and release readiness. Automated controls reduce
risk but do not replace responsible disclosure or manual review.

Release acceptance also runs `php tests/smoke.php` with a production-only install on PHP 8.4 and 8.5.
One 100-cycle pass covers every generator and supported format, checking valid output, sample uniqueness,
and relevant round trips. Long HTTP benchmarks and soak runs are not release gates.

---

<div align="center">
Expand Down
58 changes: 32 additions & 26 deletions TBSL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,50 +23,53 @@ Character positions are 1-based.

| Characters | Length | Field | Description |
|:--|--:|:--|:--|
| 1-15 | 15 hex chars | Time-machine payload | Uppercase hexadecimal encoding of the decimal payload `SSSSSSSSSSUUUUUUMM`, left-padded with zeroes to 15 characters. |
| 16-20 | 5 hex chars | Entropy or sequence | Random suffix by default, or a sequence suffix when sequenced mode is enabled. |
| 1-15 | 15 hex chars | Time-machine payload | Uppercase hexadecimal encoding of the 60-bit integer `(unixMicroseconds * 100) + machineId`, left-padded with zeroes to 15 characters. |
| 16-20 | 5 hex chars | Sequence or entropy | Zero-based sequence suffix by default, or random entropy when sequenced mode is disabled. |

The decimal payload is composed as follows:
The time-machine payload is numeric, not a fixed-width decimal string. Parsing is:

| Decimal digits | Length | Field | Description |
|:--|--:|:--|:--|
| 1-10 | 10 digits | Unix seconds | Seconds since the Unix epoch. |
| 11-16 | 6 digits | Microseconds | Microsecond fraction of the current second. |
| 17-18 | 2 digits | Machine ID | Machine identifier from `00` to `99`. |
```text
unixMicroseconds = intdiv(payload, 100)
machineId = payload % 100
```

This representation remains valid when Unix seconds grow beyond ten decimal
digits, as long as the combined value still fits the 60-bit field.

## Generation

Generation accepts:

- `machineId`: integer from `0` to `99`; default is `0`.
- `sequenced`: boolean; default is `false`.
- `sequenced`: boolean; default is `true`. Use `generateRandom()` or `sequenced: false` for the entropy suffix.

The generator:

1. Reads the current Unix time with microsecond precision.
2. Builds the decimal time sequence as `seconds + microseconds`.
3. Appends the two-digit machine ID to form `SSSSSSSSSSUUUUUUMM`.
4. Converts that decimal payload to hexadecimal and left-pads it to 15
characters.
2. Converts that time to an integer count of Unix microseconds.
3. Forms the 60-bit time-machine payload as `(unixMicroseconds * 100) + machineId`.
4. Converts that integer to hexadecimal and left-pads it to 15 characters.
5. Appends a 5-character hexadecimal suffix:
- random mode: first 5 hex characters from 3 random bytes;
- sequenced mode: the next sequence value for the
`(type = "tbsl", machineId, timestamp)` key, encoded as hex and padded to
5 characters.
- sequenced mode (default): obtains a positive provider allocation from
`(type = "tbsl", machineId, timestamp)`, then encodes
`allocation - 1` as five hexadecimal characters;
- random mode: first 5 hex characters from 3 random bytes.
6. Returns the 20-character uppercase hexadecimal string.

Sequence providers should keep returned sequence values within the 20-bit suffix
range, `0x00000` through `0xFFFFF`.
In sequenced mode, provider allocations `1..0x100000` map to encoded suffixes
`00000..FFFFF`. If that range is exhausted for one timestamp, generation waits
for the next usable timestamp or fails according to the configured bounded-wait
and clock-backward policy.

## Parsing

To parse a canonical TBSL value:

1. Validate the string against `^[0-9A-F]{20}$`.
2. Decode characters `1-15` from hexadecimal to the decimal payload.
3. Read the first 10 decimal digits as Unix seconds.
4. Read the next 6 decimal digits as microseconds.
5. Read the final 2 decimal digits as the machine ID.
2. Decode characters `1-15` from hexadecimal to the 60-bit payload.
3. Recover Unix microseconds with `intdiv(payload, 100)`.
4. Recover the machine ID with `payload % 100`.
5. Build the timestamp from the recovered seconds and microsecond fraction.

The suffix is intentionally opaque. It is not needed to recover the timestamp or
machine ID.
Expand All @@ -80,18 +83,20 @@ move forward.
For IDs generated within the same microsecond and machine ID:

- random mode provides uniqueness through entropy, but not generation order;
- sequenced mode provides deterministic suffix ordering while the sequence value
remains within the 5-character hexadecimal suffix.
- sequenced mode provides deterministic suffix ordering while the sequence range
remains available.

If the clock moves backward, the implementation either waits for the next usable
time sequence or throws, depending on the configured clock-backward policy.
Waits are bounded; a frozen injected clock cannot spin indefinitely.

## Encodings

The canonical representation is uppercase hexadecimal. The package can also
convert canonical TBSL values to and from:

- raw 10-byte binary;
- base10;
- base16;
- base32;
- base36;
Expand All @@ -105,7 +110,8 @@ canonical 20-character uppercase hexadecimal TBSL string.

- Canonical size: 20 hex characters.
- Binary size: 10 bytes.
- Time-machine payload: 60 bits.
- Timestamp precision: microseconds.
- Machine ID range: `0` through `99`.
- Suffix size: 5 hex characters, or 20 bits.
- Maximum suffix cardinality per `(timestamp, machineId)` key: 1,048,576 values.
- Sequenced suffix cardinality per `(timestamp, machineId)` key: 1,048,576 values.
78 changes: 76 additions & 2 deletions benchmarks/BaseCodecBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,40 @@
namespace Infocyph\UID\Benchmarks;

use Infocyph\UID\Support\BaseEncoder;
use Infocyph\UID\Support\DecimalBytes;
use Infocyph\UID\Support\NumericIdCodec;
use Infocyph\UID\Support\TypeIdCodec;
use PhpBench\Attributes as Bench;

final class BaseCodecBench
{
/** @var array<int, string> */
private array $decimal = [];

/** @var array<int, array<int, string>> */
private array $encoded = [];

/** @var array<int, string> */
private array $samples = [];

private string $typeIdEncoded;

public function __construct()
{
require_once __DIR__ . '/BenchBootstrap.php';
BenchBootstrap::load();
foreach ([8, 10, 12, 16, 20, 32] as $length) {
$this->samples[$length] = random_bytes($length);

foreach ([8, 10, 12, 16, 20, 32, 64] as $length) {
$sample = random_bytes($length);
$this->samples[$length] = $sample;
$this->decimal[$length] = DecimalBytes::fromBytes($sample);

foreach ([10, 16, 32, 36, 58, 62] as $base) {
$this->encoded[$base][$length] = BaseEncoder::encodeBytes($sample, $base);
}
}

$this->typeIdEncoded = TypeIdCodec::encode($this->samples[16]);
}

#[Bench\Revs(1000), Bench\Iterations(5), Bench\ParamProviders('provideLengths')]
Expand Down Expand Up @@ -57,6 +77,59 @@ public function benchDecimal(array $params): void
BaseEncoder::encodeBytes($this->sample($params), 10);
}

#[Bench\Revs(250), Bench\Iterations(5), Bench\ParamProviders('provideBaseLengthPairs')]
public function benchDecode(array $params): void
{
BaseEncoder::decodeToBytes(
$this->encoded[$params['base']][$params['length']],
$params['base'],
$params['length'],
);
}

#[Bench\Revs(500), Bench\Iterations(5), Bench\ParamProviders('provideLengths')]
public function benchNumericFromBytes(array $params): void
{
NumericIdCodec::decimalFromBytes($this->sample($params), $params['length']);
}

#[Bench\Revs(500), Bench\Iterations(5), Bench\ParamProviders('provideLengths')]
public function benchNumericToBytes(array $params): void
{
DecimalBytes::toFixedBytes($this->decimal[$params['length']], $params['length']);
}

#[Bench\Revs(1000), Bench\Iterations(5)]
public function benchTypeIdDecode(): void
{
TypeIdCodec::decode($this->typeIdEncoded);
}

#[Bench\Revs(1000), Bench\Iterations(5)]
public function benchTypeIdEncode(): void
{
TypeIdCodec::encode($this->samples[16]);
}

/**
* @return array<string, array{base:int,length:int}>
*/
public function provideBaseLengthPairs(): array
{
$pairs = [];

foreach ([10, 16, 32, 36, 58, 62] as $base) {
foreach ([8, 10, 12, 16, 20, 32] as $length) {
$pairs['base-' . $base . '-' . $length . '-bytes'] = [
'base' => $base,
'length' => $length,
];
}
}

return $pairs;
}

/**
* @return array<string, array{length:int}>
*/
Expand All @@ -69,6 +142,7 @@ public function provideLengths(): array
'16-bytes' => ['length' => 16],
'20-bytes' => ['length' => 20],
'32-bytes' => ['length' => 32],
'64-bytes' => ['length' => 64],
];
}

Expand Down
Loading
Loading