@sanity/ailf-studio 2.0.3 → 2.1.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.
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,
@@ -1142,6 +1156,14 @@ var evalRequestSchema = defineType({
1142
1156
  type: "string",
1143
1157
  validation: (rule) => rule.required()
1144
1158
  }),
1159
+ defineField({
1160
+ 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.",
1161
+ group: ["optional", "all-fields"],
1162
+ name: "jobId",
1163
+ readOnly: true,
1164
+ title: "Job ID",
1165
+ type: "string"
1166
+ }),
1145
1167
  defineField({
1146
1168
  description: "Links to the resulting ailf.report document's reportId",
1147
1169
  group: ["optional", "all-fields"],
@@ -1194,7 +1216,6 @@ var evalRequestSchema = defineType({
1194
1216
  validation: (rule) => rule.required()
1195
1217
  })
1196
1218
  ],
1197
- liveEdit: true,
1198
1219
  name: "ailf.evalRequest",
1199
1220
  preview: {
1200
1221
  prepare({ status }) {
@@ -1253,6 +1274,14 @@ var featureAreaSchema = defineType2({
1253
1274
  of: [{ type: "string" }],
1254
1275
  title: "Tags",
1255
1276
  type: "array"
1277
+ }),
1278
+ defineField2({
1279
+ description: "Primary team responsible for this area. Optional; unowned areas surface in a triage view.",
1280
+ group: ["optional", "all-fields"],
1281
+ name: "team",
1282
+ title: "Owning Team",
1283
+ to: [{ type: "ailf.team" }],
1284
+ type: "reference"
1256
1285
  })
1257
1286
  ],
1258
1287
  name: "ailf.featureArea",
@@ -4005,16 +4034,381 @@ var taskSchema = defineType5({
4005
4034
  type: "document"
4006
4035
  });
4007
4036
 
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({
4037
+ // src/schema/team.ts
4038
+ import {
4039
+ ALL_FIELDS_GROUP as ALL_FIELDS_GROUP6,
4040
+ defineArrayMember,
4041
+ defineField as defineField6,
4042
+ defineType as defineType6
4043
+ } from "sanity";
4044
+ var EVENT_TYPE_SUGGESTIONS = KNOWN_EVENT_TYPES.map((value) => ({
4045
+ title: value,
4046
+ value
4047
+ }));
4048
+ var MEMBER_ROLE_SUGGESTIONS = KNOWN_MEMBER_ROLES.map((value) => ({
4049
+ title: value,
4050
+ value
4051
+ }));
4052
+ var SCOPE_TYPES = [
4053
+ { title: "Owned (areas + repos)", value: "owned" },
4054
+ { title: "All events", value: "all" },
4055
+ { title: "Specific areas", value: "areas" },
4056
+ { title: "Specific repos", value: "repos" },
4057
+ { title: "Specific tags", value: "tags" }
4058
+ ];
4059
+ var channelScopeField = defineField6({
4060
+ 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.",
4061
+ fields: [
4062
+ defineField6({
4063
+ description: "Scope discriminator",
4064
+ name: "type",
4065
+ options: {
4066
+ layout: "radio",
4067
+ list: [...SCOPE_TYPES]
4068
+ },
4069
+ title: "Scope Type",
4070
+ type: "string",
4071
+ validation: (rule) => rule.required()
4072
+ }),
4073
+ defineField6({
4074
+ description: "Feature areas this channel listens to",
4075
+ hidden: ({ parent }) => parent?.type !== "areas",
4076
+ name: "areas",
4077
+ of: [
4078
+ defineArrayMember({
4079
+ to: [{ type: "ailf.featureArea" }],
4080
+ type: "reference"
4081
+ })
4082
+ ],
4083
+ title: "Areas",
4084
+ type: "array"
4085
+ }),
4086
+ defineField6({
4087
+ description: "Repository identifiers this channel listens to",
4088
+ hidden: ({ parent }) => parent?.type !== "repos",
4089
+ name: "repos",
4090
+ of: [defineArrayMember({ type: "string" })],
4091
+ title: "Repos",
4092
+ type: "array"
4093
+ }),
4094
+ defineField6({
4095
+ description: "Task tags this channel listens to",
4096
+ hidden: ({ parent }) => parent?.type !== "tags",
4097
+ name: "tags",
4098
+ of: [defineArrayMember({ type: "string" })],
4099
+ title: "Tags",
4100
+ type: "array"
4101
+ })
4102
+ ],
4103
+ initialValue: { type: "owned" },
4104
+ name: "scope",
4105
+ title: "Scope",
4106
+ type: "object"
4107
+ });
4108
+ var channelPurposeField = defineField6({
4109
+ description: "Short human-readable label describing what this channel is used for (e.g. 'GROQ on-call', 'Visual editing weekly digest').",
4110
+ name: "purpose",
4111
+ title: "Purpose",
4112
+ type: "string"
4113
+ });
4114
+ var channelEventsField = defineField6({
4115
+ description: "Event types this channel receives. Suggestions come from the known event-type registry, but custom strings are accepted.",
4116
+ name: "events",
4117
+ of: [defineArrayMember({ type: "string" })],
4118
+ options: {
4119
+ layout: "tags",
4120
+ list: EVENT_TYPE_SUGGESTIONS
4121
+ },
4122
+ title: "Events",
4123
+ type: "array"
4124
+ });
4125
+ var teamMemberMember = defineArrayMember({
4126
+ fields: [
4127
+ defineField6({
4128
+ description: "Email address (preferred routing identity)",
4129
+ name: "email",
4130
+ title: "Email",
4131
+ type: "string",
4132
+ validation: (rule) => rule.email()
4133
+ }),
4134
+ defineField6({
4135
+ description: "Sanity user id (for Studio mentions / permissions)",
4136
+ name: "sanityUserId",
4137
+ title: "Sanity User ID",
4138
+ type: "string"
4139
+ }),
4140
+ defineField6({
4141
+ description: "GitHub username (for PR review routing)",
4142
+ name: "githubUsername",
4143
+ title: "GitHub Username",
4144
+ type: "string"
4145
+ }),
4146
+ defineField6({
4147
+ description: "Human-readable display name",
4148
+ name: "displayName",
4149
+ title: "Display Name",
4150
+ type: "string"
4151
+ }),
4152
+ defineField6({
4153
+ description: "Role within the team. Suggestions come from the known member-role registry, but custom roles are accepted.",
4154
+ name: "role",
4155
+ options: {
4156
+ list: [...MEMBER_ROLE_SUGGESTIONS]
4157
+ },
4158
+ title: "Role",
4159
+ type: "string"
4160
+ }),
4161
+ defineField6({
4162
+ description: "When this member's identity was last verified",
4163
+ name: "lastVerifiedAt",
4164
+ title: "Last Verified At",
4165
+ type: "datetime"
4166
+ })
4167
+ ],
4168
+ name: "teamMember",
4169
+ preview: {
4170
+ prepare({
4171
+ displayName,
4172
+ email,
4173
+ githubUsername,
4174
+ role
4175
+ }) {
4176
+ const name = typeof displayName === "string" && displayName ? displayName : typeof email === "string" && email ? email : typeof githubUsername === "string" && githubUsername ? githubUsername : "(unnamed member)";
4177
+ const handle = typeof email === "string" && email ? email : typeof githubUsername === "string" && githubUsername ? `@${githubUsername}` : "";
4178
+ const subtitle = [typeof role === "string" ? role : "", handle].filter(Boolean).join(" \xB7 ");
4179
+ return { subtitle, title: name };
4180
+ },
4181
+ select: {
4182
+ displayName: "displayName",
4183
+ email: "email",
4184
+ githubUsername: "githubUsername",
4185
+ role: "role"
4186
+ }
4187
+ },
4188
+ title: "Team Member",
4189
+ type: "object",
4190
+ validation: (rule) => rule.custom((m) => {
4191
+ const member = m;
4192
+ if (!member) return true;
4193
+ 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;
4194
+ return hasIdentity || "At least one of email, Sanity user id, or GitHub username is required";
4195
+ })
4196
+ });
4197
+ var slackChannelMember = defineArrayMember({
4198
+ fields: [
4199
+ defineField6({
4200
+ description: "Slack channel id (e.g. 'C01ABCDEF'). Must start with 'C' and contain only uppercase letters and digits.",
4201
+ name: "channelId",
4202
+ title: "Channel ID",
4203
+ type: "string",
4204
+ validation: (rule) => rule.required().custom((value) => {
4205
+ if (typeof value !== "string") return "Channel id is required";
4206
+ if (!/^C[A-Z0-9]+$/.test(value)) {
4207
+ return "Must start with 'C' followed by uppercase letters and digits";
4208
+ }
4209
+ return true;
4210
+ })
4211
+ }),
4212
+ defineField6({
4213
+ description: "Human-readable channel name (e.g. '#groq-oncall')",
4214
+ name: "channelName",
4215
+ title: "Channel Name",
4216
+ type: "string"
4217
+ }),
4218
+ channelPurposeField,
4219
+ channelEventsField,
4220
+ channelScopeField
4221
+ ],
4222
+ name: "slackChannel",
4223
+ preview: {
4224
+ prepare({ channelId, channelName, purpose }) {
4225
+ const name = typeof channelName === "string" && channelName ? channelName : typeof channelId === "string" ? channelId : "(slack)";
4226
+ return {
4227
+ subtitle: typeof purpose === "string" && purpose ? purpose : "Slack channel",
4228
+ title: name
4229
+ };
4230
+ },
4231
+ select: {
4232
+ channelId: "channelId",
4233
+ channelName: "channelName",
4234
+ purpose: "purpose"
4235
+ }
4236
+ },
4237
+ title: "Slack Channel",
4238
+ type: "object"
4239
+ });
4240
+ var emailChannelMember = defineArrayMember({
4241
+ fields: [
4242
+ defineField6({
4243
+ description: "Email addresses that receive notifications on this channel",
4244
+ name: "addresses",
4245
+ of: [defineArrayMember({ type: "string" })],
4246
+ title: "Addresses",
4247
+ type: "array",
4248
+ validation: (rule) => rule.min(1)
4249
+ }),
4250
+ channelPurposeField,
4251
+ channelEventsField,
4252
+ channelScopeField
4253
+ ],
4254
+ name: "emailChannel",
4255
+ preview: {
4256
+ prepare({ addresses, purpose }) {
4257
+ const list = Array.isArray(addresses) ? addresses.filter((a) => typeof a === "string") : [];
4258
+ const first = list[0] ?? "(email)";
4259
+ const more = list.length > 1 ? ` (+${list.length - 1} more)` : "";
4260
+ return {
4261
+ subtitle: typeof purpose === "string" && purpose ? purpose : "Email channel",
4262
+ title: `${first}${more}`
4263
+ };
4264
+ },
4265
+ select: {
4266
+ addresses: "addresses",
4267
+ purpose: "purpose"
4268
+ }
4269
+ },
4270
+ title: "Email Channel",
4271
+ type: "object"
4272
+ });
4273
+ var webhookChannelMember = defineArrayMember({
4274
+ fields: [
4275
+ defineField6({
4276
+ 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.",
4277
+ name: "logicalName",
4278
+ title: "Logical Name",
4279
+ type: "string",
4280
+ validation: (rule) => rule.required()
4281
+ }),
4282
+ channelPurposeField,
4283
+ channelEventsField,
4284
+ channelScopeField
4285
+ ],
4286
+ name: "webhookChannel",
4287
+ preview: {
4288
+ prepare({ logicalName, purpose }) {
4289
+ return {
4290
+ subtitle: typeof purpose === "string" && purpose ? purpose : "Webhook channel",
4291
+ title: typeof logicalName === "string" && logicalName ? logicalName : "(webhook)"
4292
+ };
4293
+ },
4294
+ select: {
4295
+ logicalName: "logicalName",
4296
+ purpose: "purpose"
4297
+ }
4298
+ },
4299
+ title: "Webhook Channel",
4300
+ type: "object"
4301
+ });
4302
+ var teamSchema = defineType6({
4011
4303
  groups: [
4012
4304
  { name: "main", title: "Main", default: true },
4013
- { name: "optional", title: "Optional" },
4305
+ { name: "members", title: "Members" },
4306
+ { name: "ownership", title: "Ownership" },
4307
+ { name: "notifications", title: "Notifications" },
4014
4308
  ALL_FIELDS_GROUP6
4015
4309
  ],
4016
4310
  fields: [
4017
4311
  defineField6({
4312
+ description: "Unique stable identifier (e.g. 'groq-team'). Lowercase alphanumeric with hyphens. Used for cross-document references and routing keys.",
4313
+ group: ["main", "all-fields"],
4314
+ name: "slug",
4315
+ options: {
4316
+ maxLength: 64,
4317
+ source: "displayName"
4318
+ },
4319
+ title: "Slug",
4320
+ type: "slug",
4321
+ validation: (rule) => rule.required().custom((slug) => {
4322
+ if (slug?.current && !/^[a-z0-9][a-z0-9-]*$/.test(slug.current)) {
4323
+ return "Must be lowercase alphanumeric with hyphens (e.g. 'groq-team')";
4324
+ }
4325
+ return true;
4326
+ })
4327
+ }),
4328
+ defineField6({
4329
+ description: "Human-readable team name (e.g. 'GROQ Platform')",
4330
+ group: ["main", "all-fields"],
4331
+ name: "displayName",
4332
+ title: "Display Name",
4333
+ type: "string",
4334
+ validation: (rule) => rule.required().min(1)
4335
+ }),
4336
+ defineField6({
4337
+ description: "What this team owns and how they prefer to be contacted",
4338
+ group: ["main", "all-fields"],
4339
+ name: "description",
4340
+ title: "Description",
4341
+ type: "string"
4342
+ }),
4343
+ defineField6({
4344
+ description: "Lifecycle status. Active teams receive routed notifications; archived teams are preserved for historical references but excluded from default routing.",
4345
+ group: ["main", "all-fields"],
4346
+ initialValue: "active",
4347
+ name: "status",
4348
+ options: {
4349
+ list: [
4350
+ { title: "Active", value: "active" },
4351
+ { title: "Archived", value: "archived" }
4352
+ ]
4353
+ },
4354
+ title: "Status",
4355
+ type: "string",
4356
+ validation: (rule) => rule.required()
4357
+ }),
4358
+ defineField6({
4359
+ 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.",
4360
+ group: ["members", "all-fields"],
4361
+ name: "members",
4362
+ of: [teamMemberMember],
4363
+ title: "Members",
4364
+ type: "array",
4365
+ validation: (rule) => rule.required().min(1)
4366
+ }),
4367
+ defineField6({
4368
+ description: "Repository identifiers this team owns (e.g. 'sanity-io/sanity'). Used by channel scope 'owned' and 'repos'.",
4369
+ group: ["ownership", "all-fields"],
4370
+ name: "repos",
4371
+ of: [defineArrayMember({ type: "string" })],
4372
+ title: "Repos",
4373
+ type: "array"
4374
+ }),
4375
+ defineField6({
4376
+ description: "Notification channels for this team. Each channel carries its own purpose, event subscriptions, and scope.",
4377
+ group: ["notifications", "all-fields"],
4378
+ name: "notifications",
4379
+ of: [slackChannelMember, emailChannelMember, webhookChannelMember],
4380
+ title: "Notification Channels",
4381
+ type: "array"
4382
+ })
4383
+ ],
4384
+ name: "ailf.team",
4385
+ preview: {
4386
+ prepare({ displayName, slug }) {
4387
+ const slugCurrent = slug !== null && typeof slug === "object" && "current" in slug ? slug.current : void 0;
4388
+ return {
4389
+ subtitle: typeof slugCurrent === "string" ? slugCurrent : "",
4390
+ title: typeof displayName === "string" && displayName ? displayName : "Team"
4391
+ };
4392
+ },
4393
+ select: {
4394
+ displayName: "displayName",
4395
+ slug: "slug"
4396
+ }
4397
+ },
4398
+ title: "AILF Team",
4399
+ type: "document"
4400
+ });
4401
+
4402
+ // src/schema/webhook-config.ts
4403
+ import { ALL_FIELDS_GROUP as ALL_FIELDS_GROUP7, defineField as defineField7, defineType as defineType7 } from "sanity";
4404
+ var webhookConfigSchema = defineType7({
4405
+ groups: [
4406
+ { name: "main", title: "Main", default: true },
4407
+ { name: "optional", title: "Optional" },
4408
+ ALL_FIELDS_GROUP7
4409
+ ],
4410
+ fields: [
4411
+ defineField7({
4018
4412
  description: "When enabled, publishing articles will automatically trigger AI Literacy evaluations for affected feature areas.",
4019
4413
  group: ["main", "all-fields"],
4020
4414
  initialValue: false,
@@ -4022,7 +4416,7 @@ var webhookConfigSchema = defineType6({
4022
4416
  title: "Evaluate on Publish",
4023
4417
  type: "boolean"
4024
4418
  }),
4025
- defineField6({
4419
+ defineField7({
4026
4420
  description: "Which evaluation mode to use for webhook-triggered evaluations.",
4027
4421
  group: ["main", "all-fields"],
4028
4422
  initialValue: "baseline",
@@ -4038,7 +4432,7 @@ var webhookConfigSchema = defineType6({
4038
4432
  title: "Evaluation Mode",
4039
4433
  type: "string"
4040
4434
  }),
4041
- defineField6({
4435
+ defineField7({
4042
4436
  description: "Maximum evaluations per day. Prevents runaway costs from rapid editing.",
4043
4437
  group: ["main", "all-fields"],
4044
4438
  initialValue: 20,
@@ -4047,7 +4441,7 @@ var webhookConfigSchema = defineType6({
4047
4441
  type: "number",
4048
4442
  validation: (rule) => rule.min(1).max(100)
4049
4443
  }),
4050
- defineField6({
4444
+ defineField7({
4051
4445
  description: "Seconds to wait after the last edit before dispatching. Coalesces rapid edits into a single evaluation.",
4052
4446
  group: ["optional", "all-fields"],
4053
4447
  initialValue: 300,
@@ -4056,7 +4450,7 @@ var webhookConfigSchema = defineType6({
4056
4450
  type: "number",
4057
4451
  validation: (rule) => rule.min(10).max(3600)
4058
4452
  }),
4059
- defineField6({
4453
+ defineField7({
4060
4454
  description: "Specific feature areas to evaluate. Leave empty to evaluate all affected areas automatically.",
4061
4455
  group: ["optional", "all-fields"],
4062
4456
  name: "areas",
@@ -4064,7 +4458,7 @@ var webhookConfigSchema = defineType6({
4064
4458
  title: "Area Filter",
4065
4459
  type: "array"
4066
4460
  }),
4067
- defineField6({
4461
+ defineField7({
4068
4462
  description: "Slack webhook URL for notifications about webhook-triggered evaluations.",
4069
4463
  group: ["optional", "all-fields"],
4070
4464
  name: "notifySlack",
@@ -4835,6 +5229,11 @@ var latestReportsQuery = (
4835
5229
  "trigger": provenance.trigger.type,
4836
5230
  "classification": provenance.classification,
4837
5231
  "ownerTeam": provenance.owner.team,
5232
+ "teamDoc": *[_type == "ailf.team" && slug.current == ^.provenance.owner.team][0]{
5233
+ _id,
5234
+ displayName,
5235
+ status
5236
+ },
4838
5237
  "ownerIndividual": provenance.owner.individual,
4839
5238
  "executorType": provenance.executor.type,
4840
5239
  "executorName": provenance.executor.name,
@@ -4917,6 +5316,11 @@ var reportDetailQuery = (
4917
5316
  tag,
4918
5317
  title,
4919
5318
  provenance,
5319
+ "teamDoc": *[_type == "ailf.team" && slug.current == ^.provenance.owner.team][0]{
5320
+ _id,
5321
+ displayName,
5322
+ status
5323
+ },
4920
5324
  summary,
4921
5325
  "comparison": comparison {
4922
5326
  areas,
@@ -15331,7 +15735,7 @@ function ailfTool(options = {}) {
15331
15735
  }
15332
15736
 
15333
15737
  // src/structure.ts
15334
- import { ArchiveIcon as ArchiveIcon2, TaskIcon } from "@sanity/icons";
15738
+ import { ArchiveIcon as ArchiveIcon2, TaskIcon, WarningOutlineIcon as WarningOutlineIcon5 } from "@sanity/icons";
15335
15739
  function ailfTaskStructureItem(S) {
15336
15740
  return S.listItem().id("ailfTasks").title("AILF Tasks").icon(TaskIcon).child(
15337
15741
  S.list().id("ailfTasksViews").title("AILF Tasks").items([
@@ -15354,12 +15758,68 @@ function ailfTaskStructureItem(S) {
15354
15758
  ])
15355
15759
  );
15356
15760
  }
15761
+ function ailfTeamsStructureItem(S) {
15762
+ return S.listItem().id("ailfTeams").title("Teams").child(
15763
+ S.list().id("ailfTeamsViews").title("Teams").items([
15764
+ S.listItem().id("activeTeams").title("Active teams").child(
15765
+ S.documentTypeList("ailf.team").id("ailfTeamsActive").title("Active teams").apiVersion(API_VERSION).filter('_type == "ailf.team" && status == "active"')
15766
+ ),
15767
+ S.listItem().id("archivedTeams").title("Archived teams").icon(ArchiveIcon2).child(
15768
+ S.documentTypeList("ailf.team").id("ailfTeamsArchived").title("Archived teams").apiVersion(API_VERSION).filter('_type == "ailf.team" && status == "archived"')
15769
+ ),
15770
+ S.divider(),
15771
+ S.listItem().id("allTeams").title("All teams").child(
15772
+ S.documentTypeList("ailf.team").id("ailfTeamsAll").title("All teams")
15773
+ )
15774
+ ])
15775
+ );
15776
+ }
15777
+ function ailfAreasStructureItem(S) {
15778
+ return S.listItem().id("ailfAreas").title("Areas").child(
15779
+ S.list().id("ailfAreasViews").title("Areas").items([
15780
+ S.listItem().id("allAreas").title("All areas").child(
15781
+ S.documentTypeList("ailf.featureArea").id("ailfAreasAll").title("All areas")
15782
+ ),
15783
+ S.divider(),
15784
+ S.listItem().id("unownedAreas").title("\u26A0 Unowned areas").icon(WarningOutlineIcon5).child(
15785
+ S.documentTypeList("ailf.featureArea").id("ailfAreasUnowned").title("\u26A0 Unowned areas").apiVersion(API_VERSION).filter('_type == "ailf.featureArea" && !defined(team)')
15786
+ )
15787
+ ])
15788
+ );
15789
+ }
15790
+ function ailfReportsStructureItem(S) {
15791
+ return S.listItem().id("ailfReports").title("Reports").child(
15792
+ S.list().id("ailfReportsViews").title("Reports").items([
15793
+ S.listItem().id("allReports").title("All reports").child(
15794
+ S.documentTypeList("ailf.report").id("ailfReportsAll").title("All reports")
15795
+ ),
15796
+ S.divider(),
15797
+ S.listItem().id("unresolvedTeamReports").title("\u26A0 Reports with unresolved team").icon(WarningOutlineIcon5).child(
15798
+ S.documentList().id("ailfReportsUnresolvedTeam").title("\u26A0 Reports with unresolved team").apiVersion(API_VERSION).filter(
15799
+ `_type == "ailf.report" && defined(provenance.owner.team) && provenance.owner.team != "unknown" && !(provenance.owner.team in *[_type == "ailf.team"].slug.current)`
15800
+ )
15801
+ )
15802
+ ])
15803
+ );
15804
+ }
15805
+ function ailfChannelsWithUnknownEventsItem(S) {
15806
+ return S.listItem().id("ailfChannelsUnknownEvents").title("\u26A0 Channels with unknown events").icon(WarningOutlineIcon5).child(
15807
+ S.documentList().id("ailfChannelsUnknownEventsList").title("Teams whose channels reference unknown event types").apiVersion(API_VERSION).filter(
15808
+ `_type == "ailf.team" && count(notifications[count(events[!(@ in $known)]) > 0]) > 0`
15809
+ ).params({ known: [...KNOWN_EVENT_TYPES] })
15810
+ );
15811
+ }
15357
15812
  var ailfStructure = (S) => S.list().id("root").title("Content").items([
15358
15813
  ailfTaskStructureItem(S),
15814
+ ailfTeamsStructureItem(S),
15815
+ ailfAreasStructureItem(S),
15816
+ ailfReportsStructureItem(S),
15817
+ ailfChannelsWithUnknownEventsItem(S),
15359
15818
  S.divider(),
15360
- ...S.documentTypeListItems().filter(
15361
- (listItem) => listItem.getId() !== "ailf.task"
15362
- )
15819
+ ...S.documentTypeListItems().filter((listItem) => {
15820
+ const id = listItem.getId();
15821
+ return id !== "ailf.task" && id !== "ailf.team" && id !== "ailf.featureArea" && id !== "ailf.report";
15822
+ })
15363
15823
  ]);
15364
15824
 
15365
15825
  // src/actions/RunEvaluationAction.tsx
@@ -15647,6 +16107,7 @@ var ailfPlugin = definePlugin((options) => ({
15647
16107
  referenceSolutionSchema,
15648
16108
  reportSchema,
15649
16109
  taskSchema,
16110
+ teamSchema,
15650
16111
  webhookConfigSchema
15651
16112
  ]
15652
16113
  },
@@ -15690,6 +16151,7 @@ export {
15690
16151
  scoreTimelineQuery,
15691
16152
  searchTopics,
15692
16153
  taskSchema,
16154
+ teamSchema,
15693
16155
  useHelp,
15694
16156
  webhookConfigSchema
15695
16157
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf-studio",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
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
  }