@lunora/advisor 1.0.0-alpha.55 → 1.0.0-alpha.57

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/README.md CHANGED
@@ -93,6 +93,59 @@ const findings = runAdvisor({ schema: fromServerSchema(schema), ...metrics }, {
93
93
 
94
94
  A missing metric degrades to an empty array rather than throwing, so a partially configured read path still returns what it can.
95
95
 
96
+ ### Observability map (score, coverage, baseline)
97
+
98
+ `runAdvisor` answers "what is wrong?". `scoreAdvisor` answers "how are we doing, and did it get worse?" — a scored coverage map over your procedures. It is a pure function over findings you already have, so it never re-runs a lint:
99
+
100
+ ```ts
101
+ import { fromServerSchema, runAdvisor, scoreAdvisor } from "@lunora/advisor";
102
+
103
+ import schema from "./lunora/schema";
104
+
105
+ const context = { schema: fromServerSchema(schema) };
106
+ const findings = runAdvisor(context, { source: "static" });
107
+ const map = scoreAdvisor(context.procedureProtections ?? [], findings);
108
+
109
+ console.log(map.score, map.grade); // e.g. 84 "good"
110
+ console.log(map.summary); // { clean: 9, exempt: 0, failing: 1, procedures: 12, rulesFired: 6, warned: 2 }
111
+ ```
112
+
113
+ Each procedure starts at 100 and loses each fired **rule's** weight (`Lint.weight`, else a severity ladder: `ERROR` 20 / `WARN` 10 / `INFO` 5) — charged once however many times that rule fires — then rolls up into a weighted global mean: public handlers count double, internal ones and queries half. Findings that name no procedure (schema shape, wrangler config) land in a project bucket weighted against the procedure population, so schema debt genuinely moves the grade.
114
+
115
+ The verdicts are `clean` / `warned` / `failing` / `exempt` — named for severity, because the score is driven by every lint family rather than an observability family.
116
+
117
+ Commit the map and gate CI on it:
118
+
119
+ ```ts
120
+ import { compareToBaseline, parseAdvisorMap } from "@lunora/advisor";
121
+
122
+ const baseline = parseAdvisorMap(JSON.parse(await readFile("lunora.advisor.map.json", "utf8")));
123
+
124
+ if (baseline === undefined) {
125
+ // Missing, hand-edited, or written by an older MAP_VERSION. Fail loudly —
126
+ // treating it as "no regression" would silently disable the gate forever.
127
+ throw new Error("advisor baseline is unreadable; regenerate lunora.advisor.map.json");
128
+ }
129
+
130
+ const diff = compareToBaseline(map, baseline);
131
+
132
+ // `comparable` must be narrowed before `regressed` is reachable, so a stale
133
+ // baseline cannot read as a clean run.
134
+ if (!diff.comparable) {
135
+ throw new Error(`advisor baseline not comparable: ${diff.reason}`);
136
+ }
137
+
138
+ // Five independent signals: the global score fell, an existing procedure got
139
+ // worse, one started failing, one's findings grew without its score moving, or
140
+ // the project bucket gained findings. The growth signals matter because a rule is
141
+ // charged once however many times it fires, and the project score saturates at 0.
142
+ if (diff.regressed) {
143
+ process.exitCode = 1;
144
+ }
145
+ ```
146
+
147
+ `@lunora/codegen` exposes `toAdvisorContext()` to build the context straight from the feeder, and `lunora advisor` wraps all of this as a command. Full reference for the scoring, verdicts, and the baseline gate is in the [package docs](https://lunora.sh/docs/packages/advisor).
148
+
96
149
  > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/addons/studio)**.
97
150
 
98
151
  ## Related
package/dist/index.d.mts CHANGED
@@ -814,36 +814,6 @@ interface AdvisorOwnerFieldWrite {
814
814
  /** The `ctx.db` write method (`insert` / `replace` / `patch` / `insertManyUnsafe`). */
815
815
  method: string;
816
816
  }
817
- /**
818
- * One branching `defineShape({ where })` / `definePolicy({ when })` predicate arm
819
- * that returns an unrestricted predicate — the `unrestricted_where_branch` lint
820
- * input.
821
- *
822
- * A row predicate returns a *filter*, not a boolean, so the denial arm has to be a
823
- * predicate matching **no** rows: `deny()` / `{ OR: [] }`, a disjunction over zero
824
- * branches. The plausible-looking `{}` is its exact opposite — it matches every row,
825
- * so an arm meaning "this caller sees nothing" silently replicates or exposes the
826
- * whole table, with no error and no log line.
827
- *
828
- * Only reported for a *branching* predicate: a single-exit `where: () => ({})` is an
829
- * author deliberately replicating everything, which is legitimate. Produced by the
830
- * codegen feeder; runtime callers don't supply it, so the lint finds nothing there.
831
- * Structurally identical to `UnrestrictedWhereBranchIR`.
832
- */
833
- interface AdvisorUnrestrictedWhereBranch {
834
- /** The exported binding name of the shape / policy the predicate belongs to. */
835
- exportName: string;
836
- /** Source file relative to the lunora dir, no extension. */
837
- file: string;
838
- /** Which unrestricted form was returned. */
839
- form: "empty-object" | "undefined";
840
- /** The config key carrying the predicate (`where` for a shape, `when` for a policy). */
841
- key: string;
842
- /** 1-based line of the offending returned expression. */
843
- line: number;
844
- /** The declaring call (`defineShape` / `definePolicy`). */
845
- owner: string;
846
- }
847
817
  /**
848
818
  * One payment webhook-adapter construction (`createStripeAdapter` /
849
819
  * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`) —
@@ -906,12 +876,24 @@ interface AdvisorPrivilegedDispatch {
906
876
  interface AdvisorProcedureProtection {
907
877
  /** `true` when the handler references `ctx.mail` / `ctx.email` (sends mail). */
908
878
  callsMail: boolean;
879
+ /** `true` when the handler emits a structured observability event (`ctx.log` / `ctx.span` / `ctx.trace`). */
880
+ emitsEvent?: boolean;
881
+ /**
882
+ * `true` when the source carries a `// lunora-advisor-exempt` directive above
883
+ * the export — the developer's explicit opt-out. The row still appears in the
884
+ * map, marked `exempt`, but pulls no weight.
885
+ */
886
+ exempt?: boolean;
887
+ /** The `-- reason` from the directive, so an exemption is argued rather than silent. */
888
+ exemptReason?: string;
909
889
  /** The exported binding name of the procedure (e.g. `signUp`). */
910
890
  exportName: string;
911
891
  /** `true` when the handler fans work out to a privileged, cost-bearing dispatch surface (scheduler `runAfter`/`runAt`, a queue producer send, or a workflow create). Read by the privileged-fanout lint, paired with public visibility and no rate limit. */
912
892
  fanOut: boolean;
913
893
  /** Source file relative to the lunora dir, no extension. */
914
894
  file: string;
895
+ /** `true` when the handler wraps work in `try`/`catch`. */
896
+ handlesErrors?: boolean;
915
897
  /**
916
898
  * `true` when the procedure declares an email-shaped argument. Read by the
917
899
  * `signup_mutation_without_disposable_gating` lint: `emailGateMiddleware`
@@ -924,6 +906,12 @@ interface AdvisorProcedureProtection {
924
906
  hasEmailArg?: boolean;
925
907
  /** Registration kind — `query` is read-only; `mutation`/`action` are write-shaped. */
926
908
  kind: "action" | "mutation" | "query";
909
+ /** `true` when the handler runs any AI generation, bounded or not. */
910
+ runsAiGeneration?: boolean;
911
+ /** `true` when the handler reaches an outbound surface (`ctx.fetch`, mail, queues, storage, sql, ai, …) that can fail. */
912
+ reachesOutbound?: boolean;
913
+ /** `true` when the handler throws a bare `new Error(...)` rather than a coded `LunoraError`. */
914
+ throwsBareError?: boolean;
927
915
  /** `true` when the handler runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound. Read by the `ai_unbounded_generation_public` lint (paired with public visibility). */
928
916
  unboundedAiGeneration: boolean;
929
917
  /** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
@@ -950,6 +938,16 @@ interface AdvisorProcedureProtection {
950
938
  * supply it, so the lint simply finds nothing there.
951
939
  */
952
940
  interface AdvisorQueryRead {
941
+ /**
942
+ * The exported procedure the read sits in, when the feeder could resolve one.
943
+ * Carried so the advisor map can attribute the finding to that procedure's row
944
+ * rather than to the project bucket; a read outside any export (module scope)
945
+ * legitimately has none.
946
+ *
947
+ * Optional so a feeder predating this field still typechecks — such a run
948
+ * simply attributes project-wide, exactly as before.
949
+ */
950
+ exportName?: string;
953
951
  /** Source file the read appears in (relative to the lunora dir, no extension). */
954
952
  file: string;
955
953
  /** True when the chain calls `.filter(...)`. */
@@ -1468,6 +1466,36 @@ interface AdvisorTableSample {
1468
1466
  /** Whether more rows exist beyond the cap. */
1469
1467
  readonly truncated: boolean;
1470
1468
  }
1469
+ /**
1470
+ * One branching `defineShape({ where })` / `definePolicy({ when })` predicate arm
1471
+ * that returns an unrestricted predicate — the `unrestricted_where_branch` lint
1472
+ * input.
1473
+ *
1474
+ * A row predicate returns a *filter*, not a boolean, so the denial arm has to be a
1475
+ * predicate matching **no** rows: `deny()` / `{ OR: [] }`, a disjunction over zero
1476
+ * branches. The plausible-looking `{}` is its exact opposite — it matches every row,
1477
+ * so an arm meaning "this caller sees nothing" silently replicates or exposes the
1478
+ * whole table, with no error and no log line.
1479
+ *
1480
+ * Only reported for a *branching* predicate: a single-exit `where: () => ({})` is an
1481
+ * author deliberately replicating everything, which is legitimate. Produced by the
1482
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing there.
1483
+ * Structurally identical to `UnrestrictedWhereBranchIR`.
1484
+ */
1485
+ interface AdvisorUnrestrictedWhereBranch {
1486
+ /** The exported binding name of the shape / policy the predicate belongs to. */
1487
+ exportName: string;
1488
+ /** Source file relative to the lunora dir, no extension. */
1489
+ file: string;
1490
+ /** Which unrestricted form was returned. */
1491
+ form: "empty-object" | "undefined";
1492
+ /** The config key carrying the predicate (`where` for a shape, `when` for a policy). */
1493
+ key: string;
1494
+ /** 1-based line of the offending returned expression. */
1495
+ line: number;
1496
+ /** The declaring call (`defineShape` / `definePolicy`). */
1497
+ owner: string;
1498
+ }
1471
1499
  /**
1472
1500
  * One `ctx.vectors.<method>(indexName, input)` call whose `input.namespace` is
1473
1501
  * derived from the handler's `args` with no server-side scoping — the input the
@@ -1912,12 +1940,6 @@ interface LintContext {
1912
1940
  * absent for runtime callers, where the lint finds nothing.
1913
1941
  */
1914
1942
  ownerFieldWrites?: ReadonlyArray<AdvisorOwnerFieldWrite>;
1915
- /**
1916
- * Branching shape/policy predicate arms returning an unrestricted filter (`{}` /
1917
- * `undefined`) — the `unrestricted_where_branch` lint input. Supplied by the
1918
- * codegen feeder only.
1919
- */
1920
- unrestrictedWhereBranches?: ReadonlyArray<AdvisorUnrestrictedWhereBranch>;
1921
1943
  /**
1922
1944
  * Payment webhook-adapter constructions (`createStripeAdapter` /
1923
1945
  * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`) — the payment-webhook wide-tolerance lint's input. Each row's
@@ -2090,6 +2112,12 @@ interface LintContext {
2090
2112
  * and shards. Absent for static callers, where the lint finds nothing.
2091
2113
  */
2092
2114
  tableScans?: ReadonlyArray<AdvisorTableScan>;
2115
+ /**
2116
+ * Branching shape/policy predicate arms returning an unrestricted filter (`{}` /
2117
+ * `undefined`) — the `unrestricted_where_branch` lint input. Supplied by the
2118
+ * codegen feeder only.
2119
+ */
2120
+ unrestrictedWhereBranches?: ReadonlyArray<AdvisorUnrestrictedWhereBranch>;
2093
2121
  /**
2094
2122
  * `ctx.vectors.&lt;method>(index, { namespace, … })` calls whose `namespace` is
2095
2123
  * derived from the handler's `args` with no server-side scoping — the
@@ -2322,6 +2350,21 @@ declare const indexUtilization: Lint;
2322
2350
  * arg-derived `ctx.fetch` call.
2323
2351
  */
2324
2352
  declare const actionFetchSsrf: Lint;
2353
+ /**
2354
+ * Flags an `action` that reaches an outbound surface with no `try`/`catch`
2355
+ * anywhere in its body.
2356
+ *
2357
+ * Actions are where Lunora talks to things it does not control — `ctx.fetch`,
2358
+ * mail, queues, storage, external SQL, AI. Every one of those fails routinely
2359
+ * (timeout, 5xx, quota), and an uncaught rejection there surfaces to the caller
2360
+ * as an opaque failure with no indication of which dependency broke. Catching it
2361
+ * is what lets the handler add that context, or degrade instead of failing.
2362
+ *
2363
+ * Only `action` is checked: queries and mutations run inside the Durable Object
2364
+ * and cannot reach these surfaces. Runs only when the codegen feeder supplies
2365
+ * procedure evidence.
2366
+ */
2367
+ declare const actionWithoutErrorHandling: Lint;
2325
2368
  /**
2326
2369
  * Flags an `httpRoute` on an admin/privileged-looking path whose handler shows no
2327
2370
  * auth/admin guard.
@@ -2359,6 +2402,22 @@ declare const adminRouteWithoutGuard: Lint;
2359
2402
  * arg-derived, unscoped `ctx.ai.run` call.
2360
2403
  */
2361
2404
  declare const aiRawRunEscapeHatch: Lint;
2405
+ /**
2406
+ * Flags a procedure that runs an AI generation but emits no structured event.
2407
+ *
2408
+ * Model calls are the least reproducible thing in an app and the only one that
2409
+ * bills per invocation: the same input can return a different answer tomorrow,
2410
+ * and a runaway loop is a cost incident rather than an outage. Without an event
2411
+ * recording that the call happened there is no way to attribute spend, compare a
2412
+ * bad answer against the prompt that produced it, or notice a retry storm.
2413
+ *
2414
+ * Keyed on whether the handler runs a model at all — bounded or not. An earlier
2415
+ * cut reused the `unboundedAiGeneration` / raw-run signals, which meant the
2416
+ * correctly-bounded `generateText({ …, maxOutputTokens })` — the common case, and
2417
+ * the one this rule exists for — was never flagged, while procedures another lint
2418
+ * had already caught were charged twice.
2419
+ */
2420
+ declare const aiRunWithoutLogging: Lint;
2362
2421
  /**
2363
2422
  * Flags a `generateText` / `streamText` call whose model input is user-derived
2364
2423
  * **and** whose model-callable `tools` reach a privileged side effect.
@@ -2476,6 +2535,30 @@ declare const authCsrfCheckDisabled: Lint;
2476
2535
  * call.
2477
2536
  */
2478
2537
  declare const authEmailVerificationDisabled: Lint;
2538
+ /**
2539
+ * Flags a `createAuth({...})` call that loads `scim()` on an adapter with no native
2540
+ * transactions.
2541
+ *
2542
+ * `@better-auth/scim` refuses to serve at all unless its adapter exposes a
2543
+ * `transaction` function — its provisioning writes go through a read-then-conditional-write
2544
+ * decommission lease. `lunoraD1Adapter` and `lunoraAuthAdapter` are single-table CRUD,
2545
+ * and D1 has no interactive transactions to expose in the first place, so this pairing
2546
+ * throws on the first SCIM request:
2547
+ *
2548
+ * ```
2549
+ * BetterAuthError: The scim plugin requires a database adapter with native transaction support.
2550
+ * ```
2551
+ *
2552
+ * This exists because the combination shipped in real documentation once. It is not a
2553
+ * subtle misconfiguration — it is a deployment where directory sync cannot work at all —
2554
+ * and it is entirely visible at build time, so there is no reason for the first
2555
+ * indication to be a 500 from an IdP's provisioning call.
2556
+ *
2557
+ * Runs only when the codegen feeder supplies auth-config evidence
2558
+ * (`context.authConfigs`), and only for an analyzable config; an opaque one could pass
2559
+ * a different `database` and is skipped rather than guessed at.
2560
+ */
2561
+ declare const authScimWithoutTransactions: Lint;
2479
2562
  /**
2480
2563
  * Flags a `createAuth({...})` call whose `advanced.useSecureCookies` is
2481
2564
  * explicitly `false`.
@@ -2515,30 +2598,6 @@ declare const authSecureCookiesDisabled: Lint;
2515
2598
  * call.
2516
2599
  */
2517
2600
  declare const authSessionFreshageZero: Lint;
2518
- /**
2519
- * Flags a `createAuth({...})` call that loads `scim()` on an adapter with no native
2520
- * transactions.
2521
- *
2522
- * `@better-auth/scim` refuses to serve at all unless its adapter exposes a
2523
- * `transaction` function — its provisioning writes go through a read-then-conditional-write
2524
- * decommission lease. `lunoraD1Adapter` and `lunoraAuthAdapter` are single-table CRUD,
2525
- * and D1 has no interactive transactions to expose in the first place, so this pairing
2526
- * throws on the first SCIM request:
2527
- *
2528
- * ```
2529
- * BetterAuthError: The scim plugin requires a database adapter with native transaction support.
2530
- * ```
2531
- *
2532
- * This exists because the combination shipped in real documentation once. It is not a
2533
- * subtle misconfiguration — it is a deployment where directory sync cannot work at all —
2534
- * and it is entirely visible at build time, so there is no reason for the first
2535
- * indication to be a 500 from an IdP's provisioning call.
2536
- *
2537
- * Runs only when the codegen feeder supplies auth-config evidence
2538
- * (`context.authConfigs`), and only for an analyzable config; an opaque one could pass
2539
- * a different `database` and is skipped rather than guessed at.
2540
- */
2541
- declare const authScimWithoutTransactions: Lint;
2542
2601
  /**
2543
2602
  * Flags a `createAuth({...})` call whose `trustedOrigins` array literal
2544
2603
  * contains a `"*"` entry.
@@ -2694,6 +2753,19 @@ declare const duplicateIndex: Lint;
2694
2753
  * least one field by construction, so only `kind: "index"` is checked.)
2695
2754
  */
2696
2755
  declare const emptyIndex: Lint;
2756
+ /**
2757
+ * Flags a procedure that throws a bare `new Error(...)`.
2758
+ *
2759
+ * A bare `Error` crosses the RPC boundary as an opaque message: the client
2760
+ * cannot branch on it, `@lunora/fingerprint` cannot group it into an issue, and
2761
+ * the message itself is free text that changes whenever someone edits the
2762
+ * string. `LunoraError` carries a stable code from `ERROR_CATALOG`, which is
2763
+ * what makes an error both matchable on the client and groupable in Studio.
2764
+ *
2765
+ * Runs only when the codegen feeder supplies procedure evidence; a runtime
2766
+ * caller with no evidence flags nothing.
2767
+ */
2768
+ declare const errorWithoutCatalog: Lint;
2697
2769
  /**
2698
2770
  * Flags a misconfigured CDC export sink (plan 170) — a `defineExportSink` /
2699
2771
  * `webhookExportSink` / `r2Sink` construction with a required config field
@@ -3378,6 +3450,23 @@ declare const privilegedDispatchUnvalidatedPayload: Lint;
3378
3450
  * per unguarded public fan-out procedure.
3379
3451
  */
3380
3452
  declare const privilegedFanoutFromPublicProcedure: Lint;
3453
+ /**
3454
+ * Flags a public `mutation`/`action` whose handler emits no structured
3455
+ * observability event.
3456
+ *
3457
+ * When one of these fails in production you get a stack trace and nothing about
3458
+ * the request that caused it — no ids, no tenant, no outcome. `ctx.log` /
3459
+ * `ctx.span` attach that context to the invocation, so the failure is
3460
+ * searchable instead of merely visible. Reads are excluded: a `query` that
3461
+ * returns the wrong rows is diagnosable from its arguments, while a write that
3462
+ * half-succeeded is not.
3463
+ *
3464
+ * Runs only when the codegen feeder supplies procedure evidence; a runtime
3465
+ * caller with no evidence flags nothing. The feeder reports "no event" only when
3466
+ * it could read the body, so a procedure whose handler it cannot analyze is left
3467
+ * alone rather than nagged.
3468
+ */
3469
+ declare const procedureWithoutStructuredEvent: Lint;
3381
3470
  /**
3382
3471
  * Flags a `v.any()` argument on a public procedure.
3383
3472
  *
@@ -3982,6 +4071,253 @@ declare const workflowUnknownTarget: Lint;
3982
4071
  * (`context.workflows` present); a runtime caller flags nothing.
3983
4072
  */
3984
4073
  declare const workflowUnused: Lint;
4074
+ /** Letter band for a 0–100 score. */
4075
+ type Grade = "at-risk" | "excellent" | "good" | "needs-work";
4076
+ /**
4077
+ * How a single procedure came out of the lints that apply to it.
4078
+ *
4079
+ * Named for severity rather than instrumentation, because the score is driven by
4080
+ * every lint family (security, performance, schema): a verdict like
4081
+ * "uninstrumented" would claim something the map does not measure, and would
4082
+ * report a security regression as a telemetry gap. `clean` means no lint fired.
4083
+ * `exempt` rows pull no weight.
4084
+ */
4085
+ type Coverage = "clean" | "exempt" | "failing" | "warned";
4086
+ /**
4087
+ * One *rule* that fired against a procedure, reduced to its score contribution.
4088
+ *
4089
+ * Deduplicated by lint: a rule that fires on five call sites is one check with
4090
+ * `occurrences: 5`, costing its weight once. Counting occurrences instead would
4091
+ * let a single rule zero a procedure and would fill the artifact with identical
4092
+ * rows.
4093
+ */
4094
+ interface CheckResult {
4095
+ /** Worst severity seen across this rule's occurrences. */
4096
+ level: Level;
4097
+ /** The lint id that fired, e.g. `unindexed_foreign_key`. */
4098
+ name: string;
4099
+ /** How many findings this rule produced on the procedure (>= 1). */
4100
+ occurrences: number;
4101
+ /** Points subtracted from the procedure's score — charged once, not per occurrence. */
4102
+ weight: number;
4103
+ }
4104
+ /** How much a procedure's failures matter — see `classifySensitivity`. */
4105
+ type SensitivityLevel = "high" | "none";
4106
+ /** A procedure's sensitivity plus the declarations that produced it. */
4107
+ interface Sensitivity {
4108
+ /** `high` when any signal fired; `none` means no signal, not "safe". */
4109
+ level: SensitivityLevel;
4110
+ /** Human-readable signals, e.g. "writes an identity table". Empty when `none`. */
4111
+ reasons: string[];
4112
+ }
4113
+ /** One scored procedure — the unit the map is built from. */
4114
+ interface ProcedureScore {
4115
+ /** Rules that fired against this procedure, sorted by `name`. */
4116
+ checks: CheckResult[];
4117
+ /** Verdict derived from {@link ProcedureScore.score}. */
4118
+ coverage: Coverage;
4119
+ /** Why this row is `exempt`, when a source directive said so. Empty when none was given. */
4120
+ exemptReason?: string;
4121
+ /** Exported binding name, e.g. `sendMessage`. */
4122
+ exportName: string;
4123
+ /** Source file relative to the lunora dir, no extension. */
4124
+ file: string;
4125
+ /** Stable `file#exportName` identity — the baseline diff key. */
4126
+ id: string;
4127
+ /** Registration kind. */
4128
+ kind: "action" | "mutation" | "query";
4129
+ /** 0–100, starting at 100 less each fired rule's weight. */
4130
+ score: number;
4131
+ /** How much this procedure's failures matter, and why. */
4132
+ sensitivity: Sensitivity;
4133
+ /** Public (client-callable) or internal (server-called). */
4134
+ visibility: "internal" | "public";
4135
+ /** This row's weight in the global mean — see `procedureWeight`. */
4136
+ weight: number;
4137
+ }
4138
+ /**
4139
+ * Findings that name no procedure — schema-shape and project-wide lints
4140
+ * (missing index, circular FK, plaintext wrangler secret), plus any finding
4141
+ * whose `file`/`exportName` matches no declared procedure.
4142
+ *
4143
+ * Folded into the global mean at a weight proportional to the procedure
4144
+ * population (see `projectWeight`) so schema debt genuinely moves the grade.
4145
+ * Because this single score saturates at 0, `compareToBaseline` also diffs
4146
+ * `checks.length` — otherwise new schema errors would be free once the bucket
4147
+ * bottoms out.
4148
+ */
4149
+ interface ProjectScore {
4150
+ /** Rules that fired at project level, sorted by `name`. */
4151
+ checks: CheckResult[];
4152
+ /** 0–100, same formula as a procedure. */
4153
+ score: number;
4154
+ }
4155
+ /** Coverage tallies for the summary line and the Studio matrix header. */
4156
+ interface MapSummary {
4157
+ /** Procedures with no lint firing. */
4158
+ clean: number;
4159
+ /** Procedures excluded from scoring. */
4160
+ exempt: number;
4161
+ /** Procedures scoring below the failing floor. */
4162
+ failing: number;
4163
+ /** Total procedures in the map, exempt included. */
4164
+ procedures: number;
4165
+ /** Findings attributed to a scored row, after per-rule deduplication. */
4166
+ rulesFired: number;
4167
+ /** Procedures scoring at or above the failing floor, but not clean. */
4168
+ warned: number;
4169
+ }
4170
+ /**
4171
+ * The `lunora.advisor.map.json` artifact — the unit a baseline is diffed
4172
+ * against and the Studio health panel renders.
4173
+ */
4174
+ interface AdvisorMap {
4175
+ /**
4176
+ * ISO-8601 stamp. Defaults to the current time; pass
4177
+ * `ScoreAdvisorOptions.generatedAt` explicitly when the artifact must be
4178
+ * byte-stable (tests, reproducible builds) — everything else in the map is
4179
+ * a pure function of the findings.
4180
+ */
4181
+ generatedAt: string;
4182
+ /** Band for {@link AdvisorMap.score}. */
4183
+ grade: Grade;
4184
+ /** Per-procedure rows, sorted by `id` for a stable diff. */
4185
+ procedures: ProcedureScore[];
4186
+ /** Findings not attributable to a procedure. */
4187
+ project: ProjectScore;
4188
+ /** Weighted mean over non-exempt procedures plus the project entry, 0–100. */
4189
+ score: number;
4190
+ /** Coverage tallies. */
4191
+ summary: MapSummary;
4192
+ /** Artifact shape version; bump on a breaking change so an old baseline is rejected rather than mis-read. */
4193
+ version: number;
4194
+ }
4195
+ /** One procedure whose score moved between the baseline and the current map. */
4196
+ interface ProcedureDelta {
4197
+ /** Score in the current map. */
4198
+ after: number;
4199
+ /** Score in the baseline. */
4200
+ before: number;
4201
+ /** `file#exportName`. */
4202
+ id: string;
4203
+ }
4204
+ /**
4205
+ * The verdict a CI gate acts on.
4206
+ *
4207
+ * A discriminated union on purpose: the obvious shape (a flat object with
4208
+ * `comparable: boolean` and `regressed: false` when incomparable) reads as
4209
+ * "no regression" for a missing, stale, or corrupt baseline, so a gate written
4210
+ * as `if (diff.regressed)` silently passes forever after a `MAP_VERSION` bump.
4211
+ * Forcing the caller to narrow makes that mistake unrepresentable.
4212
+ */
4213
+ type BaselineComparison = {
4214
+ comparable: false;
4215
+ /** Why no comparison was possible — a gate should treat this as "cannot verify", not "clean". */
4216
+ reason: "version-mismatch";
4217
+ } | {
4218
+ comparable: true;
4219
+ /** Procedures present in both maps whose score fell, sorted by `id`. */
4220
+ dropped: ProcedureDelta[];
4221
+ /** Procedures that are `failing` now and were not before — new rows included. */
4222
+ newFailing: string[];
4223
+ /** `true` when the project bucket gained rules or occurrences, even if its saturated score did not move. */
4224
+ projectRegressed: boolean;
4225
+ /** `true` when any signal above fired. */
4226
+ regressed: boolean;
4227
+ /** Current global score less the baseline's; negative is a regression. */
4228
+ scoreDelta: number;
4229
+ /**
4230
+ * Procedures whose score held but whose findings grew — a new rule fired,
4231
+ * or an existing one fired at more call sites. Scoring charges a rule once
4232
+ * however many times it fires, so without this signal "same rule, five more
4233
+ * violations" would look identical to the baseline.
4234
+ */
4235
+ worsened: string[];
4236
+ };
4237
+ /**
4238
+ * Diff a freshly-scored map against a committed one.
4239
+ *
4240
+ * Five independent regression signals, any of which fails a gate: the global
4241
+ * score fell, a procedure that existed before got worse, a procedure started
4242
+ * failing, a procedure's findings grew without its score moving, or the project
4243
+ * bucket gained findings. The per-procedure signals matter because a refactor can
4244
+ * leave the global mean flat while gutting one handler; the growth signals matter
4245
+ * because a rule is charged once however many times it fires, and the project
4246
+ * score saturates at 0 — without them, new violations would be free.
4247
+ */
4248
+ declare const compareToBaseline: (current: AdvisorMap, baseline: AdvisorMap) => BaselineComparison;
4249
+ /**
4250
+ * Narrow a parsed `lunora.advisor.map.json` to an {@link AdvisorMap}, returning
4251
+ * `undefined` when it is not one this build can read.
4252
+ *
4253
+ * Validates *shape* — the header and every procedure row — because
4254
+ * `compareToBaseline` dereferences `entry.id` / `entry.score` /
4255
+ * `entry.coverage`: a truncated or merge-conflicted baseline with a `null` row
4256
+ * would otherwise crash the gate, and a row of `{}` would compare as a silent
4257
+ * no-op. Non-finite scores are rejected for the same reason — `NaN < 0` is
4258
+ * `false`, which reads as "no regression".
4259
+ *
4260
+ * Version *policy* deliberately lives in {@link compareToBaseline}, not here.
4261
+ * Rejecting a mismatch in both places made that function's `comparable: false`
4262
+ * arm unreachable through every shipped path, so the union that exists to stop a
4263
+ * stale baseline reading as "clean" was never exercised. Here we only require a
4264
+ * version to be present and finite.
4265
+ */
4266
+ declare const parseAdvisorMap: (value: unknown) => AdvisorMap | undefined;
4267
+ /** Band a 0–100 score. */
4268
+ declare const gradeFromScore: (score: number) => Grade;
4269
+ /**
4270
+ * Codepoint ordering. `localeCompare` would sort by the host's `LANG`/`LC_ALL`,
4271
+ * so the same repo scored on a Danish-locale runner emits a different row order
4272
+ * than an `en_US` one — the committed artifact would churn and a
4273
+ * `git diff --exit-code` gate would fail for no reason. Ordering must be a
4274
+ * property of the data, not the machine.
4275
+ */
4276
+ declare const byCodepoint: (a: string, b: string) => number;
4277
+ /**
4278
+ * {@link AdvisorMap.version} this build emits. Bump on a breaking shape change
4279
+ * so `compareToBaseline` rejects a stale artifact instead of mis-reading it.
4280
+ */
4281
+ declare const MAP_VERSION = 1;
4282
+ /** Options for {@link scoreAdvisor}. */
4283
+ interface ScoreAdvisorOptions {
4284
+ /**
4285
+ * Procedure ids (`file#exportName`) to exclude from scoring, for a caller
4286
+ * that computes exemptions itself. Source-level `// lunora-advisor-exempt`
4287
+ * directives are honoured independently; the two compose.
4288
+ */
4289
+ exempt?: ReadonlyArray<string>;
4290
+ /**
4291
+ * Stamp written to {@link AdvisorMap.generatedAt}. Defaults to now; pass it
4292
+ * explicitly when the map must be byte-stable (tests, reproducible builds).
4293
+ */
4294
+ generatedAt?: string;
4295
+ }
4296
+ /**
4297
+ * Roll a lint run up into a scored coverage map (see the package's
4298
+ * `docs/index.mdx` for the scoring rules and the baseline gate).
4299
+ *
4300
+ * Pure: it re-reads the `findings` a caller already got from `runAdvisor` rather
4301
+ * than running lints itself, so scoring never double-runs a rule and the lint
4302
+ * core stays untouched. Findings are attributed to a procedure via their
4303
+ * `metadata.file` + `metadata.exportName`; everything else lands in the project
4304
+ * bucket, which is folded into the global mean at a weight proportional to the
4305
+ * procedure population so schema debt genuinely moves the grade.
4306
+ *
4307
+ * Takes the procedure list directly rather than a `LintContext`: it reads no
4308
+ * other feeder, and asking for the whole context forced callers with only a
4309
+ * procedure array to fabricate an empty schema.
4310
+ */
4311
+ declare const scoreAdvisor: (procedures: ReadonlyArray<AdvisorProcedureProtection>, findings: ReadonlyArray<Finding>, options?: ScoreAdvisorOptions) => AdvisorMap;
4312
+ /**
4313
+ * Classify how much a procedure's failures matter, from the protective
4314
+ * declarations and behavioural facts the feeder already collected.
4315
+ *
4316
+ * Runs before any rule so a lint can gate on it, and feeds the global weighting:
4317
+ * a handler touching identity, mail, or tenant-scoped rows pulls harder on the
4318
+ * grade than a plain read. `none` means no signal fired — not "safe".
4319
+ */
4320
+ declare const classifySensitivity: (procedure: AdvisorProcedureProtection) => Sensitivity;
3985
4321
  /**
3986
4322
  * Every lint that runs against the declared schema (and, for
3987
4323
  * `filter_without_index`, the discovered query reads) — no running shard
@@ -4013,4 +4349,4 @@ interface RunAdvisorOptions {
4013
4349
  * `static` lints at build time and defer `runtime` lints to a live shard.
4014
4350
  */
4015
4351
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
4016
- export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorExportSink, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorGeoIndexUsage, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorNotifyCall, type AdvisorNotifyConfig, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authScimWithoutTransactions, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, notifyMissingPushConfig, notifySendOutsideAction, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, signupMutationWithoutDisposableGating, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, ttlFieldNotTimestamp, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, unrestrictedWhereBranch, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
4352
+ export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorExportSink, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorGeoIndexUsage, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMap, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorNotifyCall, type AdvisorNotifyConfig, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type BaselineComparison, type Category, type CheckResult, type Coverage, type Facing, type Finding, type Grade, type Level, type Lint, type LintContext, type LintSource, MAP_VERSION, type MapSummary, type ProcedureDelta, type ProcedureScore, type ProjectScore, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, type ScoreAdvisorOptions, type Sensitivity, type SensitivityLevel, actionFetchSsrf, actionWithoutErrorHandling, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiRunWithoutLogging, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authScimWithoutTransactions, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, byCodepoint, circularFk, classifySensitivity, compareToBaseline, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, errorWithoutCatalog, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, gradeFromScore, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, notifyMissingPushConfig, notifySendOutsideAction, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, parseAdvisorMap, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, procedureWithoutStructuredEvent, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, scoreAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, signupMutationWithoutDisposableGating, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, ttlFieldNotTimestamp, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, unrestrictedWhereBranch, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };