@paths.design/caws-cli 9.3.2 → 10.1.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/README.md +71 -32
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/archive.js +67 -28
- package/dist/commands/burnup.js +20 -11
- package/dist/commands/diagnose.js +34 -22
- package/dist/commands/evaluate.js +41 -15
- package/dist/commands/gates.js +149 -0
- package/dist/commands/init.js +150 -19
- package/dist/commands/iterate.js +81 -4
- package/dist/commands/parallel.js +4 -0
- package/dist/commands/plan.js +9 -19
- package/dist/commands/provenance.js +53 -17
- package/dist/commands/quality-monitor.js +64 -45
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +74 -0
- package/dist/commands/specs.js +381 -45
- package/dist/commands/status.js +117 -9
- package/dist/commands/templates.js +0 -8
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +70 -6
- package/dist/commands/verify-acs.js +48 -76
- package/dist/commands/waivers.js +212 -13
- package/dist/commands/worktree.js +131 -26
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +121 -0
- package/dist/gates/feedback.js +260 -0
- package/dist/gates/format.js +179 -0
- package/dist/gates/god-object.js +117 -0
- package/dist/gates/pipeline.js +167 -0
- package/dist/gates/scope-boundary.js +93 -0
- package/dist/gates/spec-completeness.js +109 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +157 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +51 -17
- package/dist/scaffold/claude-hooks.js +24 -1
- package/dist/scaffold/git-hooks.js +45 -102
- package/dist/scaffold/index.js +4 -3
- package/dist/session/session-manager.js +105 -14
- package/dist/sidecars/index.js +33 -0
- package/dist/sidecars/listeners.js +40 -0
- package/dist/sidecars/provenance-summary.js +238 -0
- package/dist/sidecars/quality-gaps.js +258 -0
- package/dist/sidecars/schema.js +149 -0
- package/dist/sidecars/spec-drift.js +151 -0
- package/dist/sidecars/waiver-draft.js +176 -0
- package/dist/templates/.caws/schemas/policy.schema.json +112 -0
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
- package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/audit.sh +0 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/dist/templates/.claude/hooks/classify_command.py +592 -0
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
- package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/dist/templates/.claude/hooks/session-log.sh +76 -3
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/dist/templates/.claude/settings.json +31 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/dist/templates/.cursor/hooks/session-log.sh +924 -0
- package/dist/templates/.cursor/hooks.json +25 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/dist/templates/.github/copilot-instructions.md +5 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/dist/templates/.junie/guidelines.md +2 -2
- package/dist/templates/.vscode/settings.json +3 -1
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/dist/templates/CLAUDE.md +77 -8
- package/dist/templates/agents.md +50 -9
- package/dist/templates/docs/README.md +8 -7
- package/dist/templates/scripts/new_feature.sh +80 -0
- package/dist/test-analysis.js +43 -30
- package/dist/tool-loader.js +1 -1
- package/dist/utils/agent-session.js +202 -0
- package/dist/utils/detection.js +8 -2
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/finalization.js +7 -6
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/lifecycle-events.js +94 -0
- package/dist/utils/quality-gates-utils.js +29 -44
- package/dist/utils/schema-validator.js +50 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +530 -0
- package/dist/validation/spec-validation.js +191 -31
- package/dist/waivers-manager.js +144 -6
- package/dist/worktree/worktree-manager.js +598 -95
- package/package.json +9 -8
- package/templates/.caws/schemas/policy.schema.json +112 -0
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +96 -20
- package/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +10 -4
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/templates/.claude/hooks/classify_command.py +592 -0
- package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +136 -55
- package/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/templates/.claude/hooks/session-log.sh +76 -3
- package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/templates/.claude/hooks/test_classify_command.py +370 -0
- package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/templates/.claude/settings.json +31 -0
- package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/templates/.cursor/hooks/session-log.sh +924 -0
- package/templates/.cursor/hooks.json +25 -0
- package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/templates/.github/copilot-instructions.md +5 -5
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/templates/.junie/guidelines.md +2 -2
- package/templates/.vscode/settings.json +3 -1
- package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/templates/CLAUDE.md +77 -8
- package/templates/{AGENTS.md → agents.md} +50 -9
- package/templates/docs/README.md +8 -7
- package/templates/scripts/new_feature.sh +80 -0
- package/dist/budget-derivation.d.ts +0 -74
- package/dist/budget-derivation.d.ts.map +0 -1
- package/dist/cicd-optimizer.d.ts +0 -142
- package/dist/cicd-optimizer.d.ts.map +0 -1
- package/dist/commands/archive.d.ts +0 -51
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/burnup.d.ts +0 -6
- package/dist/commands/burnup.d.ts.map +0 -1
- package/dist/commands/diagnose.d.ts +0 -52
- package/dist/commands/diagnose.d.ts.map +0 -1
- package/dist/commands/evaluate.d.ts +0 -8
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/iterate.d.ts +0 -8
- package/dist/commands/iterate.d.ts.map +0 -1
- package/dist/commands/mode.d.ts +0 -25
- package/dist/commands/mode.d.ts.map +0 -1
- package/dist/commands/parallel.d.ts +0 -7
- package/dist/commands/parallel.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -49
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/commands/provenance.d.ts +0 -32
- package/dist/commands/provenance.d.ts.map +0 -1
- package/dist/commands/quality-gates.d.ts +0 -6
- package/dist/commands/quality-gates.d.ts.map +0 -1
- package/dist/commands/quality-gates.js +0 -444
- package/dist/commands/quality-monitor.d.ts +0 -17
- package/dist/commands/quality-monitor.d.ts.map +0 -1
- package/dist/commands/session.d.ts +0 -7
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/specs.d.ts +0 -77
- package/dist/commands/specs.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -44
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/templates.d.ts +0 -74
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/tool.d.ts +0 -13
- package/dist/commands/tool.d.ts.map +0 -1
- package/dist/commands/troubleshoot.d.ts +0 -8
- package/dist/commands/troubleshoot.d.ts.map +0 -1
- package/dist/commands/troubleshoot.js +0 -104
- package/dist/commands/tutorial.d.ts +0 -55
- package/dist/commands/tutorial.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -15
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/waivers.d.ts +0 -8
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/workflow.d.ts +0 -85
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/commands/worktree.d.ts +0 -7
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -29
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/lite-scope.d.ts +0 -33
- package/dist/config/lite-scope.d.ts.map +0 -1
- package/dist/config/modes.d.ts +0 -264
- package/dist/config/modes.d.ts.map +0 -1
- package/dist/constants/spec-types.d.ts +0 -93
- package/dist/constants/spec-types.d.ts.map +0 -1
- package/dist/error-handler.d.ts +0 -151
- package/dist/error-handler.d.ts.map +0 -1
- package/dist/generators/jest-config-generator.d.ts +0 -32
- package/dist/generators/jest-config-generator.d.ts.map +0 -1
- package/dist/generators/jest-config.d.ts +0 -32
- package/dist/generators/jest-config.d.ts.map +0 -1
- package/dist/generators/jest-config.js +0 -242
- package/dist/generators/working-spec.d.ts +0 -13
- package/dist/generators/working-spec.d.ts.map +0 -1
- package/dist/index-new.d.ts +0 -5
- package/dist/index-new.d.ts.map +0 -1
- package/dist/index-new.js +0 -317
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.backup +0 -4711
- package/dist/minimal-cli.d.ts +0 -3
- package/dist/minimal-cli.d.ts.map +0 -1
- package/dist/parallel/parallel-manager.d.ts +0 -67
- package/dist/parallel/parallel-manager.d.ts.map +0 -1
- package/dist/policy/PolicyManager.d.ts +0 -104
- package/dist/policy/PolicyManager.d.ts.map +0 -1
- package/dist/scaffold/claude-hooks.d.ts +0 -28
- package/dist/scaffold/claude-hooks.d.ts.map +0 -1
- package/dist/scaffold/cursor-hooks.d.ts +0 -7
- package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
- package/dist/scaffold/git-hooks.d.ts +0 -38
- package/dist/scaffold/git-hooks.d.ts.map +0 -1
- package/dist/scaffold/index.d.ts +0 -17
- package/dist/scaffold/index.d.ts.map +0 -1
- package/dist/session/session-manager.d.ts +0 -94
- package/dist/session/session-manager.d.ts.map +0 -1
- package/dist/spec/SpecFileManager.d.ts +0 -146
- package/dist/spec/SpecFileManager.d.ts.map +0 -1
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
- package/dist/templates/.github/copilot/instructions.md +0 -311
- package/dist/test-analysis.d.ts +0 -231
- package/dist/test-analysis.d.ts.map +0 -1
- package/dist/tool-interface.d.ts +0 -236
- package/dist/tool-interface.d.ts.map +0 -1
- package/dist/tool-loader.d.ts +0 -77
- package/dist/tool-loader.d.ts.map +0 -1
- package/dist/tool-validator.d.ts +0 -72
- package/dist/tool-validator.d.ts.map +0 -1
- package/dist/utils/async-utils.d.ts +0 -73
- package/dist/utils/async-utils.d.ts.map +0 -1
- package/dist/utils/command-wrapper.d.ts +0 -66
- package/dist/utils/command-wrapper.d.ts.map +0 -1
- package/dist/utils/detection.d.ts +0 -14
- package/dist/utils/detection.d.ts.map +0 -1
- package/dist/utils/error-categories.d.ts +0 -52
- package/dist/utils/error-categories.d.ts.map +0 -1
- package/dist/utils/finalization.d.ts +0 -17
- package/dist/utils/finalization.d.ts.map +0 -1
- package/dist/utils/git-lock.d.ts +0 -13
- package/dist/utils/git-lock.d.ts.map +0 -1
- package/dist/utils/gitignore-updater.d.ts +0 -39
- package/dist/utils/gitignore-updater.d.ts.map +0 -1
- package/dist/utils/ide-detection.d.ts +0 -89
- package/dist/utils/ide-detection.d.ts.map +0 -1
- package/dist/utils/project-analysis.d.ts +0 -34
- package/dist/utils/project-analysis.d.ts.map +0 -1
- package/dist/utils/promise-utils.d.ts +0 -30
- package/dist/utils/promise-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates-utils.d.ts +0 -49
- package/dist/utils/quality-gates-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates.d.ts +0 -49
- package/dist/utils/quality-gates.d.ts.map +0 -1
- package/dist/utils/quality-gates.js +0 -402
- package/dist/utils/spec-resolver.d.ts +0 -80
- package/dist/utils/spec-resolver.d.ts.map +0 -1
- package/dist/utils/typescript-detector.d.ts +0 -66
- package/dist/utils/typescript-detector.d.ts.map +0 -1
- package/dist/utils/yaml-validation.d.ts +0 -32
- package/dist/utils/yaml-validation.d.ts.map +0 -1
- package/dist/validation/spec-validation.d.ts +0 -43
- package/dist/validation/spec-validation.d.ts.map +0 -1
- package/dist/waivers-manager.d.ts +0 -167
- package/dist/waivers-manager.d.ts.map +0 -1
- package/dist/worktree/worktree-manager.d.ts +0 -54
- package/dist/worktree/worktree-manager.d.ts.map +0 -1
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Event Log — append-only provenance surface
|
|
3
|
+
*
|
|
4
|
+
* CAWS events are written once to `.caws/events.jsonl` and never rewritten.
|
|
5
|
+
* Every other view (per-spec state, session registry, provenance chain) is
|
|
6
|
+
* a pure function of this log. See docs/internal/EVENTS_LOG_MIGRATION.md
|
|
7
|
+
* for the full design.
|
|
8
|
+
*
|
|
9
|
+
* Contract highlights:
|
|
10
|
+
* - Append-only. Readers tolerate partial last lines.
|
|
11
|
+
* - Hash-chained. Each event carries prev_hash and event_hash (sha256).
|
|
12
|
+
* - Fail-loud. Events missing a required spec_id throw; no silent writes.
|
|
13
|
+
* - Cross-platform file lock via `fs.openSync(lockPath, 'wx')` sentinel.
|
|
14
|
+
*
|
|
15
|
+
* This file ships as Phase 1 (dual-write). State-layer writes in
|
|
16
|
+
* working-state.js continue unchanged; this log is additive.
|
|
17
|
+
*
|
|
18
|
+
* @author @darianrosebrook
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Constants
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const EVENTS_FILE = '.caws/events.jsonl';
|
|
30
|
+
const LOCK_SUFFIX = '.lock';
|
|
31
|
+
const HASH_DOMAIN = 'caws.events.v1';
|
|
32
|
+
const LOCK_RETRY_MS = 20;
|
|
33
|
+
const LOCK_RETRY_MAX = 50; // ~1s total
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Events that require a spec_id in their payload. appendEvent throws if
|
|
37
|
+
* spec_id is missing for any event listed here. This is the fence that
|
|
38
|
+
* prevents the `.caws/state/undefined.json` bug class.
|
|
39
|
+
*/
|
|
40
|
+
const REQUIRES_SPEC_ID = new Set([
|
|
41
|
+
'validation_completed',
|
|
42
|
+
'evaluation_completed',
|
|
43
|
+
'verify_acs_completed',
|
|
44
|
+
'gates_evaluated',
|
|
45
|
+
'spec_created',
|
|
46
|
+
'spec_updated',
|
|
47
|
+
'spec_closed',
|
|
48
|
+
'spec_archived',
|
|
49
|
+
'spec_deleted',
|
|
50
|
+
'spec_drift_detected',
|
|
51
|
+
'waiver_applied',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Events that optionally carry a spec_id. These are allowed to omit it.
|
|
56
|
+
* Any event not in REQUIRES_SPEC_ID and not in OPTIONAL_SPEC_ID is
|
|
57
|
+
* treated as an unknown event type — appendEvent will still write it,
|
|
58
|
+
* but it's recorded as-is without spec_id validation.
|
|
59
|
+
*/
|
|
60
|
+
const OPTIONAL_SPEC_ID = new Set([
|
|
61
|
+
'session_started',
|
|
62
|
+
'session_ended',
|
|
63
|
+
'commit_made',
|
|
64
|
+
'branch_switched',
|
|
65
|
+
'worktree_created',
|
|
66
|
+
'worktree_merged',
|
|
67
|
+
'worktree_destroyed',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Helpers
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the absolute path to `.caws/events.jsonl` for a project root.
|
|
76
|
+
* @param {string} projectRoot
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function getEventsPath(projectRoot) {
|
|
80
|
+
return path.join(projectRoot, EVENTS_FILE);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Canonicalize an event object for hashing. Keys are sorted alphabetically
|
|
85
|
+
* and serialized with no whitespace, so two structurally-equivalent events
|
|
86
|
+
* always produce the same hash regardless of key insertion order.
|
|
87
|
+
*
|
|
88
|
+
* This is a deliberate subset of RFC 8785 (JCS) — sufficient for our flat
|
|
89
|
+
* event shape but not a full implementation.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} obj
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
function canonicalJson(obj) {
|
|
95
|
+
if (obj === null) return 'null';
|
|
96
|
+
if (typeof obj === 'number') {
|
|
97
|
+
if (!Number.isFinite(obj)) {
|
|
98
|
+
throw new Error(`canonicalJson: non-finite number ${obj}`);
|
|
99
|
+
}
|
|
100
|
+
return JSON.stringify(obj);
|
|
101
|
+
}
|
|
102
|
+
if (typeof obj === 'string') return JSON.stringify(obj);
|
|
103
|
+
if (typeof obj === 'boolean') return obj ? 'true' : 'false';
|
|
104
|
+
if (Array.isArray(obj)) {
|
|
105
|
+
return '[' + obj.map(canonicalJson).join(',') + ']';
|
|
106
|
+
}
|
|
107
|
+
if (typeof obj === 'object') {
|
|
108
|
+
const keys = Object.keys(obj).sort();
|
|
109
|
+
const parts = keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(obj[k]));
|
|
110
|
+
return '{' + parts.join(',') + '}';
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`canonicalJson: unsupported type ${typeof obj}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compute sha256 of the domain-separated canonical JSON of an event.
|
|
117
|
+
* The `event_hash` field, if present on the input, is stripped before
|
|
118
|
+
* hashing so the hash can be stored back on the event itself.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} event
|
|
121
|
+
* @returns {string} "sha256:<hex>"
|
|
122
|
+
*/
|
|
123
|
+
function computeEventHash(event) {
|
|
124
|
+
const withoutHash = { ...event };
|
|
125
|
+
delete withoutHash.event_hash;
|
|
126
|
+
const canonical = canonicalJson(withoutHash);
|
|
127
|
+
const hash = crypto
|
|
128
|
+
.createHash('sha256')
|
|
129
|
+
.update(HASH_DOMAIN)
|
|
130
|
+
.update('\x00')
|
|
131
|
+
.update(canonical)
|
|
132
|
+
.digest('hex');
|
|
133
|
+
return `sha256:${hash}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sleep for a number of milliseconds. Used by the async lock retry loop.
|
|
138
|
+
* @param {number} ms
|
|
139
|
+
* @returns {Promise<void>}
|
|
140
|
+
*/
|
|
141
|
+
function sleep(ms) {
|
|
142
|
+
return new Promise((resolve) => { globalThis.setTimeout(resolve, ms); });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Synchronously sleep for a number of milliseconds. Node has no built-in
|
|
147
|
+
* sync sleep; `Atomics.wait` on a dummy Int32Array blocks the thread
|
|
148
|
+
* without CPU burn and is the least-ugly cross-platform option.
|
|
149
|
+
* Used by the sync lock retry loop for call sites that cannot await.
|
|
150
|
+
* @param {number} ms
|
|
151
|
+
*/
|
|
152
|
+
function sleepSync(ms) {
|
|
153
|
+
const buf = new Int32Array(new SharedArrayBuffer(4));
|
|
154
|
+
Atomics.wait(buf, 0, 0, ms);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Acquire an exclusive lock on the events file by creating a sentinel
|
|
159
|
+
* lockfile with the `wx` flag (fails atomically if the file already
|
|
160
|
+
* exists). Retries with a short backoff.
|
|
161
|
+
*
|
|
162
|
+
* Returns an opaque handle that must be passed to releaseLock.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} eventsPath
|
|
165
|
+
* @returns {Promise<{lockPath: string, fd: number}>}
|
|
166
|
+
*/
|
|
167
|
+
async function acquireLock(eventsPath) {
|
|
168
|
+
const lockPath = eventsPath + LOCK_SUFFIX;
|
|
169
|
+
const dir = path.dirname(eventsPath);
|
|
170
|
+
if (!fs.existsSync(dir)) {
|
|
171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (let attempt = 0; attempt < LOCK_RETRY_MAX; attempt++) {
|
|
175
|
+
try {
|
|
176
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
177
|
+
// Write pid to the lock so stale locks are diagnosable.
|
|
178
|
+
fs.writeSync(fd, String(process.pid));
|
|
179
|
+
return { lockPath, fd };
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err.code !== 'EEXIST') throw err;
|
|
182
|
+
// Check for stale lock (>30s old) — clean it up so one crashed
|
|
183
|
+
// writer doesn't block forever. This is still race-prone against
|
|
184
|
+
// another writer that just grabbed it; we accept that risk because
|
|
185
|
+
// the worst case is a rejected append, not corruption.
|
|
186
|
+
try {
|
|
187
|
+
const stat = fs.statSync(lockPath);
|
|
188
|
+
if (Date.now() - stat.mtimeMs > 30_000) {
|
|
189
|
+
fs.unlinkSync(lockPath);
|
|
190
|
+
continue; // retry immediately without backoff
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
/* stat failed — file may have been released; retry */
|
|
194
|
+
}
|
|
195
|
+
await sleep(LOCK_RETRY_MS);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
throw new Error(
|
|
199
|
+
`event-log: could not acquire lock on ${lockPath} after ${LOCK_RETRY_MAX * LOCK_RETRY_MS}ms — another writer may be stuck`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Synchronous lock acquirer. Same behavior as `acquireLock` but blocks
|
|
205
|
+
* the thread via `sleepSync`. Intended for call sites that cannot await
|
|
206
|
+
* (e.g. session-manager.startSession/endSession which are synchronous
|
|
207
|
+
* for historical reasons).
|
|
208
|
+
*
|
|
209
|
+
* @param {string} eventsPath
|
|
210
|
+
* @returns {{lockPath: string, fd: number}}
|
|
211
|
+
*/
|
|
212
|
+
function acquireLockSync(eventsPath) {
|
|
213
|
+
const lockPath = eventsPath + LOCK_SUFFIX;
|
|
214
|
+
const dir = path.dirname(eventsPath);
|
|
215
|
+
if (!fs.existsSync(dir)) {
|
|
216
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (let attempt = 0; attempt < LOCK_RETRY_MAX; attempt++) {
|
|
220
|
+
try {
|
|
221
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
222
|
+
fs.writeSync(fd, String(process.pid));
|
|
223
|
+
return { lockPath, fd };
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (err.code !== 'EEXIST') throw err;
|
|
226
|
+
try {
|
|
227
|
+
const stat = fs.statSync(lockPath);
|
|
228
|
+
if (Date.now() - stat.mtimeMs > 30_000) {
|
|
229
|
+
fs.unlinkSync(lockPath);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
/* stat failed — retry */
|
|
234
|
+
}
|
|
235
|
+
sleepSync(LOCK_RETRY_MS);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
throw new Error(
|
|
239
|
+
`event-log: could not acquire lock on ${lockPath} after ${LOCK_RETRY_MAX * LOCK_RETRY_MS}ms — another writer may be stuck`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Release a lock acquired via acquireLock. Never throws; a failed release
|
|
245
|
+
* is logged but not propagated, because the caller has already written.
|
|
246
|
+
*
|
|
247
|
+
* @param {{lockPath: string, fd: number}} handle
|
|
248
|
+
*/
|
|
249
|
+
function releaseLock(handle) {
|
|
250
|
+
try {
|
|
251
|
+
fs.closeSync(handle.fd);
|
|
252
|
+
} catch {
|
|
253
|
+
/* close failure is non-fatal; the unlink below is the real release */
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
fs.unlinkSync(handle.lockPath);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err.code !== 'ENOENT') {
|
|
259
|
+
// Surface unexpected release failures on stderr so they're diagnosable.
|
|
260
|
+
|
|
261
|
+
console.error(`event-log: failed to release lock ${handle.lockPath}: ${err.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Read the last non-empty line of a file without loading the whole file
|
|
268
|
+
* into memory. Used to find the tail of the event log for seq/prev_hash
|
|
269
|
+
* continuity.
|
|
270
|
+
*
|
|
271
|
+
* Returns `null` if the file does not exist or contains only whitespace.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} filePath
|
|
274
|
+
* @returns {string|null}
|
|
275
|
+
*/
|
|
276
|
+
function readLastLine(filePath) {
|
|
277
|
+
if (!fs.existsSync(filePath)) return null;
|
|
278
|
+
const stat = fs.statSync(filePath);
|
|
279
|
+
if (stat.size === 0) return null;
|
|
280
|
+
|
|
281
|
+
// Read from the end in chunks until we have at least one complete line.
|
|
282
|
+
const fd = fs.openSync(filePath, 'r');
|
|
283
|
+
try {
|
|
284
|
+
const chunkSize = 4096;
|
|
285
|
+
let buffer = Buffer.alloc(0);
|
|
286
|
+
let pos = stat.size;
|
|
287
|
+
while (pos > 0) {
|
|
288
|
+
const readSize = Math.min(chunkSize, pos);
|
|
289
|
+
pos -= readSize;
|
|
290
|
+
const chunk = Buffer.alloc(readSize);
|
|
291
|
+
fs.readSync(fd, chunk, 0, readSize, pos);
|
|
292
|
+
buffer = Buffer.concat([chunk, buffer]);
|
|
293
|
+
const text = buffer.toString('utf8');
|
|
294
|
+
const lines = text.split('\n').filter((l) => l.length > 0);
|
|
295
|
+
if (lines.length >= 2 || pos === 0) {
|
|
296
|
+
return lines[lines.length - 1] || null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
} finally {
|
|
301
|
+
fs.closeSync(fd);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Public API
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Append a single event to the project's event log.
|
|
311
|
+
*
|
|
312
|
+
* The event is stamped with a monotonic `seq`, an ISO-8601 `ts`, a
|
|
313
|
+
* `prev_hash` linking it to the previous event, and an `event_hash`
|
|
314
|
+
* computed over its canonical JSON (excluding the hash field itself).
|
|
315
|
+
*
|
|
316
|
+
* This function is **intentionally non-tolerant**:
|
|
317
|
+
* - If `event` is an event type that requires `spec_id` and one is
|
|
318
|
+
* missing, it throws. Do NOT wrap calls in `try { ... } catch {}`.
|
|
319
|
+
* - If the lock cannot be acquired, it throws.
|
|
320
|
+
* - If the last line of the file is malformed, it throws.
|
|
321
|
+
*
|
|
322
|
+
* Silent loss of provenance is the current failure mode of `.caws/state/`
|
|
323
|
+
* and the whole point of this module is to reverse that default.
|
|
324
|
+
*
|
|
325
|
+
* @param {object} params
|
|
326
|
+
* @param {string} params.actor — who emitted the event (cli, hook, session, agent, subagent-name)
|
|
327
|
+
* @param {string} params.event — event type from the v0 vocabulary
|
|
328
|
+
* @param {string} [params.spec_id] — required for spec-scoped events, optional otherwise
|
|
329
|
+
* @param {object} [params.data] — event-type-specific payload
|
|
330
|
+
* @param {object} [options]
|
|
331
|
+
* @param {string} [options.projectRoot] — defaults to cwd
|
|
332
|
+
* @param {string} [options.session_id] — session correlator; defaults to env CAWS_SESSION_ID or "standalone"
|
|
333
|
+
* @returns {Promise<{seq: number, event_hash: string, prev_hash: string}>}
|
|
334
|
+
*/
|
|
335
|
+
/**
|
|
336
|
+
* Shared contract validation for both the async and sync append paths.
|
|
337
|
+
* Throws on any violation. Returns a normalized descriptor the
|
|
338
|
+
* file-writing helper consumes.
|
|
339
|
+
*
|
|
340
|
+
* @param {object} params
|
|
341
|
+
* @param {object} options
|
|
342
|
+
* @returns {{actor: string, event: string, spec_id: (string|undefined), data: (object|undefined), sessionId: string, eventsPath: string}}
|
|
343
|
+
*/
|
|
344
|
+
function validateAppendParams(params, options) {
|
|
345
|
+
const { actor, event, spec_id, data } = params || {};
|
|
346
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
347
|
+
const sessionId = options.session_id || process.env.CAWS_SESSION_ID || 'standalone';
|
|
348
|
+
|
|
349
|
+
if (!actor || typeof actor !== 'string') {
|
|
350
|
+
throw new Error('event-log.appendEvent: `actor` is required (non-empty string)');
|
|
351
|
+
}
|
|
352
|
+
if (!event || typeof event !== 'string') {
|
|
353
|
+
throw new Error('event-log.appendEvent: `event` is required (non-empty string)');
|
|
354
|
+
}
|
|
355
|
+
if (REQUIRES_SPEC_ID.has(event)) {
|
|
356
|
+
if (!spec_id || typeof spec_id !== 'string' || spec_id.trim() === '') {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`event-log.appendEvent: event "${event}" requires a non-empty spec_id ` +
|
|
359
|
+
`(got ${JSON.stringify(spec_id)}). This is the fence that prevents the ` +
|
|
360
|
+
`.caws/state/undefined.json bug class — do not catch this error and continue.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
actor,
|
|
367
|
+
event,
|
|
368
|
+
spec_id,
|
|
369
|
+
data,
|
|
370
|
+
sessionId,
|
|
371
|
+
eventsPath: getEventsPath(projectRoot),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Shared critical section: read tail, build event, write. Assumes the
|
|
377
|
+
* caller already holds the lock. Returns the new event (with seq, hashes).
|
|
378
|
+
*
|
|
379
|
+
* @param {object} ctx — output of validateAppendParams
|
|
380
|
+
* @returns {{seq: number, event_hash: string, prev_hash: string}}
|
|
381
|
+
*/
|
|
382
|
+
function writeEventUnderLock(ctx) {
|
|
383
|
+
const { actor, event, spec_id, data, sessionId, eventsPath } = ctx;
|
|
384
|
+
|
|
385
|
+
const lastLine = readLastLine(eventsPath);
|
|
386
|
+
let seq = 1;
|
|
387
|
+
let prevHash = '';
|
|
388
|
+
if (lastLine !== null) {
|
|
389
|
+
let lastEvent;
|
|
390
|
+
try {
|
|
391
|
+
lastEvent = JSON.parse(lastLine);
|
|
392
|
+
} catch (parseErr) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`event-log.appendEvent: last line of ${eventsPath} is malformed: ${parseErr.message}. ` +
|
|
395
|
+
`The log is corrupt; manual inspection required before continuing.`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (typeof lastEvent.seq !== 'number' || !Number.isInteger(lastEvent.seq)) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`event-log.appendEvent: last event missing integer seq (got ${JSON.stringify(lastEvent.seq)})`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
seq = lastEvent.seq + 1;
|
|
404
|
+
prevHash = typeof lastEvent.event_hash === 'string' ? lastEvent.event_hash : '';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const newEvent = {
|
|
408
|
+
seq,
|
|
409
|
+
ts: new Date().toISOString(),
|
|
410
|
+
session_id: sessionId,
|
|
411
|
+
actor,
|
|
412
|
+
event,
|
|
413
|
+
};
|
|
414
|
+
if (spec_id !== undefined && spec_id !== null && spec_id !== '') {
|
|
415
|
+
newEvent.spec_id = spec_id;
|
|
416
|
+
}
|
|
417
|
+
if (data !== undefined) {
|
|
418
|
+
newEvent.data = data;
|
|
419
|
+
}
|
|
420
|
+
newEvent.prev_hash = prevHash;
|
|
421
|
+
newEvent.event_hash = computeEventHash(newEvent);
|
|
422
|
+
|
|
423
|
+
fs.appendFileSync(eventsPath, JSON.stringify(newEvent) + '\n', { encoding: 'utf8' });
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
seq: newEvent.seq,
|
|
427
|
+
event_hash: newEvent.event_hash,
|
|
428
|
+
prev_hash: newEvent.prev_hash,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function appendEvent(params, options = {}) {
|
|
433
|
+
const ctx = validateAppendParams(params, options);
|
|
434
|
+
const handle = await acquireLock(ctx.eventsPath);
|
|
435
|
+
try {
|
|
436
|
+
return writeEventUnderLock(ctx);
|
|
437
|
+
} finally {
|
|
438
|
+
releaseLock(handle);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Synchronous variant of `appendEvent`. Same contract, same fail-loud
|
|
444
|
+
* behavior. Intended for call sites that cannot await (synchronous
|
|
445
|
+
* session manager functions, hooks, etc.). Blocks the thread during
|
|
446
|
+
* lock contention via `Atomics.wait`.
|
|
447
|
+
*
|
|
448
|
+
* Prefer `appendEvent` in async contexts — it cooperates with the event
|
|
449
|
+
* loop instead of blocking it.
|
|
450
|
+
*
|
|
451
|
+
* @param {object} params — same as appendEvent
|
|
452
|
+
* @param {object} [options] — same as appendEvent
|
|
453
|
+
* @returns {{seq: number, event_hash: string, prev_hash: string}}
|
|
454
|
+
*/
|
|
455
|
+
function appendEventSync(params, options = {}) {
|
|
456
|
+
const ctx = validateAppendParams(params, options);
|
|
457
|
+
const handle = acquireLockSync(ctx.eventsPath);
|
|
458
|
+
try {
|
|
459
|
+
return writeEventUnderLock(ctx);
|
|
460
|
+
} finally {
|
|
461
|
+
releaseLock(handle);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Read all events from the project's event log, in seq order.
|
|
467
|
+
*
|
|
468
|
+
* Tolerates a partial trailing line (from a crashed writer) by discarding
|
|
469
|
+
* it. Returns an array of parsed events. The caller is responsible for
|
|
470
|
+
* filtering by spec_id or event type.
|
|
471
|
+
*
|
|
472
|
+
* This is intentionally eager (not a stream) in Phase 1 — the expected
|
|
473
|
+
* event log size for CLI-scale projects is under 10k lines, well within
|
|
474
|
+
* memory. A streaming reader is a future addition when compaction lands.
|
|
475
|
+
*
|
|
476
|
+
* @param {object} [options]
|
|
477
|
+
* @param {string} [options.projectRoot] — defaults to cwd
|
|
478
|
+
* @param {boolean} [options.strict] — if true, throw on any malformed line (default false: discard trailing partial)
|
|
479
|
+
* @returns {object[]}
|
|
480
|
+
*/
|
|
481
|
+
function readEvents(options = {}) {
|
|
482
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
483
|
+
const strict = options.strict === true;
|
|
484
|
+
const eventsPath = getEventsPath(projectRoot);
|
|
485
|
+
|
|
486
|
+
if (!fs.existsSync(eventsPath)) return [];
|
|
487
|
+
const content = fs.readFileSync(eventsPath, 'utf8');
|
|
488
|
+
if (content.length === 0) return [];
|
|
489
|
+
|
|
490
|
+
const lines = content.split('\n');
|
|
491
|
+
// The file ends in \n, so the split yields a trailing empty element.
|
|
492
|
+
// Any other empty element is a malformed blank line.
|
|
493
|
+
const events = [];
|
|
494
|
+
for (let i = 0; i < lines.length; i++) {
|
|
495
|
+
const line = lines[i];
|
|
496
|
+
const isLast = i === lines.length - 1;
|
|
497
|
+
if (line.length === 0) {
|
|
498
|
+
if (isLast) continue; // normal trailing newline
|
|
499
|
+
if (strict) {
|
|
500
|
+
throw new Error(`event-log.readEvents: empty line at index ${i} (strict mode)`);
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
events.push(JSON.parse(line));
|
|
506
|
+
} catch (err) {
|
|
507
|
+
if (isLast) {
|
|
508
|
+
// Partial trailing line from a crashed writer — tolerate unless strict.
|
|
509
|
+
if (strict) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`event-log.readEvents: partial trailing line (strict mode): ${err.message}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
// Drop it silently; the next append will overwrite it.
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
throw new Error(
|
|
518
|
+
`event-log.readEvents: malformed line at index ${i}: ${err.message}. ` +
|
|
519
|
+
`The log is corrupt; manual inspection required.`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return events;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Verify the hash chain of the event log end-to-end. Walks every event,
|
|
528
|
+
* recomputes its event_hash, and asserts prev_hash matches the previous
|
|
529
|
+
* event's event_hash.
|
|
530
|
+
*
|
|
531
|
+
* Intended for a future `caws events verify` command; exported here so
|
|
532
|
+
* tests can use it to prove chain continuity.
|
|
533
|
+
*
|
|
534
|
+
* @param {object} [options]
|
|
535
|
+
* @param {string} [options.projectRoot]
|
|
536
|
+
* @returns {{ok: boolean, count: number, firstBadSeq?: number, reason?: string}}
|
|
537
|
+
*/
|
|
538
|
+
function verifyChain(options = {}) {
|
|
539
|
+
const events = readEvents({ ...options, strict: true });
|
|
540
|
+
let prevHash = '';
|
|
541
|
+
for (const event of events) {
|
|
542
|
+
if (event.prev_hash !== prevHash) {
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
count: events.length,
|
|
546
|
+
firstBadSeq: event.seq,
|
|
547
|
+
reason: `prev_hash mismatch at seq ${event.seq}: expected ${JSON.stringify(prevHash)}, got ${JSON.stringify(event.prev_hash)}`,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const recomputed = computeEventHash(event);
|
|
551
|
+
if (recomputed !== event.event_hash) {
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
count: events.length,
|
|
555
|
+
firstBadSeq: event.seq,
|
|
556
|
+
reason: `event_hash mismatch at seq ${event.seq}: stored ${event.event_hash}, recomputed ${recomputed}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
prevHash = event.event_hash;
|
|
560
|
+
}
|
|
561
|
+
return { ok: true, count: events.length };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// Exports
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
module.exports = {
|
|
569
|
+
appendEvent,
|
|
570
|
+
appendEventSync,
|
|
571
|
+
readEvents,
|
|
572
|
+
verifyChain,
|
|
573
|
+
|
|
574
|
+
// Exposed for tests and the renderer; not part of the stable public API.
|
|
575
|
+
_internal: {
|
|
576
|
+
canonicalJson,
|
|
577
|
+
computeEventHash,
|
|
578
|
+
readLastLine,
|
|
579
|
+
REQUIRES_SPEC_ID,
|
|
580
|
+
OPTIONAL_SPEC_ID,
|
|
581
|
+
HASH_DOMAIN,
|
|
582
|
+
EVENTS_FILE,
|
|
583
|
+
},
|
|
584
|
+
};
|