@qmilab/lodestar-core 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +12 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schemas/action.d.ts +31 -13
  6. package/dist/schemas/action.d.ts.map +1 -1
  7. package/dist/schemas/action.js +20 -1
  8. package/dist/schemas/action.js.map +1 -1
  9. package/dist/schemas/approval.d.ts +271 -0
  10. package/dist/schemas/approval.d.ts.map +1 -0
  11. package/dist/schemas/approval.js +119 -0
  12. package/dist/schemas/approval.js.map +1 -0
  13. package/dist/schemas/belief.d.ts.map +1 -1
  14. package/dist/schemas/belief.js +7 -1
  15. package/dist/schemas/belief.js.map +1 -1
  16. package/dist/schemas/calibration.d.ts +977 -0
  17. package/dist/schemas/calibration.d.ts.map +1 -0
  18. package/dist/schemas/calibration.js +187 -0
  19. package/dist/schemas/calibration.js.map +1 -0
  20. package/dist/schemas/claim.d.ts.map +1 -1
  21. package/dist/schemas/claim.js +4 -2
  22. package/dist/schemas/claim.js.map +1 -1
  23. package/dist/schemas/common.d.ts.map +1 -1
  24. package/dist/schemas/common.js +11 -5
  25. package/dist/schemas/common.js.map +1 -1
  26. package/dist/schemas/policy.d.ts +768 -0
  27. package/dist/schemas/policy.d.ts.map +1 -0
  28. package/dist/schemas/policy.js +200 -0
  29. package/dist/schemas/policy.js.map +1 -0
  30. package/dist/schemas/probe-pack.d.ts +152 -0
  31. package/dist/schemas/probe-pack.d.ts.map +1 -0
  32. package/dist/schemas/probe-pack.js +140 -0
  33. package/dist/schemas/probe-pack.js.map +1 -0
  34. package/dist/schemas/reflection.d.ts +405 -0
  35. package/dist/schemas/reflection.d.ts.map +1 -0
  36. package/dist/schemas/reflection.js +154 -0
  37. package/dist/schemas/reflection.js.map +1 -0
  38. package/dist/schemas/revision.d.ts.map +1 -1
  39. package/dist/schemas/revision.js.map +1 -1
  40. package/dist/schemas/sentinel.d.ts +134 -0
  41. package/dist/schemas/sentinel.d.ts.map +1 -0
  42. package/dist/schemas/sentinel.js +97 -0
  43. package/dist/schemas/sentinel.js.map +1 -0
  44. package/package.json +2 -7
  45. package/src/index.ts +18 -0
  46. package/src/schemas/action.ts +20 -1
  47. package/src/schemas/approval.ts +136 -0
  48. package/src/schemas/belief.ts +7 -1
  49. package/src/schemas/calibration.ts +212 -0
  50. package/src/schemas/claim.ts +15 -8
  51. package/src/schemas/common.ts +16 -10
  52. package/src/schemas/policy.ts +231 -0
  53. package/src/schemas/probe-pack.ts +169 -0
  54. package/src/schemas/reflection.ts +166 -0
  55. package/src/schemas/revision.ts +7 -5
  56. package/src/schemas/sentinel.ts +104 -0
@@ -0,0 +1,169 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * The probe-pack format spec version. This is the version of the
5
+ * *manifest schema itself*, not of any given pack. A loader declares
6
+ * which spec versions it understands; a manifest declares which one it
7
+ * was written against. v0 of the harness understands spec "1" only.
8
+ *
9
+ * Bump this when the manifest shape changes in a way older loaders
10
+ * cannot read. Adding an optional field is not a bump; removing or
11
+ * re-typing a field is.
12
+ */
13
+ export const PROBE_PACK_SPEC_VERSION = "1" as const
14
+
15
+ /**
16
+ * Where a pack's probe files come from.
17
+ *
18
+ * `local` — the pack lives on the filesystem; probe `file` paths are
19
+ * resolved relative to the directory containing the manifest.
20
+ * `npm` — the pack ships as a published package; the loader reads the
21
+ * manifest from the package's `./lodestar.probe-pack.json` export and
22
+ * resolves probe files relative to the package root.
23
+ *
24
+ * Both source types are part of the spec from day one so external
25
+ * authors can target a stable schema. The v0 loader resolves `local`
26
+ * only; `npm` resolution follows the first external pack that needs it
27
+ * (see docs/architecture/reflection-pass.md Q6).
28
+ */
29
+ export const ProbePackSourceTypeSchema = z.enum(["local", "npm"])
30
+ export type ProbePackSourceType = z.infer<typeof ProbePackSourceTypeSchema>
31
+
32
+ /**
33
+ * One probe entry in a pack manifest.
34
+ *
35
+ * `name` is the probe's stable identifier, unique within the pack — the
36
+ * harness runner reports results under it and external references
37
+ * address probes by `<pack>/<name>`. `file` is the probe source,
38
+ * relative to the pack root.
39
+ */
40
+ export const ProbeEntrySchema = z.object({
41
+ name: z
42
+ .string()
43
+ .min(1)
44
+ .regex(
45
+ /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
46
+ "probe name must be kebab-case (lowercase alphanumerics separated by single hyphens)",
47
+ )
48
+ .describe("Stable probe identifier, unique within the pack."),
49
+ file: z
50
+ .string()
51
+ .min(1)
52
+ // Must be relative to the pack root: a pack that names an absolute
53
+ // path loads on its author's machine but breaks once moved or
54
+ // published. Reject POSIX (`/`), UNC/Windows-root (`\`), and
55
+ // drive-letter (`C:`) prefixes so the contract holds cross-platform.
56
+ .refine((f) => !/^([/\\]|[A-Za-z]:)/.test(f), {
57
+ message:
58
+ "probe file must be a relative path inside the pack (absolute paths are not allowed)",
59
+ })
60
+ .describe(
61
+ "Probe source file, relative to the pack root (the directory containing the manifest).",
62
+ ),
63
+ })
64
+ export type ProbeEntry = z.infer<typeof ProbeEntrySchema>
65
+
66
+ /**
67
+ * One sentinel entry in a pack manifest.
68
+ *
69
+ * Unlike a probe — a `bun run`-able script the pack carries as a `file` —
70
+ * a sentinel is a stateful in-process class the harness instantiates and
71
+ * feeds the event stream. There is no subprocess contract for it, so the
72
+ * manifest references a sentinel by a stable `id` and the harness resolves
73
+ * that id against its built-in registry of first-party sentinels
74
+ * (`FIRST_PARTY_SENTINELS` in `@qmilab/lodestar-harness`). A pack thus
75
+ * *declares* which built-in sentinels it ships rather than carrying their
76
+ * source.
77
+ *
78
+ * Per-pack construction-option overrides and third-party (file-referenced)
79
+ * sentinels are a deliberate later refinement — see the harness loader and
80
+ * `docs/architecture/sentinels.md`. v0 resolves first-party ids only.
81
+ */
82
+ export const SentinelEntrySchema = z.object({
83
+ id: z
84
+ .string()
85
+ .min(1)
86
+ .regex(
87
+ /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
88
+ "sentinel id must be kebab-case (lowercase alphanumerics separated by single hyphens)",
89
+ )
90
+ .describe(
91
+ "Stable id of a first-party sentinel, resolved by the harness against its built-in registry. Matches the sentinel's own `name`.",
92
+ ),
93
+ })
94
+ export type SentinelEntry = z.infer<typeof SentinelEntrySchema>
95
+
96
+ /**
97
+ * A `lodestar.probe-pack.json` manifest.
98
+ *
99
+ * This is the on-disk / on-wire contract every probe pack — first-party
100
+ * and external — is written against. It is deliberately declarative:
101
+ * the manifest names probes and their files but contains no executable
102
+ * logic. The harness loader (in `@qmilab/lodestar-harness`) reads it,
103
+ * validates it against this schema, and resolves the probe files; the
104
+ * runner (Batch 4 step 5) executes them.
105
+ *
106
+ * `coverage_areas` and `invariants` are free-form taxonomy tags the
107
+ * pack author declares. They are not validated against a closed list —
108
+ * the harness uses them for `lodestar harness list` grouping and for
109
+ * answering "which pack exercises invariant X?", not for gating. Keeping
110
+ * them open lets external packs name coverage the core taxonomy has not
111
+ * yet enumerated.
112
+ */
113
+ export const ProbePackManifestSchema = z.object({
114
+ name: z
115
+ .string()
116
+ .min(1)
117
+ .regex(
118
+ /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
119
+ "pack name must be kebab-case (lowercase alphanumerics separated by single hyphens)",
120
+ )
121
+ .describe("Pack identifier, e.g. 'lodestar-core'."),
122
+ version: z
123
+ .string()
124
+ .min(1)
125
+ .describe("Pack version (the author's, not the spec version). Conventionally semver."),
126
+ spec_version: z
127
+ .literal(PROBE_PACK_SPEC_VERSION)
128
+ .describe(
129
+ "Manifest-schema spec version. v0 loaders accept only '1' and reject unknown versions with a clear error rather than guessing.",
130
+ ),
131
+ source_type: ProbePackSourceTypeSchema.describe(
132
+ "How the loader resolves probe files. The v0 loader resolves 'local' only.",
133
+ ),
134
+ description: z
135
+ .string()
136
+ .min(1)
137
+ .optional()
138
+ .describe("Human-readable one-liner shown by `lodestar harness list`."),
139
+ coverage_areas: z
140
+ .array(z.string().min(1))
141
+ .describe("Free-form tags naming the threat-model / subsystem areas this pack covers."),
142
+ invariants: z
143
+ .array(z.string().min(1))
144
+ .describe("Free-form tags naming the Lodestar invariants this pack's probes exercise."),
145
+ probes: z
146
+ .array(ProbeEntrySchema)
147
+ .min(1, "a pack must declare at least one probe")
148
+ .describe("The probes this pack ships."),
149
+ // `.optional()` rather than `.default([])` on purpose: a default makes the
150
+ // field REQUIRED in the `z.infer` *output* type, so external TS code that
151
+ // constructs a manifest without `sentinels` would fail to compile — breaking
152
+ // the "additive optional field is free" promise. Keeping it optional leaves
153
+ // the public type backward-compatible; the loader treats an absent value as
154
+ // "no sentinels". Do not reintroduce `.default([])`.
155
+ sentinels: z
156
+ .array(SentinelEntrySchema)
157
+ .optional()
158
+ .describe(
159
+ "The sentinels this pack ships, referenced by stable id and resolved by the harness against its built-in registry. Optional; an absent field means the pack ships no sentinels. Additive since spec '1' — a manifest without it still loads.",
160
+ ),
161
+ })
162
+ export type ProbePackManifest = z.infer<typeof ProbePackManifestSchema>
163
+
164
+ /**
165
+ * The manifest filename a `local` pack carries at its root, and the
166
+ * export key an `npm` pack exposes it under. Defined as a constant so
167
+ * loaders and pack authors agree on the spelling.
168
+ */
169
+ export const PROBE_PACK_MANIFEST_FILENAME = "lodestar.probe-pack.json" as const
@@ -0,0 +1,166 @@
1
+ import { z } from "zod"
2
+ import { TruthStatusSchema } from "./belief.js"
3
+ import { TimestampSchema } from "./common.js"
4
+
5
+ /**
6
+ * What triggered a reflection pass.
7
+ *
8
+ * `cli` — invoked by `lodestar reflect` from a human operator.
9
+ * `programmatic` — invoked from host code (e.g. runGuarded, the MCP
10
+ * proxy) at a deliberate point.
11
+ * `tail_cascade` — a `belief.transitioned` event recorded a transition
12
+ * to `truth_status: contradicted`; the tail watcher dispatched a
13
+ * pass to look for dependent fall-out.
14
+ * `tail_batch` — N `belief.adopted` events have accrued since the
15
+ * last pass for the partition (configurable batch size).
16
+ * `sentinel` — a `sentinel.alerted` event named a `belief_id` as
17
+ * subject; the sentinel asked reflection to follow up.
18
+ */
19
+ export const ReflectionTriggerSchema = z.enum([
20
+ "cli",
21
+ "programmatic",
22
+ "tail_cascade",
23
+ "tail_batch",
24
+ "sentinel",
25
+ ])
26
+ export type ReflectionTrigger = z.infer<typeof ReflectionTriggerSchema>
27
+
28
+ /**
29
+ * The lifecycle axes a reflection proposal can target. Mirrors
30
+ * `LifecycleAxis` in `@qmilab/lodestar-memory-firewall` deliberately —
31
+ * the core package cannot import from downstream packages, so the
32
+ * literal union is duplicated here. Keep these in sync.
33
+ */
34
+ export const ReflectionLifecycleAxisSchema = z.enum([
35
+ "truth_status",
36
+ "retrieval_status",
37
+ "security_status",
38
+ "freshness_status",
39
+ ])
40
+ export type ReflectionLifecycleAxis = z.infer<typeof ReflectionLifecycleAxisSchema>
41
+
42
+ /**
43
+ * Subject of a `no_op` proposal — reflection looked at this thing
44
+ * and decided no change. Recorded so the audit trail distinguishes
45
+ * "reflection considered X and did nothing" from "reflection did
46
+ * not consider X."
47
+ */
48
+ export const ReflectionSubjectSchema = z.object({
49
+ kind: z.enum(["belief", "claim", "decision"]),
50
+ id: z.string(),
51
+ })
52
+ export type ReflectionSubject = z.infer<typeof ReflectionSubjectSchema>
53
+
54
+ /**
55
+ * One typed proposal from a reflection pass.
56
+ *
57
+ * Proposals are *suggestions*. They do not mutate state. When a
58
+ * proposal is acted on, the runner calls the existing MemoryFirewall
59
+ * API with `by_authority: "reflection"`, and the firewall emits its
60
+ * own normal `belief.adopted` / `belief.transitioned` event whose
61
+ * `causal_parent_ids` includes the reflection pass's event id.
62
+ *
63
+ * The `rationale_id` on every variant points to an Explanation the
64
+ * runner generated alongside the proposal — same shape as the
65
+ * Explanations the firewall consumes for transitions.
66
+ */
67
+ export const ReflectionProposalSchema = z.discriminatedUnion("kind", [
68
+ z.object({
69
+ kind: z.literal("claim_promotion"),
70
+ claim_id: z.string(),
71
+ target_truth_status: TruthStatusSchema,
72
+ evidence_id: z.string(),
73
+ rationale_id: z.string(),
74
+ }),
75
+ z.object({
76
+ kind: z.literal("belief_transition"),
77
+ belief_id: z.string(),
78
+ axis: ReflectionLifecycleAxisSchema,
79
+ from_value: z.string(),
80
+ to_value: z.string(),
81
+ evidence_id: z.string().optional(),
82
+ rationale_id: z.string(),
83
+ }),
84
+ z.object({
85
+ kind: z.literal("belief_supersession"),
86
+ old_belief_id: z.string(),
87
+ new_belief_id: z.string(),
88
+ rationale_id: z.string(),
89
+ }),
90
+ /**
91
+ * The decision-dependency cascade. When a belief that a past
92
+ * Decision depended on transitions to `truth_status: contradicted`,
93
+ * reflection proposes flagging that Decision as having a contradicted
94
+ * dependency. Applying the proposal emits a Revision event with
95
+ * `target_type: "decision"` so the decision's epistemic status
96
+ * change is recorded in the audit chain. (Closes the Batch-2-deferred
97
+ * "contradicted belief flags dependent decisions" invariant.)
98
+ */
99
+ z.object({
100
+ kind: z.literal("decision_dependency_flagged"),
101
+ decision_id: z.string(),
102
+ contradicted_belief_id: z.string(),
103
+ /**
104
+ * The contradicted belief's truth_status BEFORE the transition.
105
+ * Captured from the firing `belief.transitioned` payload's
106
+ * `from_value` (defensively a TruthStatus, but stored as the raw
107
+ * string the transition emitted). Used by the application step
108
+ * to record an accurate `old_value` in the dependent Decision's
109
+ * Revision — a belief can transition `unverified → contradicted`
110
+ * directly, not only from `supported`.
111
+ */
112
+ previous_truth_status: TruthStatusSchema,
113
+ rationale_id: z.string(),
114
+ }),
115
+ z.object({
116
+ kind: z.literal("no_op"),
117
+ subject: ReflectionSubjectSchema,
118
+ rationale_id: z.string(),
119
+ }),
120
+ ])
121
+ export type ReflectionProposal = z.infer<typeof ReflectionProposalSchema>
122
+
123
+ /**
124
+ * The payload of a `reflection.completed@1` event.
125
+ *
126
+ * Idempotence is by cursor: a pass over events with `seq` strictly
127
+ * greater than `cursor.from_seq` and less than or equal to
128
+ * `cursor.to_seq` produces the same proposals on re-run. Re-running
129
+ * is safe — proposals are typed, no state has been mutated, and the
130
+ * harness can compare proposal sets across runs.
131
+ *
132
+ * `observed_event_ids` lists every event the pass actually read.
133
+ * `proposals` is non-empty in v0 — a pass that found nothing to act
134
+ * on emits a `no_op` proposal for at least one subject it inspected,
135
+ * so the harness can distinguish "ran and silent" from "did not run."
136
+ */
137
+ export const ReflectionCompletedPayloadSchema = z.object({
138
+ pass_id: z.string(),
139
+ triggered_by: ReflectionTriggerSchema,
140
+ cursor: z.object({
141
+ from_seq: z.number().int().min(-1).describe("-1 if this is the first pass for the partition"),
142
+ to_seq: z
143
+ .number()
144
+ .int()
145
+ .min(-1)
146
+ .describe(
147
+ "Equal to from_seq when the window is empty — the pass ran but observed no new events. " +
148
+ "Encoded explicitly (rather than skipping emission) so the audit chain can distinguish " +
149
+ "'reflection ran and was silent' from 'reflection did not run.'",
150
+ ),
151
+ }),
152
+ observed_event_ids: z.array(z.string()),
153
+ proposals: z
154
+ .array(ReflectionProposalSchema)
155
+ .min(1, "every reflection pass emits at least one proposal (no_op counts)"),
156
+ started_at: TimestampSchema,
157
+ finished_at: TimestampSchema,
158
+ })
159
+ export type ReflectionCompletedPayload = z.infer<typeof ReflectionCompletedPayloadSchema>
160
+
161
+ /**
162
+ * Event-type literal. Use this constant rather than the bare string
163
+ * so a future rename is grep-safe.
164
+ */
165
+ export const REFLECTION_COMPLETED_EVENT_TYPE = "reflection.completed" as const
166
+ export const REFLECTION_COMPLETED_SCHEMA_VERSION = "1" as const
@@ -13,11 +13,13 @@ export const RevisionSchema = z.object({
13
13
  id: z.string(),
14
14
  target_type: z.enum(["claim", "belief", "decision"]),
15
15
  target_id: z.string(),
16
- changes: z.array(z.object({
17
- field: z.string(),
18
- old_value: z.unknown(),
19
- new_value: z.unknown(),
20
- })),
16
+ changes: z.array(
17
+ z.object({
18
+ field: z.string(),
19
+ old_value: z.unknown(),
20
+ new_value: z.unknown(),
21
+ }),
22
+ ),
21
23
  triggered_by: z.string().describe("actor_id or event_id"),
22
24
  rationale_id: z.string().describe("Explanation id"),
23
25
  at: TimestampSchema,
@@ -0,0 +1,104 @@
1
+ import { z } from "zod"
2
+ import { TimestampSchema } from "./common.js"
3
+
4
+ /**
5
+ * Sentinel alerts — the wire format a Sentinel emits when it pattern-matches
6
+ * a suspicious shape in the event stream.
7
+ *
8
+ * Design lock: `docs/architecture/sentinels.md` (and Q7 of
9
+ * `docs/architecture/reflection-pass.md`, which settled the execution model).
10
+ * The short version:
11
+ *
12
+ * - Sentinels are an async tail of the event stream. They never block the
13
+ * Action Kernel; they emit `sentinel.alerted@1` events and a future
14
+ * (additive) `arbitrate` hook honours them on the *next* action that
15
+ * depends on a flagged subject.
16
+ * - This is a governance event, not an Observation. Like
17
+ * `reflection.completed@1`, the payload is the event payload directly and
18
+ * is NOT registered in the observation schema registry.
19
+ *
20
+ * Core owns the wire format only. The base class, the runner, and the
21
+ * concrete sentinels live in `@qmilab/lodestar-harness`.
22
+ */
23
+
24
+ /**
25
+ * What a sentinel alert is *about*. Kept deliberately small — the four
26
+ * epistemic-chain nouns a v0 sentinel can point at.
27
+ *
28
+ * `belief` is the load-bearing kind for the eventual kernel hook: the
29
+ * `arbitrate` lookup scopes recent alerts to a candidate action's
30
+ * `belief_dependencies`, so a sentinel that names a belief gates the next
31
+ * action that leans on it. `tool_sequence` is a synthetic subject — its
32
+ * `id` identifies the *completion* of a matched sequence (the id of the
33
+ * final action in the run), since no single chain-noun owns the pattern.
34
+ */
35
+ export const SentinelSubjectSchema = z.object({
36
+ kind: z.enum(["belief", "action", "decision", "tool_sequence"]),
37
+ id: z.string().min(1),
38
+ })
39
+ export type SentinelSubject = z.infer<typeof SentinelSubjectSchema>
40
+
41
+ /**
42
+ * Triage weight. `critical` is reserved for patterns that, left unflagged,
43
+ * map onto a concrete attack (e.g. the read → external-egress → write
44
+ * exfiltration shape). `warning` is the default for "under-supported but
45
+ * not obviously hostile". `info` is for advisory signal a calibrator may
46
+ * later promote or demote.
47
+ */
48
+ export const SentinelSeveritySchema = z.enum(["info", "warning", "critical"])
49
+ export type SentinelSeverity = z.infer<typeof SentinelSeveritySchema>
50
+
51
+ /**
52
+ * The payload of a `sentinel.alerted@1` event.
53
+ *
54
+ * `observed_event_ids` lists exactly the events the sentinel read to reach
55
+ * this conclusion; they are also the alert envelope's `causal_parent_ids`,
56
+ * so `lodestar report` can walk back from an alert to the events that
57
+ * triggered it.
58
+ *
59
+ * `detail` is rule-specific structured context (offending belief ids, the
60
+ * matched tool run, the confidence that tripped the floor, …). It is a
61
+ * record rather than a discriminated union so a new sentinel can ship
62
+ * without a core schema bump; the human-readable `message` is always
63
+ * present so an alert is legible without decoding `detail`.
64
+ *
65
+ * Every field is required (no optionals besides `rationale_id`) so the
66
+ * event-log writer's canonical hash and `JSON.stringify` never disagree on
67
+ * a dropped `undefined` key — the same discipline the firewall audit
68
+ * events follow.
69
+ */
70
+ export const SentinelAlertPayloadSchema = z.object({
71
+ alert_id: z.string().min(1),
72
+ sentinel_name: z.string().min(1).describe("Stable name of the emitting sentinel."),
73
+ rule: z
74
+ .string()
75
+ .min(1)
76
+ .describe("Stable id of the specific rule/pattern that fired within the sentinel."),
77
+ severity: SentinelSeveritySchema,
78
+ subject: SentinelSubjectSchema,
79
+ message: z.string().min(1).describe("Human-readable account of what tripped the sentinel."),
80
+ observed_event_ids: z
81
+ .array(z.string())
82
+ .min(1)
83
+ .describe("The events the sentinel read to reach this alert; also the alert's causal parents."),
84
+ detail: z
85
+ .record(z.string(), z.unknown())
86
+ .describe("Rule-specific structured context. May be empty; never undefined."),
87
+ detected_at: TimestampSchema,
88
+ /**
89
+ * Reserved. A sentinel may attach a generated Explanation id here, the
90
+ * way reflection proposals carry one. v0 sentinels rely on `message` +
91
+ * `detail` and leave this unset (omitted entirely, not `undefined`, so
92
+ * the canonical hash stays stable).
93
+ */
94
+ rationale_id: z.string().optional(),
95
+ })
96
+ export type SentinelAlertPayload = z.infer<typeof SentinelAlertPayloadSchema>
97
+
98
+ /**
99
+ * Event-type literal and version. Use the constants rather than the bare
100
+ * string so a future rename is grep-safe — same convention as
101
+ * `REFLECTION_COMPLETED_EVENT_TYPE`.
102
+ */
103
+ export const SENTINEL_ALERTED_EVENT_TYPE = "sentinel.alerted" as const
104
+ export const SENTINEL_ALERTED_SCHEMA_VERSION = "1" as const