opencode-swarm 7.136.4 → 7.137.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/dist/agents/agent-output-schema.d.ts +2 -2
- package/dist/cli/{config-doctor-qwp5y9dk.js → config-doctor-vyx82nx2.js} +2 -2
- package/dist/cli/{core-jpjk2qvt.js → core-6w5sd5bc.js} +1 -1
- package/dist/cli/{curation-policy-8tsbaa4q.js → curation-policy-8tvs4pa0.js} +7 -6
- package/dist/cli/{curator-763smew7.js → curator-43xqs75k.js} +25 -25
- package/dist/cli/{curator-llm-factory-dg8mmzy7.js → curator-llm-factory-8nywfdsh.js} +25 -25
- package/dist/cli/{evidence-summary-service-nse06dqr.js → evidence-summary-service-85gw9p2m.js} +7 -7
- package/dist/cli/{gate-evidence-kdygjr56.js → gate-evidence-s2kytsrj.js} +4 -4
- package/dist/cli/{guardrail-explain-6jc012g3.js → guardrail-explain-8kkqz8tx.js} +26 -26
- package/dist/cli/{guardrail-log-86aw7fne.js → guardrail-log-e2v9qw71.js} +3 -3
- package/dist/cli/{hive-promoter-en786ja4.js → hive-promoter-jt9ky0hn.js} +25 -25
- package/dist/cli/{index-45y0y3xh.js → index-1zrb3vt2.js} +7 -7
- package/dist/cli/{index-d2sf9an1.js → index-5r6kkhd7.js} +4 -4
- package/dist/cli/{index-ne28wyyc.js → index-5wjw05dy.js} +2 -2
- package/dist/cli/{index-06htxvzq.js → index-72wzb81c.js} +3 -3
- package/dist/cli/{index-7t3vjw5e.js → index-877ah4kx.js} +1 -1
- package/dist/cli/{index-tncy55bp.js → index-8ehyt3rx.js} +1 -1
- package/dist/cli/{index-gjs5g97v.js → index-91kk2e9j.js} +1 -1
- package/dist/cli/{index-t1s4b5ha.js → index-92v63zzr.js} +688 -637
- package/dist/cli/{index-rkp6jvc3.js → index-aze9n64z.js} +2 -2
- package/dist/cli/{index-wy2f83j5.js → index-d00q9kpq.js} +5 -5
- package/dist/cli/{index-qv1xc6rd.js → index-d0nhz96c.js} +2 -2
- package/dist/cli/{index-hh34tyv7.js → index-dxatvs3f.js} +1 -1
- package/dist/cli/{index-4cv991ra.js → index-f24r6yqk.js} +3 -3
- package/dist/cli/{index-bb3ks0v3.js → index-fgkhpdc6.js} +6 -6
- package/dist/cli/{index-w6j1n5az.js → index-gv00q5gc.js} +1 -1
- package/dist/cli/index-h5sbn1zr.js +1670 -0
- package/dist/cli/{index-et8b70c8.js → index-j2ghsdzz.js} +27 -27
- package/dist/cli/{index-89fpahtg.js → index-j6ewv2pe.js} +6 -1
- package/dist/cli/{index-nfm9f10v.js → index-k3mezk96.js} +1 -1
- package/dist/cli/{index-94pvh7hc.js → index-mg26zd8x.js} +2 -2
- package/dist/cli/{index-pq8ge6fe.js → index-mwdzkr6k.js} +3 -2
- package/dist/cli/{index-b3jfcptk.js → index-n3h6rmbe.js} +1 -1
- package/dist/cli/{index-fkdxfdbx.js → index-p124rnwb.js} +4 -4
- package/dist/cli/{index-1hef020t.js → index-pcwjz5de.js} +1 -1
- package/dist/cli/{index-kym0cctr.js → index-qy3rgmkc.js} +1 -1
- package/dist/cli/{index-d61h3f3h.js → index-s3j9fsab.js} +2 -2
- package/dist/cli/{index-zzhyws9g.js → index-vft1pg34.js} +1 -1
- package/dist/cli/{index-fhnbzz75.js → index-wg98bsxz.js} +2 -2
- package/dist/cli/{index-g0sdhyqk.js → index-y25nsyaq.js} +1 -1
- package/dist/cli/index.js +25 -25
- package/dist/cli/{knowledge-escalator-e815bveb.js → knowledge-escalator-9k0kbx6b.js} +8 -7
- package/dist/cli/{knowledge-events-dz2tyhpw.js → knowledge-events-8nz3sa2d.js} +6 -5
- package/dist/cli/{knowledge-link-zr40rnwr.js → knowledge-link-t94b79qe.js} +5 -4
- package/dist/cli/{knowledge-store-97qr7m9k.js → knowledge-store-esghe3f9.js} +6 -5
- package/dist/cli/{knowledge-validator-xsetvy4v.js → knowledge-validator-bjxag1jp.js} +9 -8
- package/dist/cli/{pending-delegations-0h5b18p7.js → pending-delegations-kshh66s2.js} +3 -3
- package/dist/cli/{pr-subscriptions-jn0h047q.js → pr-subscriptions-5fpzz4f3.js} +3 -3
- package/dist/cli/{runner-deeswadt.js → runner-d820ws7y.js} +5 -5
- package/dist/cli/{scan-cursor-129fwf7e.js → scan-cursor-t212v00f.js} +7 -6
- package/dist/cli/{schema-3xdza5gg.js → schema-11gyrec3.js} +1 -1
- package/dist/cli/{scope-persistence-5xc9ntdh.js → scope-persistence-sjz200dt.js} +4 -4
- package/dist/cli/{skill-generator-8zhprasg.js → skill-generator-a07hpmw6.js} +10 -9
- package/dist/cli/{telemetry-6678gya0.js → telemetry-h7h7f0ez.js} +2 -1
- package/dist/cli/{worktree-collision-ownership-wt7cc850.js → worktree-collision-ownership-xzwab4xt.js} +3 -3
- package/dist/config/constants.d.ts +1 -1
- package/dist/config/evidence-schema.d.ts +103 -103
- package/dist/config/plan-schema.d.ts +10 -10
- package/dist/config/schema.d.ts +8 -8
- package/dist/consensus/contracts.d.ts +3 -3
- package/dist/hooks/guardrails/file-authority.d.ts +20 -1
- package/dist/hooks/hive-promoter.d.ts +1 -1
- package/dist/index.js +320 -321
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/local-jsonl-provider.d.ts +4 -1
- package/dist/memory/redaction.d.ts +91 -0
- package/dist/memory/sqlite-provider.d.ts +5 -1
- package/dist/observability/catalog.d.ts +84 -0
- package/dist/observability/envelope.d.ts +401 -0
- package/dist/observability/ids.d.ts +97 -0
- package/dist/observability/index.d.ts +58 -0
- package/dist/observability/legacy.d.ts +68 -0
- package/dist/observability/observe.d.ts +112 -0
- package/dist/observability/otel-mapping.d.ts +48 -0
- package/dist/observability/relationships.d.ts +44 -0
- package/dist/observability/sampling.d.ts +77 -0
- package/dist/summaries/schema.d.ts +2 -2
- package/dist/telemetry.d.ts +4 -1
- package/dist/utils/arg-hash.d.ts +113 -0
- package/dist/utils/stable-stringify.d.ts +47 -2
- package/package.json +3 -2
- package/dist/cli/index-cze4bq1x.js +0 -358
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Hex width of a W3C `trace-id`. */
|
|
2
|
+
export declare const TRACE_ID_HEX_LENGTH: number;
|
|
3
|
+
/** Hex width of a W3C span id. */
|
|
4
|
+
export declare const SPAN_ID_HEX_LENGTH: number;
|
|
5
|
+
/**
|
|
6
|
+
* Environment variable that overrides the lineage salt.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately NOT read at module scope: reading `process.env` during module
|
|
9
|
+
* evaluation would make the salt depend on import order and would defeat any
|
|
10
|
+
* test that sets it after import. `resolveLineageSalt()` reads it per call.
|
|
11
|
+
*/
|
|
12
|
+
export declare const LINEAGE_SALT_ENV = "SWARM_OBSERVABILITY_LINEAGE_SALT";
|
|
13
|
+
/**
|
|
14
|
+
* Salt used when {@link LINEAGE_SALT_ENV} is unset.
|
|
15
|
+
*
|
|
16
|
+
* A constant default is intentional: lineage refs must be stable for a given
|
|
17
|
+
* path across processes and restarts, otherwise `projectRef` could not correlate
|
|
18
|
+
* two runs of the same project. Operators who want cross-install unlinkability
|
|
19
|
+
* set {@link LINEAGE_SALT_ENV} to a private value.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_LINEAGE_SALT = "opencode-swarm/observability/lineage/v1";
|
|
22
|
+
/** Hex characters retained from the SHA-256 digest for a lineage ref. */
|
|
23
|
+
export declare const PSEUDONYMOUS_REF_LENGTH = 16;
|
|
24
|
+
/** A correlated `traceId` + `spanId` pair produced from one CSPRNG call. */
|
|
25
|
+
export interface TraceAndSpanId {
|
|
26
|
+
readonly traceId: string;
|
|
27
|
+
readonly spanId: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate a W3C-compatible trace id: 32 lowercase hex characters, never
|
|
31
|
+
* all-zero.
|
|
32
|
+
*
|
|
33
|
+
* NAME COLLISION, deliberate to record rather than rename: `newTraceId` in
|
|
34
|
+
* `src/hooks/knowledge-events.ts` has the identical `(): string` signature but
|
|
35
|
+
* a DIFFERENT on-wire shape — a 36-character RFC 4122 UUID, not 32 hex
|
|
36
|
+
* characters. Nothing outside this directory imports this one, and it is no
|
|
37
|
+
* longer re-exported from `src/observability/index.ts`, so the two cannot be
|
|
38
|
+
* confused through the barrel. (`newEventId` is unaffected: both return
|
|
39
|
+
* `randomUUID()`.)
|
|
40
|
+
*/
|
|
41
|
+
export declare function newTraceId(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Generate a W3C-compatible span id: 16 lowercase hex characters, never
|
|
44
|
+
* all-zero.
|
|
45
|
+
*/
|
|
46
|
+
export declare function newSpanId(): string;
|
|
47
|
+
/** Generate an event id (RFC 4122 UUID v4). */
|
|
48
|
+
export declare function newEventId(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Derive a correlated trace/span pair, consuming 24 bytes from the pool above.
|
|
51
|
+
*
|
|
52
|
+
* This exists for the hot path: `createObservation` runs on every `emit()`, and
|
|
53
|
+
* src/telemetry.ts:315-317 documents that path as deliberately frugal.
|
|
54
|
+
*/
|
|
55
|
+
export declare function newTraceAndSpanId(): TraceAndSpanId;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the lineage salt.
|
|
58
|
+
*
|
|
59
|
+
* Reads {@link LINEAGE_SALT_ENV} at call time (never at module scope) and falls
|
|
60
|
+
* back to {@link DEFAULT_LINEAGE_SALT}. Reading an environment variable is not
|
|
61
|
+
* I/O — no file, socket, or subprocess is touched.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveLineageSalt(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Produce a pseudonymous reference for an absolute path.
|
|
66
|
+
*
|
|
67
|
+
* `sha256(salt + NUL + absolutePath)`, truncated to 16 hex characters.
|
|
68
|
+
*
|
|
69
|
+
* Properties this guarantees, and why each matters (issue #2029 item 2 / AC3):
|
|
70
|
+
* - The result never CONTAINS or ENCODES the input path. It is a one-way
|
|
71
|
+
* digest, not an encoding.
|
|
72
|
+
* - Two different project paths carrying the SAME cohort label produce
|
|
73
|
+
* DIFFERENT refs, because the path is part of the digest input.
|
|
74
|
+
* - The result is stable for a given (salt, path) pair, so the same project
|
|
75
|
+
* correlates across processes and restarts.
|
|
76
|
+
*
|
|
77
|
+
* What it does NOT guarantee, stated plainly rather than overclaimed: this is a
|
|
78
|
+
* PSEUDONYM, not an anonymous value, and it is not "irreversible" in the sense
|
|
79
|
+
* that matters. With the PUBLIC {@link DEFAULT_LINEAGE_SALT} the digest input is
|
|
80
|
+
* fully known except for the path, and real paths are low entropy, so a holder
|
|
81
|
+
* of an export can CONFIRM a guessed path by re-hashing candidates and matching
|
|
82
|
+
* the ref. Setting {@link LINEAGE_SALT_ENV} to a private per-install value
|
|
83
|
+
* restores guess-resistance and makes refs unlinkable across installs. The
|
|
84
|
+
* algorithm is deliberately left alone here: deriving and persisting a
|
|
85
|
+
* per-install salt needs init-path I/O, which AGENTS.md invariant 1 forbids, so
|
|
86
|
+
* it belongs with #2047.
|
|
87
|
+
*
|
|
88
|
+
* SCOPE of the AC3 claim: `cohortRef` and `worktreeRef` are computed only when a
|
|
89
|
+
* caller supplies `cohortLabel` / `worktreeId` to `initObservability`. The sole
|
|
90
|
+
* production caller (`src/index.ts:732-744`) supplies neither, so both are
|
|
91
|
+
* `undefined` in every real run today; AC3 is asserted at unit level against
|
|
92
|
+
* this API, not against a production emission path.
|
|
93
|
+
*
|
|
94
|
+
* @param absolutePath - Value to pseudonymize. Never stored or echoed.
|
|
95
|
+
* @param salt - Salt from {@link resolveLineageSalt}.
|
|
96
|
+
*/
|
|
97
|
+
export declare function pseudonymousRef(absolutePath: string, salt: string): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The observability event contract (issue #2029).
|
|
3
|
+
*
|
|
4
|
+
* Reference documentation: `docs/observability-event-contract.md`.
|
|
5
|
+
*
|
|
6
|
+
* ## What this module is
|
|
7
|
+
*
|
|
8
|
+
* A versioned, discriminated, catalogued envelope for every observability record
|
|
9
|
+
* this plugin produces, plus the adapter that projects the existing untyped
|
|
10
|
+
* telemetry payloads onto it. The defect class it closes: *a production
|
|
11
|
+
* telemetry record is written without a versioned, discriminated, catalogued
|
|
12
|
+
* envelope — so a consumer cannot determine producer, schema generation, or
|
|
13
|
+
* unknown-versus-absent, and a new event kind can enter the stream without any
|
|
14
|
+
* registration.*
|
|
15
|
+
*
|
|
16
|
+
* ## The guarantees callers can rely on
|
|
17
|
+
*
|
|
18
|
+
* - **Zero I/O.** No filesystem, network, subprocess, dynamic import, or
|
|
19
|
+
* OpenTelemetry SDK anywhere in this directory. `node:crypto` and `zod` are
|
|
20
|
+
* the only imports. That is what keeps `initObservability` safe on the plugin
|
|
21
|
+
* init path (AGENTS.md invariant 1) and the bundle Node-ESM-portable
|
|
22
|
+
* (invariant 2).
|
|
23
|
+
* - **`createObservation` never throws** — not on a circular object, a
|
|
24
|
+
* function/`Symbol`/`BigInt` payload, an unknown kind, or `null` data. It
|
|
25
|
+
* never serializes, clones, or deep-traverses the payload.
|
|
26
|
+
* - **`legacy.raw` is an ALIAS** to the caller's payload, never a copy. Key
|
|
27
|
+
* order, key collisions and `undefined`-key elision in the written line depend
|
|
28
|
+
* on it.
|
|
29
|
+
* - **Nothing is synthesized.** An identifier the producer does not hold stays
|
|
30
|
+
* `undefined`; an unversioned store records `sourceSchemaVersion: null`
|
|
31
|
+
* (unknown, not zero); an absent field is listed in `legacy.unknown` rather
|
|
32
|
+
* than defaulted.
|
|
33
|
+
* - **Validation returns, never throws.** `validateEventRelationships` and
|
|
34
|
+
* `assertBoundedCardinality` both yield verdicts.
|
|
35
|
+
*
|
|
36
|
+
* ## What it is NOT, stated plainly
|
|
37
|
+
*
|
|
38
|
+
* The envelope is not yet authoritative. `toLegacyTelemetryLine` is a
|
|
39
|
+
* deliberately lossy, legacy-pinned projection: the non-legacy fields
|
|
40
|
+
* (`eventId`, `trace`, `lineage`, `provenance`, `policy`, `writerSequence`,
|
|
41
|
+
* `relationshipViolations`) are currently discarded, and their consumer lands in
|
|
42
|
+
* **#2047**. Consequently `validateEventRelationships` does not bite at runtime
|
|
43
|
+
* today — it bites in unit tests and in the static contract check.
|
|
44
|
+
*/
|
|
45
|
+
export type { CatalogEntry, OtelMappingKind } from './catalog.js';
|
|
46
|
+
export { CATALOG_KINDS, EVENT_CATALOG, getCatalogEntry, } from './catalog.js';
|
|
47
|
+
export type { Lineage, ObservabilityEvent, Outcome, Policy, Provenance, } from './envelope.js';
|
|
48
|
+
export { OBSERVABILITY_SCHEMA_VERSION, ObservabilityEventSchema, } from './envelope.js';
|
|
49
|
+
export type { TraceAndSpanId } from './ids.js';
|
|
50
|
+
export { newEventId, newSpanId, pseudonymousRef, SPAN_ID_HEX_LENGTH, TRACE_ID_HEX_LENGTH, } from './ids.js';
|
|
51
|
+
export { adaptLegacyTelemetryPayload, extractOutcome, extractWorkflowIds, KNOWN_TELEMETRY_KEYS, LEGACY_ADAPTER_RULES, NON_OBJECT_PAYLOAD_MARKER, } from './legacy.js';
|
|
52
|
+
export type { InitObservabilityInput } from './observe.js';
|
|
53
|
+
export { createObservation, initObservability, resetObservabilityForTesting, toLegacyTelemetryLine, } from './observe.js';
|
|
54
|
+
export { mappingForEntry, OPENINFERENCE_ATTRIBUTES, OPENINFERENCE_MAPPING_VERSION, OTEL_GENAI_ATTRIBUTES, OTEL_GENAI_MAPPING_VERSION, } from './otel-mapping.js';
|
|
55
|
+
export type { RelationshipValidationResult } from './relationships.js';
|
|
56
|
+
export { RELATIONSHIP_VIOLATION_CODES, validateEventRelationships, } from './relationships.js';
|
|
57
|
+
export type { CardinalityResult } from './sampling.js';
|
|
58
|
+
export { assertBoundedCardinality, METRIC_LABEL_ALLOWLIST, shouldSample, } from './sampling.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy telemetry adapter (issue #2029 item 4 / AC2).
|
|
3
|
+
*
|
|
4
|
+
* Its real production input is the untyped `data: Record<string, unknown>` that
|
|
5
|
+
* `src/telemetry.ts:emit()` receives from 40+ call sites: caller-ordered,
|
|
6
|
+
* unversioned, with extra and missing fields. That is legacy-shaped input by
|
|
7
|
+
* definition, and adapting it is this module's genuine job.
|
|
8
|
+
*
|
|
9
|
+
* Two hard constraints govern every function here:
|
|
10
|
+
* 1. **Never throw.** `emit()` documents a never-throw guarantee
|
|
11
|
+
* (`src/telemetry.ts:266,294`) and `src/telemetry.test.ts:137-162`
|
|
12
|
+
* exercises circular objects, functions, `Symbol`s and `BigInt`s.
|
|
13
|
+
* 2. **Never deep-traverse, clone, or serialize the payload.** Only
|
|
14
|
+
* `Object.keys` (shallow) and own-key reads. See `LegacyProjectionSchema`
|
|
15
|
+
* for why `raw` must stay an alias.
|
|
16
|
+
*
|
|
17
|
+
* Import rules: no filesystem, network, subprocess, or OTel SDK.
|
|
18
|
+
*/
|
|
19
|
+
import type { LegacyProjection, Outcome, WorkflowIds } from './envelope.js';
|
|
20
|
+
/** The store these payloads are written to. */
|
|
21
|
+
export declare const LEGACY_TELEMETRY_SOURCE_STORE = ".swarm/telemetry.jsonl";
|
|
22
|
+
/** Marker recorded in `legacy.unknown` when the payload is not an object. */
|
|
23
|
+
export declare const NON_OBJECT_PAYLOAD_MARKER = "<non-object-payload>";
|
|
24
|
+
/**
|
|
25
|
+
* Marker recorded when the payload resisted shallow introspection — a Proxy
|
|
26
|
+
* whose `ownKeys` trap throws, or a throwing getter. The payload is still kept
|
|
27
|
+
* by reference; only our description of it is degraded.
|
|
28
|
+
*/
|
|
29
|
+
export declare const INTROSPECTION_FAILED_MARKER = "<payload-introspection-failed>";
|
|
30
|
+
/**
|
|
31
|
+
* The adapter rules, stated as a checkable list.
|
|
32
|
+
*
|
|
33
|
+
* These are the item-4 rules of issue #2029. They are exported as data (not
|
|
34
|
+
* prose in a doc file) so the static contract check and the unit tests can
|
|
35
|
+
* assert against the same statement of intent the implementation follows.
|
|
36
|
+
*/
|
|
37
|
+
export declare const LEGACY_ADAPTER_RULES: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* Payload keys each producer is known to emit.
|
|
40
|
+
*
|
|
41
|
+
* Derived from the emit call sites in source, NOT from captured output: a key
|
|
42
|
+
* whose value is `undefined` is elided by `JSON.stringify`, so a captured line
|
|
43
|
+
* under-reports the producer's key set. Listing those keys here is precisely
|
|
44
|
+
* what makes `legacy.unknown` non-vacuous — `delegation_end` naming `model`,
|
|
45
|
+
* `gate` and `retry_index` lets the adapter say "the producer did not know
|
|
46
|
+
* these" instead of silently omitting them.
|
|
47
|
+
*/
|
|
48
|
+
export declare const KNOWN_TELEMETRY_KEYS: Readonly<Record<string, readonly string[]>>;
|
|
49
|
+
export declare function adaptLegacyTelemetryPayload(kind: string, data: unknown, knownKeys: readonly string[]): LegacyProjection;
|
|
50
|
+
/**
|
|
51
|
+
* Extract recognized correlation IDs from a legacy payload.
|
|
52
|
+
*
|
|
53
|
+
* Shallow, never throws, and NEVER synthesizes: an ID that is absent, empty, or
|
|
54
|
+
* of the wrong type stays `undefined`. Only the mappings a producer actually
|
|
55
|
+
* populates are implemented — `sessionId` to `hostSessionId`, `taskId`, `phase`
|
|
56
|
+
* to `phaseId`, `laneId`, `batchId`. The remaining eight recognized IDs have no
|
|
57
|
+
* current producer, and inventing an extraction for them would manufacture
|
|
58
|
+
* exactly the joins issue #2029 item 2 forbids.
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractWorkflowIds(data: unknown): WorkflowIds;
|
|
61
|
+
/**
|
|
62
|
+
* Extract terminal disposition from a legacy payload.
|
|
63
|
+
*
|
|
64
|
+
* Shallow and never throws. `durationMs` is deliberately never populated: no
|
|
65
|
+
* current producer reports a duration, and deriving one would fabricate a
|
|
66
|
+
* measurement.
|
|
67
|
+
*/
|
|
68
|
+
export declare function extractOutcome(data: unknown): Outcome;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observation construction and the legacy line projection (issue #2029).
|
|
3
|
+
*
|
|
4
|
+
* Import rules: no filesystem, network, subprocess, or OTel SDK. Everything here
|
|
5
|
+
* is synchronous and allocation-only.
|
|
6
|
+
*
|
|
7
|
+
* ## Module state
|
|
8
|
+
*
|
|
9
|
+
* Four process-scoped variables. None is session-keyed, so AGENTS.md invariant 8
|
|
10
|
+
* (session-scoped state must be keyed by `sessionID` and bounded) does not
|
|
11
|
+
* apply: `_provenance` and `_lineage` are single small frozen-by-convention
|
|
12
|
+
* records describing THIS process, `_sampleRate` is a scalar, and
|
|
13
|
+
* `_writerSequence` is a scalar counter. None of them grows with session count,
|
|
14
|
+
* so there is nothing to evict. All four have a reset seam
|
|
15
|
+
* ({@link resetObservabilityForTesting}) so no test's assertions become order
|
|
16
|
+
* dependent.
|
|
17
|
+
*/
|
|
18
|
+
import { type ObservabilityEvent, type Provenance } from './envelope.js';
|
|
19
|
+
/** Violation code stamped on a fallback event. */
|
|
20
|
+
export declare const OBSERVATION_BUILD_FAILED = "observation_build_failed";
|
|
21
|
+
/** Input to {@link initObservability}. */
|
|
22
|
+
export interface InitObservabilityInput {
|
|
23
|
+
/** Absolute project root. Pseudonymized, never stored. */
|
|
24
|
+
directory?: string;
|
|
25
|
+
/** Cohort label (e.g. a swarm id). Pseudonymized together with `directory`. */
|
|
26
|
+
cohortLabel?: string;
|
|
27
|
+
/** Worktree identity. Pseudonymized, never stored. */
|
|
28
|
+
worktreeId?: string;
|
|
29
|
+
provenance?: Provenance;
|
|
30
|
+
sampleRate?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Install process provenance and lineage.
|
|
34
|
+
*
|
|
35
|
+
* Performs ZERO I/O — it hashes strings the caller already holds. That matters
|
|
36
|
+
* for AGENTS.md invariant 1: this runs on the plugin init path, and init-path
|
|
37
|
+
* work must be fast, bounded and side-effect-minimal.
|
|
38
|
+
*
|
|
39
|
+
* Lineage refs are computed ONCE here and reused by every subsequent
|
|
40
|
+
* observation. Computing them per-emit would put three SHA-256 digests on the
|
|
41
|
+
* hot path for values that never change within a process.
|
|
42
|
+
*
|
|
43
|
+
* NEVER THROWS. The whole body is guarded: a failure here must not prevent the
|
|
44
|
+
* plugin manifest from being returned (invariant 1, fail-open).
|
|
45
|
+
*/
|
|
46
|
+
export declare function initObservability(input: InitObservabilityInput): void;
|
|
47
|
+
/**
|
|
48
|
+
* Build the canonical observation for one emit.
|
|
49
|
+
*
|
|
50
|
+
* **NEVER THROWS.** Not on a circular object, not on a function / `Symbol` /
|
|
51
|
+
* `BigInt` payload, not on an unknown kind, not on `null` or `undefined` data.
|
|
52
|
+
* It never calls `JSON.stringify`, never clones, and never deep-traverses
|
|
53
|
+
* `data` — `src/telemetry.test.ts:137-162` emits exactly those payloads and
|
|
54
|
+
* asserts `emit()` does not throw, and `src/telemetry.ts:266` documents the
|
|
55
|
+
* never-throw guarantee. If anything inside fails, a minimal valid event is
|
|
56
|
+
* returned carrying `relationshipViolations: ['observation_build_failed']`.
|
|
57
|
+
*
|
|
58
|
+
* An UNCATALOGUED kind is classified (`category: 'unrecognized'`), never
|
|
59
|
+
* dropped. Preventing new unrecognized kinds is the static contract check's job,
|
|
60
|
+
* not a runtime drop.
|
|
61
|
+
*
|
|
62
|
+
* `occurredAt` is set equal to `observedAt`. That is a recorded finding, not an
|
|
63
|
+
* invention: no current producer supplies a distinct time at which the described
|
|
64
|
+
* thing happened, so claiming one would fabricate precision the source does not
|
|
65
|
+
* have.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createObservation(kind: string, data: unknown): ObservabilityEvent;
|
|
68
|
+
/**
|
|
69
|
+
* Project a canonical event onto the legacy `.swarm/telemetry.jsonl` record.
|
|
70
|
+
*
|
|
71
|
+
* ## This projection is LOSSY and LEGACY-PINNED
|
|
72
|
+
*
|
|
73
|
+
* The envelope's non-legacy fields — `eventId`, `trace` (`traceId`, `spanId`,
|
|
74
|
+
* `parentSpanId`, `links`), `lineage`, `provenance`, `policy`, `writerSequence`,
|
|
75
|
+
* `schemaVersion` and `relationshipViolations` — are **currently DISCARDED**.
|
|
76
|
+
* Nothing in this change consumes them; their consumer lands in **#2047**. That
|
|
77
|
+
* is stated here rather than buried: a reader of this function must not conclude
|
|
78
|
+
* the written line is the canonical record. It is not.
|
|
79
|
+
*
|
|
80
|
+
* ## Byte-for-byte preservation
|
|
81
|
+
*
|
|
82
|
+
* The caller's object is spread **LAST**, exactly as the pre-change inline line
|
|
83
|
+
* construction in `src/telemetry.ts` `emit()` did. Three properties depend on
|
|
84
|
+
* that ordering, and all three are observable in
|
|
85
|
+
* `tests/fixtures/observability/telemetry-lines-golden.json` (issue #2029 item
|
|
86
|
+
* 5 arm (a) — "preserve the existing output"):
|
|
87
|
+
*
|
|
88
|
+
* 1. **Caller key order** is preserved after `timestamp` and `event`.
|
|
89
|
+
* 2. **Caller key collisions win on value.** `conflict-resolution.ts:55-66`
|
|
90
|
+
* supplies its own `timestamp` and `type`; the caller's `timestamp` value
|
|
91
|
+
* overwrites the envelope's while the key keeps position 1.
|
|
92
|
+
* 3. **`undefined`-key elision** is unchanged, because the caller's own object
|
|
93
|
+
* is spread rather than a reconstruction of it.
|
|
94
|
+
*
|
|
95
|
+
* `timestamp` is taken from `event.observedAt` — a real, load-bearing data
|
|
96
|
+
* dependency on the canonical event, which is why this composition is not the
|
|
97
|
+
* identity function on the emit path.
|
|
98
|
+
*
|
|
99
|
+
* A non-object, `null`, or array payload yields just `{ timestamp, event }`,
|
|
100
|
+
* matching what `JSON.stringify({ timestamp, event, ...data })` produces for
|
|
101
|
+
* those inputs.
|
|
102
|
+
*/
|
|
103
|
+
export declare function toLegacyTelemetryLine(event: ObservabilityEvent): Record<string, unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* Reset all four module-scoped variables.
|
|
106
|
+
*
|
|
107
|
+
* Called alongside `resetTelemetryForTesting` so no test's assertions become
|
|
108
|
+
* dependent on how many events an earlier test emitted.
|
|
109
|
+
*
|
|
110
|
+
* @internal - For testing only
|
|
111
|
+
*/
|
|
112
|
+
export declare function resetObservabilityForTesting(): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External-convention attribute mappings (issue #2029 item 6).
|
|
3
|
+
*
|
|
4
|
+
* These are INERT LOOKUP TABLES. There is no OpenTelemetry SDK dependency, no
|
|
5
|
+
* exporter, and no runtime consumer in this change: the tables are consumed as
|
|
6
|
+
* data by the static contract check (which asserts every catalog entry records a
|
|
7
|
+
* mapping decision) and by `docs/observability-event-contract.md`. The runtime
|
|
8
|
+
* consumer lands in #2049.
|
|
9
|
+
*
|
|
10
|
+
* Import rules: no filesystem, network, subprocess, or OTel SDK.
|
|
11
|
+
*/
|
|
12
|
+
import type { OtelMappingKind } from './catalog.js';
|
|
13
|
+
/**
|
|
14
|
+
* OpenTelemetry semantic-convention generation these GenAI attribute names track.
|
|
15
|
+
*
|
|
16
|
+
* Pinned SEPARATELY from `OBSERVABILITY_SCHEMA_VERSION`. That separation is the
|
|
17
|
+
* whole point of issue #2029 item 6: the GenAI conventions are an external,
|
|
18
|
+
* still-unstable vocabulary. If they were versioned together, an upstream rename
|
|
19
|
+
* would force a bump of the internal envelope version and every consumer would
|
|
20
|
+
* be told the domain shape changed when nothing about the domain changed.
|
|
21
|
+
* External convention churn must never change internal domain state.
|
|
22
|
+
*/
|
|
23
|
+
export declare const OTEL_GENAI_MAPPING_VERSION = "1.29.0";
|
|
24
|
+
/**
|
|
25
|
+
* OpenInference specification generation these attribute names track. Pinned
|
|
26
|
+
* separately from both {@link OTEL_GENAI_MAPPING_VERSION} and
|
|
27
|
+
* `OBSERVABILITY_SCHEMA_VERSION`, for the same reason.
|
|
28
|
+
*/
|
|
29
|
+
export declare const OPENINFERENCE_MAPPING_VERSION = "0.1.14";
|
|
30
|
+
/**
|
|
31
|
+
* Internal envelope path to OTel GenAI attribute name.
|
|
32
|
+
*
|
|
33
|
+
* Keys are dotted paths into `ObservabilityEvent`. `legacy.raw.*` paths reach
|
|
34
|
+
* into the aliased producer payload, which is where token counts and the
|
|
35
|
+
* resolved model live today.
|
|
36
|
+
*/
|
|
37
|
+
export declare const OTEL_GENAI_ATTRIBUTES: Readonly<Record<string, string>>;
|
|
38
|
+
/** Internal envelope path to OpenInference attribute name. */
|
|
39
|
+
export declare const OPENINFERENCE_ATTRIBUTES: Readonly<Record<string, string>>;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the attribute table a catalog entry projects onto.
|
|
42
|
+
*
|
|
43
|
+
* `'none'` is a real, recorded decision — "this kind has no external
|
|
44
|
+
* equivalent" — not a missing value. It returns an empty table, never
|
|
45
|
+
* `undefined`, so a consumer never has to distinguish "no mapping" from "not
|
|
46
|
+
* asked".
|
|
47
|
+
*/
|
|
48
|
+
export declare function mappingForEntry(mapping: OtelMappingKind): Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ObservabilityEvent } from './envelope.js';
|
|
2
|
+
/** Verdict from {@link validateEventRelationships}. */
|
|
3
|
+
export type RelationshipValidationResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
violations: string[];
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Stable violation codes.
|
|
11
|
+
*
|
|
12
|
+
* Codes are machine-readable and must not be renamed once emitted — they are the
|
|
13
|
+
* join key a downstream consumer will group on. The suffix after `:` is the
|
|
14
|
+
* offending identifier or link index.
|
|
15
|
+
*/
|
|
16
|
+
export declare const RELATIONSHIP_VIOLATION_CODES: Readonly<{
|
|
17
|
+
unknownKind: "unknown_kind";
|
|
18
|
+
requiredWorkflowIdMissing: "required_workflow_id_missing";
|
|
19
|
+
forbiddenWorkflowIdPresent: "forbidden_workflow_id_present";
|
|
20
|
+
parentSpanNotAllowed: "parent_span_not_allowed";
|
|
21
|
+
parentSpanMissing: "parent_span_missing";
|
|
22
|
+
malformedParentSpanId: "malformed_parent_span_id";
|
|
23
|
+
linksNotAllowed: "links_not_allowed";
|
|
24
|
+
malformedLinkTraceId: "malformed_link_trace_id";
|
|
25
|
+
malformedLinkSpanId: "malformed_link_span_id";
|
|
26
|
+
validationFailed: "relationship_validation_failed";
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Validate an event against its catalogued relationship rules.
|
|
30
|
+
*
|
|
31
|
+
* Checks, in order:
|
|
32
|
+
* (a) the kind is catalogued;
|
|
33
|
+
* (b) every `requiredWorkflowIds` entry is present;
|
|
34
|
+
* (c) no `forbiddenWorkflowIds` entry is present (a present one means an ID
|
|
35
|
+
* was manufactured upstream — issue #2029 item 2);
|
|
36
|
+
* (d) a parent span is absent when the kind does not take one;
|
|
37
|
+
* (e) a parent span is present when the kind requires one;
|
|
38
|
+
* (f) a present parent span is a well-formed W3C span id;
|
|
39
|
+
* (g) links are absent when the kind does not allow them;
|
|
40
|
+
* (h) every link carries a well-formed trace/span id pair.
|
|
41
|
+
*
|
|
42
|
+
* @returns `{ ok: true }` or `{ ok: false, violations }`. Never throws.
|
|
43
|
+
*/
|
|
44
|
+
export declare function validateEventRelationships(event: ObservabilityEvent): RelationshipValidationResult;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sampling and metric-cardinality policy (issue #2029).
|
|
3
|
+
*
|
|
4
|
+
* Import rules: no filesystem, network, subprocess, or OTel SDK.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Default sample rate.
|
|
8
|
+
*
|
|
9
|
+
* `1` = sample everything. This PR introduces no dropping: a drop that no
|
|
10
|
+
* consumer can see is exactly the silent data loss the issue names, and the
|
|
11
|
+
* consumer that would make a drop observable lands in #2047.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_SAMPLE_RATE = 1;
|
|
14
|
+
/**
|
|
15
|
+
* Decide whether a trace is sampled.
|
|
16
|
+
*
|
|
17
|
+
* **Deterministic by construction.** The decision is a pure function of the
|
|
18
|
+
* trace id and the rate: the LAST 8 hex characters of the trace id are read as
|
|
19
|
+
* an unsigned integer and compared against `rate * 0xffffffff`. The same trace
|
|
20
|
+
* therefore samples identically in every process, on every host, and across
|
|
21
|
+
* restarts — no shared state, no RNG, no coordination. That is what makes a
|
|
22
|
+
* sampled distributed trace complete rather than a set of disconnected
|
|
23
|
+
* fragments, and it is why the decision is not `Math.random()`.
|
|
24
|
+
*
|
|
25
|
+
* **CAVEAT — that property is currently VACUOUS in this system, and saying so
|
|
26
|
+
* is the point.** `createObservation` mints a FRESH `traceId` per event, with
|
|
27
|
+
* `links: []` and no `parentSpanId`, so every event today is its own
|
|
28
|
+
* single-span trace. At a rate below 1 the decision is therefore independent
|
|
29
|
+
* PER EVENT, not per logical trace: a "trace" never spans two events, so
|
|
30
|
+
* nothing can be kept whole or torn apart. Trace continuation — propagating one
|
|
31
|
+
* trace id across the events that belong together — is #2047's work, and only
|
|
32
|
+
* then does trace-coherent sampling become a real property rather than a
|
|
33
|
+
* latent one. Nothing is dropped today regardless: {@link DEFAULT_SAMPLE_RATE}
|
|
34
|
+
* is `1`.
|
|
35
|
+
*
|
|
36
|
+
* **Fail-open.** Every unusable input returns `true`. Dropping an event because
|
|
37
|
+
* an id was malformed would lose data silently, which is the failure mode this
|
|
38
|
+
* contract exists to prevent. A non-finite rate also fails open, for the same
|
|
39
|
+
* reason: `NaN` would otherwise fall through both bounds and make every
|
|
40
|
+
* comparison `false`, silently dropping everything.
|
|
41
|
+
*
|
|
42
|
+
* @param traceId - 32 lowercase hex characters.
|
|
43
|
+
* @param rate - `>= 1` samples everything, `<= 0` samples nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function shouldSample(traceId: string, rate: number): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The only labels permitted on a metric.
|
|
48
|
+
*
|
|
49
|
+
* Every member is bounded: a small, enumerable set of values that cannot grow
|
|
50
|
+
* with traffic. The rule this enforces, from issue #2029: **IDs, paths, users,
|
|
51
|
+
* tasks, and repositories belong in traces and logs, not metric labels.** A
|
|
52
|
+
* single unbounded label multiplies the time-series count by its cardinality and
|
|
53
|
+
* takes a metrics backend down; the trace already carries that detail, keyed to
|
|
54
|
+
* the same event.
|
|
55
|
+
*/
|
|
56
|
+
export declare const METRIC_LABEL_ALLOWLIST: readonly string[];
|
|
57
|
+
/** Verdict from {@link assertBoundedCardinality}. */
|
|
58
|
+
export type CardinalityResult = {
|
|
59
|
+
ok: true;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
violations: string[];
|
|
63
|
+
};
|
|
64
|
+
/** Stable violation codes, suffixed with the offending label. */
|
|
65
|
+
export declare const CARDINALITY_VIOLATION_CODES: Readonly<{
|
|
66
|
+
notAllowlisted: "label_not_allowlisted";
|
|
67
|
+
highCardinalityShape: "high_cardinality_label_shape";
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Check a metric's label set against the allowlist and against high-cardinality
|
|
71
|
+
* shapes.
|
|
72
|
+
*
|
|
73
|
+
* RETURNS a verdict; never throws. A label can produce both codes — the shape
|
|
74
|
+
* check runs independently of the allowlist check so that a newly-allowlisted
|
|
75
|
+
* label with an unbounded shape is still reported.
|
|
76
|
+
*/
|
|
77
|
+
export declare function assertBoundedCardinality(labels: readonly string[]): CardinalityResult;
|
|
@@ -95,10 +95,10 @@ export declare const PhaseArchitectureSummarySchema: z.ZodObject<{
|
|
|
95
95
|
export type PhaseArchitectureSummary = z.infer<typeof PhaseArchitectureSummarySchema>;
|
|
96
96
|
export declare const SupervisorFindingSchema: z.ZodObject<{
|
|
97
97
|
severity: z.ZodEnum<{
|
|
98
|
+
critical: "critical";
|
|
98
99
|
low: "low";
|
|
99
100
|
medium: "medium";
|
|
100
101
|
high: "high";
|
|
101
|
-
critical: "critical";
|
|
102
102
|
}>;
|
|
103
103
|
category: z.ZodString;
|
|
104
104
|
agents: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -135,10 +135,10 @@ export declare const ArchitectureSupervisorReportSchema: z.ZodObject<{
|
|
|
135
135
|
}>;
|
|
136
136
|
findings: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
137
137
|
severity: z.ZodEnum<{
|
|
138
|
+
critical: "critical";
|
|
138
139
|
low: "low";
|
|
139
140
|
medium: "medium";
|
|
140
141
|
high: "high";
|
|
141
|
-
critical: "critical";
|
|
142
142
|
}>;
|
|
143
143
|
category: z.ZodString;
|
|
144
144
|
agents: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createObservation, toLegacyTelemetryLine } from './observability/index.js';
|
|
1
2
|
import type { DelegationCostFields } from './services/cost-accounting.js';
|
|
2
3
|
export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activated' | 'delegation_begin' | 'delegation_end' | 'task_state_changed' | 'gate_passed' | 'gate_failed' | 'gate_parse_error' | 'reviewer_gate_decision' | 'phase_changed' | 'budget_updated' | 'model_fallback' | 'hard_limit_hit' | 'revision_limit_hit' | 'loop_detected' | 'no_op_strong_warning' | 'scope_violation' | 'qa_skip_violation' | 'heartbeat' | 'turbo_mode_changed' | 'auto_oversight_escalation' | 'environment_detected' | 'evidence_lock_acquired' | 'evidence_lock_contended' | 'evidence_lock_stale_recovered' | 'plan_ledger_cas_retry' | 'plan_md_write_failed' | 'snapshot_failed' | 'gate_denial_loop'
|
|
3
4
|
/**
|
|
@@ -25,7 +26,7 @@ export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activa
|
|
|
25
26
|
* `prm_hard_stop` TRIGGER emitted by `src/prm/escalation.ts`. A trigger with
|
|
26
27
|
* no matching delivery means the containment never reached the agent.
|
|
27
28
|
*/
|
|
28
|
-
| 'prm_hard_stop_delivered';
|
|
29
|
+
| 'prm_hard_stop_delivered' | 'agent_conflict_detected';
|
|
29
30
|
/** Stable classification for how a reviewer-gate decision was established. */
|
|
30
31
|
export type ReviewerGateEvidenceKind = 'genuine' | 'fallback' | 'data_quality' | 'block';
|
|
31
32
|
/**
|
|
@@ -157,4 +158,6 @@ export declare const _internals: {
|
|
|
157
158
|
emit: typeof emit;
|
|
158
159
|
rotateTelemetryIfNeeded: typeof rotateTelemetryIfNeeded;
|
|
159
160
|
heartbeatListenerCount: () => number;
|
|
161
|
+
createObservation: typeof createObservation;
|
|
162
|
+
toLegacyTelemetryLine: typeof toLegacyTelemetryLine;
|
|
160
163
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared primitives for hashing tool-call arguments (issue #2060 follow-ups
|
|
3
|
+
* F-009 / F-010).
|
|
4
|
+
*
|
|
5
|
+
* # Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Two subsystems hash tool arguments to detect "the agent is repeating
|
|
8
|
+
* itself": `hashArgsForSpiral` in `hooks/adversarial-detector.ts` (spiral
|
|
9
|
+
* advisory) and `hashArgs` in `hooks/guardrails/file-authority.ts` (which
|
|
10
|
+
* feeds the consecutive-repetition circuit breaker in
|
|
11
|
+
* `hooks/guardrails/tool-before.ts` — a path that THROWS, not warns).
|
|
12
|
+
*
|
|
13
|
+
* Both need the same two primitives. They live here rather than as two inline
|
|
14
|
+
* copies for the same reason `src/utils/stable-stringify.ts` was extracted: a
|
|
15
|
+
* bug class that exists at two call sites must be fixed once, in one place,
|
|
16
|
+
* and tested once. Keep them here; do not re-inline a copy at a call site.
|
|
17
|
+
*
|
|
18
|
+
* The two call sites differ only in the OUTPUT SHAPE they need — the detector
|
|
19
|
+
* wants a compact base-36 string, file-authority wants a `number` — so
|
|
20
|
+
* `boundedBunHash` returns `bigint` (matching `bunHash`) and each caller
|
|
21
|
+
* formats it.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Cap on the amount of input fed into `bunHash`.
|
|
25
|
+
*
|
|
26
|
+
* Both call sites run synchronously on a per-tool-call hot path, and payloads
|
|
27
|
+
* (patch bodies, file writes) can reach ~1 MB. Node's `bunHash` fallback
|
|
28
|
+
* (djb2, see `bun-compat.ts`) is genuinely reachable in production — the
|
|
29
|
+
* OpenCode plugin host and the Desktop sidecar can run under Node — and is
|
|
30
|
+
* O(n): ~141 ms for a 2 MB input versus ~5 ms for 64 KB. The threat model is
|
|
31
|
+
* "same tool, same args, repeatedly", not adversarial collision resistance,
|
|
32
|
+
* so bounding the hashed input keeps worst-case per-call cost flat.
|
|
33
|
+
*
|
|
34
|
+
* NOTE ON UNITS: this bounds `String.prototype.length`, i.e. UTF-16 code
|
|
35
|
+
* units, not encoded bytes. It is a cost bound, not a byte-exact limit; a
|
|
36
|
+
* non-BMP-heavy string can encode to more bytes than the name suggests. That
|
|
37
|
+
* is fine — the point is a fixed ceiling, and the ceiling is fixed.
|
|
38
|
+
*/
|
|
39
|
+
export declare const HASH_INPUT_CAP_BYTES: number;
|
|
40
|
+
/**
|
|
41
|
+
* Reduces an arbitrarily long string to a bounded, length-prefixed
|
|
42
|
+
* head-and-tail sample suitable as hash input.
|
|
43
|
+
*
|
|
44
|
+
* ## Why not a bare prefix
|
|
45
|
+
*
|
|
46
|
+
* The obvious bound — `input.slice(0, CAP)` — makes every pair of inputs that
|
|
47
|
+
* share a `CAP`-length prefix hash identically. For the circuit-breaker call
|
|
48
|
+
* site that is a false-positive engine: ten consecutive large writes that
|
|
49
|
+
* share a boilerplate header but differ in their (appended) bodies look like
|
|
50
|
+
* ten identical calls and the breaker throws. Sampling the head AND the tail
|
|
51
|
+
* removes the whole append-collision class, and sampling the head removes the
|
|
52
|
+
* prepend-collision class, at exactly the same bounded cost.
|
|
53
|
+
*
|
|
54
|
+
* ## Why the length prefix is load-bearing
|
|
55
|
+
*
|
|
56
|
+
* It is not decoration. Without it the two branches below can collide across
|
|
57
|
+
* the cap boundary: an input of exactly `CAP` characters passes through
|
|
58
|
+
* untransformed, while a longer input produces a `CAP`-character head+tail
|
|
59
|
+
* concatenation — two different inputs, one identical hash input. Prefixing
|
|
60
|
+
* the true length separates the classes. It also makes the under-cap branch
|
|
61
|
+
* injective, so this transform adds ZERO collisions for inputs at or below
|
|
62
|
+
* the cap.
|
|
63
|
+
*
|
|
64
|
+
* ## Residual, accepted lossiness
|
|
65
|
+
*
|
|
66
|
+
* Two inputs longer than the cap collide only if they have the SAME total
|
|
67
|
+
* length AND the same first `CAP/2` characters AND the same last `CAP/2`
|
|
68
|
+
* characters, differing only in the discarded middle. That is unavoidable for
|
|
69
|
+
* any fixed-cost sampler and is the deliberate trade documented on
|
|
70
|
+
* `HASH_INPUT_CAP_BYTES`.
|
|
71
|
+
*
|
|
72
|
+
* Cannot throw: `String.prototype.slice` and template interpolation of a
|
|
73
|
+
* number are total.
|
|
74
|
+
*/
|
|
75
|
+
export declare function sampleForHash(input: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* `bunHash` over a bounded head+tail sample of `input`.
|
|
78
|
+
*
|
|
79
|
+
* Returns `bigint` (the `bunHash` shape) so each caller can format it for its
|
|
80
|
+
* own storage: the detector uses `.toString(36)`, file-authority uses
|
|
81
|
+
* `Number(...)`.
|
|
82
|
+
*
|
|
83
|
+
* Cannot throw for a string input: `sampleForHash` is total and `bunHash`
|
|
84
|
+
* on a string is `TextEncoder.encode` plus BigInt arithmetic (or `Bun.hash`)
|
|
85
|
+
* — no throwing path. Callers rely on this: both use it inside a `catch`
|
|
86
|
+
* block, where a second throw would escape into a hook.
|
|
87
|
+
*/
|
|
88
|
+
export declare function boundedBunHash(input: string): bigint;
|
|
89
|
+
/**
|
|
90
|
+
* Shallow, non-recursive structural summary of a value's own enumerable keys,
|
|
91
|
+
* used only as a fallback discriminator when `stableCanonicalStringify`
|
|
92
|
+
* throws (issue #2060 follow-up F-009).
|
|
93
|
+
*
|
|
94
|
+
* Before this existed, both call sites answered an unstringifiable argument
|
|
95
|
+
* with a CONSTANT ('h:fallback' / `0`). That made every distinct-but-
|
|
96
|
+
* unserializable argument collide, so N consecutive calls with genuinely
|
|
97
|
+
* different arguments looked identical and fired a false positive — a spiral
|
|
98
|
+
* advisory in one case, a thrown circuit breaker in the other.
|
|
99
|
+
*
|
|
100
|
+
* Deliberately shallow (no recursion into nested values) so it cannot itself
|
|
101
|
+
* throw on the very inputs that broke `stableCanonicalStringify`: recursing
|
|
102
|
+
* into a cyclic reference would revisit the cycle, and BigInt values are
|
|
103
|
+
* handled fine by `typeof` / `String()` (unlike `JSON.stringify`, which
|
|
104
|
+
* throws on them). `Object.keys` and `typeof` cannot throw for ordinary
|
|
105
|
+
* objects; the outer try/catch guards only against exotic Proxy traps, and
|
|
106
|
+
* even then identical arguments still collide, so true-positive repetition
|
|
107
|
+
* detection is preserved.
|
|
108
|
+
*
|
|
109
|
+
* This is a DISCRIMINATOR, not an identity function. It is only ever used to
|
|
110
|
+
* break up false collisions on a detection path — never as a fail-closed
|
|
111
|
+
* identity (see the caller guidance on `stableCanonicalStringify`).
|
|
112
|
+
*/
|
|
113
|
+
export declare function coarseObjectDiscriminator(args: unknown): string;
|