crumbtrail-core 0.2.0 → 0.2.1

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/index.cjs CHANGED
@@ -4634,21 +4634,139 @@ function inferIntent(evidence, commits) {
4634
4634
  var FUSION_SCHEMA_VERSION = "fusion.v1";
4635
4635
  function assembleBundle(input) {
4636
4636
  const evidence = rankEvidence(input.symptom, input.evidence);
4637
- const hypotheses = classifyHypotheses(
4637
+ const classified = classifyHypotheses(
4638
4638
  input.symptom,
4639
4639
  input.evidence,
4640
4640
  input.intent
4641
4641
  );
4642
4642
  const gaps = input.gaps ?? [];
4643
+ const located = input.located;
4644
+ const hypotheses = classified.map((hypothesis) => {
4645
+ const verification = deriveVerification(hypothesis, evidence);
4646
+ return verification.length > 0 ? { ...hypothesis, verification } : hypothesis;
4647
+ });
4648
+ const contextCompleteness = deriveContextCompleteness(
4649
+ evidence,
4650
+ gaps,
4651
+ hypotheses,
4652
+ located
4653
+ );
4654
+ const escalation = deriveEscalation(contextCompleteness, hypotheses);
4643
4655
  return {
4644
4656
  schemaVersion: FUSION_SCHEMA_VERSION,
4645
4657
  symptom: input.symptom,
4646
4658
  evidence,
4647
4659
  opinion: { stance: "advisory", hypotheses },
4648
4660
  gaps,
4649
- directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
4661
+ directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps),
4662
+ contextCompleteness,
4663
+ escalation,
4664
+ ...located ? { located } : {}
4650
4665
  };
4651
4666
  }
4667
+ function clamp01(value) {
4668
+ if (Number.isNaN(value)) return 0;
4669
+ return Math.max(0, Math.min(1, value));
4670
+ }
4671
+ var COMPLETENESS_LANES = [
4672
+ "network",
4673
+ "db",
4674
+ "flow",
4675
+ "browser",
4676
+ "env"
4677
+ ];
4678
+ function deriveContextCompleteness(evidence, gaps, hypotheses, located) {
4679
+ const lanesPresent = new Set(evidence.map((item) => item.lane));
4680
+ const informativePresent = COMPLETENESS_LANES.filter(
4681
+ (lane) => lanesPresent.has(lane)
4682
+ );
4683
+ const breadth = Math.min(1, informativePresent.length / 3);
4684
+ const volume = Math.min(1, evidence.length / 5);
4685
+ const top = hypotheses[0];
4686
+ const hypothesisStrength = !top || top.kind === "inconclusive" ? 0 : top.confidence;
4687
+ let score = 0.4 * breadth + 0.25 * volume + 0.35 * hypothesisStrength;
4688
+ if (located) {
4689
+ score = located.outcome === "matched" ? 0.85 * score + 0.15 * clamp01(located.confidence) : score * 0.6;
4690
+ }
4691
+ const hardGaps = gaps.filter((gap) => gap.kind === "source-unavailable").length;
4692
+ const softGaps = gaps.length - hardGaps;
4693
+ const gapPenalty = Math.min(0.5, softGaps * 0.1 + hardGaps * 0.3);
4694
+ score = clamp01(score - gapPenalty);
4695
+ const level = score < 0.34 ? "low" : score < 0.67 ? "medium" : "high";
4696
+ const reasons = [];
4697
+ const missingLanes = COMPLETENESS_LANES.filter(
4698
+ (lane) => !lanesPresent.has(lane)
4699
+ );
4700
+ if (informativePresent.length === 0) {
4701
+ reasons.push("no network/db/flow/browser/env evidence captured");
4702
+ } else if (missingLanes.length > 0) {
4703
+ reasons.push(`missing evidence lanes: ${missingLanes.join(", ")}`);
4704
+ }
4705
+ if (evidence.length > 0 && evidence.length < 3) {
4706
+ reasons.push(`thin evidence (${evidence.length} item(s))`);
4707
+ }
4708
+ if (hardGaps > 0) reasons.push(`source unavailable for ${hardGaps} lane(s)`);
4709
+ if (softGaps > 0) reasons.push(`${softGaps} evidence gap(s)`);
4710
+ if (located?.outcome === "inconclusive") {
4711
+ reasons.push("incident location inconclusive");
4712
+ }
4713
+ if (!top || top.kind === "inconclusive") {
4714
+ reasons.push("no distinguishing hypothesis");
4715
+ }
4716
+ return { score, level, reasons };
4717
+ }
4718
+ function pkString(pk) {
4719
+ return Object.keys(pk).sort().map((key) => `${key}=${String(pk[key])}`).join(", ");
4720
+ }
4721
+ function deriveVerification(hypothesis, evidence) {
4722
+ if (hypothesis.kind === "inconclusive") return [];
4723
+ const cited = new Set(hypothesis.evidenceIds);
4724
+ const out = [];
4725
+ for (const item of evidence) {
4726
+ if (!cited.has(item.id)) continue;
4727
+ const ref = item.ref;
4728
+ if (item.lane === "db" && ref.table && ref.pk && Object.keys(ref.pk).length > 0) {
4729
+ out.push({
4730
+ observation: `row in ${ref.table} (${pkString(ref.pk)}) matches the intended post-fix state`,
4731
+ evidenceIds: [item.id],
4732
+ how: "db"
4733
+ });
4734
+ } else if (ref.requestId) {
4735
+ const sigPart = ref.sig ? ` for signature ${ref.sig}` : "";
4736
+ out.push({
4737
+ observation: `request ${ref.requestId} succeeds${sigPart} on a fresh run (no error response)`,
4738
+ evidenceIds: [item.id],
4739
+ how: "request"
4740
+ });
4741
+ } else if (ref.sig) {
4742
+ out.push({
4743
+ observation: `error signature ${ref.sig} no longer appears in a fresh session over the same route`,
4744
+ evidenceIds: [item.id],
4745
+ how: "session"
4746
+ });
4747
+ }
4748
+ }
4749
+ return out;
4750
+ }
4751
+ function deriveEscalation(completeness, hypotheses) {
4752
+ const allInconclusive = hypotheses.length > 0 && hypotheses.every((hypothesis) => hypothesis.kind === "inconclusive");
4753
+ const recommended = completeness.level === "low" || allInconclusive;
4754
+ if (!recommended) return { recommended: false, when: [] };
4755
+ const when = [
4756
+ "if you cannot reproduce the symptom via the anchored request or session, stop and request human triage \u2014 do not widen the search"
4757
+ ];
4758
+ if (completeness.level === "low") {
4759
+ when.push(
4760
+ "context is thin: confirm the missing evidence lanes before acting on the top hypothesis"
4761
+ );
4762
+ }
4763
+ if (allInconclusive) {
4764
+ when.push(
4765
+ "no hypothesis is distinguished; treat the listed causes as equally unproven"
4766
+ );
4767
+ }
4768
+ return { recommended, when };
4769
+ }
4652
4770
  var LANE_PRIOR = {
4653
4771
  db: 0.2,
4654
4772
  network: 0.2,
package/dist/index.d.cts CHANGED
@@ -831,6 +831,76 @@ interface Hypothesis {
831
831
  rationale: string;
832
832
  /** EvidenceItem.id values backing this hypothesis. */
833
833
  evidenceIds: string[];
834
+ /**
835
+ * Advisory, additive: concrete observations that would confirm a fix for
836
+ * THIS hypothesis worked. Emitted only for non-inconclusive hypotheses whose
837
+ * cited evidence carries a concrete anchor (signature / requestId /
838
+ * table+pk); absent otherwise. Sparse-and-concrete by design — never vacuous
839
+ * prose like "verify the bug is gone". See {@link Verification}.
840
+ */
841
+ verification?: Verification[];
842
+ }
843
+ /**
844
+ * A concrete, post-fix observation that would confirm a hypothesis's fix
845
+ * worked. `observation` always names a concrete signal (an error signature, a
846
+ * request id, or a table + primary key) — the derivation emits nothing when it
847
+ * cannot anchor to one, so an emitted verification is never a vacuous
848
+ * restatement.
849
+ */
850
+ interface Verification {
851
+ /** Names a concrete signal — signature, request id, or table/pk. */
852
+ observation: string;
853
+ /** EvidenceItem.id values this observation is anchored to. */
854
+ evidenceIds: string[];
855
+ how: "session" | "request" | "db";
856
+ }
857
+ /**
858
+ * Where the incident was located, when a locate ran. Advisory and optional
859
+ * (absent for explicit baseline/current comparison bundles that never locate).
860
+ * Shape shared with the node locate engine's `LocateMatch` and War-game 02's
861
+ * deterministic token join — `method` distinguishes the two so 02 can populate
862
+ * "token" without a schema change (War-game 02 Fork C: one definition, reused).
863
+ */
864
+ interface Located {
865
+ outcome: "matched" | "inconclusive";
866
+ /** 0..1 locate confidence. */
867
+ confidence: number;
868
+ /**
869
+ * How the incident was located. "fuzzy" = the scored locate engine;
870
+ * "token" = a deterministic token join (War-game 02). Absent when a caller
871
+ * supplies neither.
872
+ */
873
+ method?: "fuzzy" | "token";
874
+ /** The matched session, present ONLY when outcome === "matched". Never fabricated. */
875
+ sessionId?: string;
876
+ reasons?: string[];
877
+ }
878
+ /**
879
+ * Bundle-level answer to "how much context do we actually have here?" — the
880
+ * LOW_CONTEXT signal. Derived purely from the assembled bundle (evidence
881
+ * lanes, gaps, hypothesis strength, locate confidence). Advisory: it NEVER
882
+ * gates or blocks bundle emission. `reasons` is the load-bearing part; `score`
883
+ * and `level` summarize it.
884
+ */
885
+ interface ContextCompleteness {
886
+ /** 0..1; higher = richer, more actionable context. */
887
+ score: number;
888
+ level: "high" | "medium" | "low";
889
+ /** Human-legible drivers: missing lanes, thin evidence, inconclusive locate. */
890
+ reasons: string[];
891
+ }
892
+ /**
893
+ * Consumer-side advisory: what the CONSUMING agent should do when context is
894
+ * thin. Distinct from {@link EvidenceGap} (capture-side: what evidence is
895
+ * missing) — escalation is what to do about it. Always present; `recommended`
896
+ * is false with an empty `when` when context is adequate. Never gates the
897
+ * bundle (VISION: advisory, never a boolean verdict on the bug itself).
898
+ */
899
+ interface Escalation {
900
+ recommended: boolean;
901
+ /** Conditions phrased for the consuming agent, e.g. "if you cannot reproduce
902
+ * via the anchored request, stop and request human triage". */
903
+ when: string[];
834
904
  }
835
905
  interface EvidenceGap {
836
906
  lane: EvidenceLane;
@@ -871,12 +941,22 @@ interface RankedBundle {
871
941
  gaps: EvidenceGap[];
872
942
  /** Advisory-only: suggested capture escalations when evidence is thin. */
873
943
  directives: CaptureDirective[];
944
+ /** How much actionable context this bundle carries. Advisory, never gates. */
945
+ contextCompleteness: ContextCompleteness;
946
+ /** What the consuming agent should do when context is thin. Advisory. */
947
+ escalation: Escalation;
948
+ /** Where the incident was located, when a locate ran. Absent otherwise. */
949
+ located?: Located;
874
950
  }
875
951
  interface AssembleBundleInput {
876
952
  symptom: Symptom;
877
953
  evidence: EvidenceItem[];
878
954
  intent: IntentSignal[];
879
955
  gaps?: EvidenceGap[];
956
+ /** The locate decision, when one ran (auto-locate / token join). Threaded
957
+ * onto the bundle as {@link RankedBundle.located} and folded into
958
+ * completeness. Omit for explicit baseline/current comparison bundles. */
959
+ located?: Located;
880
960
  }
881
961
  /**
882
962
  * Compose the RankedBundle: rank the complete evidence set (nothing dropped),
@@ -971,4 +1051,4 @@ type Stack = "nextjs" | "react" | "vue" | "svelte" | "vite" | "express" | "hono"
971
1051
  /** Stable ordered id list, handy for iterating or building pickers. */
972
1052
  declare const STACK_IDS: readonly Stack[];
973
1053
 
974
- export { type AbandonedFlowOptions, type AddBugEventOptions, type AssembleBundleInput, type AutoFlagController, type AutoFlagOptions, BROWSER_REDACTION_POLICY, type BodyRedactionOptions, type BodyRedactionResult, type BugEvent, type BugReport, CRUMBTRAIL_EVENT_KINDS, CRUMBTRAIL_REQUEST_HEADER, CRUMBTRAIL_REQUEST_HEADER_LOWER, CRUMBTRAIL_REQUEST_ID_MAX_LENGTH, CRUMBTRAIL_SCHEMA_VERSION, CRUMBTRAIL_SESSION_HEADER, CRUMBTRAIL_SESSION_HEADER_LOWER, type CaptureDirective, type CollectorCleanup, type CollectorContext, type CommitInfo, Crumbtrail, type CrumbtrailCapabilities, type CrumbtrailConfig, type CrumbtrailPlatform, type CrumbtrailPreset, type CrumbtrailSdkDescriptor, type CrumbtrailTransport, DB_DIFF_BULK_EVENT_KIND, DB_DIFF_EVENT_KIND, DB_READ_BULK_EVENT_KIND, DB_READ_EVENT_KIND, DEFAULT_CONFIG, DEFAULT_SESSION_STORAGE_KEY, type DbDiffBulkEventData, type DbDiffEventData, type DbDiffOp, type DbEngine, type DbReadBulkEventData, type DbReadEventData, EVIDENCE_SCHEMA_VERSION, EVIDENCE_SOURCE_SCHEMA_VERSION, type ElementSignature, type EnvDeclaration, type EnvSnapshot, EventBus, type EvidenceGap, type EvidenceItem, type EvidenceJoinKey, type EvidenceLane, type EvidenceQuery, type EvidenceRef, type EvidenceSourceDescriptor, type EvidenceSourceResult, FUSION_SCHEMA_VERSION, type FlagBugOptions, type GitHostClient, type GitHostRef, HttpTransport, type Hypothesis, type HypothesisKind, type InputValueRedactionOptions, type IntentSignal, type InteractionElementDescriptor, type InteractionElementDescriptorFactory, type OutboundCorrelation, PRESET_FULL, PRESET_LIGHT, PRESET_PASSIVE, type PayloadSummary, type PersistedSession, REDACTED_STORAGE_KEY, REDACTED_VALUE, type RageClickOptions, type RankedBundle, type RedactionAction, type RedactionField, type RedactionMetadata, type RedactionResult, type RetryStormOptions, RingBuffer, STACK_IDS, type SessionStore, type Signal, type SignalDetector, type SlowResponseOptions, type Stack, type StoredValueRedactionOptions, type Symptom, type TargetDescriptor, type TargetDescriptorResolver, type W3CTraceContext, W3C_TRACEPARENT_HEADER, WebTargetDescriptorResolver, abandonedFlowDetector, assembleBundle, attachRedactionMetadata, buildEnvDelta, buildEnvSnapshot, canInjectCorrelationHeaders, computeElementPath, computeElementSignature, createAutoFlagController, createCrumbtrailRequestHeaders, createWebSessionStore, environmentCollector, errorDetector, errorSignature, formatTraceparent, generateRequestId, generateSpanId, generateTraceContext, generateTraceId, hashString, inferIntent, mergeRedactionMetadata, parseTraceparent, rageClickDetector, redactCookieMap, redactCookieName, redactCookieValue, redactHeaders, redactInputValue, redactNetworkTextBody, redactStorageKey, redactStoredValue, redactTokenLikeString, redactUrl, redactUrlsInText, redactValue, resolveOutboundCorrelation, retryStormDetector, slowResponseDetector, summarizeBinaryPayload, summarizeOmittedPayload, webTargetDescriptorResolver };
1054
+ export { type AbandonedFlowOptions, type AddBugEventOptions, type AssembleBundleInput, type AutoFlagController, type AutoFlagOptions, BROWSER_REDACTION_POLICY, type BodyRedactionOptions, type BodyRedactionResult, type BugEvent, type BugReport, CRUMBTRAIL_EVENT_KINDS, CRUMBTRAIL_REQUEST_HEADER, CRUMBTRAIL_REQUEST_HEADER_LOWER, CRUMBTRAIL_REQUEST_ID_MAX_LENGTH, CRUMBTRAIL_SCHEMA_VERSION, CRUMBTRAIL_SESSION_HEADER, CRUMBTRAIL_SESSION_HEADER_LOWER, type CaptureDirective, type CollectorCleanup, type CollectorContext, type CommitInfo, type ContextCompleteness, Crumbtrail, type CrumbtrailCapabilities, type CrumbtrailConfig, type CrumbtrailPlatform, type CrumbtrailPreset, type CrumbtrailSdkDescriptor, type CrumbtrailTransport, DB_DIFF_BULK_EVENT_KIND, DB_DIFF_EVENT_KIND, DB_READ_BULK_EVENT_KIND, DB_READ_EVENT_KIND, DEFAULT_CONFIG, DEFAULT_SESSION_STORAGE_KEY, type DbDiffBulkEventData, type DbDiffEventData, type DbDiffOp, type DbEngine, type DbReadBulkEventData, type DbReadEventData, EVIDENCE_SCHEMA_VERSION, EVIDENCE_SOURCE_SCHEMA_VERSION, type ElementSignature, type EnvDeclaration, type EnvSnapshot, type Escalation, EventBus, type EvidenceGap, type EvidenceItem, type EvidenceJoinKey, type EvidenceLane, type EvidenceQuery, type EvidenceRef, type EvidenceSourceDescriptor, type EvidenceSourceResult, FUSION_SCHEMA_VERSION, type FlagBugOptions, type GitHostClient, type GitHostRef, HttpTransport, type Hypothesis, type HypothesisKind, type InputValueRedactionOptions, type IntentSignal, type InteractionElementDescriptor, type InteractionElementDescriptorFactory, type Located, type OutboundCorrelation, PRESET_FULL, PRESET_LIGHT, PRESET_PASSIVE, type PayloadSummary, type PersistedSession, REDACTED_STORAGE_KEY, REDACTED_VALUE, type RageClickOptions, type RankedBundle, type RedactionAction, type RedactionField, type RedactionMetadata, type RedactionResult, type RetryStormOptions, RingBuffer, STACK_IDS, type SessionStore, type Signal, type SignalDetector, type SlowResponseOptions, type Stack, type StoredValueRedactionOptions, type Symptom, type TargetDescriptor, type TargetDescriptorResolver, type Verification, type W3CTraceContext, W3C_TRACEPARENT_HEADER, WebTargetDescriptorResolver, abandonedFlowDetector, assembleBundle, attachRedactionMetadata, buildEnvDelta, buildEnvSnapshot, canInjectCorrelationHeaders, computeElementPath, computeElementSignature, createAutoFlagController, createCrumbtrailRequestHeaders, createWebSessionStore, environmentCollector, errorDetector, errorSignature, formatTraceparent, generateRequestId, generateSpanId, generateTraceContext, generateTraceId, hashString, inferIntent, mergeRedactionMetadata, parseTraceparent, rageClickDetector, redactCookieMap, redactCookieName, redactCookieValue, redactHeaders, redactInputValue, redactNetworkTextBody, redactStorageKey, redactStoredValue, redactTokenLikeString, redactUrl, redactUrlsInText, redactValue, resolveOutboundCorrelation, retryStormDetector, slowResponseDetector, summarizeBinaryPayload, summarizeOmittedPayload, webTargetDescriptorResolver };
package/dist/index.d.ts CHANGED
@@ -831,6 +831,76 @@ interface Hypothesis {
831
831
  rationale: string;
832
832
  /** EvidenceItem.id values backing this hypothesis. */
833
833
  evidenceIds: string[];
834
+ /**
835
+ * Advisory, additive: concrete observations that would confirm a fix for
836
+ * THIS hypothesis worked. Emitted only for non-inconclusive hypotheses whose
837
+ * cited evidence carries a concrete anchor (signature / requestId /
838
+ * table+pk); absent otherwise. Sparse-and-concrete by design — never vacuous
839
+ * prose like "verify the bug is gone". See {@link Verification}.
840
+ */
841
+ verification?: Verification[];
842
+ }
843
+ /**
844
+ * A concrete, post-fix observation that would confirm a hypothesis's fix
845
+ * worked. `observation` always names a concrete signal (an error signature, a
846
+ * request id, or a table + primary key) — the derivation emits nothing when it
847
+ * cannot anchor to one, so an emitted verification is never a vacuous
848
+ * restatement.
849
+ */
850
+ interface Verification {
851
+ /** Names a concrete signal — signature, request id, or table/pk. */
852
+ observation: string;
853
+ /** EvidenceItem.id values this observation is anchored to. */
854
+ evidenceIds: string[];
855
+ how: "session" | "request" | "db";
856
+ }
857
+ /**
858
+ * Where the incident was located, when a locate ran. Advisory and optional
859
+ * (absent for explicit baseline/current comparison bundles that never locate).
860
+ * Shape shared with the node locate engine's `LocateMatch` and War-game 02's
861
+ * deterministic token join — `method` distinguishes the two so 02 can populate
862
+ * "token" without a schema change (War-game 02 Fork C: one definition, reused).
863
+ */
864
+ interface Located {
865
+ outcome: "matched" | "inconclusive";
866
+ /** 0..1 locate confidence. */
867
+ confidence: number;
868
+ /**
869
+ * How the incident was located. "fuzzy" = the scored locate engine;
870
+ * "token" = a deterministic token join (War-game 02). Absent when a caller
871
+ * supplies neither.
872
+ */
873
+ method?: "fuzzy" | "token";
874
+ /** The matched session, present ONLY when outcome === "matched". Never fabricated. */
875
+ sessionId?: string;
876
+ reasons?: string[];
877
+ }
878
+ /**
879
+ * Bundle-level answer to "how much context do we actually have here?" — the
880
+ * LOW_CONTEXT signal. Derived purely from the assembled bundle (evidence
881
+ * lanes, gaps, hypothesis strength, locate confidence). Advisory: it NEVER
882
+ * gates or blocks bundle emission. `reasons` is the load-bearing part; `score`
883
+ * and `level` summarize it.
884
+ */
885
+ interface ContextCompleteness {
886
+ /** 0..1; higher = richer, more actionable context. */
887
+ score: number;
888
+ level: "high" | "medium" | "low";
889
+ /** Human-legible drivers: missing lanes, thin evidence, inconclusive locate. */
890
+ reasons: string[];
891
+ }
892
+ /**
893
+ * Consumer-side advisory: what the CONSUMING agent should do when context is
894
+ * thin. Distinct from {@link EvidenceGap} (capture-side: what evidence is
895
+ * missing) — escalation is what to do about it. Always present; `recommended`
896
+ * is false with an empty `when` when context is adequate. Never gates the
897
+ * bundle (VISION: advisory, never a boolean verdict on the bug itself).
898
+ */
899
+ interface Escalation {
900
+ recommended: boolean;
901
+ /** Conditions phrased for the consuming agent, e.g. "if you cannot reproduce
902
+ * via the anchored request, stop and request human triage". */
903
+ when: string[];
834
904
  }
835
905
  interface EvidenceGap {
836
906
  lane: EvidenceLane;
@@ -871,12 +941,22 @@ interface RankedBundle {
871
941
  gaps: EvidenceGap[];
872
942
  /** Advisory-only: suggested capture escalations when evidence is thin. */
873
943
  directives: CaptureDirective[];
944
+ /** How much actionable context this bundle carries. Advisory, never gates. */
945
+ contextCompleteness: ContextCompleteness;
946
+ /** What the consuming agent should do when context is thin. Advisory. */
947
+ escalation: Escalation;
948
+ /** Where the incident was located, when a locate ran. Absent otherwise. */
949
+ located?: Located;
874
950
  }
875
951
  interface AssembleBundleInput {
876
952
  symptom: Symptom;
877
953
  evidence: EvidenceItem[];
878
954
  intent: IntentSignal[];
879
955
  gaps?: EvidenceGap[];
956
+ /** The locate decision, when one ran (auto-locate / token join). Threaded
957
+ * onto the bundle as {@link RankedBundle.located} and folded into
958
+ * completeness. Omit for explicit baseline/current comparison bundles. */
959
+ located?: Located;
880
960
  }
881
961
  /**
882
962
  * Compose the RankedBundle: rank the complete evidence set (nothing dropped),
@@ -971,4 +1051,4 @@ type Stack = "nextjs" | "react" | "vue" | "svelte" | "vite" | "express" | "hono"
971
1051
  /** Stable ordered id list, handy for iterating or building pickers. */
972
1052
  declare const STACK_IDS: readonly Stack[];
973
1053
 
974
- export { type AbandonedFlowOptions, type AddBugEventOptions, type AssembleBundleInput, type AutoFlagController, type AutoFlagOptions, BROWSER_REDACTION_POLICY, type BodyRedactionOptions, type BodyRedactionResult, type BugEvent, type BugReport, CRUMBTRAIL_EVENT_KINDS, CRUMBTRAIL_REQUEST_HEADER, CRUMBTRAIL_REQUEST_HEADER_LOWER, CRUMBTRAIL_REQUEST_ID_MAX_LENGTH, CRUMBTRAIL_SCHEMA_VERSION, CRUMBTRAIL_SESSION_HEADER, CRUMBTRAIL_SESSION_HEADER_LOWER, type CaptureDirective, type CollectorCleanup, type CollectorContext, type CommitInfo, Crumbtrail, type CrumbtrailCapabilities, type CrumbtrailConfig, type CrumbtrailPlatform, type CrumbtrailPreset, type CrumbtrailSdkDescriptor, type CrumbtrailTransport, DB_DIFF_BULK_EVENT_KIND, DB_DIFF_EVENT_KIND, DB_READ_BULK_EVENT_KIND, DB_READ_EVENT_KIND, DEFAULT_CONFIG, DEFAULT_SESSION_STORAGE_KEY, type DbDiffBulkEventData, type DbDiffEventData, type DbDiffOp, type DbEngine, type DbReadBulkEventData, type DbReadEventData, EVIDENCE_SCHEMA_VERSION, EVIDENCE_SOURCE_SCHEMA_VERSION, type ElementSignature, type EnvDeclaration, type EnvSnapshot, EventBus, type EvidenceGap, type EvidenceItem, type EvidenceJoinKey, type EvidenceLane, type EvidenceQuery, type EvidenceRef, type EvidenceSourceDescriptor, type EvidenceSourceResult, FUSION_SCHEMA_VERSION, type FlagBugOptions, type GitHostClient, type GitHostRef, HttpTransport, type Hypothesis, type HypothesisKind, type InputValueRedactionOptions, type IntentSignal, type InteractionElementDescriptor, type InteractionElementDescriptorFactory, type OutboundCorrelation, PRESET_FULL, PRESET_LIGHT, PRESET_PASSIVE, type PayloadSummary, type PersistedSession, REDACTED_STORAGE_KEY, REDACTED_VALUE, type RageClickOptions, type RankedBundle, type RedactionAction, type RedactionField, type RedactionMetadata, type RedactionResult, type RetryStormOptions, RingBuffer, STACK_IDS, type SessionStore, type Signal, type SignalDetector, type SlowResponseOptions, type Stack, type StoredValueRedactionOptions, type Symptom, type TargetDescriptor, type TargetDescriptorResolver, type W3CTraceContext, W3C_TRACEPARENT_HEADER, WebTargetDescriptorResolver, abandonedFlowDetector, assembleBundle, attachRedactionMetadata, buildEnvDelta, buildEnvSnapshot, canInjectCorrelationHeaders, computeElementPath, computeElementSignature, createAutoFlagController, createCrumbtrailRequestHeaders, createWebSessionStore, environmentCollector, errorDetector, errorSignature, formatTraceparent, generateRequestId, generateSpanId, generateTraceContext, generateTraceId, hashString, inferIntent, mergeRedactionMetadata, parseTraceparent, rageClickDetector, redactCookieMap, redactCookieName, redactCookieValue, redactHeaders, redactInputValue, redactNetworkTextBody, redactStorageKey, redactStoredValue, redactTokenLikeString, redactUrl, redactUrlsInText, redactValue, resolveOutboundCorrelation, retryStormDetector, slowResponseDetector, summarizeBinaryPayload, summarizeOmittedPayload, webTargetDescriptorResolver };
1054
+ export { type AbandonedFlowOptions, type AddBugEventOptions, type AssembleBundleInput, type AutoFlagController, type AutoFlagOptions, BROWSER_REDACTION_POLICY, type BodyRedactionOptions, type BodyRedactionResult, type BugEvent, type BugReport, CRUMBTRAIL_EVENT_KINDS, CRUMBTRAIL_REQUEST_HEADER, CRUMBTRAIL_REQUEST_HEADER_LOWER, CRUMBTRAIL_REQUEST_ID_MAX_LENGTH, CRUMBTRAIL_SCHEMA_VERSION, CRUMBTRAIL_SESSION_HEADER, CRUMBTRAIL_SESSION_HEADER_LOWER, type CaptureDirective, type CollectorCleanup, type CollectorContext, type CommitInfo, type ContextCompleteness, Crumbtrail, type CrumbtrailCapabilities, type CrumbtrailConfig, type CrumbtrailPlatform, type CrumbtrailPreset, type CrumbtrailSdkDescriptor, type CrumbtrailTransport, DB_DIFF_BULK_EVENT_KIND, DB_DIFF_EVENT_KIND, DB_READ_BULK_EVENT_KIND, DB_READ_EVENT_KIND, DEFAULT_CONFIG, DEFAULT_SESSION_STORAGE_KEY, type DbDiffBulkEventData, type DbDiffEventData, type DbDiffOp, type DbEngine, type DbReadBulkEventData, type DbReadEventData, EVIDENCE_SCHEMA_VERSION, EVIDENCE_SOURCE_SCHEMA_VERSION, type ElementSignature, type EnvDeclaration, type EnvSnapshot, type Escalation, EventBus, type EvidenceGap, type EvidenceItem, type EvidenceJoinKey, type EvidenceLane, type EvidenceQuery, type EvidenceRef, type EvidenceSourceDescriptor, type EvidenceSourceResult, FUSION_SCHEMA_VERSION, type FlagBugOptions, type GitHostClient, type GitHostRef, HttpTransport, type Hypothesis, type HypothesisKind, type InputValueRedactionOptions, type IntentSignal, type InteractionElementDescriptor, type InteractionElementDescriptorFactory, type Located, type OutboundCorrelation, PRESET_FULL, PRESET_LIGHT, PRESET_PASSIVE, type PayloadSummary, type PersistedSession, REDACTED_STORAGE_KEY, REDACTED_VALUE, type RageClickOptions, type RankedBundle, type RedactionAction, type RedactionField, type RedactionMetadata, type RedactionResult, type RetryStormOptions, RingBuffer, STACK_IDS, type SessionStore, type Signal, type SignalDetector, type SlowResponseOptions, type Stack, type StoredValueRedactionOptions, type Symptom, type TargetDescriptor, type TargetDescriptorResolver, type Verification, type W3CTraceContext, W3C_TRACEPARENT_HEADER, WebTargetDescriptorResolver, abandonedFlowDetector, assembleBundle, attachRedactionMetadata, buildEnvDelta, buildEnvSnapshot, canInjectCorrelationHeaders, computeElementPath, computeElementSignature, createAutoFlagController, createCrumbtrailRequestHeaders, createWebSessionStore, environmentCollector, errorDetector, errorSignature, formatTraceparent, generateRequestId, generateSpanId, generateTraceContext, generateTraceId, hashString, inferIntent, mergeRedactionMetadata, parseTraceparent, rageClickDetector, redactCookieMap, redactCookieName, redactCookieValue, redactHeaders, redactInputValue, redactNetworkTextBody, redactStorageKey, redactStoredValue, redactTokenLikeString, redactUrl, redactUrlsInText, redactValue, resolveOutboundCorrelation, retryStormDetector, slowResponseDetector, summarizeBinaryPayload, summarizeOmittedPayload, webTargetDescriptorResolver };
package/dist/index.js CHANGED
@@ -4319,21 +4319,139 @@ function inferIntent(evidence, commits) {
4319
4319
  var FUSION_SCHEMA_VERSION = "fusion.v1";
4320
4320
  function assembleBundle(input) {
4321
4321
  const evidence = rankEvidence(input.symptom, input.evidence);
4322
- const hypotheses = classifyHypotheses(
4322
+ const classified = classifyHypotheses(
4323
4323
  input.symptom,
4324
4324
  input.evidence,
4325
4325
  input.intent
4326
4326
  );
4327
4327
  const gaps = input.gaps ?? [];
4328
+ const located = input.located;
4329
+ const hypotheses = classified.map((hypothesis) => {
4330
+ const verification = deriveVerification(hypothesis, evidence);
4331
+ return verification.length > 0 ? { ...hypothesis, verification } : hypothesis;
4332
+ });
4333
+ const contextCompleteness = deriveContextCompleteness(
4334
+ evidence,
4335
+ gaps,
4336
+ hypotheses,
4337
+ located
4338
+ );
4339
+ const escalation = deriveEscalation(contextCompleteness, hypotheses);
4328
4340
  return {
4329
4341
  schemaVersion: FUSION_SCHEMA_VERSION,
4330
4342
  symptom: input.symptom,
4331
4343
  evidence,
4332
4344
  opinion: { stance: "advisory", hypotheses },
4333
4345
  gaps,
4334
- directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
4346
+ directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps),
4347
+ contextCompleteness,
4348
+ escalation,
4349
+ ...located ? { located } : {}
4335
4350
  };
4336
4351
  }
4352
+ function clamp01(value) {
4353
+ if (Number.isNaN(value)) return 0;
4354
+ return Math.max(0, Math.min(1, value));
4355
+ }
4356
+ var COMPLETENESS_LANES = [
4357
+ "network",
4358
+ "db",
4359
+ "flow",
4360
+ "browser",
4361
+ "env"
4362
+ ];
4363
+ function deriveContextCompleteness(evidence, gaps, hypotheses, located) {
4364
+ const lanesPresent = new Set(evidence.map((item) => item.lane));
4365
+ const informativePresent = COMPLETENESS_LANES.filter(
4366
+ (lane) => lanesPresent.has(lane)
4367
+ );
4368
+ const breadth = Math.min(1, informativePresent.length / 3);
4369
+ const volume = Math.min(1, evidence.length / 5);
4370
+ const top = hypotheses[0];
4371
+ const hypothesisStrength = !top || top.kind === "inconclusive" ? 0 : top.confidence;
4372
+ let score = 0.4 * breadth + 0.25 * volume + 0.35 * hypothesisStrength;
4373
+ if (located) {
4374
+ score = located.outcome === "matched" ? 0.85 * score + 0.15 * clamp01(located.confidence) : score * 0.6;
4375
+ }
4376
+ const hardGaps = gaps.filter((gap) => gap.kind === "source-unavailable").length;
4377
+ const softGaps = gaps.length - hardGaps;
4378
+ const gapPenalty = Math.min(0.5, softGaps * 0.1 + hardGaps * 0.3);
4379
+ score = clamp01(score - gapPenalty);
4380
+ const level = score < 0.34 ? "low" : score < 0.67 ? "medium" : "high";
4381
+ const reasons = [];
4382
+ const missingLanes = COMPLETENESS_LANES.filter(
4383
+ (lane) => !lanesPresent.has(lane)
4384
+ );
4385
+ if (informativePresent.length === 0) {
4386
+ reasons.push("no network/db/flow/browser/env evidence captured");
4387
+ } else if (missingLanes.length > 0) {
4388
+ reasons.push(`missing evidence lanes: ${missingLanes.join(", ")}`);
4389
+ }
4390
+ if (evidence.length > 0 && evidence.length < 3) {
4391
+ reasons.push(`thin evidence (${evidence.length} item(s))`);
4392
+ }
4393
+ if (hardGaps > 0) reasons.push(`source unavailable for ${hardGaps} lane(s)`);
4394
+ if (softGaps > 0) reasons.push(`${softGaps} evidence gap(s)`);
4395
+ if (located?.outcome === "inconclusive") {
4396
+ reasons.push("incident location inconclusive");
4397
+ }
4398
+ if (!top || top.kind === "inconclusive") {
4399
+ reasons.push("no distinguishing hypothesis");
4400
+ }
4401
+ return { score, level, reasons };
4402
+ }
4403
+ function pkString(pk) {
4404
+ return Object.keys(pk).sort().map((key) => `${key}=${String(pk[key])}`).join(", ");
4405
+ }
4406
+ function deriveVerification(hypothesis, evidence) {
4407
+ if (hypothesis.kind === "inconclusive") return [];
4408
+ const cited = new Set(hypothesis.evidenceIds);
4409
+ const out = [];
4410
+ for (const item of evidence) {
4411
+ if (!cited.has(item.id)) continue;
4412
+ const ref = item.ref;
4413
+ if (item.lane === "db" && ref.table && ref.pk && Object.keys(ref.pk).length > 0) {
4414
+ out.push({
4415
+ observation: `row in ${ref.table} (${pkString(ref.pk)}) matches the intended post-fix state`,
4416
+ evidenceIds: [item.id],
4417
+ how: "db"
4418
+ });
4419
+ } else if (ref.requestId) {
4420
+ const sigPart = ref.sig ? ` for signature ${ref.sig}` : "";
4421
+ out.push({
4422
+ observation: `request ${ref.requestId} succeeds${sigPart} on a fresh run (no error response)`,
4423
+ evidenceIds: [item.id],
4424
+ how: "request"
4425
+ });
4426
+ } else if (ref.sig) {
4427
+ out.push({
4428
+ observation: `error signature ${ref.sig} no longer appears in a fresh session over the same route`,
4429
+ evidenceIds: [item.id],
4430
+ how: "session"
4431
+ });
4432
+ }
4433
+ }
4434
+ return out;
4435
+ }
4436
+ function deriveEscalation(completeness, hypotheses) {
4437
+ const allInconclusive = hypotheses.length > 0 && hypotheses.every((hypothesis) => hypothesis.kind === "inconclusive");
4438
+ const recommended = completeness.level === "low" || allInconclusive;
4439
+ if (!recommended) return { recommended: false, when: [] };
4440
+ const when = [
4441
+ "if you cannot reproduce the symptom via the anchored request or session, stop and request human triage \u2014 do not widen the search"
4442
+ ];
4443
+ if (completeness.level === "low") {
4444
+ when.push(
4445
+ "context is thin: confirm the missing evidence lanes before acting on the top hypothesis"
4446
+ );
4447
+ }
4448
+ if (allInconclusive) {
4449
+ when.push(
4450
+ "no hypothesis is distinguished; treat the listed causes as equally unproven"
4451
+ );
4452
+ }
4453
+ return { recommended, when };
4454
+ }
4337
4455
  var LANE_PRIOR = {
4338
4456
  db: 0.2,
4339
4457
  network: 0.2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crumbtrail-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Browser SDK for capturing Crumbtrail sessions and sending evidence to a local Crumbtrail server.",
5
5
  "license": "MIT",
6
6
  "repository": {