@sanity/ailf-studio 0.1.26 → 0.1.27

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
@@ -7,22 +7,24 @@ import { DocumentRef } from './document-ref.js';
7
7
  * actions/GraduateToNativeAction.tsx
8
8
  *
9
9
  * Sanity Studio document action that "graduates" a mirrored task to
10
- * a native task by removing the `origin` field.
10
+ * a native (Studio-owned) task by changing the `ownership` field from
11
+ * "repo" to "studio".
11
12
  *
12
- * This is a one-way, irreversible operation. Once graduated:
13
+ * This is a one-way operation. Once graduated:
13
14
  * - The task becomes fully editable in Studio
14
- * - Future pipeline mirror syncs will NOT overwrite it (the mirror
15
- * uses createOrReplace with the same _id, but since origin is gone,
16
- * the document-level readOnly check returns false and the task
17
- * behaves like any other native task)
15
+ * - Future pipeline mirror syncs will skip this document (the mirror
16
+ * step checks `ownership` and skips documents with `ownership: "studio"`)
17
+ * - The `origin` field is preserved as provenance — you can always
18
+ * see where the task originally came from
18
19
  * - The task's _id is unchanged — it keeps the mirror prefix
19
20
  * (ailf.task.mirror.*) but that's just an ID string, not a
20
21
  * behavioral marker
21
22
  *
22
- * The action only appears on ailf.task documents that have an `origin`
23
- * field (i.e., mirrored tasks). Native tasks never see it.
23
+ * The action only appears on ailf.task documents that have
24
+ * `ownership: "repo"` (i.e., active mirrors). Native tasks and
25
+ * already-graduated tasks never see it.
24
26
  *
25
- * @see docs/exec-plans/tasks-as-content/phase-5-content-lake-mirroring.md
27
+ * @see docs/exec-plans/task-lifecycle/phase-1-ownership.md
26
28
  */
27
29
 
28
30
  declare const GraduateToNativeAction: DocumentActionComponent;
@@ -75,15 +77,29 @@ declare function ReleasePicker(props: StringInputProps): react_jsx_runtime.JSX.E
75
77
  /**
76
78
  * components/MirrorBanner.tsx
77
79
  *
78
- * Informational banner shown at the top of mirrored task documents.
79
- * Communicates that the task is managed in an external repo and links
80
- * to the source file on GitHub.
80
+ * Informational banner shown at the top of task documents that have
81
+ * origin provenance (both active mirrors and graduated tasks).
81
82
  *
82
- * Paired with `SyncStatusBadge` to show both the source and freshness
83
- * of the mirror.
83
+ * For active mirrors (`ownership: "repo"`): communicates that the task
84
+ * is managed in an external repo and links to the source file on GitHub.
84
85
  *
85
- * @see docs/exec-plans/tasks-as-content/phase-5-content-lake-mirroring.md
86
+ * For graduated tasks (`ownership: "studio"` with origin): shows
87
+ * provenance info about where the task originally came from.
88
+ *
89
+ * Paired with `SyncStatusBadge` to show sync freshness for active mirrors.
90
+ *
91
+ * @see docs/exec-plans/task-lifecycle/phase-1-ownership.md
86
92
  */
93
+ interface GitAuthorInfo {
94
+ gitName?: string;
95
+ gitEmail?: string;
96
+ githubUsername?: string;
97
+ }
98
+ interface GraduatedByInfo {
99
+ sanityId?: string;
100
+ name?: string;
101
+ email?: string;
102
+ }
87
103
  interface MirrorBannerProps {
88
104
  origin: {
89
105
  repo?: string;
@@ -93,9 +109,15 @@ interface MirrorBannerProps {
93
109
  branch?: string;
94
110
  commitSha?: string;
95
111
  lastSyncedAt?: string;
112
+ author?: GitAuthorInfo;
113
+ lastEditor?: GitAuthorInfo;
114
+ graduatedAt?: string;
115
+ graduatedBy?: GraduatedByInfo;
96
116
  };
117
+ /** Task ownership — "repo" for active mirrors, "studio" for graduated */
118
+ ownership?: string;
97
119
  }
98
- declare function MirrorBanner({ origin }: MirrorBannerProps): react_jsx_runtime.JSX.Element;
120
+ declare function MirrorBanner({ origin, ownership }: MirrorBannerProps): react_jsx_runtime.JSX.Element;
99
121
 
100
122
  /**
101
123
  * components/SyncStatusBadge.tsx
@@ -468,6 +490,8 @@ declare const taskSchema: {
468
490
  description: string;
469
491
  id: string;
470
492
  origin: string;
493
+ ownership: string;
494
+ status: string;
471
495
  }, Record<string, unknown>> | undefined;
472
496
  };
473
497
 
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { definePlugin } from "sanity";
5
5
  import { EditIcon } from "@sanity/icons";
6
6
  import { Box, Stack, Text } from "@sanity/ui";
7
7
  import { useCallback, useState } from "react";
8
- import { useClient } from "sanity";
8
+ import { useClient, useCurrentUser } from "sanity";
9
9
 
10
10
  // src/lib/constants.ts
11
11
  var API_VERSION = "2026-03-11";
@@ -15,6 +15,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
15
15
  var GraduateToNativeAction = (props) => {
16
16
  const { id, type, draft, published, onComplete } = props;
17
17
  const client = useClient({ apiVersion: API_VERSION });
18
+ const currentUser = useCurrentUser();
18
19
  const [isConfirming, setIsConfirming] = useState(false);
19
20
  const doc = draft ?? published;
20
21
  const origin = doc?.origin;
@@ -23,17 +24,35 @@ var GraduateToNativeAction = (props) => {
23
24
  const handleGraduate = useCallback(async () => {
24
25
  try {
25
26
  const publishedId = id.replace(/^drafts\./, "");
26
- await client.patch(publishedId).unset(["origin"]).commit();
27
+ const graduationPatch = {
28
+ ownership: "studio",
29
+ "origin.graduatedAt": (/* @__PURE__ */ new Date()).toISOString(),
30
+ "origin.graduatedBy": {
31
+ sanityId: currentUser?.id ?? "unknown",
32
+ name: currentUser?.name ?? "unknown",
33
+ email: currentUser?.email ?? "unknown"
34
+ }
35
+ };
36
+ await client.patch(publishedId).set(graduationPatch).commit();
27
37
  if (draft) {
28
- await client.patch(`drafts.${publishedId}`).unset(["origin"]).commit().catch(() => {
38
+ await client.patch(`drafts.${publishedId}`).set(graduationPatch).commit().catch(() => {
29
39
  });
30
40
  }
31
41
  onComplete();
32
42
  } catch (err) {
33
43
  console.error("Failed to graduate task:", err);
34
44
  }
35
- }, [client, draft, id, onComplete]);
36
- if (type !== "ailf.task" || !origin) return null;
45
+ }, [
46
+ client,
47
+ currentUser?.email,
48
+ currentUser?.id,
49
+ currentUser?.name,
50
+ draft,
51
+ id,
52
+ onComplete
53
+ ]);
54
+ const ownership = doc?.ownership;
55
+ if (type !== "ailf.task" || ownership !== "repo") return null;
37
56
  return {
38
57
  dialog: isConfirming ? {
39
58
  type: "confirm",
@@ -44,8 +63,8 @@ var GraduateToNativeAction = (props) => {
44
63
  /* @__PURE__ */ jsx("strong", { children: repoDisplay }),
45
64
  " and make it fully editable in Studio."
46
65
  ] }),
47
- /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "\u26A0\uFE0F This is a one-way operation. The task will no longer sync from the repo. Future pipeline runs will not overwrite your changes." }),
48
- repoUrl && /* @__PURE__ */ jsxs(Text, { size: 1, muted: true, children: [
66
+ /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "\u26A0\uFE0F This is a one-way operation. The task will no longer sync from the repo \u2014 future mirror syncs will not overwrite your edits. The task will continue to run in evaluations. Source repo provenance is preserved for reference." }),
67
+ repoUrl && origin && /* @__PURE__ */ jsxs(Text, { size: 1, muted: true, children: [
49
68
  "Current source:",
50
69
  " ",
51
70
  /* @__PURE__ */ jsx("a", { href: repoUrl, target: "_blank", rel: "noopener noreferrer", children: origin.path }),
@@ -62,7 +81,7 @@ var GraduateToNativeAction = (props) => {
62
81
  icon: EditIcon,
63
82
  label: "Graduate to native task",
64
83
  onHandle: () => setIsConfirming(true),
65
- title: "Disconnect this task from its source repo so it becomes fully editable in Studio. This cannot be undone.",
84
+ title: "Transfer ownership to Studio so this task becomes fully editable. Source repo provenance is preserved.",
66
85
  tone: "caution"
67
86
  };
68
87
  };
@@ -74,7 +93,7 @@ import { useToast } from "@sanity/ui";
74
93
  import { useCallback as useCallback2, useEffect, useRef, useState as useState2 } from "react";
75
94
  import {
76
95
  useClient as useClient2,
77
- useCurrentUser,
96
+ useCurrentUser as useCurrentUser2,
78
97
  useDataset,
79
98
  useProjectId
80
99
  } from "sanity";
@@ -184,7 +203,7 @@ var RunTaskEvaluationAction = (props) => {
184
203
  const client = useClient2({ apiVersion: API_VERSION });
185
204
  const dataset = useDataset();
186
205
  const projectId = useProjectId();
187
- const currentUser = useCurrentUser();
206
+ const currentUser = useCurrentUser2();
188
207
  const toast = useToast();
189
208
  const [state, setState] = useState2({ status: "idle" });
190
209
  const requestedAtRef = useRef(null);
@@ -1879,6 +1898,7 @@ function CanonicalDocPreview(props) {
1879
1898
 
1880
1899
  // src/components/OriginInput.tsx
1881
1900
  import { Stack as Stack5 } from "@sanity/ui";
1901
+ import { useFormValue } from "sanity";
1882
1902
 
1883
1903
  // src/components/MirrorBanner.tsx
1884
1904
  import { LinkIcon } from "@sanity/icons";
@@ -1925,32 +1945,45 @@ function SyncStatusBadge({
1925
1945
  }
1926
1946
 
1927
1947
  // src/components/MirrorBanner.tsx
1928
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1929
- function MirrorBanner({ origin }) {
1948
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1949
+ function formatAuthor(author) {
1950
+ if (!author) return null;
1951
+ const parts = [];
1952
+ if (author.gitName) parts.push(author.gitName);
1953
+ if (author.githubUsername) parts.push(`(${author.githubUsername})`);
1954
+ if (parts.length === 0 && author.gitEmail) parts.push(author.gitEmail);
1955
+ return parts.length > 0 ? parts.join(" ") : null;
1956
+ }
1957
+ function MirrorBanner({ origin, ownership }) {
1930
1958
  const { repo, path, branch, commitSha, lastSyncedAt } = origin;
1959
+ const isGraduated = ownership === "studio";
1931
1960
  const repoUrl = repo && path && branch ? `https://github.com/${repo}/blob/${branch}/${path}` : null;
1932
1961
  const repoDisplay = repo ?? "an external repository";
1962
+ const repoLink = repoUrl ? /* @__PURE__ */ jsx6(
1963
+ "a",
1964
+ {
1965
+ href: repoUrl,
1966
+ target: "_blank",
1967
+ rel: "noopener noreferrer",
1968
+ style: { fontWeight: 600 },
1969
+ children: repoDisplay
1970
+ }
1971
+ ) : /* @__PURE__ */ jsx6("strong", { children: repoDisplay });
1933
1972
  return /* @__PURE__ */ jsx6(Card3, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ jsxs6(Stack4, { space: 3, children: [
1934
1973
  /* @__PURE__ */ jsxs6(Flex4, { align: "center", gap: 2, children: [
1935
1974
  /* @__PURE__ */ jsx6(Text6, { size: 2, children: /* @__PURE__ */ jsx6(LinkIcon, {}) }),
1936
- /* @__PURE__ */ jsxs6(Text6, { size: 2, children: [
1937
- "This task is managed in",
1938
- " ",
1939
- repoUrl ? /* @__PURE__ */ jsx6(
1940
- "a",
1941
- {
1942
- href: repoUrl,
1943
- target: "_blank",
1944
- rel: "noopener noreferrer",
1945
- style: { fontWeight: 600 },
1946
- children: repoDisplay
1947
- }
1948
- ) : /* @__PURE__ */ jsx6("strong", { children: repoDisplay }),
1975
+ /* @__PURE__ */ jsx6(Text6, { size: 2, children: isGraduated ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
1976
+ "This task was originally mirrored from ",
1977
+ repoLink,
1978
+ " and has been graduated to Studio ownership."
1979
+ ] }) : /* @__PURE__ */ jsxs6(Fragment3, { children: [
1980
+ "This task is managed in ",
1981
+ repoLink,
1949
1982
  ". Edit it there to make changes."
1950
- ] })
1983
+ ] }) })
1951
1984
  ] }),
1952
1985
  /* @__PURE__ */ jsxs6(Flex4, { align: "center", gap: 2, wrap: "wrap", children: [
1953
- lastSyncedAt && /* @__PURE__ */ jsx6(
1986
+ !isGraduated && lastSyncedAt && /* @__PURE__ */ jsx6(
1954
1987
  SyncStatusBadge,
1955
1988
  {
1956
1989
  lastSyncedAt,
@@ -1963,6 +1996,19 @@ function MirrorBanner({ origin }) {
1963
1996
  commitSha ? ` @ ${commitSha.slice(0, 7)}` : ""
1964
1997
  ] }),
1965
1998
  path && /* @__PURE__ */ jsx6(Text6, { size: 1, muted: true, children: path })
1999
+ ] }),
2000
+ isGraduated && origin.graduatedBy?.name && /* @__PURE__ */ jsxs6(Text6, { size: 1, muted: true, children: [
2001
+ "Graduated by ",
2002
+ origin.graduatedBy.name,
2003
+ origin.graduatedAt && ` \xB7 ${new Date(origin.graduatedAt).toLocaleDateString()}`
2004
+ ] }),
2005
+ !isGraduated && formatAuthor(origin.lastEditor) && /* @__PURE__ */ jsxs6(Text6, { size: 1, muted: true, children: [
2006
+ "Last edited by ",
2007
+ formatAuthor(origin.lastEditor)
2008
+ ] }),
2009
+ isGraduated && formatAuthor(origin.author) && /* @__PURE__ */ jsxs6(Text6, { size: 1, muted: true, children: [
2010
+ "Originally authored by ",
2011
+ formatAuthor(origin.author)
1966
2012
  ] })
1967
2013
  ] }) });
1968
2014
  }
@@ -1972,17 +2018,25 @@ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1972
2018
  function OriginInput(props) {
1973
2019
  const value = props.value;
1974
2020
  if (!value) return props.renderDefault(props);
2021
+ const str = (key) => typeof value[key] === "string" ? value[key] : void 0;
2022
+ const obj = (key) => value[key] !== null && typeof value[key] === "object" ? value[key] : void 0;
1975
2023
  const origin = {
1976
- repo: typeof value.repo === "string" ? value.repo : void 0,
1977
- repoOwner: typeof value.repoOwner === "string" ? value.repoOwner : void 0,
1978
- repoName: typeof value.repoName === "string" ? value.repoName : void 0,
1979
- path: typeof value.path === "string" ? value.path : void 0,
1980
- branch: typeof value.branch === "string" ? value.branch : void 0,
1981
- commitSha: typeof value.commitSha === "string" ? value.commitSha : void 0,
1982
- lastSyncedAt: typeof value.lastSyncedAt === "string" ? value.lastSyncedAt : void 0
2024
+ repo: str("repo"),
2025
+ repoOwner: str("repoOwner"),
2026
+ repoName: str("repoName"),
2027
+ path: str("path"),
2028
+ branch: str("branch"),
2029
+ commitSha: str("commitSha"),
2030
+ lastSyncedAt: str("lastSyncedAt"),
2031
+ author: obj("author"),
2032
+ lastEditor: obj("lastEditor"),
2033
+ graduatedAt: str("graduatedAt"),
2034
+ graduatedBy: obj("graduatedBy")
1983
2035
  };
2036
+ const ownershipValue = useFormValue(["ownership"]);
2037
+ const ownership = typeof ownershipValue === "string" ? ownershipValue : void 0;
1984
2038
  return /* @__PURE__ */ jsxs7(Stack5, { space: 3, children: [
1985
- /* @__PURE__ */ jsx7(MirrorBanner, { origin }),
2039
+ /* @__PURE__ */ jsx7(MirrorBanner, { origin, ownership }),
1986
2040
  props.renderDefault(props)
1987
2041
  ] });
1988
2042
  }
@@ -2154,6 +2208,57 @@ var taskSchema = defineType5({
2154
2208
  validation: (rule) => rule.required()
2155
2209
  }),
2156
2210
  // -----------------------------------------------------------------------
2211
+ // Ownership — who is the source of truth for this task
2212
+ //
2213
+ // "studio" tasks are fully editable in Studio. "repo" tasks are managed
2214
+ // in an external repository and read-only in Studio. Mirrored tasks are
2215
+ // set to "repo" by the pipeline; graduation changes this to "studio".
2216
+ //
2217
+ // Absent = "studio" for backwards compatibility with existing native tasks.
2218
+ // -----------------------------------------------------------------------
2219
+ defineField5({
2220
+ description: 'Who is the source of truth for this task. "studio" tasks are fully editable here. "repo" tasks are managed in an external repository and read-only in Studio.',
2221
+ group: ["optional", "all-fields"],
2222
+ hidden: true,
2223
+ // Managed by system actions, not manually edited
2224
+ initialValue: "studio",
2225
+ name: "ownership",
2226
+ options: {
2227
+ list: [
2228
+ { title: "Studio", value: "studio" },
2229
+ { title: "Repository", value: "repo" }
2230
+ ]
2231
+ },
2232
+ title: "Ownership",
2233
+ type: "string"
2234
+ }),
2235
+ // -----------------------------------------------------------------------
2236
+ // Status — task lifecycle state
2237
+ //
2238
+ // Active tasks run in evaluations and appear in default list views.
2239
+ // Exploratory tasks are excluded from production evals (for testing).
2240
+ // Archived tasks are retired, hidden from default views, and preserved
2241
+ // for historical report references.
2242
+ // -----------------------------------------------------------------------
2243
+ defineField5({
2244
+ description: "Task lifecycle status. Active tasks run in evaluations. Draft tasks are work-in-progress, excluded from production evals. Paused tasks are temporarily suspended. Archived tasks are retired and excluded from evaluations.",
2245
+ group: ["main", "all-fields"],
2246
+ initialValue: "active",
2247
+ name: "status",
2248
+ options: {
2249
+ direction: "horizontal",
2250
+ layout: "radio",
2251
+ list: [
2252
+ { title: "Active", value: "active" },
2253
+ { title: "Draft", value: "draft" },
2254
+ { title: "Paused", value: "paused" },
2255
+ { title: "Archived", value: "archived" }
2256
+ ]
2257
+ },
2258
+ title: "Status",
2259
+ type: "string"
2260
+ }),
2261
+ // -----------------------------------------------------------------------
2157
2262
  // Task prompt
2158
2263
  // -----------------------------------------------------------------------
2159
2264
  defineField5({
@@ -2686,6 +2791,95 @@ var taskSchema = defineType5({
2686
2791
  readOnly: true,
2687
2792
  title: "Last Synced At",
2688
2793
  type: "datetime"
2794
+ }),
2795
+ // --- Authorship tracking (Phase 4: Task Lifecycle) ---
2796
+ defineField5({
2797
+ description: "Who originally created this task in the source repo. Set on first mirror, never overwritten on subsequent syncs.",
2798
+ fields: [
2799
+ defineField5({
2800
+ name: "gitName",
2801
+ readOnly: true,
2802
+ title: "Git Name",
2803
+ type: "string"
2804
+ }),
2805
+ defineField5({
2806
+ name: "gitEmail",
2807
+ readOnly: true,
2808
+ title: "Git Email",
2809
+ type: "string"
2810
+ }),
2811
+ defineField5({
2812
+ name: "githubUsername",
2813
+ readOnly: true,
2814
+ title: "GitHub Username",
2815
+ type: "string"
2816
+ })
2817
+ ],
2818
+ name: "author",
2819
+ readOnly: true,
2820
+ title: "Original Author",
2821
+ type: "object"
2822
+ }),
2823
+ defineField5({
2824
+ description: "Who last modified this task in the source repo. Updated on every content-changing mirror sync.",
2825
+ fields: [
2826
+ defineField5({
2827
+ name: "gitName",
2828
+ readOnly: true,
2829
+ title: "Git Name",
2830
+ type: "string"
2831
+ }),
2832
+ defineField5({
2833
+ name: "gitEmail",
2834
+ readOnly: true,
2835
+ title: "Git Email",
2836
+ type: "string"
2837
+ }),
2838
+ defineField5({
2839
+ name: "githubUsername",
2840
+ readOnly: true,
2841
+ title: "GitHub Username",
2842
+ type: "string"
2843
+ })
2844
+ ],
2845
+ name: "lastEditor",
2846
+ readOnly: true,
2847
+ title: "Last Editor",
2848
+ type: "object"
2849
+ }),
2850
+ defineField5({
2851
+ description: "When this task was graduated to Studio ownership",
2852
+ name: "graduatedAt",
2853
+ readOnly: true,
2854
+ title: "Graduated At",
2855
+ type: "datetime"
2856
+ }),
2857
+ defineField5({
2858
+ description: "Who graduated this task from repo to Studio ownership",
2859
+ fields: [
2860
+ defineField5({
2861
+ name: "sanityId",
2862
+ readOnly: true,
2863
+ title: "Sanity User ID",
2864
+ type: "string"
2865
+ }),
2866
+ defineField5({
2867
+ name: "name",
2868
+ readOnly: true,
2869
+ title: "Name",
2870
+ type: "string"
2871
+ }),
2872
+ defineField5({
2873
+ name: "email",
2874
+ readOnly: true,
2875
+ title: "Email",
2876
+ type: "string"
2877
+ })
2878
+ ],
2879
+ name: "graduatedBy",
2880
+ readOnly: true,
2881
+ title: "Graduated By",
2882
+ type: "object"
2689
2883
  })
2690
2884
  ],
2691
2885
  components: {
@@ -2704,9 +2898,16 @@ var taskSchema = defineType5({
2704
2898
  ],
2705
2899
  name: "ailf.task",
2706
2900
  preview: {
2707
- prepare({ area, description, id, origin }) {
2901
+ prepare({
2902
+ area,
2903
+ description,
2904
+ id,
2905
+ origin,
2906
+ ownership,
2907
+ status
2908
+ }) {
2708
2909
  const taskId = id !== null && typeof id === "object" && "current" in id ? id.current : void 0;
2709
- const isMirror = origin !== null && typeof origin === "object" && "repo" in origin;
2910
+ const isMirror = ownership === "repo" || !ownership && origin !== null && typeof origin === "object" && "repo" in origin;
2710
2911
  const areaId = area !== null && typeof area === "object" && "current" in area ? area.current : void 0;
2711
2912
  const prefix = isMirror ? "\u{1F517} " : "";
2712
2913
  const areaStr = typeof areaId === "string" ? `[${areaId}] ` : "";
@@ -2724,22 +2925,32 @@ var taskSchema = defineType5({
2724
2925
  syncInfo = ` \xB7 ${icon} ${ageLabel}`;
2725
2926
  }
2726
2927
  }
2928
+ const statusIcon = status === "archived" ? "\u{1F4E6} " : status === "draft" ? "\u{1F9EA} " : status === "paused" ? "\u23F8\uFE0F " : "";
2727
2929
  return {
2728
2930
  subtitle: `${areaStr}${typeof taskId === "string" ? taskId : ""}${syncInfo}`,
2729
- title: `${prefix}${typeof description === "string" ? description : "Task"}`
2931
+ title: `${prefix}${statusIcon}${typeof description === "string" ? description : "Task"}`
2730
2932
  };
2731
2933
  },
2732
2934
  select: {
2733
2935
  area: "featureArea.areaId",
2734
2936
  description: "description",
2735
2937
  id: "id",
2736
- origin: "origin"
2938
+ origin: "origin",
2939
+ ownership: "ownership",
2940
+ status: "status"
2737
2941
  }
2738
2942
  },
2739
- // Document-level read-only when mirrored from a repo.
2740
- // Native tasks (no origin) are fully editable; mirrored tasks
2741
- // are read-only because the source of truth is the repo.
2742
- readOnly: ({ document: document2 }) => !!document2?.origin,
2943
+ // Document-level read-only when owned by a repo.
2944
+ // Native tasks (ownership: "studio" or absent) are fully editable;
2945
+ // repo-owned tasks are read-only because the source of truth is the repo.
2946
+ // Falls back to checking origin for legacy mirrored tasks that don't
2947
+ // have the ownership field yet (pre-migration).
2948
+ readOnly: ({ document: document2 }) => {
2949
+ const doc = document2;
2950
+ if (doc?.ownership === "repo") return true;
2951
+ if (doc?.ownership === "studio" || doc?.ownership) return false;
2952
+ return !!doc?.origin;
2953
+ },
2743
2954
  title: "AILF Task",
2744
2955
  type: "document"
2745
2956
  });
@@ -3279,7 +3490,7 @@ function LoadingState({ message = "Loading\u2026" }) {
3279
3490
  }
3280
3491
 
3281
3492
  // src/components/ComparisonView.tsx
3282
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3493
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3283
3494
  function ComparisonView() {
3284
3495
  const client = useClient3({ apiVersion: API_VERSION });
3285
3496
  const [baselineId, setBaselineId] = useState3(null);
@@ -3377,7 +3588,7 @@ function ComparisonView() {
3377
3588
  ] }),
3378
3589
  hasBoth && loading && /* @__PURE__ */ jsx12(LoadingState, { message: "Loading comparison\u2026" }),
3379
3590
  hasBoth && !loading && !hasData && /* @__PURE__ */ jsx12(Card6, { padding: 4, tone: "caution", children: /* @__PURE__ */ jsx12(Text11, { size: 2, children: "Report not found." }) }),
3380
- hasBoth && !loading && hasData && /* @__PURE__ */ jsxs10(Fragment3, { children: [
3591
+ hasBoth && !loading && hasData && /* @__PURE__ */ jsxs10(Fragment4, { children: [
3381
3592
  /* @__PURE__ */ jsx12(
3382
3593
  AreaComparisonTable,
3383
3594
  {
@@ -3824,7 +4035,7 @@ function formatCardDate(iso) {
3824
4035
  }
3825
4036
 
3826
4037
  // src/components/report-table/ReportTable.tsx
3827
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
4038
+ import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3828
4039
  function GitBranchIcon({ style }) {
3829
4040
  return /* @__PURE__ */ jsxs12(
3830
4041
  "svg",
@@ -3869,7 +4080,7 @@ function ReportTable({
3869
4080
  onSortChange
3870
4081
  }) {
3871
4082
  const { ref, tier } = useContainerWidth();
3872
- return /* @__PURE__ */ jsxs12(Fragment4, { children: [
4083
+ return /* @__PURE__ */ jsxs12(Fragment5, { children: [
3873
4084
  /* @__PURE__ */ jsx15("style", { children: TABLE_HOVER_STYLES }),
3874
4085
  /* @__PURE__ */ jsxs12("div", { ref, children: [
3875
4086
  /* @__PURE__ */ jsxs12(
@@ -3902,7 +4113,7 @@ function ReportTable({
3902
4113
  onClick: () => onSortChange("score")
3903
4114
  }
3904
4115
  ),
3905
- tier === "full" && /* @__PURE__ */ jsxs12(Fragment4, { children: [
4116
+ tier === "full" && /* @__PURE__ */ jsxs12(Fragment5, { children: [
3906
4117
  /* @__PURE__ */ jsx15(ColHeader, { label: "Mode" }),
3907
4118
  /* @__PURE__ */ jsx15(ColHeader, { label: "Trigger" }),
3908
4119
  /* @__PURE__ */ jsx15(
@@ -4430,7 +4641,7 @@ import {
4430
4641
  // src/components/primitives/StatCard.tsx
4431
4642
  import { HelpCircleIcon as HelpCircleIcon4 } from "@sanity/icons";
4432
4643
  import { Box as Box8, Card as Card8, Stack as Stack11, Text as Text15, Tooltip as Tooltip3 } from "@sanity/ui";
4433
- import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
4644
+ import { Fragment as Fragment6, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
4434
4645
  function StatCard({
4435
4646
  label,
4436
4647
  value,
@@ -4441,7 +4652,7 @@ function StatCard({
4441
4652
  return /* @__PURE__ */ jsx17(Card8, { padding: 3, radius: 2, shadow: 1, tone, children: /* @__PURE__ */ jsxs14(Stack11, { space: 2, children: [
4442
4653
  /* @__PURE__ */ jsxs14(Text15, { muted: true, size: 2, children: [
4443
4654
  label,
4444
- tooltip && /* @__PURE__ */ jsxs14(Fragment5, { children: [
4655
+ tooltip && /* @__PURE__ */ jsxs14(Fragment6, { children: [
4445
4656
  " ",
4446
4657
  /* @__PURE__ */ jsx17(
4447
4658
  Tooltip3,
@@ -5113,10 +5324,10 @@ import { HelpCircleIcon as HelpCircleIcon6 } from "@sanity/icons";
5113
5324
  import { Badge as Badge8, Box as Box11, Card as Card14, Flex as Flex14, Stack as Stack16, Text as Text21, Tooltip as Tooltip6 } from "@sanity/ui";
5114
5325
 
5115
5326
  // src/components/primitives/InlineCode.tsx
5116
- import { Fragment as Fragment6, jsx as jsx23 } from "react/jsx-runtime";
5327
+ import { Fragment as Fragment7, jsx as jsx23 } from "react/jsx-runtime";
5117
5328
  function InlineCode({ text }) {
5118
5329
  const parts = text.split(/`([^`]+)`/);
5119
- return /* @__PURE__ */ jsx23(Fragment6, { children: parts.map(
5330
+ return /* @__PURE__ */ jsx23(Fragment7, { children: parts.map(
5120
5331
  (part, i) => i % 2 === 1 ? /* @__PURE__ */ jsx23(
5121
5332
  "code",
5122
5333
  {
@@ -5889,7 +6100,7 @@ function DownloadReportAction({
5889
6100
  import { PlayIcon as PlayIcon2 } from "@sanity/icons";
5890
6101
  import { MenuItem as MenuItem6, useToast as useToast6 } from "@sanity/ui";
5891
6102
  import { useCallback as useCallback16, useState as useState11 } from "react";
5892
- import { useClient as useClient8, useCurrentUser as useCurrentUser2 } from "sanity";
6103
+ import { useClient as useClient8, useCurrentUser as useCurrentUser3 } from "sanity";
5893
6104
 
5894
6105
  // src/lib/eval-scope.ts
5895
6106
  function extractEvalScope(provenance) {
@@ -5941,7 +6152,7 @@ function RerunEvaluationAction({
5941
6152
  reportId
5942
6153
  }) {
5943
6154
  const client = useClient8({ apiVersion: API_VERSION });
5944
- const currentUser = useCurrentUser2();
6155
+ const currentUser = useCurrentUser3();
5945
6156
  const toast = useToast6();
5946
6157
  const [requesting, setRequesting] = useState11(false);
5947
6158
  const handleClick = useCallback16(async () => {
@@ -5983,7 +6194,7 @@ function RerunEvaluationAction({
5983
6194
  }
5984
6195
 
5985
6196
  // src/components/report-detail/report-actions/ReportActions.tsx
5986
- import { Fragment as Fragment7, jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
6197
+ import { Fragment as Fragment8, jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
5987
6198
  function ReportActions({
5988
6199
  documentId,
5989
6200
  onDeleted,
@@ -6039,7 +6250,7 @@ function ReportActions({
6039
6250
  setDeleting(false);
6040
6251
  }
6041
6252
  }, [client, documentId, onDeleted, toast]);
6042
- return /* @__PURE__ */ jsxs24(Fragment7, { children: [
6253
+ return /* @__PURE__ */ jsxs24(Fragment8, { children: [
6043
6254
  /* @__PURE__ */ jsxs24(Flex18, { children: [
6044
6255
  /* @__PURE__ */ jsx35(
6045
6256
  Button3,
@@ -6819,7 +7030,7 @@ import { useCallback as useCallback21, useEffect as useEffect9, useRef as useRef
6819
7030
  import {
6820
7031
  getReleaseIdFromReleaseDocumentId as getReleaseIdFromReleaseDocumentId3,
6821
7032
  useClient as useClient12,
6822
- useCurrentUser as useCurrentUser3,
7033
+ useCurrentUser as useCurrentUser4,
6823
7034
  useDataset as useDataset2,
6824
7035
  useProjectId as useProjectId2
6825
7036
  } from "sanity";
@@ -6844,7 +7055,7 @@ function createRunEvaluationAction(options = {}) {
6844
7055
  const client = useClient12({ apiVersion: API_VERSION2 });
6845
7056
  const dataset = useDataset2();
6846
7057
  const projectId = useProjectId2();
6847
- const currentUser = useCurrentUser3();
7058
+ const currentUser = useCurrentUser4();
6848
7059
  const toast = useToast8();
6849
7060
  const [state, setState] = useState15({ status: "loading" });
6850
7061
  const requestedAtRef = useRef4(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf-studio",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "AI Literacy Framework — Sanity Studio dashboard plugin",
5
5
  "type": "module",
6
6
  "license": "MIT",