wicked-vault 0.2.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.
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: wicked-vault:cross-check-evidence
3
+ description: Declare a consumer-authored contract and get a mechanical PASS/REJECT verdict for a scope+phase by re-deriving every required artifact. Use when answering "is this claim actually backed by evidence that still holds?" — gate logic, release readiness, or merge checks. Fail-closed when no contract is declared.
4
+ ---
5
+
6
+ # wicked-vault:cross-check-evidence
7
+
8
+ Evaluate a whole **contract** — the set of evidence a scope+phase requires — and
9
+ return a single mechanical verdict. Cross-check re-derives every required
10
+ artifact (it does not trust cached statuses) and reports `PASS` only when all of
11
+ them hold.
12
+
13
+ This is the "is the work *backed*?" question. The contract is **consumer-
14
+ authored**; the vault only decides *whether* it's satisfied, never *what* it
15
+ should require (G9). The vault has no gate logic of its own to leak.
16
+
17
+ ## When to use
18
+
19
+ - A gate / release-readiness / merge check that aggregates several claims.
20
+ - Answering: "is this claim actually backed by evidence that still holds?"
21
+ - Any time a stored "ready to merge" / "tests pass" must be re-proven, not
22
+ asserted.
23
+
24
+ For a single artifact, use `wicked-vault:verify-evidence` instead.
25
+
26
+ ## Step 1 — Declare the contract
27
+
28
+ Write a JSON spec listing the required evidence, then declare it for a
29
+ scope+phase. A claim with a `verifier` pin also constrains how its evidence may
30
+ be recorded (G8 — see `wicked-vault:record-evidence`).
31
+
32
+ ```jsonc
33
+ // contract.json
34
+ {
35
+ "required_evidence": [
36
+ { "claim_id": "tests-pass", "kind": "test-run", "criteria": "all unit tests pass (exit 0)", "verifier": { "kind": "exit_code_eq" } },
37
+ { "claim_id": "no-secrets", "kind": "test-run", "criteria": "no secrets in the diff", "verifier": { "kind": "not_contains" } },
38
+ { "claim_id": "design-ok", "kind": "review-verdict", "criteria": "the change adequately addresses the documented failure modes", "require_attestation": true },
39
+ { "claim_id": "changelog", "kind": "file", "required": false }
40
+ ]
41
+ }
42
+ ```
43
+
44
+ ```bash
45
+ npx wicked-vault declare-contract --scope checkout --phase release --spec contract.json
46
+ # -> { "contract_version": "<16-char hash>" }
47
+ ```
48
+
49
+ - `required: false` makes a claim optional — its absence is `PASS`, not `MISSING`.
50
+ - **`criteria`** pins the acceptance criteria for the claim. This is the
51
+ **trusted path** — criteria authored in the contract (separately from the
52
+ worker), so a recorded artifact must match it (`criteria_authored_by:
53
+ contract`). Strongly preferred over worker-supplied criteria.
54
+ - **`require_attestation: true`** marks a claim that needs an independent
55
+ judgment (the judgment tier) — see Step 4. Use it for free-form criteria a
56
+ deterministic verifier can't express ("adequately addresses the failure
57
+ modes").
58
+ - The `contract_version` is a hash of the required-evidence set (G8 pinning).
59
+
60
+ ## Step 2 — Record the evidence
61
+
62
+ Record one artifact per required claim (`wicked-vault:record-evidence`). Each `claim_id`
63
+ must match the contract, and `--criteria` must match the contract's pin. The
64
+ **latest active** artifact for a claim wins.
65
+
66
+ ## Step 3 — Cross-check (integrity tier — the default, CI-safe)
67
+
68
+ ```bash
69
+ npx wicked-vault cross-check --scope checkout --phase release
70
+ ```
71
+
72
+ This is **`--integrity-only`** by default: deterministic, offline, no model
73
+ calls — safe to put on a CI gate. It evaluates hash integrity + any
74
+ deterministic verifier per claim.
75
+
76
+ ```json
77
+ {
78
+ "scope": "checkout", "phase": "release",
79
+ "contract_version": "...",
80
+ "overall": "PASS",
81
+ "claims": [
82
+ { "claim_id": "tests-pass", "artifact_id": "...", "hash_ok": true, "verifier_status": "pass", "result": "PASS", "detail": "exit_code=0" },
83
+ { "claim_id": "no-secrets", "artifact_id": "...", "hash_ok": true, "verifier_status": "pass", "result": "PASS", "detail": "/(?i)secret/ absent" }
84
+ ],
85
+ "evaluated_at": "..."
86
+ }
87
+ ```
88
+
89
+ ## Verdicts and exit code
90
+
91
+ Exit `0` **iff** `overall === "PASS"`. Per-claim `result` is one of:
92
+
93
+ | result | meaning |
94
+ |---|---|
95
+ | `PASS` | required artifact present, hash intact, verifier passed (and — in `--with-attestations` — an independent `pass` opinion when `require_attestation`) |
96
+ | `MISSING` | a required claim has no active artifact |
97
+ | `FAIL` | artifact present but tamper or verifier failed |
98
+ | `UNATTESTED` | `require_attestation` claim has no independent opinion recorded |
99
+ | `REJECT` | `require_attestation` claim has a non-pass / stale independent opinion |
100
+ | `ERROR` | verifier-kind pin mismatch against the contract |
101
+
102
+ `overall` is `PASS` only if every claim is `PASS`; `ERROR` if any claim errored;
103
+ otherwise `REJECT`.
104
+
105
+ ## Step 4 — Judgment tier (opt-in, NOT for the default CI gate)
106
+
107
+ ```bash
108
+ npx wicked-vault cross-check --scope checkout --phase release --with-attestations
109
+ ```
110
+
111
+ `--with-attestations` consults the latest independent opinion per claim (the
112
+ attestations recorded by `wicked-vault:analyze-evidence`). For a claim with
113
+ `require_attestation: true`, it `PASS`es only when integrity passes **and** a
114
+ non-stale, independent `pass` opinion exists; otherwise `UNATTESTED` / `REJECT`.
115
+ For other claims the opinion is advisory (surfaced, doesn't change the result).
116
+
117
+ This mode is **not deterministic and not for the default gate** — opinions come
118
+ from a model and are point-in-time. Run `wicked-vault:analyze-evidence` first to
119
+ produce the attestations, then use this mode for a release sign-off that
120
+ requires a third-party judgment. Keep `--integrity-only` on the fast CI path.
121
+
122
+ ## Fail-closed (G5)
123
+
124
+ If **no contract is declared** for the scope+phase, cross-check returns
125
+ `overall: "ERROR"` and a non-zero exit — it never reports PASS by default. An
126
+ undeclared expectation can't be silently satisfied.
127
+
128
+ ## wicked-bus event
129
+
130
+ If wicked-bus is installed, `cross-check` publishes `wicked.contract.checked`
131
+ (domain `wicked-vault`, subdomain `vault.cross_check`) carrying the `overall`
132
+ verdict — this is the signal a gate consumer subscribes to. A detected tamper
133
+ also publishes `wicked.evidence.tampered`. `declare-contract` publishes
134
+ `wicked.contract.declared`. Emission is fire-and-forget and a no-op when the bus
135
+ is absent or `WICKED_VAULT_NO_BUS=1`.
136
+
137
+ ## Inspecting what's recorded
138
+
139
+ ```bash
140
+ npx wicked-vault list --scope checkout --phase release # all entries (active + superseded)
141
+ ```
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: wicked-vault:init
3
+ description: Initialize a wicked-vault in a repository so claims can be backed by re-derivable evidence. Use when setting up the vault for the first time, when a vault command reports "no .wicked-vault/ found", or before the first record/cross-check in a project.
4
+ ---
5
+
6
+ # wicked-vault:init
7
+
8
+ Set up the local-first **evidence primitive** in the current repository. The
9
+ vault records claim-backing artifacts, hashes them tamper-evidently, and
10
+ *re-derives* their verdict on demand — it never trusts a stored status.
11
+
12
+ ## When to use
13
+
14
+ - First time using the vault in a repo.
15
+ - A command failed with `no .wicked-vault/ found; run \`wicked-vault init\``.
16
+ - Before the first `record` or `declare-contract` in a project.
17
+
18
+ ## Initialize
19
+
20
+ ```bash
21
+ npx wicked-vault init
22
+ ```
23
+
24
+ This creates `.wicked-vault/` at the repo root with:
25
+
26
+ ```
27
+ .wicked-vault/
28
+ vault.json # schema_version, store_mode: in-repo, payload_max_bytes
29
+ entries/ # one JSON envelope per recorded artifact (append-only)
30
+ payloads/ # content-addressed payload blobs (sha256-named, deduped)
31
+ contracts/ # consumer-authored contracts, per scope/phase
32
+ ```
33
+
34
+ `record`, `declare-contract`, and `supersede` auto-create the vault if one
35
+ isn't found, so explicit `init` is mostly for clarity. `verify`, `cross-check`,
36
+ and `list` do **not** auto-create — they fail-closed when no vault exists.
37
+
38
+ The vault is discovered by walking up from the current directory, so any
39
+ subdirectory of the repo can run vault commands.
40
+
41
+ ## Should this be committed?
42
+
43
+ `store_mode` defaults to `in-repo` — the vault lives inside the working tree
44
+ and git becomes the audit chain. Decide per project whether `.wicked-vault/` is
45
+ committed (shared, auditable evidence) or git-ignored (local-only scratch). The
46
+ repo's own `.gitignore` ignores `.wicked-vault/` by default; remove that line to
47
+ commit evidence.
48
+
49
+ ## Next steps
50
+
51
+ - `wicked-vault:record-evidence` — capture an artifact and attach a verifier.
52
+ - `wicked-vault:cross-check-evidence` — declare a contract and get a mechanical verdict.
53
+
54
+ ## Output
55
+
56
+ Every command emits JSON to stdout and uses the **exit code as the gate
57
+ signal** (`0` = PASS / success). `init` returns the absolute path of the
58
+ created `.wicked-vault/` directory.
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: wicked-vault:record-evidence
3
+ description: Record a claim-backing artifact in the vault and attach a deterministic verifier. Use when capturing evidence that "tests pass", "build clean", a commit exists, or a file's contents back a claim — and when replacing stale evidence via supersede. Covers --run vs --artifact, verifier syntax, and contract pinning.
4
+ ---
5
+
6
+ # wicked-vault:record-evidence
7
+
8
+ Capture an artifact, hash it tamper-evidently, and attach a verifier that can
9
+ **re-derive** its verdict later. The vault does the capture itself — it never
10
+ trusts a claimed status (G4).
11
+
12
+ ## When to use
13
+
14
+ - Backing a claim with evidence: "tests pass", "build clean", "no secrets",
15
+ "commit landed", "config has the required field".
16
+ - Replacing stale evidence with a fresh artifact (use **supersede**, below).
17
+
18
+ ## Two ways to capture
19
+
20
+ ### 1. Run a command and capture its result (`--run`)
21
+
22
+ The vault executes the source command and stores `{command, exit_code, stdout,
23
+ stderr, captured_at}` as the payload.
24
+
25
+ ```bash
26
+ npx wicked-vault record \
27
+ --scope checkout --phase build --claim tests-pass --kind test-run \
28
+ --source "npm test" --criteria "all unit tests pass (exit 0)" \
29
+ --verifier "exit_code_eq:0" --run
30
+ ```
31
+
32
+ ### 2. Hash an existing file (`--artifact`)
33
+
34
+ The vault reads the file and stores its bytes as the payload.
35
+
36
+ ```bash
37
+ npx wicked-vault record \
38
+ --scope checkout --phase build --claim coverage-report --kind file \
39
+ --source "coverage/summary.json" --artifact coverage/summary.json \
40
+ --criteria "line coverage is at least 80%" \
41
+ --verifier "jq_pred:.total.lines.pct >= 80"
42
+ ```
43
+
44
+ `record` requires **either** `--run` **or** `--artifact`, and **always**
45
+ `--criteria`.
46
+
47
+ ## Required fields
48
+
49
+ | Flag | Meaning |
50
+ |---|---|
51
+ | `--scope` | the unit the claim is about (e.g. a service, module, PR) |
52
+ | `--phase` | lifecycle phase (e.g. `build`, `review`, `release`) |
53
+ | `--claim` | claim id this artifact backs (e.g. `tests-pass`) |
54
+ | `--kind` | artifact kind (e.g. `test-run`, `file`, `commit`) |
55
+ | `--source` | the command (`--run`) or path/description (`--artifact`) |
56
+ | `--criteria` | **mandatory** — the acceptance criteria this evidence claims to clear; inline text or `@file`. Hashed into the envelope and frozen to the evidence (G10) |
57
+ | `--verifier` | *optional* deterministic sub-check (see below) — a composable signal an independent evaluator can cite |
58
+
59
+ ## Acceptance criteria are mandatory (G10/D1)
60
+
61
+ Every artifact must state the bar it claims to clear — `record` rejects evidence
62
+ with no `--criteria`. The criteria are hashed into the envelope, so the bar is
63
+ **frozen to the evidence**: the same evidence can never later be judged against
64
+ weaker criteria (anti-downgrade).
65
+
66
+ The **trusted path** is contract-pinned criteria: when `declare-contract` pins
67
+ `criteria` for the claim, a matching `--criteria` is stamped
68
+ `criteria_authored_by: contract`. Worker-supplied criteria are stamped
69
+ `record` (a weaker provenance class — see `wicked-vault:analyze-evidence`'s
70
+ threat model). A `--criteria` that contradicts a contract pin is a G8 downgrade
71
+ and is rejected.
72
+
73
+ The independent judgment of *whether the evidence meets these criteria* is the
74
+ job of `wicked-vault:analyze-evidence` (the judgment tier), not `record`.
75
+
76
+ ## Verifier syntax
77
+
78
+ `--verifier "kind:arg"` (or a JSON object for advanced params). The v1 core
79
+ verifiers are **deterministic and pure** (G7):
80
+
81
+ | Verifier | Example | Passes when |
82
+ |---|---|---|
83
+ | `exit_code_eq` | `exit_code_eq:0` | captured exit code equals N (requires `--run`) |
84
+ | `regex_match` | `regex_match:[0-9a-f]{40}` | pattern matches stdout+stderr / file text |
85
+ | `not_contains` | `not_contains:(?i)error` | pattern is **absent** |
86
+ | `jq_pred` | `jq_pred:.ok == true` | `jq -e` on the JSON payload is truthy (needs `jq`) |
87
+ | `commit_exists` | `commit_exists:<sha>` | the git commit exists in the repo |
88
+
89
+ `llm_eval` is intentionally **not** a verifier kind — a nondeterministic judge
90
+ would falsify the purity guarantee (G7).
91
+
92
+ ## Output
93
+
94
+ ```json
95
+ { "id": "...", "envelope_hash": "...", "status_at_record": "pass", "status_detail": "exit_code=0" }
96
+ ```
97
+
98
+ `status_at_record` is **informational only** — `verify` and `cross-check` never
99
+ read it; they re-run the verifier (G3). The exit code is `0` on a successful
100
+ record (the recording succeeded), regardless of the verdict — to gate on the
101
+ verdict, use `verify` or `cross-check`.
102
+
103
+ ## wicked-bus event
104
+
105
+ If wicked-bus is installed, `record` publishes `wicked.evidence.recorded`
106
+ (domain `wicked-vault`, subdomain `vault.record`) fire-and-forget. `supersede`
107
+ publishes `wicked.evidence.superseded`. Emission is a silent no-op when the bus
108
+ is absent or `WICKED_VAULT_NO_BUS=1` — it never affects the output or exit code.
109
+
110
+ ## Replacing evidence (supersede)
111
+
112
+ Evidence is append-only (G6). To replace a stale artifact, record a replacement
113
+ and link it — the old entry is flipped to `superseded`, never deleted:
114
+
115
+ ```bash
116
+ npx wicked-vault supersede <old-id> \
117
+ --scope checkout --phase build --claim tests-pass --kind test-run \
118
+ --source "npm test" --verifier "exit_code_eq:0" --run
119
+ ```
120
+
121
+ Crash-safe ordering: the replacement is written and confirmed on disk before
122
+ the old entry is flipped, so there is always an active artifact for the claim.
123
+
124
+ ## Contract pinning (G8)
125
+
126
+ If a contract pins this claim (see `wicked-vault:cross-check-evidence`), `record` rejects
127
+ a downgrade — a `kind`, `source`, or `verifier` that differs from the pin throws
128
+ a `G8 pin violation`. This stops a weaker verifier from being swapped in to make
129
+ a claim pass.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: wicked-vault:verify-evidence
3
+ description: Deterministically re-derive a single recorded artifact's integrity — recompute the payload/criteria/envelope hashes and re-run its pure verifier. Model-free, reproducible, CI-gate-safe. Use to confirm a piece of evidence is still intact and its deterministic check passes, or to detect tamper. Never trusts the cached status. For an INDEPENDENT judgment of whether the evidence MEETS its criteria, use wicked-vault:analyze-evidence.
4
+ ---
5
+
6
+ # wicked-vault:verify-evidence
7
+
8
+ The **integrity tier** (G1–G9). Re-derive the verdict for **one** recorded
9
+ artifact by id: recompute the payload, criteria, and envelope hashes from the
10
+ stored bytes and **re-run the pure verifier** — never reading the status stored
11
+ at record time (G3). Deterministic, offline, no model — safe on a CI gate.
12
+
13
+ **This is not the judgment tier.** It answers *"is this artifact intact and does
14
+ its deterministic check still pass?"* — not *"does this evidence actually meet
15
+ its acceptance criteria?"* For the latter (an independent model analysis of
16
+ evidence-vs-criteria), use **`wicked-vault:analyze-evidence`**.
17
+
18
+ ## When to use
19
+
20
+ - Confirming a specific artifact still holds *right now* (cheap, reproducible).
21
+ - Detecting tamper: a payload, criteria, or envelope field modified after
22
+ recording.
23
+ - The integrity precheck before an independent analysis or a gate.
24
+
25
+ To check a whole contract at once, use `wicked-vault:cross-check-evidence`.
26
+
27
+ ## Verify
28
+
29
+ ```bash
30
+ npx wicked-vault verify <artifact-id>
31
+ ```
32
+
33
+ Get artifact ids from `record` output or `npx wicked-vault list --scope <S>`.
34
+
35
+ ## Output
36
+
37
+ ```json
38
+ {
39
+ "id": "...",
40
+ "hash_ok": true,
41
+ "payload_ok": true,
42
+ "criteria_ok": true,
43
+ "envelope_ok": true,
44
+ "status": "pass",
45
+ "rederived": true,
46
+ "detail": "exit_code=0",
47
+ "ignored_cached_status": "pass",
48
+ "latest_attestation": { "opinion": "pass", "evaluator": "gemini-reviewer", "stale": false }
49
+ }
50
+ ```
51
+
52
+ | Field | Meaning |
53
+ |---|---|
54
+ | `hash_ok` | payload, criteria, and envelope hashes all match what was stored |
55
+ | `payload_ok` / `criteria_ok` / `envelope_ok` | which part of the tamper check passed |
56
+ | `status` | re-derived integrity verdict: `pass` / `fail` / `error` |
57
+ | `rederived` | always `true` — proves the check was actually re-run |
58
+ | `ignored_cached_status` | the stored status the vault refused to trust |
59
+ | `latest_attestation` | the most recent **independent opinion** (from `analyze-evidence`), shown **for reference only** — not re-derived, not trusted as reproducible; `stale: true` if it judged different bytes |
60
+
61
+ ## Exit code is the gate
62
+
63
+ Exit `0` **iff** `hash_ok && status === "pass"`. Any tamper or a failing
64
+ verifier exits non-zero — script against the exit code:
65
+
66
+ ```bash
67
+ if npx wicked-vault verify "$id" >/dev/null; then echo "still intact"; fi
68
+ ```
69
+
70
+ ## Fail-closed (G5)
71
+
72
+ A pass requires an intact hash **and** (if the artifact carries a verifier) a
73
+ passing verifier. If any hash diverges, `status` is forced to `fail` with a
74
+ `TAMPER:` detail. A missing entry or payload blob returns `status: "error"` —
75
+ never a silent pass. The `latest_attestation` is informational and never affects
76
+ this exit code (an independent opinion is a separate, non-reproducible tier).
package/src/bus.mjs ADDED
@@ -0,0 +1,75 @@
1
+ // Optional, fire-and-forget wicked-bus integration.
2
+ //
3
+ // The vault is a zero-dependency, local-first primitive. wicked-bus is a
4
+ // *sibling* primitive, never a hard dependency: this module dynamic-imports it
5
+ // at runtime and degrades to a silent no-op when it is absent or disabled.
6
+ // Emission NEVER blocks or breaks a vault command — by the time we publish, the
7
+ // evidence write has already happened (G6 append-only), and a bus problem must
8
+ // not change a verdict, the stdout JSON, or an exit code.
9
+ //
10
+ // Opt out entirely with WICKED_VAULT_NO_BUS=1.
11
+
12
+ import { createRequire } from 'node:module';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { join } from 'node:path';
15
+
16
+ const DOMAIN = 'wicked-vault';
17
+
18
+ // Resolve the wicked-bus module namespace, or null. Two layers, in order:
19
+ // 1. bare specifier — works when wicked-bus is installed globally or hoisted
20
+ // alongside the vault.
21
+ // 2. the consumer project's node_modules (anchored at cwd) — the common case,
22
+ // since sibling tools live in the same repo the vault is invoked from, and
23
+ // the vault itself ships no node_modules.
24
+ async function resolveBus(cwd) {
25
+ try {
26
+ return await import('wicked-bus');
27
+ } catch {
28
+ /* fall through to layer 2 */
29
+ }
30
+ try {
31
+ const require = createRequire(join(cwd, '__vault_bus_anchor__.js'));
32
+ const entry = require.resolve('wicked-bus');
33
+ return await import(pathToFileURL(entry).href);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Build the bus publisher.
41
+ *
42
+ * @param {string} [cwd] directory to anchor module resolution from.
43
+ * @returns {Promise<(event_type: string, subdomain: string, payload: object) => void>}
44
+ * a synchronous publish closure, or a no-op when the bus is unavailable or
45
+ * disabled. The returned function never throws.
46
+ */
47
+ export async function initBus(cwd = process.cwd()) {
48
+ const NOOP = () => {};
49
+ if (process.env.WICKED_VAULT_NO_BUS === '1') return NOOP;
50
+
51
+ const bus = await resolveBus(cwd);
52
+ if (!bus || typeof bus.emit !== 'function' || typeof bus.openDb !== 'function') {
53
+ return NOOP;
54
+ }
55
+
56
+ let db;
57
+ let config;
58
+ try {
59
+ config = typeof bus.loadConfig === 'function' ? bus.loadConfig() : {};
60
+ // openDb opens (and idempotently initializes) the local bus db — this is
61
+ // wicked-bus's own auto-init model, the same path the wicked-brain
62
+ // installer uses.
63
+ db = bus.openDb(config);
64
+ } catch {
65
+ return NOOP;
66
+ }
67
+
68
+ return (event_type, subdomain, payload) => {
69
+ try {
70
+ bus.emit(db, config, { event_type, domain: DOMAIN, subdomain, payload });
71
+ } catch {
72
+ // swallow — a bus error must never break a vault command
73
+ }
74
+ };
75
+ }
package/src/hash.mjs ADDED
@@ -0,0 +1,40 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export function sha256(buf) {
4
+ return createHash('sha256').update(buf).digest('hex');
5
+ }
6
+
7
+ // Deterministic canonical JSON (recursively key-sorted, no whitespace) so a
8
+ // hash over a structure is stable regardless of key order.
9
+ function sortKeys(o) {
10
+ if (o === null || typeof o !== 'object') return o;
11
+ if (Array.isArray(o)) return o.map(sortKeys);
12
+ const out = {};
13
+ for (const k of Object.keys(o).sort()) out[k] = sortKeys(o[k]);
14
+ return out;
15
+ }
16
+
17
+ export function canonical(obj) {
18
+ return JSON.stringify(sortKeys(obj));
19
+ }
20
+
21
+ // G2 — the envelope hash binds the identifying tuple (now including the
22
+ // acceptance-criteria hash) to the payload hash. Mutating ANY of these fields,
23
+ // the criteria, or the payload changes the envelope, so a later `verify`
24
+ // recomputation will diverge from the stored value. The verifier is optional
25
+ // (ADR-0002: it became a composable sub-check, not the whole story).
26
+ export function envelopeHash(fields) {
27
+ const tuple = {
28
+ scope: fields.scope,
29
+ phase: fields.phase,
30
+ claim_id: fields.claim_id,
31
+ kind: fields.kind,
32
+ source: fields.source,
33
+ verifier: fields.verifier
34
+ ? { kind: fields.verifier.kind, params: fields.verifier.params || {} }
35
+ : null,
36
+ criteria_sha256: fields.criteria_sha256,
37
+ payload_sha256: fields.payload_sha256,
38
+ };
39
+ return sha256(Buffer.from(canonical(tuple), 'utf8'));
40
+ }
package/src/id.mjs ADDED
@@ -0,0 +1,9 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ // G1 — server-minted id. Time-prefixed hex so ids sort by creation order
4
+ // (ULID-grade ordering; not strict ULID encoding). Callers cannot supply it.
5
+ export function newId() {
6
+ const t = Date.now().toString(16).padStart(12, '0');
7
+ const r = randomBytes(8).toString('hex');
8
+ return (t + r).toUpperCase();
9
+ }