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 +144 -2
- package/dist/index.d.cts +95 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.js +143 -2
- package/package.json +6 -2
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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/
|
|
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",
|