@sanity/ailf-studio 2.0.3 → 2.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.d.ts CHANGED
@@ -100,9 +100,9 @@ declare const RunTaskEvaluationAction: DocumentActionComponent;
100
100
  /**
101
101
  * structure.ts
102
102
  *
103
- * Structure helpers for the `ailf.task` document type. Exposes filtered
104
- * list views so editors see only active tasks by default and can toggle
105
- * into draft / paused / archived / all.
103
+ * Structure helpers for AILF document types. Exposes filtered list views so
104
+ * editors see only the documents they care about by default, with diagnostic
105
+ * sub-lists that surface routing/ownership gaps.
106
106
  *
107
107
  * ## Usage
108
108
  *
@@ -118,10 +118,13 @@ declare const RunTaskEvaluationAction: DocumentActionComponent;
118
118
  * })
119
119
  * ```
120
120
  *
121
- * `ailfStructure` renders a filtered "AILF Tasks" entry in place of the
122
- * default `ailf.task` list item and preserves every other document type
123
- * at its Studio default. Consumers who already maintain a custom
124
- * structure can splice just the list item in via `ailfTaskStructureItem`.
121
+ * `ailfStructure` renders filtered entries for `ailf.task`, `ailf.team`,
122
+ * `ailf.featureArea`, and `ailf.report` in place of their default Studio
123
+ * list items, plus a top-level diagnostic for teams whose channel events
124
+ * reference unknown event-type strings. Other document types are preserved
125
+ * at their Studio default via `S.documentTypeListItems().filter(...)`.
126
+ * Consumers who already maintain a custom structure can splice individual
127
+ * helpers in via the per-type exports.
125
128
  */
126
129
 
127
130
  /**
@@ -131,9 +134,10 @@ declare const RunTaskEvaluationAction: DocumentActionComponent;
131
134
  */
132
135
  declare function ailfTaskStructureItem(S: StructureBuilder): ReturnType<StructureBuilder["listItem"]>;
133
136
  /**
134
- * Full structure resolver that replaces the default `ailf.task` entry
135
- * with the filtered AILF Tasks item and keeps every other document
136
- * type list item at its Studio default.
137
+ * Full structure resolver that replaces the default entries for
138
+ * `ailf.task`, `ailf.team`, `ailf.featureArea`, and `ailf.report` with
139
+ * filtered AILF views, exposes a top-level unknown-events diagnostic,
140
+ * and keeps every other document type list item at its Studio default.
137
141
  */
138
142
  declare const ailfStructure: StructureResolver;
139
143
 
@@ -388,7 +392,7 @@ declare const scoreTimelineQuery: string;
388
392
  *
389
393
  * Used by: ReportDetail view
390
394
  */
391
- declare const reportDetailQuery = "\n *[_type == \"ailf.report\" && reportId == $reportId][0] {\n _id,\n reportId,\n completedAt,\n durationMs,\n tag,\n title,\n provenance,\n summary,\n \"comparison\": comparison {\n areas,\n deltas,\n generatedAt,\n improved,\n mismatched,\n noiseThreshold,\n noiseThresholdEmpirical,\n notEvaluated,\n regressed,\n unchanged\n }\n }\n";
395
+ declare const reportDetailQuery = "\n *[_type == \"ailf.report\" && reportId == $reportId][0] {\n _id,\n reportId,\n completedAt,\n durationMs,\n tag,\n title,\n provenance,\n \"teamDoc\": *[_type == \"ailf.team\" && slug.current == ^.provenance.owner.team][0]{\n _id,\n displayName,\n status\n },\n summary,\n \"comparison\": comparison {\n areas,\n deltas,\n generatedAt,\n improved,\n mismatched,\n noiseThreshold,\n noiseThresholdEmpirical,\n notEvaluated,\n regressed,\n unchanged\n }\n }\n";
392
396
  /**
393
397
  * Find all reports that evaluated a specific Sanity document or perspective.
394
398
  *
@@ -606,6 +610,35 @@ declare const taskSchema: {
606
610
  }, Record<string, unknown>> | undefined;
607
611
  };
608
612
 
613
+ /**
614
+ * schema/team.ts
615
+ *
616
+ * Sanity document schema for `ailf.team` — a team that owns feature areas,
617
+ * repos, or task tags, and the notification channels used to route events
618
+ * to its members.
619
+ *
620
+ * The team entity is the routing target for the AILF notification system.
621
+ * A team owns members (people, identified by email / Sanity user id /
622
+ * GitHub username) and zero-or-more notification channels (Slack, email,
623
+ * webhook). Each channel carries a `scope` that says which events the
624
+ * channel cares about: the team's own areas/repos, all events, or a
625
+ * specific list of areas/repos/tags.
626
+ *
627
+ * Soft enums (member role, event type) are seeded from `@sanity/ailf-shared`
628
+ * but not closed — teams can introduce custom roles/events without a code
629
+ * change. The `options.list` provides suggestions; Sanity's `string` field
630
+ * doesn't enforce the list as a closed enum.
631
+ */
632
+ declare const teamSchema: {
633
+ type: "document";
634
+ name: "ailf.team";
635
+ } & Omit<sanity.DocumentDefinition, "preview"> & {
636
+ preview?: sanity.PreviewConfig<{
637
+ displayName: string;
638
+ slug: string;
639
+ }, Record<string, unknown>> | undefined;
640
+ };
641
+
609
642
  /**
610
643
  * schema/webhook-config.ts
611
644
  *
@@ -812,6 +845,20 @@ interface ProvenanceData {
812
845
  workflow?: string;
813
846
  };
814
847
  }
848
+ /**
849
+ * Resolved `ailf.team` document referenced by `provenance.owner.team`.
850
+ *
851
+ * Projected as a peer of `ownerTeam`/`provenance` in `latestReportsQuery`
852
+ * and `reportDetailQuery` so the dashboard can render the team's
853
+ * displayName and detect archived/missing teams without a second
854
+ * roundtrip. `null` when no team doc matches the owner slug (legacy
855
+ * reports, or a team that has not yet been seeded).
856
+ */
857
+ interface TeamDocRef {
858
+ _id: string;
859
+ displayName: string;
860
+ status: "active" | "archived";
861
+ }
815
862
  /** Shape returned by reportDetailQuery */
816
863
  interface ReportDetail {
817
864
  _id: string;
@@ -822,6 +869,8 @@ interface ReportDetail {
822
869
  reportId: string;
823
870
  summary: SummaryData;
824
871
  tag: null | string;
872
+ /** Resolved owner team document (T10) — null when no `ailf.team` doc matches. */
873
+ teamDoc?: TeamDocRef | null;
825
874
  title: null | string;
826
875
  }
827
876
  /** Shape returned by latestReportsQuery */
@@ -871,6 +920,8 @@ interface ReportListItem {
871
920
  scores: ScoreItem[];
872
921
  source: string;
873
922
  tag: null | string;
923
+ /** Resolved owner team document (T10) — null when no `ailf.team` doc matches. */
924
+ teamDoc?: TeamDocRef | null;
874
925
  title: null | string;
875
926
  /** Target document slugs (when evaluated with --changed-docs) */
876
927
  targetDocuments?: null | string[];
@@ -1179,4 +1230,4 @@ interface AilfPluginOptions {
1179
1230
  */
1180
1231
  declare const ailfPlugin: sanity.Plugin<void | AilfPluginOptions>;
1181
1232
 
1182
- export { type AilfPluginOptions, ArchiveTaskAction, AssertionInput, CanonicalDocInput, type ComparisonData, type ContentImpactItem, GraduateToNativeAction, HelpDrawer, HelpProvider, MirrorBanner, type PerModelData, type ProvenanceData, ReleasePicker, type ReportDetail, type ReportListItem, RestoreTaskAction, type RunEvaluationActionOptions, RunTaskEvaluationAction, type ScoreItem, type SummaryData, SyncStatusBadge, type TimelineDataPoint, ailfPlugin, ailfStructure, ailfTaskStructureItem, ailfTool, articleSearchQuery, comparisonPairQuery, contentImpactQuery, createRunEvaluationAction, deriveHelpTopic, distinctAreasQuery, distinctModesQuery, distinctPerspectivesQuery, distinctSourcesQuery, distinctTargetDocumentsQuery, distinctTriggersQuery, evalRequestSchema, featureAreaSchema, findTopic, latestReportsQuery, recentDocumentEvalsQuery, referenceSolutionSchema, reportDetailQuery, reportSchema, scoreTimelineQuery, searchTopics, taskSchema, useHelp, webhookConfigSchema };
1233
+ export { type AilfPluginOptions, ArchiveTaskAction, AssertionInput, CanonicalDocInput, type ComparisonData, type ContentImpactItem, GraduateToNativeAction, HelpDrawer, HelpProvider, MirrorBanner, type PerModelData, type ProvenanceData, ReleasePicker, type ReportDetail, type ReportListItem, RestoreTaskAction, type RunEvaluationActionOptions, RunTaskEvaluationAction, type ScoreItem, type SummaryData, SyncStatusBadge, type TimelineDataPoint, ailfPlugin, ailfStructure, ailfTaskStructureItem, ailfTool, articleSearchQuery, comparisonPairQuery, contentImpactQuery, createRunEvaluationAction, deriveHelpTopic, distinctAreasQuery, distinctModesQuery, distinctPerspectivesQuery, distinctSourcesQuery, distinctTargetDocumentsQuery, distinctTriggersQuery, evalRequestSchema, featureAreaSchema, findTopic, latestReportsQuery, recentDocumentEvalsQuery, referenceSolutionSchema, reportDetailQuery, reportSchema, scoreTimelineQuery, searchTopics, taskSchema, teamSchema, useHelp, webhookConfigSchema };
package/dist/index.js CHANGED
@@ -191,6 +191,17 @@ import {
191
191
  useProjectId
192
192
  } from "sanity";
193
193
 
194
+ // ../shared/dist/event-types.js
195
+ var KNOWN_EVENT_TYPES = [
196
+ "eval.failed",
197
+ "eval.completed",
198
+ "eval.threshold-breached",
199
+ "eval.score-regressed",
200
+ "task.created",
201
+ "task.archived",
202
+ "area.unowned-tasks"
203
+ ];
204
+
194
205
  // ../shared/dist/feature-flags.js
195
206
  var FEATURE_FLAGS = {
196
207
  showFailureModes: {
@@ -692,7 +703,7 @@ Click into any report for the full breakdown: per-area scores, diagnostics, and
692
703
  {
693
704
  "id": "weaknesses-recommendations",
694
705
  "title": "Weaknesses & Recommendations",
695
- "body": "## Understanding weaknesses\n\nThe Issues sub-tab in Diagnostics lists every area or dimension that scored\nbelow threshold. Each weakness entry shows:\n\n- **The feature area** \u2014 Which product feature is affected (e.g., GROQ,\n Functions, Webhooks).\n- **The bottleneck dimension** \u2014 Which scoring dimension is dragging the area\n down: task completion, code correctness, or doc coverage.\n- **The score** \u2014 How far below threshold the dimension scored.\n\n## Gap analysis recommendations\n\nWhen an evaluation runs with gap analysis enabled, the dashboard shows\n**prioritized recommendations** \u2014 specific actions ranked by estimated impact.\n\nEach recommendation includes:\n\n- **Failure mode** \u2014 The type of doc problem identified:\n - `missing-docs` \u2014 The functionality isn't documented at all.\n - `incorrect-docs` \u2014 The docs contain factual errors.\n - `outdated-docs` \u2014 The docs describe an old API version or pattern.\n - `poor-structure` \u2014 The docs exist but are hard to find or understand.\n- **Estimated lift** \u2014 How many score points fixing this gap would add. Based on\n raising the bottleneck dimension to the median of non-bottleneck dimensions.\n Conservative estimate \u2014 actual improvement may be higher.\n- **Confidence** \u2014 How sure the analysis is about this diagnosis (high, medium,\n or low).\n- **Affected tasks** \u2014 Which specific evaluation tasks exposed this gap.\n\n## Low-scoring judgments\n\nBelow the recommendations, you'll find the **grader's explanations** for tests\nthat scored below 70. These are the raw assessments from the grading model\nexplaining exactly what went wrong \u2014 missing API calls, incorrect patterns,\nhallucinated features, etc.\n\nEach judgment shows the task, the dimension, the score, and the grader's natural\nlanguage reason. These are the most granular diagnostic signal available and\noften point directly to the doc section that needs fixing.",
706
+ "body": "## Understanding weaknesses\n\nThe Issues sub-tab in Diagnostics lists every area or dimension that scored\nbelow threshold. Each weakness entry shows:\n\n- **The feature area** \u2014 Which product feature is affected (e.g., GROQ,\n Functions, Webhooks).\n- **The bottleneck dimension** \u2014 Which scoring dimension is dragging the area\n down: task completion, code correctness, or doc coverage.\n- **The score** \u2014 How far below threshold the dimension scored.\n\n## Gap analysis recommendations\n\nWhen an evaluation runs with gap analysis enabled, the dashboard shows\n**prioritized recommendations** \u2014 specific actions ranked by estimated impact.\n\nEach recommendation includes:\n\n- **Failure mode** \u2014 The type of doc problem identified:\n - `missing-docs` \u2014 The functionality isn't documented at all.\n - `incorrect-docs` \u2014 The docs contain factual errors.\n - `outdated-docs` \u2014 The docs describe an old API version or pattern.\n - `poor-structure` \u2014 The docs exist but are hard to find or understand.\n- **Estimated lift** \u2014 How many score points fixing this gap would add. Based on\n raising the bottleneck dimension to the median of non-bottleneck dimensions.\n Conservative estimate \u2014 actual improvement may be higher.\n- **Confidence** \u2014 How sure the analysis is about this diagnosis (high, medium,\n or low).\n- **Affected tasks** \u2014 Which specific evaluation tasks exposed this gap.\n\n## Diagnosis cards\n\nEvery published report now carries a **diagnosis artifact** \u2014 a set of cards\nproduced by the post-pipeline hook (`ailf interpret`). The Studio diagnosis\npanel renders these cards directly; the dashboard's Recommendations and\nFailure-modes panels migrate to the same source in a follow-up.\n\nThe hook runs by default for every pipeline invocation. To opt out for a single\nrun, pass `--no-summary`; to opt out in CI, set `AILF_INTERPRET_ON_RUN=0` in the\nworkflow env block; to opt out project-wide, set `summary.onRun: never` in\n`.ailf/config.yaml`.\n\n## Low-scoring judgments\n\nBelow the recommendations, you'll find the **grader's explanations** for tests\nthat scored below 70. These are the raw assessments from the grading model\nexplaining exactly what went wrong \u2014 missing API calls, incorrect patterns,\nhallucinated features, etc.\n\nEach judgment shows the task, the dimension, the score, and the grader's natural\nlanguage reason. These are the most granular diagnostic signal available and\noften point directly to the doc section that needs fixing.",
696
707
  "source": "docs/help/weaknesses-recommendations.md",
697
708
  "related": [
698
709
  "interpreting-diagnostics",
@@ -732,6 +743,9 @@ Click into any report for the full breakdown: per-area scores, diagnostics, and
732
743
  }
733
744
  ];
734
745
 
746
+ // ../shared/dist/member-roles.js
747
+ var KNOWN_MEMBER_ROLES = ["lead", "member", "oncall"];
748
+
735
749
  // ../shared/dist/score-grades.js
736
750
  var GRADE_BOUNDARIES = {
737
751
  good: 80,
@@ -769,11 +783,21 @@ var RAW_EVAL_MODES = [
769
783
 
770
784
  // ../shared/dist/owner-teams.js
771
785
  var KNOWN_OWNER_TEAMS = [
786
+ "ai-growth",
787
+ "billing-and-integrations",
788
+ "content-agent",
772
789
  "content-lake",
773
- "core-docs",
774
- "growth",
775
- "media",
776
- "platform",
790
+ "data",
791
+ "design-and-research",
792
+ "docs",
793
+ "editorial-experience",
794
+ "engineering",
795
+ "identity",
796
+ "media-library",
797
+ "product",
798
+ "runtime",
799
+ "sdk",
800
+ "ssi",
777
801
  "studio"
778
802
  ];
779
803
 
@@ -1142,6 +1166,14 @@ var evalRequestSchema = defineType({
1142
1166
  type: "string",
1143
1167
  validation: (rule) => rule.required()
1144
1168
  }),
1169
+ defineField({
1170
+ description: "ailf.job document id this request was dispatched to. Set by the webhook handler when GHA dispatch succeeds; the pipeline orchestrator reads it back to patch this doc on job terminal state.",
1171
+ group: ["optional", "all-fields"],
1172
+ name: "jobId",
1173
+ readOnly: true,
1174
+ title: "Job ID",
1175
+ type: "string"
1176
+ }),
1145
1177
  defineField({
1146
1178
  description: "Links to the resulting ailf.report document's reportId",
1147
1179
  group: ["optional", "all-fields"],
@@ -1194,7 +1226,6 @@ var evalRequestSchema = defineType({
1194
1226
  validation: (rule) => rule.required()
1195
1227
  })
1196
1228
  ],
1197
- liveEdit: true,
1198
1229
  name: "ailf.evalRequest",
1199
1230
  preview: {
1200
1231
  prepare({ status }) {
@@ -1253,6 +1284,14 @@ var featureAreaSchema = defineType2({
1253
1284
  of: [{ type: "string" }],
1254
1285
  title: "Tags",
1255
1286
  type: "array"
1287
+ }),
1288
+ defineField2({
1289
+ description: "Primary team responsible for this area. Optional; unowned areas surface in a triage view.",
1290
+ group: ["optional", "all-fields"],
1291
+ name: "team",
1292
+ title: "Owning Team",
1293
+ to: [{ type: "ailf.team" }],
1294
+ type: "reference"
1256
1295
  })
1257
1296
  ],
1258
1297
  name: "ailf.featureArea",
@@ -4005,16 +4044,381 @@ var taskSchema = defineType5({
4005
4044
  type: "document"
4006
4045
  });
4007
4046
 
4008
- // src/schema/webhook-config.ts
4009
- import { ALL_FIELDS_GROUP as ALL_FIELDS_GROUP6, defineField as defineField6, defineType as defineType6 } from "sanity";
4010
- var webhookConfigSchema = defineType6({
4047
+ // src/schema/team.ts
4048
+ import {
4049
+ ALL_FIELDS_GROUP as ALL_FIELDS_GROUP6,
4050
+ defineArrayMember,
4051
+ defineField as defineField6,
4052
+ defineType as defineType6
4053
+ } from "sanity";
4054
+ var EVENT_TYPE_SUGGESTIONS = KNOWN_EVENT_TYPES.map((value) => ({
4055
+ title: value,
4056
+ value
4057
+ }));
4058
+ var MEMBER_ROLE_SUGGESTIONS = KNOWN_MEMBER_ROLES.map((value) => ({
4059
+ title: value,
4060
+ value
4061
+ }));
4062
+ var SCOPE_TYPES = [
4063
+ { title: "Owned (areas + repos)", value: "owned" },
4064
+ { title: "All events", value: "all" },
4065
+ { title: "Specific areas", value: "areas" },
4066
+ { title: "Specific repos", value: "repos" },
4067
+ { title: "Specific tags", value: "tags" }
4068
+ ];
4069
+ var channelScopeField = defineField6({
4070
+ description: "Which events this channel cares about. 'Owned' uses the team's own areas + repos. 'All' receives every event. The remaining variants scope to a specific list of areas, repos, or tags.",
4071
+ fields: [
4072
+ defineField6({
4073
+ description: "Scope discriminator",
4074
+ name: "type",
4075
+ options: {
4076
+ layout: "radio",
4077
+ list: [...SCOPE_TYPES]
4078
+ },
4079
+ title: "Scope Type",
4080
+ type: "string",
4081
+ validation: (rule) => rule.required()
4082
+ }),
4083
+ defineField6({
4084
+ description: "Feature areas this channel listens to",
4085
+ hidden: ({ parent }) => parent?.type !== "areas",
4086
+ name: "areas",
4087
+ of: [
4088
+ defineArrayMember({
4089
+ to: [{ type: "ailf.featureArea" }],
4090
+ type: "reference"
4091
+ })
4092
+ ],
4093
+ title: "Areas",
4094
+ type: "array"
4095
+ }),
4096
+ defineField6({
4097
+ description: "Repository identifiers this channel listens to",
4098
+ hidden: ({ parent }) => parent?.type !== "repos",
4099
+ name: "repos",
4100
+ of: [defineArrayMember({ type: "string" })],
4101
+ title: "Repos",
4102
+ type: "array"
4103
+ }),
4104
+ defineField6({
4105
+ description: "Task tags this channel listens to",
4106
+ hidden: ({ parent }) => parent?.type !== "tags",
4107
+ name: "tags",
4108
+ of: [defineArrayMember({ type: "string" })],
4109
+ title: "Tags",
4110
+ type: "array"
4111
+ })
4112
+ ],
4113
+ initialValue: { type: "owned" },
4114
+ name: "scope",
4115
+ title: "Scope",
4116
+ type: "object"
4117
+ });
4118
+ var channelPurposeField = defineField6({
4119
+ description: "Short human-readable label describing what this channel is used for (e.g. 'GROQ on-call', 'Visual editing weekly digest').",
4120
+ name: "purpose",
4121
+ title: "Purpose",
4122
+ type: "string"
4123
+ });
4124
+ var channelEventsField = defineField6({
4125
+ description: "Event types this channel receives. Suggestions come from the known event-type registry, but custom strings are accepted.",
4126
+ name: "events",
4127
+ of: [defineArrayMember({ type: "string" })],
4128
+ options: {
4129
+ layout: "tags",
4130
+ list: EVENT_TYPE_SUGGESTIONS
4131
+ },
4132
+ title: "Events",
4133
+ type: "array"
4134
+ });
4135
+ var teamMemberMember = defineArrayMember({
4136
+ fields: [
4137
+ defineField6({
4138
+ description: "Email address (preferred routing identity)",
4139
+ name: "email",
4140
+ title: "Email",
4141
+ type: "string",
4142
+ validation: (rule) => rule.email()
4143
+ }),
4144
+ defineField6({
4145
+ description: "Sanity user id (for Studio mentions / permissions)",
4146
+ name: "sanityUserId",
4147
+ title: "Sanity User ID",
4148
+ type: "string"
4149
+ }),
4150
+ defineField6({
4151
+ description: "GitHub username (for PR review routing)",
4152
+ name: "githubUsername",
4153
+ title: "GitHub Username",
4154
+ type: "string"
4155
+ }),
4156
+ defineField6({
4157
+ description: "Human-readable display name",
4158
+ name: "displayName",
4159
+ title: "Display Name",
4160
+ type: "string"
4161
+ }),
4162
+ defineField6({
4163
+ description: "Role within the team. Suggestions come from the known member-role registry, but custom roles are accepted.",
4164
+ name: "role",
4165
+ options: {
4166
+ list: [...MEMBER_ROLE_SUGGESTIONS]
4167
+ },
4168
+ title: "Role",
4169
+ type: "string"
4170
+ }),
4171
+ defineField6({
4172
+ description: "When this member's identity was last verified",
4173
+ name: "lastVerifiedAt",
4174
+ title: "Last Verified At",
4175
+ type: "datetime"
4176
+ })
4177
+ ],
4178
+ name: "teamMember",
4179
+ preview: {
4180
+ prepare({
4181
+ displayName,
4182
+ email,
4183
+ githubUsername,
4184
+ role
4185
+ }) {
4186
+ const name = typeof displayName === "string" && displayName ? displayName : typeof email === "string" && email ? email : typeof githubUsername === "string" && githubUsername ? githubUsername : "(unnamed member)";
4187
+ const handle = typeof email === "string" && email ? email : typeof githubUsername === "string" && githubUsername ? `@${githubUsername}` : "";
4188
+ const subtitle = [typeof role === "string" ? role : "", handle].filter(Boolean).join(" \xB7 ");
4189
+ return { subtitle, title: name };
4190
+ },
4191
+ select: {
4192
+ displayName: "displayName",
4193
+ email: "email",
4194
+ githubUsername: "githubUsername",
4195
+ role: "role"
4196
+ }
4197
+ },
4198
+ title: "Team Member",
4199
+ type: "object",
4200
+ validation: (rule) => rule.custom((m) => {
4201
+ const member = m;
4202
+ if (!member) return true;
4203
+ const hasIdentity = typeof member.email === "string" && member.email.length > 0 || typeof member.sanityUserId === "string" && member.sanityUserId.length > 0 || typeof member.githubUsername === "string" && member.githubUsername.length > 0;
4204
+ return hasIdentity || "At least one of email, Sanity user id, or GitHub username is required";
4205
+ })
4206
+ });
4207
+ var slackChannelMember = defineArrayMember({
4208
+ fields: [
4209
+ defineField6({
4210
+ description: "Slack channel id (e.g. 'C01ABCDEF'). Must start with 'C' and contain only uppercase letters and digits.",
4211
+ name: "channelId",
4212
+ title: "Channel ID",
4213
+ type: "string",
4214
+ validation: (rule) => rule.required().custom((value) => {
4215
+ if (typeof value !== "string") return "Channel id is required";
4216
+ if (!/^C[A-Z0-9]+$/.test(value)) {
4217
+ return "Must start with 'C' followed by uppercase letters and digits";
4218
+ }
4219
+ return true;
4220
+ })
4221
+ }),
4222
+ defineField6({
4223
+ description: "Human-readable channel name (e.g. '#groq-oncall')",
4224
+ name: "channelName",
4225
+ title: "Channel Name",
4226
+ type: "string"
4227
+ }),
4228
+ channelPurposeField,
4229
+ channelEventsField,
4230
+ channelScopeField
4231
+ ],
4232
+ name: "slackChannel",
4233
+ preview: {
4234
+ prepare({ channelId, channelName, purpose }) {
4235
+ const name = typeof channelName === "string" && channelName ? channelName : typeof channelId === "string" ? channelId : "(slack)";
4236
+ return {
4237
+ subtitle: typeof purpose === "string" && purpose ? purpose : "Slack channel",
4238
+ title: name
4239
+ };
4240
+ },
4241
+ select: {
4242
+ channelId: "channelId",
4243
+ channelName: "channelName",
4244
+ purpose: "purpose"
4245
+ }
4246
+ },
4247
+ title: "Slack Channel",
4248
+ type: "object"
4249
+ });
4250
+ var emailChannelMember = defineArrayMember({
4251
+ fields: [
4252
+ defineField6({
4253
+ description: "Email addresses that receive notifications on this channel",
4254
+ name: "addresses",
4255
+ of: [defineArrayMember({ type: "string" })],
4256
+ title: "Addresses",
4257
+ type: "array",
4258
+ validation: (rule) => rule.min(1)
4259
+ }),
4260
+ channelPurposeField,
4261
+ channelEventsField,
4262
+ channelScopeField
4263
+ ],
4264
+ name: "emailChannel",
4265
+ preview: {
4266
+ prepare({ addresses, purpose }) {
4267
+ const list = Array.isArray(addresses) ? addresses.filter((a) => typeof a === "string") : [];
4268
+ const first = list[0] ?? "(email)";
4269
+ const more = list.length > 1 ? ` (+${list.length - 1} more)` : "";
4270
+ return {
4271
+ subtitle: typeof purpose === "string" && purpose ? purpose : "Email channel",
4272
+ title: `${first}${more}`
4273
+ };
4274
+ },
4275
+ select: {
4276
+ addresses: "addresses",
4277
+ purpose: "purpose"
4278
+ }
4279
+ },
4280
+ title: "Email Channel",
4281
+ type: "object"
4282
+ });
4283
+ var webhookChannelMember = defineArrayMember({
4284
+ fields: [
4285
+ defineField6({
4286
+ description: "Logical webhook name resolved at dispatch time. Do not paste raw URLs \u2014 the actual URL is configured outside Studio so secrets stay out of the Content Lake.",
4287
+ name: "logicalName",
4288
+ title: "Logical Name",
4289
+ type: "string",
4290
+ validation: (rule) => rule.required()
4291
+ }),
4292
+ channelPurposeField,
4293
+ channelEventsField,
4294
+ channelScopeField
4295
+ ],
4296
+ name: "webhookChannel",
4297
+ preview: {
4298
+ prepare({ logicalName, purpose }) {
4299
+ return {
4300
+ subtitle: typeof purpose === "string" && purpose ? purpose : "Webhook channel",
4301
+ title: typeof logicalName === "string" && logicalName ? logicalName : "(webhook)"
4302
+ };
4303
+ },
4304
+ select: {
4305
+ logicalName: "logicalName",
4306
+ purpose: "purpose"
4307
+ }
4308
+ },
4309
+ title: "Webhook Channel",
4310
+ type: "object"
4311
+ });
4312
+ var teamSchema = defineType6({
4011
4313
  groups: [
4012
4314
  { name: "main", title: "Main", default: true },
4013
- { name: "optional", title: "Optional" },
4315
+ { name: "members", title: "Members" },
4316
+ { name: "ownership", title: "Ownership" },
4317
+ { name: "notifications", title: "Notifications" },
4014
4318
  ALL_FIELDS_GROUP6
4015
4319
  ],
4016
4320
  fields: [
4017
4321
  defineField6({
4322
+ description: "Unique stable identifier (e.g. 'groq-team'). Lowercase alphanumeric with hyphens. Used for cross-document references and routing keys.",
4323
+ group: ["main", "all-fields"],
4324
+ name: "slug",
4325
+ options: {
4326
+ maxLength: 64,
4327
+ source: "displayName"
4328
+ },
4329
+ title: "Slug",
4330
+ type: "slug",
4331
+ validation: (rule) => rule.required().custom((slug) => {
4332
+ if (slug?.current && !/^[a-z0-9][a-z0-9-]*$/.test(slug.current)) {
4333
+ return "Must be lowercase alphanumeric with hyphens (e.g. 'groq-team')";
4334
+ }
4335
+ return true;
4336
+ })
4337
+ }),
4338
+ defineField6({
4339
+ description: "Human-readable team name (e.g. 'GROQ Platform')",
4340
+ group: ["main", "all-fields"],
4341
+ name: "displayName",
4342
+ title: "Display Name",
4343
+ type: "string",
4344
+ validation: (rule) => rule.required().min(1)
4345
+ }),
4346
+ defineField6({
4347
+ description: "What this team owns and how they prefer to be contacted",
4348
+ group: ["main", "all-fields"],
4349
+ name: "description",
4350
+ title: "Description",
4351
+ type: "string"
4352
+ }),
4353
+ defineField6({
4354
+ description: "Lifecycle status. Active teams receive routed notifications; archived teams are preserved for historical references but excluded from default routing.",
4355
+ group: ["main", "all-fields"],
4356
+ initialValue: "active",
4357
+ name: "status",
4358
+ options: {
4359
+ list: [
4360
+ { title: "Active", value: "active" },
4361
+ { title: "Archived", value: "archived" }
4362
+ ]
4363
+ },
4364
+ title: "Status",
4365
+ type: "string",
4366
+ validation: (rule) => rule.required()
4367
+ }),
4368
+ defineField6({
4369
+ description: "People on this team. Each member needs at least one of email, Sanity user id, or GitHub username so the routing layer can resolve them to a real account.",
4370
+ group: ["members", "all-fields"],
4371
+ name: "members",
4372
+ of: [teamMemberMember],
4373
+ title: "Members",
4374
+ type: "array",
4375
+ validation: (rule) => rule.required().min(1)
4376
+ }),
4377
+ defineField6({
4378
+ description: "Repository identifiers this team owns (e.g. 'sanity-io/sanity'). Used by channel scope 'owned' and 'repos'.",
4379
+ group: ["ownership", "all-fields"],
4380
+ name: "repos",
4381
+ of: [defineArrayMember({ type: "string" })],
4382
+ title: "Repos",
4383
+ type: "array"
4384
+ }),
4385
+ defineField6({
4386
+ description: "Notification channels for this team. Each channel carries its own purpose, event subscriptions, and scope.",
4387
+ group: ["notifications", "all-fields"],
4388
+ name: "notifications",
4389
+ of: [slackChannelMember, emailChannelMember, webhookChannelMember],
4390
+ title: "Notification Channels",
4391
+ type: "array"
4392
+ })
4393
+ ],
4394
+ name: "ailf.team",
4395
+ preview: {
4396
+ prepare({ displayName, slug }) {
4397
+ const slugCurrent = slug !== null && typeof slug === "object" && "current" in slug ? slug.current : void 0;
4398
+ return {
4399
+ subtitle: typeof slugCurrent === "string" ? slugCurrent : "",
4400
+ title: typeof displayName === "string" && displayName ? displayName : "Team"
4401
+ };
4402
+ },
4403
+ select: {
4404
+ displayName: "displayName",
4405
+ slug: "slug"
4406
+ }
4407
+ },
4408
+ title: "AILF Team",
4409
+ type: "document"
4410
+ });
4411
+
4412
+ // src/schema/webhook-config.ts
4413
+ import { ALL_FIELDS_GROUP as ALL_FIELDS_GROUP7, defineField as defineField7, defineType as defineType7 } from "sanity";
4414
+ var webhookConfigSchema = defineType7({
4415
+ groups: [
4416
+ { name: "main", title: "Main", default: true },
4417
+ { name: "optional", title: "Optional" },
4418
+ ALL_FIELDS_GROUP7
4419
+ ],
4420
+ fields: [
4421
+ defineField7({
4018
4422
  description: "When enabled, publishing articles will automatically trigger AI Literacy evaluations for affected feature areas.",
4019
4423
  group: ["main", "all-fields"],
4020
4424
  initialValue: false,
@@ -4022,7 +4426,7 @@ var webhookConfigSchema = defineType6({
4022
4426
  title: "Evaluate on Publish",
4023
4427
  type: "boolean"
4024
4428
  }),
4025
- defineField6({
4429
+ defineField7({
4026
4430
  description: "Which evaluation mode to use for webhook-triggered evaluations.",
4027
4431
  group: ["main", "all-fields"],
4028
4432
  initialValue: "baseline",
@@ -4038,7 +4442,7 @@ var webhookConfigSchema = defineType6({
4038
4442
  title: "Evaluation Mode",
4039
4443
  type: "string"
4040
4444
  }),
4041
- defineField6({
4445
+ defineField7({
4042
4446
  description: "Maximum evaluations per day. Prevents runaway costs from rapid editing.",
4043
4447
  group: ["main", "all-fields"],
4044
4448
  initialValue: 20,
@@ -4047,7 +4451,7 @@ var webhookConfigSchema = defineType6({
4047
4451
  type: "number",
4048
4452
  validation: (rule) => rule.min(1).max(100)
4049
4453
  }),
4050
- defineField6({
4454
+ defineField7({
4051
4455
  description: "Seconds to wait after the last edit before dispatching. Coalesces rapid edits into a single evaluation.",
4052
4456
  group: ["optional", "all-fields"],
4053
4457
  initialValue: 300,
@@ -4056,7 +4460,7 @@ var webhookConfigSchema = defineType6({
4056
4460
  type: "number",
4057
4461
  validation: (rule) => rule.min(10).max(3600)
4058
4462
  }),
4059
- defineField6({
4463
+ defineField7({
4060
4464
  description: "Specific feature areas to evaluate. Leave empty to evaluate all affected areas automatically.",
4061
4465
  group: ["optional", "all-fields"],
4062
4466
  name: "areas",
@@ -4064,7 +4468,7 @@ var webhookConfigSchema = defineType6({
4064
4468
  title: "Area Filter",
4065
4469
  type: "array"
4066
4470
  }),
4067
- defineField6({
4471
+ defineField7({
4068
4472
  description: "Slack webhook URL for notifications about webhook-triggered evaluations.",
4069
4473
  group: ["optional", "all-fields"],
4070
4474
  name: "notifySlack",
@@ -4811,6 +5215,7 @@ function readSearchParam(params, key) {
4811
5215
 
4812
5216
  // src/queries.ts
4813
5217
  var REPORT_TYPE = "ailf.report";
5218
+ var TASK_TYPE = "ailf.task";
4814
5219
  var latestReportsQuery = (
4815
5220
  /* groq */
4816
5221
  `
@@ -4835,6 +5240,11 @@ var latestReportsQuery = (
4835
5240
  "trigger": provenance.trigger.type,
4836
5241
  "classification": provenance.classification,
4837
5242
  "ownerTeam": provenance.owner.team,
5243
+ "teamDoc": *[_type == "ailf.team" && slug.current == ^.provenance.owner.team][0]{
5244
+ _id,
5245
+ displayName,
5246
+ status
5247
+ },
4838
5248
  "ownerIndividual": provenance.owner.individual,
4839
5249
  "executorType": provenance.executor.type,
4840
5250
  "executorName": provenance.executor.name,
@@ -4917,6 +5327,11 @@ var reportDetailQuery = (
4917
5327
  tag,
4918
5328
  title,
4919
5329
  provenance,
5330
+ "teamDoc": *[_type == "ailf.team" && slug.current == ^.provenance.owner.team][0]{
5331
+ _id,
5332
+ displayName,
5333
+ status
5334
+ },
4920
5335
  summary,
4921
5336
  "comparison": comparison {
4922
5337
  areas,
@@ -5123,6 +5538,34 @@ var distinctFilterValuesQuery = (
5123
5538
  }
5124
5539
  `
5125
5540
  );
5541
+ var releaseArticlesQuery = (
5542
+ /* groq */
5543
+ `
5544
+ *[_type == "article" && sanity::partOfRelease($releaseId)] {
5545
+ "id": string::split(_id, ".")[2],
5546
+ "slug": slug.current,
5547
+ "path": select(
5548
+ defined(primarySection) => primarySection->slug.current + "/" + slug.current
5549
+ )
5550
+ }
5551
+ `
5552
+ );
5553
+ var releaseTaskCoverageQuery = (
5554
+ /* groq */
5555
+ `
5556
+ count(*[
5557
+ _type == "${TASK_TYPE}"
5558
+ && status == "active"
5559
+ && !(_id in path("drafts.**"))
5560
+ && defined(contextDocs)
5561
+ && (
5562
+ count(contextDocs[refType == "id" && doc._ref in $articleIds]) > 0
5563
+ || count(contextDocs[refType == "path" && path in $articlePaths]) > 0
5564
+ || count(contextDocs[refType == "perspective" && perspective == $releaseId]) > 0
5565
+ )
5566
+ ])
5567
+ `
5568
+ );
5126
5569
  function filterModeClause(param) {
5127
5570
  return `&& (${param} == null || provenance.mode == ${param})`;
5128
5571
  }
@@ -15331,7 +15774,7 @@ function ailfTool(options = {}) {
15331
15774
  }
15332
15775
 
15333
15776
  // src/structure.ts
15334
- import { ArchiveIcon as ArchiveIcon2, TaskIcon } from "@sanity/icons";
15777
+ import { ArchiveIcon as ArchiveIcon2, TaskIcon, WarningOutlineIcon as WarningOutlineIcon5 } from "@sanity/icons";
15335
15778
  function ailfTaskStructureItem(S) {
15336
15779
  return S.listItem().id("ailfTasks").title("AILF Tasks").icon(TaskIcon).child(
15337
15780
  S.list().id("ailfTasksViews").title("AILF Tasks").items([
@@ -15354,12 +15797,68 @@ function ailfTaskStructureItem(S) {
15354
15797
  ])
15355
15798
  );
15356
15799
  }
15800
+ function ailfTeamsStructureItem(S) {
15801
+ return S.listItem().id("ailfTeams").title("Teams").child(
15802
+ S.list().id("ailfTeamsViews").title("Teams").items([
15803
+ S.listItem().id("activeTeams").title("Active teams").child(
15804
+ S.documentTypeList("ailf.team").id("ailfTeamsActive").title("Active teams").apiVersion(API_VERSION).filter('_type == "ailf.team" && status == "active"')
15805
+ ),
15806
+ S.listItem().id("archivedTeams").title("Archived teams").icon(ArchiveIcon2).child(
15807
+ S.documentTypeList("ailf.team").id("ailfTeamsArchived").title("Archived teams").apiVersion(API_VERSION).filter('_type == "ailf.team" && status == "archived"')
15808
+ ),
15809
+ S.divider(),
15810
+ S.listItem().id("allTeams").title("All teams").child(
15811
+ S.documentTypeList("ailf.team").id("ailfTeamsAll").title("All teams")
15812
+ )
15813
+ ])
15814
+ );
15815
+ }
15816
+ function ailfAreasStructureItem(S) {
15817
+ return S.listItem().id("ailfAreas").title("Areas").child(
15818
+ S.list().id("ailfAreasViews").title("Areas").items([
15819
+ S.listItem().id("allAreas").title("All areas").child(
15820
+ S.documentTypeList("ailf.featureArea").id("ailfAreasAll").title("All areas")
15821
+ ),
15822
+ S.divider(),
15823
+ S.listItem().id("unownedAreas").title("\u26A0 Unowned areas").icon(WarningOutlineIcon5).child(
15824
+ S.documentTypeList("ailf.featureArea").id("ailfAreasUnowned").title("\u26A0 Unowned areas").apiVersion(API_VERSION).filter('_type == "ailf.featureArea" && !defined(team)')
15825
+ )
15826
+ ])
15827
+ );
15828
+ }
15829
+ function ailfReportsStructureItem(S) {
15830
+ return S.listItem().id("ailfReports").title("Reports").child(
15831
+ S.list().id("ailfReportsViews").title("Reports").items([
15832
+ S.listItem().id("allReports").title("All reports").child(
15833
+ S.documentTypeList("ailf.report").id("ailfReportsAll").title("All reports")
15834
+ ),
15835
+ S.divider(),
15836
+ S.listItem().id("unresolvedTeamReports").title("\u26A0 Reports with unresolved team").icon(WarningOutlineIcon5).child(
15837
+ S.documentList().id("ailfReportsUnresolvedTeam").title("\u26A0 Reports with unresolved team").apiVersion(API_VERSION).filter(
15838
+ `_type == "ailf.report" && defined(provenance.owner.team) && provenance.owner.team != "unknown" && !(provenance.owner.team in *[_type == "ailf.team"].slug.current)`
15839
+ )
15840
+ )
15841
+ ])
15842
+ );
15843
+ }
15844
+ function ailfChannelsWithUnknownEventsItem(S) {
15845
+ return S.listItem().id("ailfChannelsUnknownEvents").title("\u26A0 Channels with unknown events").icon(WarningOutlineIcon5).child(
15846
+ S.documentList().id("ailfChannelsUnknownEventsList").title("Teams whose channels reference unknown event types").apiVersion(API_VERSION).filter(
15847
+ `_type == "ailf.team" && count(notifications[count(events[!(@ in $known)]) > 0]) > 0`
15848
+ ).params({ known: [...KNOWN_EVENT_TYPES] })
15849
+ );
15850
+ }
15357
15851
  var ailfStructure = (S) => S.list().id("root").title("Content").items([
15358
15852
  ailfTaskStructureItem(S),
15853
+ ailfTeamsStructureItem(S),
15854
+ ailfAreasStructureItem(S),
15855
+ ailfReportsStructureItem(S),
15856
+ ailfChannelsWithUnknownEventsItem(S),
15359
15857
  S.divider(),
15360
- ...S.documentTypeListItems().filter(
15361
- (listItem) => listItem.getId() !== "ailf.task"
15362
- )
15858
+ ...S.documentTypeListItems().filter((listItem) => {
15859
+ const id = listItem.getId();
15860
+ return id !== "ailf.task" && id !== "ailf.team" && id !== "ailf.featureArea" && id !== "ailf.report";
15861
+ })
15363
15862
  ]);
15364
15863
 
15365
15864
  // src/actions/RunEvaluationAction.tsx
@@ -15387,6 +15886,42 @@ function buildReleaseEvalPipelineRequest(args) {
15387
15886
  variant: mode
15388
15887
  };
15389
15888
  }
15889
+ function buildCoverageQueryParams(articles, releaseId) {
15890
+ return {
15891
+ articleIds: articles.map((a) => a.id),
15892
+ articlePaths: articles.map((a) => a.path).filter((p) => p !== null),
15893
+ releaseId
15894
+ };
15895
+ }
15896
+ var COVERAGE_CACHE_TTL_MS = 3e4;
15897
+ var coverageCache = /* @__PURE__ */ new Map();
15898
+ function getOrStartCoverageCheck(perspectiveId, baseClient, ailfClient) {
15899
+ const entry = coverageCache.get(perspectiveId);
15900
+ if (entry && entry.expiresAt > Date.now()) return entry.promise;
15901
+ const promise = (async () => {
15902
+ try {
15903
+ const articles = await baseClient.fetch(
15904
+ releaseArticlesQuery,
15905
+ { releaseId: perspectiveId }
15906
+ );
15907
+ if (!articles || articles.length === 0) return "uncovered";
15908
+ const params = buildCoverageQueryParams(articles, perspectiveId);
15909
+ const count = await ailfClient.fetch(
15910
+ releaseTaskCoverageQuery,
15911
+ params
15912
+ );
15913
+ if (typeof count !== "number") return "unknown";
15914
+ return count > 0 ? "covered" : "uncovered";
15915
+ } catch {
15916
+ return "unknown";
15917
+ }
15918
+ })();
15919
+ coverageCache.set(perspectiveId, {
15920
+ expiresAt: Date.now() + COVERAGE_CACHE_TTL_MS,
15921
+ promise
15922
+ });
15923
+ return promise;
15924
+ }
15390
15925
  var API_VERSION2 = "2026-03-11";
15391
15926
  var EVAL_REQUEST_TYPE3 = "ailf.evalRequest";
15392
15927
  var POLL_INTERVAL_MS2 = 3e4;
@@ -15415,8 +15950,29 @@ function createRunEvaluationAction(options = {}) {
15415
15950
  [baseClient, sourceDataset]
15416
15951
  );
15417
15952
  const [state, setState] = useState33({ status: "loading" });
15953
+ const [coverage, setCoverage] = useState33("loading");
15418
15954
  const requestedAtRef = useRef10(null);
15419
15955
  const perspectiveId = getReleaseIdFromReleaseDocumentId3(release._id);
15956
+ const baseClientRef = useRef10(baseClient);
15957
+ baseClientRef.current = baseClient;
15958
+ const ailfClientRef = useRef10(ailfClient);
15959
+ ailfClientRef.current = ailfClient;
15960
+ useEffect19(() => {
15961
+ let cancelled = false;
15962
+ const promise = getOrStartCoverageCheck(
15963
+ perspectiveId,
15964
+ baseClientRef.current,
15965
+ ailfClientRef.current
15966
+ );
15967
+ promise.then((status) => {
15968
+ if (!cancelled) setCoverage(status);
15969
+ }).catch(() => {
15970
+ if (!cancelled) setCoverage("unknown");
15971
+ });
15972
+ return () => {
15973
+ cancelled = true;
15974
+ };
15975
+ }, [perspectiveId]);
15420
15976
  useEffect19(() => {
15421
15977
  let cancelled = false;
15422
15978
  ailfClient.fetch(contentImpactQuery, buildReportQueryParams(perspectiveId)).then((results) => {
@@ -15559,11 +16115,11 @@ function createRunEvaluationAction(options = {}) {
15559
16115
  release.metadata?.title
15560
16116
  ]);
15561
16117
  return {
15562
- disabled: state.status === "loading" || state.status === "requested" || state.status === "polling",
16118
+ disabled: state.status === "loading" || state.status === "requested" || state.status === "polling" || coverage === "uncovered",
15563
16119
  icon: BarChartIcon2,
15564
16120
  label: getLabel2(state),
15565
16121
  onHandle: handleRequest,
15566
- title: getTitle2(state, perspectiveId)
16122
+ title: getTitle2(state, perspectiveId, coverage)
15567
16123
  };
15568
16124
  };
15569
16125
  RunEvaluationAction.displayName = "RunEvaluationAction";
@@ -15587,7 +16143,10 @@ function getLabel2(state) {
15587
16143
  return "Eval Failed";
15588
16144
  }
15589
16145
  }
15590
- function getTitle2(state, perspectiveId) {
16146
+ function getTitle2(state, perspectiveId, coverage) {
16147
+ if (coverage === "uncovered" && (state.status === "idle" || state.status === "ready")) {
16148
+ return `No active task references any document in this release (${perspectiveId}). Add a task that targets one of these docs before evaluating.`;
16149
+ }
15591
16150
  switch (state.status) {
15592
16151
  case "loading":
15593
16152
  return "Checking for existing evaluation results\u2026";
@@ -15647,6 +16206,7 @@ var ailfPlugin = definePlugin((options) => ({
15647
16206
  referenceSolutionSchema,
15648
16207
  reportSchema,
15649
16208
  taskSchema,
16209
+ teamSchema,
15650
16210
  webhookConfigSchema
15651
16211
  ]
15652
16212
  },
@@ -15690,6 +16250,7 @@ export {
15690
16250
  scoreTimelineQuery,
15691
16251
  searchTopics,
15692
16252
  taskSchema,
16253
+ teamSchema,
15693
16254
  useHelp,
15694
16255
  webhookConfigSchema
15695
16256
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf-studio",
3
- "version": "2.0.3",
3
+ "version": "2.2.1",
4
4
  "description": "AI Literacy Framework — Sanity Studio dashboard plugin",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "@testing-library/dom": "^10.4.1",
46
46
  "@testing-library/react": "^16.3.2",
47
47
  "@types/react": "^19.0.0",
48
+ "groq-js": "^1.30.1",
48
49
  "jsdom": "^29.0.2",
49
50
  "react": "^19.0.0",
50
51
  "sanity": "^5.14.0",
@@ -52,8 +53,8 @@
52
53
  "tsx": "^4.19.0",
53
54
  "typescript": "5.9.3",
54
55
  "vitest": "^4.1.5",
55
- "@sanity/ailf-shared": "0.1.0",
56
- "@sanity/ailf-core": "0.1.0"
56
+ "@sanity/ailf-core": "0.1.0",
57
+ "@sanity/ailf-shared": "0.1.0"
57
58
  },
58
59
  "dependencies": {
59
60
  "prism-react-renderer": "^2.4.0",
@@ -69,9 +70,11 @@
69
70
  "scripts": {
70
71
  "generate-hooks": "tsx scripts/generate-hooks.ts",
71
72
  "migrate-to-private-dataset": "tsx scripts/migrate-to-private-dataset.ts",
73
+ "seed-teams": "tsx scripts/seed-teams.ts",
72
74
  "build": "tsup",
73
75
  "dev": "tsup --watch",
74
76
  "test": "vitest run",
75
- "test:watch": "vitest"
77
+ "test:watch": "vitest",
78
+ "bench": "vitest bench --run"
76
79
  }
77
80
  }