crumbtrail-core 0.1.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
@@ -265,6 +265,7 @@ __export(index_exports, {
265
265
  REDACTED_STORAGE_KEY: () => REDACTED_STORAGE_KEY,
266
266
  REDACTED_VALUE: () => REDACTED_VALUE,
267
267
  RingBuffer: () => RingBuffer,
268
+ STACK_IDS: () => STACK_IDS,
268
269
  W3C_TRACEPARENT_HEADER: () => W3C_TRACEPARENT_HEADER,
269
270
  WebTargetDescriptorResolver: () => WebTargetDescriptorResolver,
270
271
  abandonedFlowDetector: () => abandonedFlowDetector,
@@ -4633,21 +4634,139 @@ function inferIntent(evidence, commits) {
4633
4634
  var FUSION_SCHEMA_VERSION = "fusion.v1";
4634
4635
  function assembleBundle(input) {
4635
4636
  const evidence = rankEvidence(input.symptom, input.evidence);
4636
- const hypotheses = classifyHypotheses(
4637
+ const classified = classifyHypotheses(
4637
4638
  input.symptom,
4638
4639
  input.evidence,
4639
4640
  input.intent
4640
4641
  );
4641
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);
4642
4655
  return {
4643
4656
  schemaVersion: FUSION_SCHEMA_VERSION,
4644
4657
  symptom: input.symptom,
4645
4658
  evidence,
4646
4659
  opinion: { stance: "advisory", hypotheses },
4647
4660
  gaps,
4648
- directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
4661
+ directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps),
4662
+ contextCompleteness,
4663
+ escalation,
4664
+ ...located ? { located } : {}
4649
4665
  };
4650
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
+ }
4651
4770
  var LANE_PRIOR = {
4652
4771
  db: 0.2,
4653
4772
  network: 0.2,
@@ -4792,6 +4911,28 @@ function suggestCaptureDirectives(symptom, evidence, gaps) {
4792
4911
 
4793
4912
  // src/evidence-source.ts
4794
4913
  var EVIDENCE_SOURCE_SCHEMA_VERSION = "evidence-source.v1";
4914
+
4915
+ // src/stacks.ts
4916
+ var STACK_IDS = [
4917
+ "nextjs",
4918
+ "react",
4919
+ "vue",
4920
+ "svelte",
4921
+ "vite",
4922
+ "express",
4923
+ "hono",
4924
+ "node",
4925
+ "django",
4926
+ "flask",
4927
+ "fastapi",
4928
+ "dotnet",
4929
+ "go",
4930
+ "rails",
4931
+ "postgres",
4932
+ "grafana",
4933
+ "loki",
4934
+ "docker"
4935
+ ];
4795
4936
  // Annotate the CommonJS export names for ESM import in node:
4796
4937
  0 && (module.exports = {
4797
4938
  BROWSER_REDACTION_POLICY,
@@ -4820,6 +4961,7 @@ var EVIDENCE_SOURCE_SCHEMA_VERSION = "evidence-source.v1";
4820
4961
  REDACTED_STORAGE_KEY,
4821
4962
  REDACTED_VALUE,
4822
4963
  RingBuffer,
4964
+ STACK_IDS,
4823
4965
  W3C_TRACEPARENT_HEADER,
4824
4966
  WebTargetDescriptorResolver,
4825
4967
  abandonedFlowDetector,
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),
@@ -957,4 +1037,18 @@ interface EvidenceSourceResult {
957
1037
  };
958
1038
  }
959
1039
 
960
- 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, type SessionStore, type Signal, type SignalDetector, type SlowResponseOptions, 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 };
1040
+ /**
1041
+ * The stack vocabulary — the set of frameworks and runtimes Crumbtrail knows how
1042
+ * to instrument.
1043
+ *
1044
+ * This lives in core because it is shared domain language, not presentation: the
1045
+ * CLI uses it to pick an install recipe, install-shared uses it to build agent
1046
+ * prompts, and the design system uses it to pick a brand mark. Core is the only
1047
+ * package all three can depend on without dragging React into a Node CLI.
1048
+ */
1049
+ /** The exact set of stacks with first-class support. */
1050
+ type Stack = "nextjs" | "react" | "vue" | "svelte" | "vite" | "express" | "hono" | "node" | "django" | "flask" | "fastapi" | "dotnet" | "go" | "rails" | "postgres" | "grafana" | "loki" | "docker";
1051
+ /** Stable ordered id list, handy for iterating or building pickers. */
1052
+ declare const STACK_IDS: readonly Stack[];
1053
+
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),
@@ -957,4 +1037,18 @@ interface EvidenceSourceResult {
957
1037
  };
958
1038
  }
959
1039
 
960
- 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, type SessionStore, type Signal, type SignalDetector, type SlowResponseOptions, 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 };
1040
+ /**
1041
+ * The stack vocabulary — the set of frameworks and runtimes Crumbtrail knows how
1042
+ * to instrument.
1043
+ *
1044
+ * This lives in core because it is shared domain language, not presentation: the
1045
+ * CLI uses it to pick an install recipe, install-shared uses it to build agent
1046
+ * prompts, and the design system uses it to pick a brand mark. Core is the only
1047
+ * package all three can depend on without dragging React into a Node CLI.
1048
+ */
1049
+ /** The exact set of stacks with first-class support. */
1050
+ type Stack = "nextjs" | "react" | "vue" | "svelte" | "vite" | "express" | "hono" | "node" | "django" | "flask" | "fastapi" | "dotnet" | "go" | "rails" | "postgres" | "grafana" | "loki" | "docker";
1051
+ /** Stable ordered id list, handy for iterating or building pickers. */
1052
+ declare const STACK_IDS: readonly Stack[];
1053
+
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,
@@ -4478,6 +4596,28 @@ function suggestCaptureDirectives(symptom, evidence, gaps) {
4478
4596
 
4479
4597
  // src/evidence-source.ts
4480
4598
  var EVIDENCE_SOURCE_SCHEMA_VERSION = "evidence-source.v1";
4599
+
4600
+ // src/stacks.ts
4601
+ var STACK_IDS = [
4602
+ "nextjs",
4603
+ "react",
4604
+ "vue",
4605
+ "svelte",
4606
+ "vite",
4607
+ "express",
4608
+ "hono",
4609
+ "node",
4610
+ "django",
4611
+ "flask",
4612
+ "fastapi",
4613
+ "dotnet",
4614
+ "go",
4615
+ "rails",
4616
+ "postgres",
4617
+ "grafana",
4618
+ "loki",
4619
+ "docker"
4620
+ ];
4481
4621
  export {
4482
4622
  BROWSER_REDACTION_POLICY,
4483
4623
  CRUMBTRAIL_EVENT_KINDS,
@@ -4505,6 +4645,7 @@ export {
4505
4645
  REDACTED_STORAGE_KEY,
4506
4646
  REDACTED_VALUE,
4507
4647
  RingBuffer,
4648
+ STACK_IDS,
4508
4649
  W3C_TRACEPARENT_HEADER,
4509
4650
  WebTargetDescriptorResolver,
4510
4651
  abandonedFlowDetector,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "crumbtrail-core",
3
- "version": "0.1.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": {
7
7
  "type": "git",
8
- "url": "https://github.com/oshabana/crumbtrail.git",
8
+ "url": "git+https://github.com/crumbtrail-dev/crumbtrail-js.git",
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "keywords": [
@@ -33,6 +33,10 @@
33
33
  "publishConfig": {
34
34
  "access": "public"
35
35
  },
36
+ "homepage": "https://github.com/crumbtrail-dev/crumbtrail-js/tree/main/packages/core#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/crumbtrail-dev/crumbtrail-js/issues"
39
+ },
36
40
  "scripts": {
37
41
  "build": "tsup",
38
42
  "test": "vitest run",