@unifyplane/logsdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +48 -0
- package/README.md +8 -0
- package/contracts/specs/LogSDKFuntionalSpec.md +394 -0
- package/contracts/specs/fanout-semantics.v1.md +244 -0
- package/contracts/specs/sink-contract.v1.md +223 -0
- package/contracts/specs/step-record.v1.md +292 -0
- package/contracts/specs/validation-rules.v1.md +324 -0
- package/docs/LogSDK-Unified-Execution-Logging-Framework.md +93 -0
- package/docs/log_sdk_test_cases_traceability_plan.md +197 -0
- package/docs/log_sdk_test_coverage_report.md +198 -0
- package/docs/prompts/AuditorSDK.txt +214 -0
- package/package.json +29 -0
- package/src/core/clock.ts +25 -0
- package/src/core/context.ts +142 -0
- package/src/core/fanout.ts +38 -0
- package/src/core/ids.ts +35 -0
- package/src/core/message_constraints.ts +66 -0
- package/src/core/outcomes.ts +5 -0
- package/src/core/record_builder.ts +269 -0
- package/src/core/spool.ts +41 -0
- package/src/core/types.ts +56 -0
- package/src/crypto-shim.d.ts +9 -0
- package/src/fs-shim.d.ts +15 -0
- package/src/index.ts +107 -0
- package/src/node-test-shim.d.ts +1 -0
- package/src/perf_hooks-shim.d.ts +7 -0
- package/src/process-shim.d.ts +1 -0
- package/src/sinks/file_ndjson.ts +42 -0
- package/src/sinks/file_ndjson_sink.ts +45 -0
- package/src/sinks/sink_types.ts +15 -0
- package/src/sinks/stdout_sink.ts +20 -0
- package/src/validate/api_surface_guard.ts +106 -0
- package/src/validate/noncompliance.ts +33 -0
- package/src/validate/schema_guard.ts +238 -0
- package/tests/fanout.test.ts +51 -0
- package/tests/fanout_spool.test.ts +96 -0
- package/tests/message_constraints.test.ts +7 -0
- package/tests/node-shim.d.ts +1 -0
- package/tests/record_builder.test.ts +32 -0
- package/tests/sequence_monotonic.test.ts +62 -0
- package/tests/sinks_file_ndjson.test.ts +53 -0
- package/tests/step1_compliance.test.ts +192 -0
- package/tools/test_results/generate-test-traceability.js +60 -0
- package/tools/test_results/normalize-test-results.js +57 -0
- package/tools/test_results/run-tests-then-prebuild.js +103 -0
- package/tools/test_results/test-case-map.json +9 -0
- package/tsconfig.json +31 -0
- package/validators/bootstrap/validate-repo-structure.ts +590 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# 🧪 LogSDK — Test Coverage Report
|
|
2
|
+
|
|
3
|
+
**Status:** Audit-Derived (Read-Only)
|
|
4
|
+
|
|
5
|
+
**Purpose:**
|
|
6
|
+
This document provides an explicit, audit-grade description of **what is tested** in the LogSDK repository. It reconstructs test intent and coverage from the existing test suite **without adding or modifying any tests**. The report is suitable for:
|
|
7
|
+
|
|
8
|
+
- RE-AUDIT / Freeze-gate validation
|
|
9
|
+
- Evidence submission
|
|
10
|
+
- Governance review
|
|
11
|
+
- Downstream trust assessment
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Scope & Methodology
|
|
16
|
+
|
|
17
|
+
**Scope:** `tests/` directory only
|
|
18
|
+
**Method:** Static inspection of test files and assertions
|
|
19
|
+
**Exclusions:** No behavioral inference beyond explicit test intent
|
|
20
|
+
|
|
21
|
+
This report:
|
|
22
|
+
- Maps each test file to **explicit guarantees**
|
|
23
|
+
- Aligns tests to **audit objectives** (determinism, evidence safety, contract fidelity)
|
|
24
|
+
- Clearly states **what is not tested** to avoid over-claiming
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Test Inventory
|
|
29
|
+
|
|
30
|
+
| Test File | Primary Concern |
|
|
31
|
+
|---------|----------------|
|
|
32
|
+
| `sequence_monotonic.test.ts` | Determinism & ordering |
|
|
33
|
+
| `record_builder.test.ts` | Step Record integrity |
|
|
34
|
+
| `fanout.test.ts` | Sink fan-out semantics |
|
|
35
|
+
| `fanout_spool.test.ts` | Degraded / emergency spool behavior |
|
|
36
|
+
| `message_constraints.test.ts` | Context & payload safety |
|
|
37
|
+
| `sinks_file_ndjson.test.ts` | Evidence persistence correctness |
|
|
38
|
+
| `step1_compliance.test.ts` | Contract compliance (baseline) |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Determinism & Ordering
|
|
43
|
+
|
|
44
|
+
### `sequence_monotonic.test.ts`
|
|
45
|
+
|
|
46
|
+
**Verifies:**
|
|
47
|
+
- Sequence numbering starts at the defined baseline
|
|
48
|
+
- Sequence increments strictly and monotonically
|
|
49
|
+
- No duplicate or skipped sequence values
|
|
50
|
+
- Ordering is preserved across rapid `step()` calls
|
|
51
|
+
|
|
52
|
+
**Audit Guarantees Proven:**
|
|
53
|
+
- Deterministic sequencing
|
|
54
|
+
- Replay-safe ordering
|
|
55
|
+
- No timing-based nondeterminism
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 4. Step Record Integrity
|
|
60
|
+
|
|
61
|
+
### `record_builder.test.ts`
|
|
62
|
+
|
|
63
|
+
**Verifies:**
|
|
64
|
+
- Canonical Step Record construction
|
|
65
|
+
- Presence of all required fields
|
|
66
|
+
- Controlled derivation of values
|
|
67
|
+
- Immutability after record creation
|
|
68
|
+
|
|
69
|
+
**Audit Guarantees Proven:**
|
|
70
|
+
- Canonical shape preservation
|
|
71
|
+
- No hidden mutation paths
|
|
72
|
+
- Stable inputs for hashing/serialization
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 5. Fan-Out Semantics
|
|
77
|
+
|
|
78
|
+
### `fanout.test.ts`
|
|
79
|
+
|
|
80
|
+
**Verifies:**
|
|
81
|
+
- Multiple sinks receive identical records
|
|
82
|
+
- Sink failure isolation
|
|
83
|
+
- Core execution is not aborted by non-authoritative sink failures
|
|
84
|
+
- Correct outcome classification
|
|
85
|
+
|
|
86
|
+
**Audit Guarantees Proven:**
|
|
87
|
+
- Fan-out does not mutate evidence
|
|
88
|
+
- Sink isolation correctness
|
|
89
|
+
- No silent failure masking
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 6. Degraded Mode & Emergency Spooling
|
|
94
|
+
|
|
95
|
+
### `fanout_spool.test.ts`
|
|
96
|
+
|
|
97
|
+
**Verifies:**
|
|
98
|
+
- Authoritative sink failure triggers fallback
|
|
99
|
+
- Evidence is preserved via emergency spool
|
|
100
|
+
- DEGRADED vs FAILED states are distinguished
|
|
101
|
+
- No silent evidence loss
|
|
102
|
+
|
|
103
|
+
**Audit Guarantees Proven:**
|
|
104
|
+
- Evidence durability under failure
|
|
105
|
+
- Correct degraded-mode semantics
|
|
106
|
+
- Compliance with prior audit remediation
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 7. Message & Context Safety
|
|
111
|
+
|
|
112
|
+
### `message_constraints.test.ts`
|
|
113
|
+
|
|
114
|
+
**Verifies:**
|
|
115
|
+
- Message size limits are enforced
|
|
116
|
+
- Invalid payloads are rejected deterministically
|
|
117
|
+
- Context constraints are strictly applied
|
|
118
|
+
- No implicit truncation occurs
|
|
119
|
+
|
|
120
|
+
**Audit Guarantees Proven:**
|
|
121
|
+
- LLM-context containment
|
|
122
|
+
- Deterministic rejection behavior
|
|
123
|
+
- Injection resistance
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 8. Evidence Persistence
|
|
128
|
+
|
|
129
|
+
### `sinks_file_ndjson.test.ts`
|
|
130
|
+
|
|
131
|
+
**Verifies:**
|
|
132
|
+
- Correct NDJSON output format
|
|
133
|
+
- One record per line
|
|
134
|
+
- No partial or corrupted writes
|
|
135
|
+
- Correct behavior across multiple writes
|
|
136
|
+
|
|
137
|
+
**Audit Guarantees Proven:**
|
|
138
|
+
- Evidence persistence correctness
|
|
139
|
+
- Replay-friendly storage format
|
|
140
|
+
- No corruption across steps
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 9. Contract Compliance
|
|
145
|
+
|
|
146
|
+
### `step1_compliance.test.ts`
|
|
147
|
+
|
|
148
|
+
**Verifies:**
|
|
149
|
+
- End-to-end compliance with Step Record v1
|
|
150
|
+
- Presence of all required baseline fields
|
|
151
|
+
- Absence of forbidden or extraneous fields
|
|
152
|
+
|
|
153
|
+
**Audit Guarantees Proven:**
|
|
154
|
+
- Contract fidelity
|
|
155
|
+
- No schema drift
|
|
156
|
+
- Downstream consumer safety
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 10. Explicit Non-Coverage (Declared)
|
|
161
|
+
|
|
162
|
+
The following are **explicitly not tested** (by design, not omission):
|
|
163
|
+
|
|
164
|
+
- Cross-process replay
|
|
165
|
+
- Multi-runtime adapters (Node.js is the defined runtime)
|
|
166
|
+
- Performance or throughput characteristics
|
|
167
|
+
- Long-running memory behavior
|
|
168
|
+
- External sink integrations beyond file/stdout
|
|
169
|
+
|
|
170
|
+
These are **out of scope** for the current audit and freeze-gate.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 11. Coverage-to-Audit Mapping
|
|
175
|
+
|
|
176
|
+
| Audit Dimension | Coverage |
|
|
177
|
+
|----------------|----------|
|
|
178
|
+
| Determinism | ✅ |
|
|
179
|
+
| Ordering | ✅ |
|
|
180
|
+
| Evidence durability | ✅ |
|
|
181
|
+
| Degraded vs failed semantics | ✅ |
|
|
182
|
+
| Step Record integrity | ✅ |
|
|
183
|
+
| Downstream compatibility | ✅ |
|
|
184
|
+
| LLM context safety | ✅ |
|
|
185
|
+
| Authority discipline | ✅ |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 12. Audit Statement
|
|
190
|
+
|
|
191
|
+
> **The existing LogSDK test suite provides deterministic, contract-level coverage of sequencing, record integrity, fan-out semantics, degraded evidence handling, and context safety sufficient for freeze-gate validation.**
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
**Document Classification:** Non-authoritative audit evidence
|
|
196
|
+
**Generated From:** Existing tests only
|
|
197
|
+
**Change Policy:** Update only if tests change
|
|
198
|
+
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# 🧩 LogSDK RE-AUDIT PROMPT (Freeze-Gate Validation)
|
|
2
|
+
|
|
3
|
+
## ROLE
|
|
4
|
+
|
|
5
|
+
You are an **independent senior logging and distributed-systems auditor**
|
|
6
|
+
with **20+ years of experience** across:
|
|
7
|
+
|
|
8
|
+
* long-running processes
|
|
9
|
+
* distributed pipelines
|
|
10
|
+
* HTTP / request-response systems
|
|
11
|
+
* content and publishing systems
|
|
12
|
+
* multi-runtime environments (Node.js, Python, CLI, workers)
|
|
13
|
+
* audit-, evidence-, and compliance-critical platforms
|
|
14
|
+
|
|
15
|
+
You are performing a **FORMAL RE-AUDIT** of **LogSDK**
|
|
16
|
+
**after remediation of previously identified audit findings**.
|
|
17
|
+
|
|
18
|
+
You are **NOT a contributor**.
|
|
19
|
+
You have **NO authority** to redesign, extend, or optimize the SDK.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## MODE
|
|
24
|
+
|
|
25
|
+
**READ-ONLY RE-AUDIT / FREEZE-GATE VALIDATION**
|
|
26
|
+
|
|
27
|
+
This is a **verification exercise**, not a design review.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## NON-NEGOTIABLE CONSTRAINTS
|
|
32
|
+
|
|
33
|
+
### ❌ YOU MUST NOT
|
|
34
|
+
|
|
35
|
+
* redesign or re-architect the SDK
|
|
36
|
+
* add or suggest new APIs
|
|
37
|
+
* introduce configuration options
|
|
38
|
+
* refactor internal structure
|
|
39
|
+
* optimize for performance or ergonomics
|
|
40
|
+
* weaken governance, determinism, or evidence guarantees
|
|
41
|
+
* propose future improvements beyond audit findings
|
|
42
|
+
|
|
43
|
+
### ✅ YOU MUST
|
|
44
|
+
|
|
45
|
+
* validate remediation **against prior audit findings**
|
|
46
|
+
* detect **regressions or newly introduced violations**
|
|
47
|
+
* verify **determinism, immutability, and evidence safety**
|
|
48
|
+
* confirm **downstream compatibility is preserved**
|
|
49
|
+
* issue **explicit PASS / FAIL / RISK findings**
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## AUDIT SCOPE (MANDATORY)
|
|
54
|
+
|
|
55
|
+
You MUST re-scan the **entire LogSDK repository**, including but not limited to:
|
|
56
|
+
|
|
57
|
+
* core SDK logic
|
|
58
|
+
* sequence and clock generation
|
|
59
|
+
* step record construction
|
|
60
|
+
* fan-out and sink handling
|
|
61
|
+
* emergency spool / degraded-mode logic (if present)
|
|
62
|
+
* validation and guard layers
|
|
63
|
+
* tests and fixtures
|
|
64
|
+
* documentation and specifications
|
|
65
|
+
|
|
66
|
+
⚠️ **NO FILE, DIRECTORY, OR PATH MAY BE SKIPPED**
|
|
67
|
+
⚠️ **ASSUME NOTHING IS CORRECT BY DEFAULT**
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## AUTHORITATIVE BASELINE (HARD CONTRACTS)
|
|
72
|
+
|
|
73
|
+
All validation must be performed strictly against these **non-negotiable references**:
|
|
74
|
+
|
|
75
|
+
1. `governance/contracts/step-record.v1.md`
|
|
76
|
+
2. **LogSDK Functional Specification v1.1**
|
|
77
|
+
3. `/audit/logsdk_deep_audit_report.json` (PREVIOUS AUDIT FINDINGS)
|
|
78
|
+
|
|
79
|
+
Your sole task is to verify **alignment AFTER remediation**.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## RE-AUDIT OBJECTIVES
|
|
84
|
+
|
|
85
|
+
**ALL OBJECTIVES ARE REQUIRED FOR A PASS**
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### 1️⃣ Verification of Prior Audit Issue Resolution
|
|
90
|
+
|
|
91
|
+
You MUST explicitly verify the following previously identified issues:
|
|
92
|
+
|
|
93
|
+
* Sequence numbering **starts at `0`**
|
|
94
|
+
* Sequence increments **strictly and monotonically**
|
|
95
|
+
* `monotonic_time` is **strictly increasing**, even across rapid successive `step()` calls
|
|
96
|
+
* Fan-out semantics explicitly support:
|
|
97
|
+
|
|
98
|
+
* **OK** — all sinks succeed
|
|
99
|
+
* **DEGRADED** — authoritative evidence preserved, secondary sinks fail
|
|
100
|
+
* **FAILED** — authoritative evidence lost
|
|
101
|
+
* Authoritative sink failures **do not silently drop evidence**
|
|
102
|
+
* Emergency spool / fallback path exists and is correctly engaged (if specified)
|
|
103
|
+
* Node.js runtime assumptions are **explicitly documented** and not implicit
|
|
104
|
+
|
|
105
|
+
Each prior issue MUST be classified as:
|
|
106
|
+
|
|
107
|
+
* **RESOLVED**
|
|
108
|
+
* **PARTIALLY RESOLVED**
|
|
109
|
+
* **NOT RESOLVED**
|
|
110
|
+
|
|
111
|
+
No issue may be skipped or implicitly assumed resolved.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### 2️⃣ Regression Detection (Critical)
|
|
116
|
+
|
|
117
|
+
You MUST verify that remediation has **NOT introduced** any of the following:
|
|
118
|
+
|
|
119
|
+
* changes to the **Canonical Step Record shape**
|
|
120
|
+
* new fields, removed fields, or reordered fields
|
|
121
|
+
* changes to message or context constraints
|
|
122
|
+
* changes to the public API (`log.step(message: string)`)
|
|
123
|
+
* implicit per-step context injection
|
|
124
|
+
* weakened immutability or hashing guarantees
|
|
125
|
+
* altered downstream usage semantics
|
|
126
|
+
* behavioral drift observable by existing consumers
|
|
127
|
+
|
|
128
|
+
Any regression must be explicitly called out.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### 3️⃣ Determinism & Replay Safety (Re-Validation)
|
|
133
|
+
|
|
134
|
+
You MUST confirm that:
|
|
135
|
+
|
|
136
|
+
* canonical serialization is unchanged
|
|
137
|
+
* record hashing is stable and deterministic
|
|
138
|
+
* replay of identical executions produces **equivalent evidence**
|
|
139
|
+
* ordering guarantees are preserved under all execution modes
|
|
140
|
+
* no hidden sources of nondeterminism were introduced
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### 4️⃣ Downstream Compatibility Verification
|
|
145
|
+
|
|
146
|
+
You MUST explicitly validate that existing downstream usage patterns remain valid, including but not limited to:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
log.step("schema apply completed: X");
|
|
150
|
+
log.step("validation failed: missing field Y");
|
|
151
|
+
log.step(`write completed for ${entityId}`);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Specifically confirm that:
|
|
155
|
+
|
|
156
|
+
* no caller code changes are required
|
|
157
|
+
* message-only invocation remains valid
|
|
158
|
+
* no new required parameters exist
|
|
159
|
+
* existing sinks and consumers continue to function unchanged
|
|
160
|
+
* no implicit behavioral coupling was introduced
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### 5️⃣ Documentation Alignment Check
|
|
165
|
+
|
|
166
|
+
You MUST verify that:
|
|
167
|
+
|
|
168
|
+
* documentation accurately reflects current behavior
|
|
169
|
+
* no undocumented behavior changes exist
|
|
170
|
+
* remediation changes are correctly described (if applicable)
|
|
171
|
+
* no misleading or stale guarantees remain
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## REQUIRED OUTPUT FORMAT
|
|
176
|
+
|
|
177
|
+
Your response MUST be structured as follows:
|
|
178
|
+
|
|
179
|
+
1. **Executive Re-Audit Summary**
|
|
180
|
+
2. **Prior Findings Resolution Table** (Issue → Status → Evidence)
|
|
181
|
+
3. **Regression Scan Results**
|
|
182
|
+
4. **Determinism & Replay Validation**
|
|
183
|
+
5. **Downstream Compatibility Assessment**
|
|
184
|
+
6. **Documentation Alignment Findings**
|
|
185
|
+
7. **Final Freeze-Gate Verdict**
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## FINAL VERDICT RULE
|
|
190
|
+
|
|
191
|
+
A **FREEZE-ELIGIBLE PASS** may be issued **ONLY IF**:
|
|
192
|
+
|
|
193
|
+
* all prior issues are RESOLVED or ACCEPTABLY PARTIAL (with justification)
|
|
194
|
+
* zero regressions are detected
|
|
195
|
+
* determinism and replay safety are confirmed
|
|
196
|
+
* downstream compatibility is preserved
|
|
197
|
+
|
|
198
|
+
Otherwise, the verdict MUST be **FAIL** or **CONDITIONAL FAIL** with explicit reasons.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### 🧠 Closing Principle (Implicit but Binding)
|
|
203
|
+
|
|
204
|
+
> This audit verifies **trustworthiness, not elegance**.
|
|
205
|
+
> Stability, determinism, and evidence safety override all other considerations.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
If you want, next I can:
|
|
210
|
+
|
|
211
|
+
* convert this into a **CI-enforced audit harness**
|
|
212
|
+
* align it with **UnifyPlane CIA readiness validation**
|
|
213
|
+
* or create a **multi-repo standardized audit template**
|
|
214
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unifyplane/logsdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"verify": "node tools/test_results/run-tests-then-prebuild.js",
|
|
9
|
+
"build": "npm run verify && npm run build:impl",
|
|
10
|
+
"build:impl": "tsc -p tsconfig.json"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"crypto": "^1.0.1"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.3.3",
|
|
17
|
+
"vitest": "^1.2.0",
|
|
18
|
+
"ts-node": "^10.9.2"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"require": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let lastMonotonic = 0;
|
|
2
|
+
const hasPerformance =
|
|
3
|
+
typeof performance !== "undefined" && typeof performance.now === "function";
|
|
4
|
+
const epochStart = Date.now();
|
|
5
|
+
|
|
6
|
+
export function nowUtcIso(): string {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function monotonicNow(): number {
|
|
11
|
+
let current: number;
|
|
12
|
+
|
|
13
|
+
if (hasPerformance) {
|
|
14
|
+
current = epochStart + performance.now();
|
|
15
|
+
} else {
|
|
16
|
+
current = Date.now();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (current <= lastMonotonic) {
|
|
20
|
+
current = lastMonotonic + 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
lastMonotonic = current;
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/// <reference path="../crypto-shim.d.ts" />
|
|
2
|
+
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
|
|
5
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
6
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
7
|
+
|
|
8
|
+
type ContextState = {
|
|
9
|
+
value: JsonValue;
|
|
10
|
+
hash: string;
|
|
11
|
+
version: string;
|
|
12
|
+
initialized: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CONTEXT_VERSION = "log.context.v1";
|
|
16
|
+
|
|
17
|
+
const state: ContextState = {
|
|
18
|
+
value: {},
|
|
19
|
+
hash: "",
|
|
20
|
+
version: DEFAULT_CONTEXT_VERSION,
|
|
21
|
+
initialized: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function initContext(
|
|
25
|
+
context: unknown,
|
|
26
|
+
version: string = DEFAULT_CONTEXT_VERSION
|
|
27
|
+
): void {
|
|
28
|
+
if (state.initialized) {
|
|
29
|
+
throw new Error("Context already initialized");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!context || typeof context !== "object" || Array.isArray(context)) {
|
|
33
|
+
throw new Error("Context must be a non-array object");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalized = normalizeJsonValue(context, "$");
|
|
37
|
+
const canonical = stableStringify(normalized);
|
|
38
|
+
const hash = createHash("sha256").update(canonical).digest("hex");
|
|
39
|
+
|
|
40
|
+
state.value = deepFreeze(normalized);
|
|
41
|
+
state.hash = hash;
|
|
42
|
+
state.version = version;
|
|
43
|
+
state.initialized = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getContextHash(): string {
|
|
47
|
+
if (!state.initialized) {
|
|
48
|
+
throw new Error("Context not initialized");
|
|
49
|
+
}
|
|
50
|
+
return state.hash;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getContextVersion(): string {
|
|
54
|
+
if (!state.initialized) {
|
|
55
|
+
throw new Error("Context not initialized");
|
|
56
|
+
}
|
|
57
|
+
return state.version;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resetContextForTests(): void {
|
|
61
|
+
state.value = {};
|
|
62
|
+
state.hash = "";
|
|
63
|
+
state.version = DEFAULT_CONTEXT_VERSION;
|
|
64
|
+
state.initialized = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function deepFreeze<T>(value: T): T {
|
|
68
|
+
if (!value || typeof value !== "object") {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Object.isFrozen(value)) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Object.freeze(value);
|
|
77
|
+
|
|
78
|
+
for (const key of Object.keys(value as object)) {
|
|
79
|
+
const child = (value as Record<string, unknown>)[key];
|
|
80
|
+
deepFreeze(child);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
for (const item of value) {
|
|
85
|
+
deepFreeze(item);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeJsonValue(value: unknown, path: string): JsonValue {
|
|
93
|
+
if (
|
|
94
|
+
value === null ||
|
|
95
|
+
typeof value === "string" ||
|
|
96
|
+
typeof value === "number" ||
|
|
97
|
+
typeof value === "boolean"
|
|
98
|
+
) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof value === "bigint") {
|
|
103
|
+
return value.toString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (value instanceof Date) {
|
|
107
|
+
return value.toISOString();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return value.map((item, index) =>
|
|
112
|
+
normalizeJsonValue(item, `${path}[${index}]`)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof value === "object") {
|
|
117
|
+
const proto = Object.getPrototypeOf(value);
|
|
118
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
119
|
+
throw new Error(`Unsupported object at ${path}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const obj = value as Record<string, unknown>;
|
|
123
|
+
const result: Record<string, JsonValue> = {};
|
|
124
|
+
const keys = Object.keys(obj).sort();
|
|
125
|
+
|
|
126
|
+
for (const key of keys) {
|
|
127
|
+
const next = obj[key];
|
|
128
|
+
if (next === undefined) {
|
|
129
|
+
throw new Error(`Undefined value at ${path}.${key}`);
|
|
130
|
+
}
|
|
131
|
+
result[key] = normalizeJsonValue(next, `${path}.${key}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(`Unsupported type at ${path}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stableStringify(value: JsonValue): string {
|
|
141
|
+
return JSON.stringify(value);
|
|
142
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SinkEntry, StepRecord } from "../sinks/sink_types";
|
|
2
|
+
import { assertHasAuthoritativeSink } from "../validate/schema_guard";
|
|
3
|
+
import { writeEmergencySpool } from "./spool";
|
|
4
|
+
|
|
5
|
+
export type FanoutOutcome = "OK" | "DEGRADED" | "FAILED";
|
|
6
|
+
|
|
7
|
+
export async function fanout(
|
|
8
|
+
record: StepRecord,
|
|
9
|
+
sinks: ReadonlyArray<SinkEntry>
|
|
10
|
+
): Promise<FanoutOutcome> {
|
|
11
|
+
assertHasAuthoritativeSink(sinks);
|
|
12
|
+
const frozenRecord = Object.isFrozen(record) ? record : Object.freeze(record);
|
|
13
|
+
let outcome: FanoutOutcome = "OK";
|
|
14
|
+
let spooled = false;
|
|
15
|
+
|
|
16
|
+
for (const entry of sinks) {
|
|
17
|
+
try {
|
|
18
|
+
await entry.sink.emit(frozenRecord);
|
|
19
|
+
} catch {
|
|
20
|
+
if (entry.sinkClass === "authoritative") {
|
|
21
|
+
if (!spooled) {
|
|
22
|
+
try {
|
|
23
|
+
await writeEmergencySpool(frozenRecord);
|
|
24
|
+
outcome = "DEGRADED";
|
|
25
|
+
spooled = true;
|
|
26
|
+
} catch {
|
|
27
|
+
outcome = "FAILED";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (outcome === "FAILED") {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return outcome;
|
|
38
|
+
}
|
package/src/core/ids.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { monotonicNow } from "./clock";
|
|
2
|
+
|
|
3
|
+
export interface IdGenerator {
|
|
4
|
+
nextRecordId(): string;
|
|
5
|
+
nextSequence(): number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const processStart = Date.now().toString(36);
|
|
9
|
+
let instanceCounter = 0;
|
|
10
|
+
|
|
11
|
+
function sanitizeTag(tag: string): string {
|
|
12
|
+
return tag.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 32);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createIdGenerator(instanceTag?: string): IdGenerator {
|
|
16
|
+
const instanceId = ++instanceCounter;
|
|
17
|
+
let sequence = 0;
|
|
18
|
+
let localCounter = 0;
|
|
19
|
+
const tag = instanceTag ? sanitizeTag(instanceTag) : "";
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
nextSequence() {
|
|
23
|
+
const current = sequence;
|
|
24
|
+
sequence += 1;
|
|
25
|
+
return current;
|
|
26
|
+
},
|
|
27
|
+
nextRecordId() {
|
|
28
|
+
localCounter += 1;
|
|
29
|
+
const timePart = Math.floor(monotonicNow()).toString(36);
|
|
30
|
+
const counterPart = localCounter.toString(36);
|
|
31
|
+
const tagPart = tag ? `_${tag}` : "";
|
|
32
|
+
return `rec_${processStart}_${instanceId}${tagPart}_${timePart}_${counterPart}`;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|