@chalksurf/cli 0.3.3 → 0.3.6

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.
@@ -20,6 +20,27 @@ var ApiClientError = class extends Error {
20
20
  var normalizeBaseUrl = (baseUrl) => {
21
21
  return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
22
22
  };
23
+ var getSafeBaseUrlForError = (baseUrl) => {
24
+ const url = new URL(baseUrl);
25
+ url.username = "";
26
+ url.password = "";
27
+ url.search = "";
28
+ url.hash = "";
29
+ const safeBaseUrl = url.toString();
30
+ return safeBaseUrl.endsWith("/") ? safeBaseUrl : `${safeBaseUrl}/`;
31
+ };
32
+ var getSafeNetworkCause = ({ error, token }) => {
33
+ if (!(error instanceof Error)) {
34
+ return void 0;
35
+ }
36
+ const causeCode = typeof error.cause === "object" && error.cause !== null && "code" in error.cause && typeof error.cause.code === "string" && /^[A-Z0-9_]+$/.test(error.cause.code) ? error.cause.code : void 0;
37
+ const errorMessage = error.message.trim();
38
+ const combinedCause = [causeCode, errorMessage].filter(Boolean).join(": ");
39
+ if (combinedCause.length === 0 || combinedCause.length > 200 || /authorization|bearer|password|secret|token/iu.test(combinedCause) || token && combinedCause.includes(token)) {
40
+ return void 0;
41
+ }
42
+ return combinedCause;
43
+ };
23
44
  var getErrorMessage = (payload, status) => {
24
45
  return payload?.error?.message ?? `API request failed with status ${status}`;
25
46
  };
@@ -51,6 +72,7 @@ var createApiClient = ({
51
72
  organizationId
52
73
  }) => {
53
74
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
75
+ const safeBaseUrlForError = getSafeBaseUrlForError(normalizedBaseUrl);
54
76
  const createHeaders = ({ includeJsonContentType }) => {
55
77
  const headers = new Headers();
56
78
  if (token) {
@@ -64,11 +86,21 @@ var createApiClient = ({
64
86
  }
65
87
  return headers;
66
88
  };
89
+ const fetchApi = async (url, init) => {
90
+ try {
91
+ return await fetch(url, init);
92
+ } catch (error) {
93
+ const safeCause = getSafeNetworkCause({ error, token });
94
+ throw new ApiClientError(
95
+ `Could not reach ChalkSurf API at ${safeBaseUrlForError}.${safeCause ? ` Cause: ${safeCause}` : ""}`
96
+ );
97
+ }
98
+ };
67
99
  const query = async (procedureName, input) => {
68
100
  const url = new URL(`trpc/${procedureName}`, normalizedBaseUrl);
69
101
  url.searchParams.set("batch", "1");
70
102
  url.searchParams.set("input", JSON.stringify({ 0: input ?? null }));
71
- const response = await fetch(url.toString(), {
103
+ const response = await fetchApi(url.toString(), {
72
104
  method: "GET",
73
105
  headers: createHeaders({ includeJsonContentType: false })
74
106
  });
@@ -76,7 +108,7 @@ var createApiClient = ({
76
108
  };
77
109
  const mutation = async (procedureName, input) => {
78
110
  const isFormDataInput = input instanceof FormData;
79
- const response = await fetch(new URL(`trpc/${procedureName}`, normalizedBaseUrl).toString(), {
111
+ const response = await fetchApi(new URL(`trpc/${procedureName}`, normalizedBaseUrl).toString(), {
80
112
  method: "POST",
81
113
  headers: createHeaders({ includeJsonContentType: !isFormDataInput }),
82
114
  body: isFormDataInput ? input : JSON.stringify(input ?? {})
@@ -119,6 +151,15 @@ var createApiClient = ({
119
151
  agentGetExerciseUsage: async (id) => {
120
152
  return await query("agentGetExerciseUsage", { id });
121
153
  },
154
+ agentListQualityIssues: async (input) => {
155
+ return await query("agentListQualityIssues", input);
156
+ },
157
+ agentResolveQualityIssue: async (input) => {
158
+ return await mutation("agentResolveQualityIssue", input);
159
+ },
160
+ agentDismissQualityIssue: async (input) => {
161
+ return await mutation("agentDismissQualityIssue", input);
162
+ },
122
163
  agentListExerciseLabels: async () => {
123
164
  return await query("agentListExerciseLabels", {});
124
165
  },
@@ -140,9 +181,6 @@ var createApiClient = ({
140
181
  agentUploadExerciseFigure: async (input) => {
141
182
  return await mutation("agentUploadExerciseFigure", input);
142
183
  },
143
- agentAttachExerciseFigures: async (input) => {
144
- return await mutation("agentAttachExerciseFigures", input);
145
- },
146
184
  agentUpdateExercise: async (input) => {
147
185
  return await mutation("agentUpdateExercise", input);
148
186
  },
@@ -182,8 +220,14 @@ var createApiClient = ({
182
220
  agentBulkUpdateSheets: async (input) => {
183
221
  return await mutation("agentBulkUpdateSheets", input);
184
222
  },
185
- agentListSheetSeries: async () => {
186
- return await query("agentListSheetSeries", {});
223
+ agentVerifyBulkSheetUpdates: async (input) => {
224
+ return await mutation("agentVerifyBulkSheetUpdates", input);
225
+ },
226
+ agentListSheets: async (input) => {
227
+ return await query("agentListSheets", input);
228
+ },
229
+ agentListSheetSeries: async (input = {}) => {
230
+ return await query("agentListSheetSeries", input);
187
231
  },
188
232
  agentSetSheetVisibility: async (input) => {
189
233
  return await mutation("agentSetSheetVisibility", input);
@@ -855,7 +899,9 @@ var Constants = {
855
899
  "generate_sheet_translation_completed",
856
900
  "upload_exercise_figure",
857
901
  "attach_exercise_figures",
858
- "bulk_update_sheets"
902
+ "bulk_update_sheets",
903
+ "resolve_quality_issue",
904
+ "dismiss_quality_issue"
859
905
  ],
860
906
  agent_audit_outcome: ["started", "success", "failed", "denied"],
861
907
  agent_audit_resource_type: ["exercise", "sheet", "folder", "job", "event", "figure_asset"],
@@ -896,7 +942,6 @@ var Constants = {
896
942
  "figure_update",
897
943
  "automatic_labeling"
898
944
  ],
899
- figure_asset_source_kind: ["document_crop", "rendered_figure"],
900
945
  job_type: [
901
946
  "exercise_sheet_import",
902
947
  "exercise_import",
@@ -931,7 +976,6 @@ import { z as z2 } from "zod";
931
976
  var exerciseStatusOptions = Constants.public.Enums.exercise_status;
932
977
  var exerciseQualityIssueSeverities = Constants.public.Enums.exercise_quality_issue_severity;
933
978
  var exerciseQualityIssueStatuses = Constants.public.Enums.exercise_quality_issue_status;
934
- var figureAssetSourceKinds = Constants.public.Enums.figure_asset_source_kind;
935
979
  var exerciseFigureRowSourceKinds = Constants.public.Enums.exercise_figure_source_kind;
936
980
  var exerciseFigureRenditionPurposes = Constants.public.Enums.exercise_figure_rendition_purpose;
937
981
  var exerciseVersionTypes = Constants.public.Enums.exercise_version_type;
@@ -1377,6 +1421,18 @@ var agentWriteOperationRegistry = {
1377
1421
  resourceType: "exercise",
1378
1422
  sync: true
1379
1423
  },
1424
+ resolve_quality_issue: {
1425
+ agentToolName: "resolve_quality_issue",
1426
+ receiptOperation: "resolve_quality_issue",
1427
+ resourceType: "exercise",
1428
+ sync: true
1429
+ },
1430
+ dismiss_quality_issue: {
1431
+ agentToolName: "dismiss_quality_issue",
1432
+ receiptOperation: "dismiss_quality_issue",
1433
+ resourceType: "exercise",
1434
+ sync: true
1435
+ },
1380
1436
  update_sheet: {
1381
1437
  agentToolName: "update_sheet",
1382
1438
  receiptOperation: "update_sheet",
@@ -1440,12 +1496,6 @@ var agentWriteOperationRegistry = {
1440
1496
  upload_exercise_figure: {
1441
1497
  agentToolName: "upload_exercise_figure",
1442
1498
  receiptOperation: "upload_exercise_figure",
1443
- resourceType: "figure_asset",
1444
- sync: true
1445
- },
1446
- attach_exercise_figures: {
1447
- agentToolName: "attach_exercise_figures",
1448
- receiptOperation: "attach_exercise_figures",
1449
1499
  resourceType: "exercise",
1450
1500
  sync: true
1451
1501
  },
@@ -1596,15 +1646,17 @@ var exerciseTranslationSchema = strictObject({
1596
1646
  status: z3.enum(exerciseStatusOptions)
1597
1647
  });
1598
1648
  var exerciseFigureWidthSchema = z3.number().finite().min(minExerciseFigureWidthInCm).max(maxExerciseFigureWidthInCm);
1599
- var agentLegacyExerciseFigureSchema = strictObject({
1649
+ var agentExerciseFigureInputSchema = strictObject({
1600
1650
  type: z3.enum(exerciseFigureTypes),
1601
1651
  url: z3.url(),
1602
1652
  widthInCm: exerciseFigureWidthSchema,
1603
1653
  exerciseFigureId: uuidSchema.optional(),
1604
- figureAssetId: uuidSchema.optional(),
1654
+ orderIndex: z3.number().int().min(0).optional(),
1655
+ rotationClockwiseDegrees: exerciseFigureRotationSchema.optional(),
1605
1656
  provenance: exerciseFigureProvenanceSchema.optional()
1606
1657
  });
1607
- var agentExerciseFigureSchema = agentLegacyExerciseFigureSchema.extend({
1658
+ var agentExerciseFigureSchema = agentExerciseFigureInputSchema.extend({
1659
+ exerciseFigureId: uuidSchema,
1608
1660
  orderIndex: z3.number().int().min(0),
1609
1661
  rotationClockwiseDegrees: exerciseFigureRotationSchema
1610
1662
  });
@@ -1703,6 +1755,39 @@ var folderSchema = strictObject({
1703
1755
  parentId: uuidSchema.nullable(),
1704
1756
  updatedAt: dateStringSchema
1705
1757
  });
1758
+ var folderPathSegmentSchema = strictObject({
1759
+ id: uuidSchema,
1760
+ name: z3.string().trim().min(1)
1761
+ });
1762
+ var listSheetsAgentInputSchema = strictObject({
1763
+ afterId: uuidSchema.optional(),
1764
+ folderId: uuidSchema.optional(),
1765
+ includeDescendants: z3.boolean().optional().default(false),
1766
+ limit: z3.number().int().min(1).max(100).optional().default(50),
1767
+ offset: z3.number().int().min(0).optional().default(0)
1768
+ }).refine(({ folderId, includeDescendants }) => folderId !== void 0 || !includeDescendants, {
1769
+ message: "folderId is required when includeDescendants is true",
1770
+ path: ["folderId"]
1771
+ }).refine(({ afterId, offset }) => afterId === void 0 || offset === 0, {
1772
+ message: "offset must be zero when afterId is provided",
1773
+ path: ["offset"]
1774
+ });
1775
+ var listSheetsAgentOutputSchema = strictObject({
1776
+ sheets: z3.array(
1777
+ strictObject({
1778
+ id: uuidSchema,
1779
+ name: z3.string().trim().min(1),
1780
+ subject: z3.enum(exerciseSubjects),
1781
+ isPublic: z3.boolean(),
1782
+ folderId: uuidSchema.nullable(),
1783
+ folderPath: z3.array(folderPathSegmentSchema),
1784
+ updatedAt: dateStringSchema,
1785
+ seriesAssignments: sheetSeriesAssignmentsSchema
1786
+ })
1787
+ ),
1788
+ inventoryToken: z3.string().trim().min(1),
1789
+ totalCount: z3.number().int().min(0)
1790
+ });
1706
1791
  var exerciseLabelSchema = strictObject({
1707
1792
  id: z3.string().trim().min(1),
1708
1793
  subject: z3.enum(exerciseSubjects),
@@ -1953,9 +2038,44 @@ var getExerciseUsageAgentInputSchema = strictObject({
1953
2038
  var getExerciseUsageAgentOutputSchema = strictObject({
1954
2039
  usage: exerciseUsageSchema
1955
2040
  });
2041
+ var qualityIssueSelectorShape = {
2042
+ exerciseIds: z3.array(uuidSchema).min(1).max(100).optional(),
2043
+ issueIds: z3.array(uuidSchema).min(1).max(100).optional(),
2044
+ severities: z3.array(z3.enum(exerciseQualityIssueSeverities)).min(1).optional(),
2045
+ limit: z3.number().int().min(1).max(100).optional().default(50),
2046
+ cursor: z3.string().trim().min(1).optional()
2047
+ };
2048
+ var listQualityIssuesAgentInputSchema = strictObject(qualityIssueSelectorShape).refine(
2049
+ ({ exerciseIds, issueIds }) => exerciseIds === void 0 || issueIds === void 0,
2050
+ { message: "exerciseIds and issueIds are mutually exclusive" }
2051
+ );
2052
+ var qualityIssueListItemSchema = strictObject({
2053
+ id: uuidSchema,
2054
+ severity: z3.enum(exerciseQualityIssueSeverities),
2055
+ comment: z3.string(),
2056
+ sourceLanguage: z3.enum(translationLanguages),
2057
+ sourceInputHash: z3.string(),
2058
+ createdAt: dateStringSchema,
2059
+ exercise: strictObject({
2060
+ id: uuidSchema,
2061
+ updatedAt: dateStringSchema,
2062
+ subject: z3.enum(exerciseSubjects),
2063
+ source: z3.string().nullable(),
2064
+ organizationId: uuidSchema
2065
+ })
2066
+ });
2067
+ var listQualityIssuesAgentOutputSchema = strictObject({
2068
+ issues: z3.array(qualityIssueListItemSchema),
2069
+ nextCursor: z3.string().nullable()
2070
+ });
2071
+ var resolveQualityIssueAgentInputSchema = strictObject({
2072
+ issueId: uuidSchema,
2073
+ expectedExerciseUpdatedAt: expectedUpdatedAtSchema
2074
+ });
2075
+ var dismissQualityIssueAgentInputSchema = resolveQualityIssueAgentInputSchema;
1956
2076
  var agentWriteReceiptSchema = strictObject({
1957
2077
  resource: strictObject({
1958
- type: z3.enum(["exercise", "sheet", "folder", "figure_asset"]),
2078
+ type: z3.enum(["exercise", "sheet", "folder"]),
1959
2079
  id: uuidSchema
1960
2080
  }),
1961
2081
  operation: agentWriteReceiptOperationSchema,
@@ -1978,6 +2098,8 @@ var agentWriteReceiptSchema = strictObject({
1978
2098
  var agentWriteReceiptOutputSchema = strictObject({
1979
2099
  receipt: agentWriteReceiptSchema
1980
2100
  });
2101
+ var resolveQualityIssueAgentOutputSchema = agentWriteReceiptOutputSchema;
2102
+ var dismissQualityIssueAgentOutputSchema = agentWriteReceiptOutputSchema;
1981
2103
  var copyExerciseAgentInputSchema = strictObject({
1982
2104
  id: uuidSchema
1983
2105
  });
@@ -2031,14 +2153,6 @@ var exerciseCreateFigureSchema = strictObject({
2031
2153
  widthInCm: z3.number().positive().finite().optional(),
2032
2154
  provenance: exerciseFigureProvenanceSchema.optional()
2033
2155
  });
2034
- var agentExerciseFigureInputSchema = strictObject({
2035
- type: z3.enum(exerciseFigureTypes),
2036
- url: z3.url(),
2037
- widthInCm: exerciseFigureWidthSchema.optional(),
2038
- exerciseFigureId: uuidSchema.optional(),
2039
- figureAssetId: uuidSchema.optional(),
2040
- provenance: exerciseFigureProvenanceSchema.optional()
2041
- });
2042
2156
  var figureReviewWarningSchema = strictObject({
2043
2157
  code: z3.string().trim().min(1),
2044
2158
  message: z3.string().trim().min(1),
@@ -2066,7 +2180,7 @@ var uploadExerciseFigureSourceSchema = z3.discriminatedUnion("kind", [
2066
2180
  })
2067
2181
  ]);
2068
2182
  var uploadExerciseFigureAgentInputSchema = strictObject({
2069
- exerciseId: uuidSchema.optional(),
2183
+ exerciseId: uuidSchema,
2070
2184
  figureType: z3.enum(exerciseFigureTypes),
2071
2185
  source: uploadExerciseFigureSourceSchema,
2072
2186
  widthInCm: exerciseFigureWidthSchema.optional().default(defaultExerciseFigureWidthInCm),
@@ -2083,53 +2197,10 @@ var figureUploadReviewSchema = strictObject({
2083
2197
  nextActions: z3.array(z3.string().trim().min(1))
2084
2198
  });
2085
2199
  var uploadExerciseFigureAgentOutputSchema = strictObject({
2086
- figure: z3.union([agentExerciseFigureSchema, agentLegacyExerciseFigureSchema]),
2200
+ figure: agentExerciseFigureSchema,
2087
2201
  review: figureUploadReviewSchema,
2088
2202
  receipt: agentWriteReceiptSchema
2089
2203
  });
2090
- var attachExerciseFiguresAgentInputSchema = strictObject({
2091
- exerciseId: uuidSchema,
2092
- expectedUpdatedAt: expectedUpdatedAtSchema,
2093
- targetSheetId: uuidSchema.nullable().optional(),
2094
- allowSharedExerciseUpdate: z3.boolean().optional().default(false),
2095
- mode: z3.enum(["append", "replace_type", "replace_all"]),
2096
- figureType: z3.enum(exerciseFigureTypes).optional(),
2097
- figures: z3.array(agentExerciseFigureInputSchema)
2098
- }).superRefine((input, ctx) => {
2099
- if (input.mode === "append" && input.figures.length === 0) {
2100
- ctx.addIssue({
2101
- code: z3.ZodIssueCode.custom,
2102
- message: "append mode requires at least one figure.",
2103
- path: ["figures"]
2104
- });
2105
- }
2106
- if (input.mode === "replace_type" && !input.figureType) {
2107
- ctx.addIssue({
2108
- code: z3.ZodIssueCode.custom,
2109
- message: "replace_type mode requires figureType.",
2110
- path: ["figureType"]
2111
- });
2112
- }
2113
- if (input.mode !== "replace_type" && input.figureType) {
2114
- ctx.addIssue({
2115
- code: z3.ZodIssueCode.custom,
2116
- message: "figureType is only supported for replace_type mode.",
2117
- path: ["figureType"]
2118
- });
2119
- }
2120
- if (input.mode === "replace_type" && input.figureType) {
2121
- input.figures.forEach((figure, index) => {
2122
- if (figure.type !== input.figureType) {
2123
- ctx.addIssue({
2124
- code: z3.ZodIssueCode.custom,
2125
- message: "Replacement figures must match figureType.",
2126
- path: ["figures", index, "type"]
2127
- });
2128
- }
2129
- });
2130
- }
2131
- });
2132
- var attachExerciseFiguresAgentOutputSchema = agentWriteReceiptOutputSchema;
2133
2204
  var exercisePatchMetadataSchema = strictObject({
2134
2205
  sourceUrl: patchNullableStringSchema,
2135
2206
  solutionUrl: patchNullableStringSchema
@@ -2214,6 +2285,34 @@ var bulkUpdateSheetsAgentOutputSchema = strictObject({
2214
2285
  results: z3.array(bulkUpdateSheetResultSchema),
2215
2286
  receipt: agentWriteReceiptSchema.nullable()
2216
2287
  });
2288
+ var verifyBulkSheetUpdateStatusSchema = z3.enum(["applied", "pending", "conflicting", "error"]);
2289
+ var verifyBulkSheetUpdateConflictSchema = strictObject({
2290
+ field: z3.enum(["expectedUpdatedAt", "expectedSeriesAssignments"]),
2291
+ code: z3.enum(["updated_at_changed", "series_assignments_changed"])
2292
+ });
2293
+ var verifyBulkSheetUpdateResultSchema = strictObject({
2294
+ id: uuidSchema,
2295
+ status: verifyBulkSheetUpdateStatusSchema,
2296
+ matchedPaths: z3.array(z3.string()),
2297
+ unmatchedPaths: z3.array(z3.string()),
2298
+ conflicts: z3.array(verifyBulkSheetUpdateConflictSchema),
2299
+ errorCode: z3.string().nullable(),
2300
+ errorMessage: z3.string().nullable()
2301
+ });
2302
+ var verifyBulkSheetUpdatesAgentInputSchema = strictObject({
2303
+ updates: z3.array(updateSheetAgentInputSchema).min(1).max(100)
2304
+ });
2305
+ var verifyBulkSheetUpdatesAgentOutputSchema = strictObject({
2306
+ state: z3.enum(["applied", "pending", "conflicting", "mixed", "error"]),
2307
+ counts: strictObject({
2308
+ total: z3.number().int().min(0),
2309
+ applied: z3.number().int().min(0),
2310
+ pending: z3.number().int().min(0),
2311
+ conflicting: z3.number().int().min(0),
2312
+ error: z3.number().int().min(0)
2313
+ }),
2314
+ results: z3.array(verifyBulkSheetUpdateResultSchema)
2315
+ });
2217
2316
  var createSheetScoredExerciseSchema = strictObject({
2218
2317
  id: uuidSchema,
2219
2318
  scorePoints: z3.number().min(0).max(99999.9).refine((value) => Number.isInteger(value * 10), "Score can have at most 1 decimal place").nullable()
@@ -2363,12 +2462,15 @@ var mcpPublicErrorCodes = [
2363
2462
  "storage_upload_failed",
2364
2463
  "unsupported_image",
2365
2464
  "validation_failed",
2465
+ "organization_selection_required",
2466
+ "organization_access_denied",
2366
2467
  "internal_error"
2367
2468
  ];
2469
+ var mcpPublicErrorCodeSchema = z5.enum(mcpPublicErrorCodes);
2368
2470
  var mcpToolErrorOutputSchema = strictObject2({
2369
2471
  isError: z5.literal(true),
2370
2472
  error: strictObject2({
2371
- code: z5.enum(mcpPublicErrorCodes),
2473
+ code: mcpPublicErrorCodeSchema,
2372
2474
  message: z5.string().trim().min(1),
2373
2475
  agentErrorDetails: agentErrorDetailsSchema.optional()
2374
2476
  })
@@ -2416,7 +2518,7 @@ var userSchema = strictObject2({
2416
2518
  email: z5.string().email().nullable(),
2417
2519
  name: z5.string().nullable()
2418
2520
  });
2419
- var organizationSchema = strictObject2({
2521
+ var mcpOrganizationSchema = strictObject2({
2420
2522
  id: z5.uuid(),
2421
2523
  name: z5.string(),
2422
2524
  mcpPermission: z5.enum(mcpOAuthGrantPermissions).nullable().optional(),
@@ -2549,14 +2651,31 @@ var chalksurfMcpScopes = {
2549
2651
  var getAuthStatusInputSchema = strictObject2({});
2550
2652
  var getAuthStatusOutputSchema = strictObject2({
2551
2653
  authenticated: z5.literal(true),
2654
+ organizations: z5.array(mcpOrganizationSchema),
2552
2655
  selectedOrganizationId: z5.uuid().nullable(),
2553
2656
  user: userSchema
2554
2657
  });
2555
2658
  var listOrganizationsInputSchema = strictObject2({});
2556
2659
  var listOrganizationsOutputSchema = strictObject2({
2557
- organizations: z5.array(organizationSchema),
2660
+ organizations: z5.array(mcpOrganizationSchema),
2558
2661
  selectedOrganizationId: z5.uuid().nullable()
2559
2662
  });
2663
+ var mcpOrganizationRecoveryOutputSchema = strictObject2({
2664
+ isError: z5.literal(true),
2665
+ error: strictObject2({
2666
+ code: z5.enum(["organization_selection_required", "organization_access_denied"]),
2667
+ message: z5.string().trim().min(1)
2668
+ }),
2669
+ organizations: z5.array(
2670
+ strictObject2({
2671
+ id: z5.uuid(),
2672
+ name: z5.string(),
2673
+ permission: z5.enum(mcpOAuthGrantPermissions),
2674
+ role: z5.enum(["owner", "editor", "viewer"]),
2675
+ type: z5.enum(["personal", "team"])
2676
+ })
2677
+ )
2678
+ });
2560
2679
  var searchExercisesInputSchema = strictObject2({
2561
2680
  organizationId: organizationIdSchema,
2562
2681
  age: z5.number().int().nullish(),
@@ -2587,9 +2706,19 @@ var getExerciseUsageInputSchema = getExerciseUsageAgentInputSchema.extend({
2587
2706
  organizationId: organizationIdSchema
2588
2707
  });
2589
2708
  var getExerciseUsageOutputSchema = getExerciseUsageAgentOutputSchema;
2590
- var listExerciseLabelsInputSchema = listExerciseLabelsAgentInputSchema.extend({
2709
+ var listQualityIssuesInputSchema = listQualityIssuesAgentInputSchema.safeExtend({
2710
+ organizationId: organizationIdSchema
2711
+ });
2712
+ var listQualityIssuesOutputSchema = listQualityIssuesAgentOutputSchema;
2713
+ var resolveQualityIssueInputSchema = resolveQualityIssueAgentInputSchema.extend({
2714
+ organizationId: organizationIdSchema
2715
+ });
2716
+ var resolveQualityIssueOutputSchema = resolveQualityIssueAgentOutputSchema;
2717
+ var dismissQualityIssueInputSchema = dismissQualityIssueAgentInputSchema.extend({
2591
2718
  organizationId: organizationIdSchema
2592
2719
  });
2720
+ var dismissQualityIssueOutputSchema = dismissQualityIssueAgentOutputSchema;
2721
+ var listExerciseLabelsInputSchema = listExerciseLabelsAgentInputSchema;
2593
2722
  var listExerciseLabelsOutputSchema = listExerciseLabelsAgentOutputSchema;
2594
2723
  var copyExerciseInputSchema = copyExerciseAgentInputSchema.extend({
2595
2724
  organizationId: organizationIdSchema
@@ -2662,13 +2791,15 @@ var searchSheetsOutputSchema = strictObject2({
2662
2791
  ),
2663
2792
  totalCount: z5.number().int().min(0)
2664
2793
  });
2665
- var getSheetInputSchema = getSheetAgentInputSchema.extend({
2794
+ var listSheetsInputSchema = listSheetsAgentInputSchema.extend({
2666
2795
  organizationId: organizationIdSchema
2667
2796
  });
2668
- var getSheetOutputSchema = getSheetAgentOutputSchema;
2669
- var listSheetSeriesInputSchema = listSheetSeriesAgentInputSchema.extend({
2797
+ var listSheetsOutputSchema = listSheetsAgentOutputSchema.omit({ inventoryToken: true });
2798
+ var getSheetInputSchema = getSheetAgentInputSchema.extend({
2670
2799
  organizationId: organizationIdSchema
2671
2800
  });
2801
+ var getSheetOutputSchema = getSheetAgentOutputSchema;
2802
+ var listSheetSeriesInputSchema = listSheetSeriesAgentInputSchema;
2672
2803
  var listSheetSeriesOutputSchema = listSheetSeriesAgentOutputSchema;
2673
2804
  var listSheetVersionsInputSchema = listSheetVersionsAgentInputSchema.extend({
2674
2805
  organizationId: organizationIdSchema
@@ -2702,6 +2833,10 @@ var bulkUpdateSheetsInputSchema = bulkUpdateSheetsAgentInputSchema.extend({
2702
2833
  organizationId: organizationIdSchema
2703
2834
  });
2704
2835
  var bulkUpdateSheetsOutputSchema = bulkUpdateSheetsAgentOutputSchema;
2836
+ var verifyBulkSheetUpdatesInputSchema = verifyBulkSheetUpdatesAgentInputSchema.extend({
2837
+ organizationId: organizationIdSchema
2838
+ });
2839
+ var verifyBulkSheetUpdatesOutputSchema = verifyBulkSheetUpdatesAgentOutputSchema;
2705
2840
  var setSheetVisibilityInputSchema = setSheetVisibilityAgentInputSchema.extend({
2706
2841
  organizationId: organizationIdSchema
2707
2842
  });
@@ -2738,9 +2873,7 @@ var renameFolderInputSchema = renameFolderAgentInputSchema.extend({
2738
2873
  organizationId: organizationIdSchema
2739
2874
  });
2740
2875
  var renameFolderOutputSchema = renameFolderAgentOutputSchema;
2741
- var validateLatexSnippetsInputSchema = validateLatexSnippetsAgentInputSchema.extend({
2742
- organizationId: organizationIdSchema
2743
- });
2876
+ var validateLatexSnippetsInputSchema = validateLatexSnippetsAgentInputSchema;
2744
2877
  var validateLatexSnippetsOutputSchema = validateLatexSnippetsAgentOutputSchema;
2745
2878
  var importSheetInputSchema = strictObject2({
2746
2879
  organizationId: organizationIdSchema,
@@ -3497,6 +3630,7 @@ var uploadExerciseFigureInputSchema = uploadExerciseFigureAgentInputSchema.exten
3497
3630
  });
3498
3631
  var uploadExerciseFigureMcpToolInputSchema = strictObject2({
3499
3632
  organizationId: organizationIdSchema,
3633
+ exerciseId: z5.uuid(),
3500
3634
  figureType: z5.enum(exerciseFigureTypes),
3501
3635
  ...chatGptFigureUploadFileParamInputShape,
3502
3636
  source: strictObject2({
@@ -3513,10 +3647,6 @@ var uploadExerciseFigureMcpToolInputSchema = strictObject2({
3513
3647
  allowNeedsRevision: z5.boolean().optional()
3514
3648
  }).passthrough();
3515
3649
  var uploadExerciseFigureOutputSchema = uploadExerciseFigureAgentOutputSchema;
3516
- var attachExerciseFiguresInputSchema = attachExerciseFiguresAgentInputSchema.extend({
3517
- organizationId: organizationIdSchema
3518
- });
3519
- var attachExerciseFiguresOutputSchema = attachExerciseFiguresAgentOutputSchema;
3520
3650
  var chalksurfMcpToolCapabilities = {
3521
3651
  get_auth_status: {
3522
3652
  name: "get_auth_status",
@@ -3568,6 +3698,16 @@ var chalksurfMcpToolCapabilities = {
3568
3698
  requiredPermission: "read",
3569
3699
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.read] }
3570
3700
  },
3701
+ list_quality_issues: {
3702
+ name: "list_quality_issues",
3703
+ title: "List quality issues",
3704
+ description: "List active quality issues for exercises owned by the selected ChalkSurf organization.",
3705
+ inputSchema: listQualityIssuesInputSchema,
3706
+ outputSchema: listQualityIssuesOutputSchema,
3707
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3708
+ requiredPermission: "read",
3709
+ security: { type: "oauth2", scopes: [chalksurfMcpScopes.read] }
3710
+ },
3571
3711
  list_exercise_labels: {
3572
3712
  name: "list_exercise_labels",
3573
3713
  title: "List exercise labels",
@@ -3618,6 +3758,26 @@ var chalksurfMcpToolCapabilities = {
3618
3758
  requiredPermission: "write",
3619
3759
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3620
3760
  },
3761
+ resolve_quality_issue: {
3762
+ name: "resolve_quality_issue",
3763
+ title: "Resolve quality issue",
3764
+ description: "Mark an active quality issue resolved after checking the exercise evidence timestamp.",
3765
+ inputSchema: resolveQualityIssueInputSchema,
3766
+ outputSchema: resolveQualityIssueOutputSchema,
3767
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3768
+ requiredPermission: "write",
3769
+ security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3770
+ },
3771
+ dismiss_quality_issue: {
3772
+ name: "dismiss_quality_issue",
3773
+ title: "Dismiss quality issue",
3774
+ description: "Dismiss an active quality issue after checking the exercise evidence timestamp.",
3775
+ inputSchema: dismissQualityIssueInputSchema,
3776
+ outputSchema: dismissQualityIssueOutputSchema,
3777
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3778
+ requiredPermission: "write",
3779
+ security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3780
+ },
3621
3781
  set_exercise_visibility: {
3622
3782
  name: "set_exercise_visibility",
3623
3783
  title: "Set exercise visibility",
@@ -3672,6 +3832,16 @@ var chalksurfMcpToolCapabilities = {
3672
3832
  requiredPermission: "read",
3673
3833
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.read] }
3674
3834
  },
3835
+ list_sheets: {
3836
+ name: "list_sheets",
3837
+ title: "List sheets",
3838
+ description: "Use this to list exercise sheets owned by a ChalkSurf organization and inspect their folder paths and series assignments.",
3839
+ inputSchema: listSheetsInputSchema,
3840
+ outputSchema: listSheetsOutputSchema,
3841
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3842
+ requiredPermission: "read",
3843
+ security: { type: "oauth2", scopes: [chalksurfMcpScopes.read] }
3844
+ },
3675
3845
  get_sheet: {
3676
3846
  name: "get_sheet",
3677
3847
  title: "Get sheet",
@@ -3772,6 +3942,16 @@ var chalksurfMcpToolCapabilities = {
3772
3942
  requiredPermission: "write",
3773
3943
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3774
3944
  },
3945
+ verify_bulk_sheet_updates: {
3946
+ name: "verify_bulk_sheet_updates",
3947
+ title: "Verify bulk sheet updates",
3948
+ description: "Use this after an uncertain bulk apply response to classify each requested sheet target as applied, pending, conflicting, or error without writing.",
3949
+ inputSchema: verifyBulkSheetUpdatesInputSchema,
3950
+ outputSchema: verifyBulkSheetUpdatesOutputSchema,
3951
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3952
+ requiredPermission: "write",
3953
+ security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3954
+ },
3775
3955
  set_sheet_visibility: {
3776
3956
  name: "set_sheet_visibility",
3777
3957
  title: "Set sheet visibility",
@@ -3950,7 +4130,7 @@ var chalksurfMcpToolCapabilities = {
3950
4130
  upload_exercise_figure: {
3951
4131
  name: "upload_exercise_figure",
3952
4132
  title: "Upload exercise figure",
3953
- description: "Use this to attach one reviewed image, MCP file, HTTPS image, base64 image, or rendered figure to an exercise by providing exerciseId, figureType, source, and width. Uploading without exerciseId is a temporary compatibility path only.",
4133
+ description: "Use this to attach one reviewed image, MCP file, HTTPS image, base64 image, or rendered figure to an exercise by providing exerciseId, figureType, source, and width.",
3954
4134
  inputSchema: uploadExerciseFigureInputSchema,
3955
4135
  mcpInputSchema: uploadExerciseFigureMcpToolInputSchema,
3956
4136
  outputSchema: uploadExerciseFigureOutputSchema,
@@ -3958,16 +4138,6 @@ var chalksurfMcpToolCapabilities = {
3958
4138
  requiredPermission: "write",
3959
4139
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] },
3960
4140
  fileParams: chatGptFigureUploadFileParamNames
3961
- },
3962
- attach_exercise_figures: {
3963
- name: "attach_exercise_figures",
3964
- title: "Attach exercise figures",
3965
- description: "Deprecated compatibility tool. Use upload_exercise_figure with exerciseId for new figures; use this only to attach already-uploaded compatibility URLs or replace existing figure lists.",
3966
- inputSchema: attachExerciseFiguresInputSchema,
3967
- outputSchema: attachExerciseFiguresOutputSchema,
3968
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
3969
- requiredPermission: "write",
3970
- security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3971
4141
  }
3972
4142
  };
3973
4143
 
@@ -8719,29 +8889,106 @@ ${formatOrganizationSummary(result.organization)}`,
8719
8889
  };
8720
8890
 
8721
8891
  // src/commands/exercise.ts
8722
- import { readFile as readFile3 } from "node:fs/promises";
8723
- import { basename as basename2, extname as extname2, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
8892
+ import { readFile as readFile4 } from "node:fs/promises";
8893
+ import { basename as basename2, extname as extname2, isAbsolute as isAbsolute2, resolve as resolve5 } from "node:path";
8724
8894
  import { z as z13 } from "zod";
8725
8895
 
8726
- // src/lib/agent-patch.ts
8727
- var formatZodIssues = (issues) => {
8728
- return issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ");
8896
+ // src/lib/json-input.ts
8897
+ import { readFile as readFile2, stat } from "node:fs/promises";
8898
+ import { resolve } from "node:path";
8899
+ var isInteractiveStdin2 = (stdin) => {
8900
+ return stdin.isTTY === true;
8729
8901
  };
8730
- var parseJsonObjectFlag = ({ label, value }) => {
8731
- if (value === void 0) {
8732
- throw new CliCommandError(`${label} is required.`, 2);
8902
+ var readTextFromStdin = async (stdin) => {
8903
+ const chunks = [];
8904
+ for await (const chunk of stdin) {
8905
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
8733
8906
  }
8907
+ return Buffer.concat(chunks).toString("utf8");
8908
+ };
8909
+ var parseJsonObjectText = ({
8910
+ invalidJsonMessage,
8911
+ nonObjectMessage,
8912
+ text
8913
+ }) => {
8734
8914
  let parsedValue;
8735
8915
  try {
8736
- parsedValue = JSON.parse(value);
8916
+ parsedValue = JSON.parse(text);
8737
8917
  } catch {
8738
- throw new CliCommandError(`${label} must be valid JSON.`, 2);
8918
+ throw new CliCommandError(invalidJsonMessage, 2);
8739
8919
  }
8740
8920
  if (typeof parsedValue !== "object" || parsedValue === null || Array.isArray(parsedValue)) {
8741
- throw new CliCommandError(`${label} must be a JSON object.`, 2);
8921
+ throw new CliCommandError(nonObjectMessage, 2);
8742
8922
  }
8743
8923
  return parsedValue;
8744
8924
  };
8925
+ var loadJsonObjectInput = async ({
8926
+ cwd,
8927
+ inputJson,
8928
+ inputPath,
8929
+ stdin
8930
+ }) => {
8931
+ if (inputPath !== void 0 && inputJson !== void 0) {
8932
+ throw new CliCommandError("Pass --input or --input-json, not both.", 2);
8933
+ }
8934
+ if (inputPath === void 0 && inputJson === void 0) {
8935
+ throw new CliCommandError("One of --input or --input-json is required.", 2);
8936
+ }
8937
+ if (inputJson !== void 0) {
8938
+ return parseJsonObjectText({
8939
+ invalidJsonMessage: "--input-json must be valid JSON.",
8940
+ nonObjectMessage: "--input-json must be a JSON object.",
8941
+ text: inputJson
8942
+ });
8943
+ }
8944
+ if (inputPath === "-") {
8945
+ if (isInteractiveStdin2(stdin)) {
8946
+ throw new CliCommandError('No JSON was piped on stdin. Pipe JSON into "--input -".', 2);
8947
+ }
8948
+ const inputText = await readTextFromStdin(stdin);
8949
+ if (inputText.trim().length === 0) {
8950
+ throw new CliCommandError('No JSON was piped on stdin. Pipe JSON into "--input -".', 2);
8951
+ }
8952
+ return parseJsonObjectText({
8953
+ invalidJsonMessage: "Standard input must contain valid JSON.",
8954
+ nonObjectMessage: "Standard input must contain a JSON object.",
8955
+ text: inputText
8956
+ });
8957
+ }
8958
+ const resolvedInputPath = resolve(cwd, inputPath);
8959
+ let inputStats;
8960
+ try {
8961
+ inputStats = await stat(resolvedInputPath);
8962
+ } catch (error) {
8963
+ if (error.code === "ENOENT") {
8964
+ throw new CliCommandError(`Input file "${inputPath}" does not exist.`, 2);
8965
+ }
8966
+ throw error;
8967
+ }
8968
+ if (!inputStats.isFile()) {
8969
+ throw new CliCommandError(`Input path "${inputPath}" is not a file.`, 2);
8970
+ }
8971
+ return parseJsonObjectText({
8972
+ invalidJsonMessage: `Input file "${inputPath}" must contain valid JSON.`,
8973
+ nonObjectMessage: `Input file "${inputPath}" must contain a JSON object.`,
8974
+ text: await readFile2(resolvedInputPath, "utf8")
8975
+ });
8976
+ };
8977
+
8978
+ // src/lib/agent-patch.ts
8979
+ var formatZodIssues = (issues) => {
8980
+ return issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ");
8981
+ };
8982
+ var parseJsonObjectFlag = ({ label, value }) => {
8983
+ if (value === void 0) {
8984
+ throw new CliCommandError(`${label} is required.`, 2);
8985
+ }
8986
+ return parseJsonObjectText({
8987
+ invalidJsonMessage: `${label} must be valid JSON.`,
8988
+ nonObjectMessage: `${label} must be a JSON object.`,
8989
+ text: value
8990
+ });
8991
+ };
8745
8992
  var parseUpdateExerciseAgentInput = (input) => {
8746
8993
  const parsedInput = updateExerciseAgentInputSchema.safeParse(input);
8747
8994
  if (!parsedInput.success) {
@@ -8777,10 +9024,46 @@ var parseBulkUpdateSheetsAgentInput = (input) => {
8777
9024
  }
8778
9025
  return parsedInput.data;
8779
9026
  };
9027
+ var parseVerifyBulkSheetUpdatesAgentInput = (input) => {
9028
+ if (typeof input === "object" && input !== null && "schemaVersion" in input) {
9029
+ throw new CliCommandError(
9030
+ "CLI result envelopes are not verifier manifests. Pass the original bulk update manifest or an object with updates.",
9031
+ 2
9032
+ );
9033
+ }
9034
+ if (typeof input === "object" && input !== null && "mode" in input) {
9035
+ const parsedBulkInput = bulkUpdateSheetsAgentInputSchema.safeParse(input);
9036
+ if (!parsedBulkInput.success) {
9037
+ throw new CliCommandError(
9038
+ `Invalid bulk sheet verification input: ${formatZodIssues(parsedBulkInput.error.issues)}`,
9039
+ 2
9040
+ );
9041
+ }
9042
+ return { updates: parsedBulkInput.data.updates };
9043
+ }
9044
+ const parsedInput = verifyBulkSheetUpdatesAgentInputSchema.safeParse(input);
9045
+ if (!parsedInput.success) {
9046
+ throw new CliCommandError(`Invalid bulk sheet verification input: ${formatZodIssues(parsedInput.error.issues)}`, 2);
9047
+ }
9048
+ return parsedInput.data;
9049
+ };
8780
9050
 
8781
9051
  // src/lib/agent-receipt.ts
9052
+ var getBulkSheetCount = (details) => {
9053
+ if (typeof details !== "object" || details === null || !("sheetIds" in details) || !Array.isArray(details.sheetIds)) {
9054
+ return null;
9055
+ }
9056
+ return details.sheetIds.filter((sheetId) => typeof sheetId === "string").length;
9057
+ };
8782
9058
  var formatAgentWriteReceiptOutput = ({ receipt }) => {
8783
- return `${receipt.operation} -> ${receipt.resource.type} ${receipt.resource.id}`;
9059
+ if (receipt.resource) {
9060
+ return `${receipt.operation} -> ${receipt.resource.type} ${receipt.resource.id}`;
9061
+ }
9062
+ const bulkSheetCount = receipt.operation === "bulk_update_sheets" ? getBulkSheetCount(receipt.details) : null;
9063
+ if (bulkSheetCount !== null) {
9064
+ return `${receipt.operation} -> ${bulkSheetCount} sheet${bulkSheetCount === 1 ? "" : "s"}`;
9065
+ }
9066
+ return `${receipt.operation} completed`;
8784
9067
  };
8785
9068
 
8786
9069
  // src/lib/command-options.ts
@@ -8865,8 +9148,8 @@ var toApiOwnership = (ownership) => {
8865
9148
 
8866
9149
  // src/lib/import-files.ts
8867
9150
  import { openAsBlob } from "node:fs";
8868
- import { stat } from "node:fs/promises";
8869
- import { extname, resolve } from "node:path";
9151
+ import { stat as stat2 } from "node:fs/promises";
9152
+ import { extname, resolve as resolve2 } from "node:path";
8870
9153
  var guessMimeType = (fileName) => {
8871
9154
  return mimeTypesByExtension[extname(fileName).toLowerCase()] ?? "application/octet-stream";
8872
9155
  };
@@ -8937,7 +9220,7 @@ var buildFileImportCommandSources = async ({
8937
9220
  continue;
8938
9221
  }
8939
9222
  try {
8940
- const pathStats = await stat(resolve(cwd, rawSource));
9223
+ const pathStats = await stat2(resolve2(cwd, rawSource));
8941
9224
  if (pathStats.isDirectory()) {
8942
9225
  if (relativePath) {
8943
9226
  throw new CliCommandError("--relative-path cannot be used with directory sources.", 2);
@@ -9065,8 +9348,8 @@ var formatCliImportTranslationJobs = (translationJobs) => {
9065
9348
  };
9066
9349
 
9067
9350
  // src/lib/manifest.ts
9068
- import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
9069
- import { resolve as resolve2 } from "node:path";
9351
+ import { readFile as readFile3, stat as stat3 } from "node:fs/promises";
9352
+ import { resolve as resolve3 } from "node:path";
9070
9353
  import { z as z12 } from "zod";
9071
9354
 
9072
9355
  // src/lib/translation-languages.ts
@@ -9230,10 +9513,10 @@ var assertUniqueSourceIds = (sources) => {
9230
9513
  seenSourceIds.add(source.sourceId);
9231
9514
  }
9232
9515
  };
9233
- var isInteractiveStdin2 = (stdin) => {
9516
+ var isInteractiveStdin3 = (stdin) => {
9234
9517
  return stdin.isTTY === true;
9235
9518
  };
9236
- var readTextFromStdin = async (stdin) => {
9519
+ var readTextFromStdin2 = async (stdin) => {
9237
9520
  let text = "";
9238
9521
  for await (const chunk of stdin) {
9239
9522
  text += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
@@ -9400,15 +9683,15 @@ var loadManifest = async ({
9400
9683
  stdin
9401
9684
  }) => {
9402
9685
  if (manifestPath === "-") {
9403
- if (isInteractiveStdin2(stdin)) {
9686
+ if (isInteractiveStdin3(stdin)) {
9404
9687
  throw new CliCommandError('No manifest was piped on stdin. Pipe JSON into "--manifest -".', 2);
9405
9688
  }
9406
- return parseManifest(await readTextFromStdin(stdin));
9689
+ return parseManifest(await readTextFromStdin2(stdin));
9407
9690
  }
9408
- const resolvedManifestPath = resolve2(cwd, manifestPath);
9691
+ const resolvedManifestPath = resolve3(cwd, manifestPath);
9409
9692
  let fileStats;
9410
9693
  try {
9411
- fileStats = await stat2(resolvedManifestPath);
9694
+ fileStats = await stat3(resolvedManifestPath);
9412
9695
  } catch (error) {
9413
9696
  if (error.code === "ENOENT") {
9414
9697
  throw new CliCommandError(`Manifest file "${manifestPath}" does not exist.`, 2);
@@ -9418,7 +9701,7 @@ var loadManifest = async ({
9418
9701
  if (!fileStats.isFile()) {
9419
9702
  throw new CliCommandError(`Manifest path "${manifestPath}" is not a file.`, 2);
9420
9703
  }
9421
- return parseManifest(await readFile2(resolvedManifestPath, "utf8"));
9704
+ return parseManifest(await readFile3(resolvedManifestPath, "utf8"));
9422
9705
  };
9423
9706
  var loadExerciseImportManifest = async ({
9424
9707
  cwd,
@@ -9471,9 +9754,9 @@ var loadSheetImportManifest = async ({
9471
9754
 
9472
9755
  // src/lib/source-resolver.ts
9473
9756
  import { createWriteStream } from "node:fs";
9474
- import { mkdtemp, open, readdir, rm as rm2, stat as stat3 } from "node:fs/promises";
9757
+ import { mkdtemp, open, readdir, rm as rm2, stat as stat4 } from "node:fs/promises";
9475
9758
  import { tmpdir } from "node:os";
9476
- import { basename, isAbsolute, relative, resolve as resolve3 } from "node:path";
9759
+ import { basename, isAbsolute, relative, resolve as resolve4 } from "node:path";
9477
9760
  import { Readable } from "node:stream";
9478
9761
  import { pipeline } from "node:stream/promises";
9479
9762
  import pLimit from "p-limit";
@@ -9498,7 +9781,7 @@ var normalizeRelativePath = (relativePath) => {
9498
9781
  var ensureExistingFile = async (filePath, label) => {
9499
9782
  let fileStats;
9500
9783
  try {
9501
- fileStats = await stat3(filePath);
9784
+ fileStats = await stat4(filePath);
9502
9785
  } catch (error) {
9503
9786
  if (error.code === "ENOENT") {
9504
9787
  throw createSourceResolutionError(`${label} "${filePath}" does not exist.`);
@@ -9512,7 +9795,7 @@ var ensureExistingFile = async (filePath, label) => {
9512
9795
  var ensureExistingDirectory = async (directoryPath, label) => {
9513
9796
  let directoryStats;
9514
9797
  try {
9515
- directoryStats = await stat3(directoryPath);
9798
+ directoryStats = await stat4(directoryPath);
9516
9799
  } catch (error) {
9517
9800
  if (error.code === "ENOENT") {
9518
9801
  throw createSourceResolutionError(`${label} "${directoryPath}" does not exist.`);
@@ -9535,7 +9818,7 @@ var collectFilesRecursively = async (directoryPath) => {
9535
9818
  const sortedEntries = directoryEntries.sort((leftEntry, rightEntry) => leftEntry.name.localeCompare(rightEntry.name));
9536
9819
  const filePaths = [];
9537
9820
  for (const directoryEntry of sortedEntries) {
9538
- const entryPath = resolve3(directoryPath, directoryEntry.name);
9821
+ const entryPath = resolve4(directoryPath, directoryEntry.name);
9539
9822
  if (directoryEntry.isDirectory()) {
9540
9823
  filePaths.push(...await collectFilesRecursively(entryPath));
9541
9824
  continue;
@@ -9596,7 +9879,7 @@ var resolveLocalFileSource = async ({
9596
9879
  cwd,
9597
9880
  sourceInputIndex
9598
9881
  }) => {
9599
- const resolvedPath = resolve3(cwd, source.path);
9882
+ const resolvedPath = resolve4(cwd, source.path);
9600
9883
  await ensureExistingFile(resolvedPath, "Local source");
9601
9884
  const relativePath = normalizeRelativePath(source.relativePath ?? basename(resolvedPath));
9602
9885
  return [
@@ -9616,8 +9899,8 @@ var resolveDirectorySource = async ({
9616
9899
  cwd,
9617
9900
  sourceInputIndex
9618
9901
  }) => {
9619
- const resolvedDirectoryPath = resolve3(cwd, source.path);
9620
- const resolvedRelativeRoot = resolve3(cwd, source.relativeRoot ?? source.path);
9902
+ const resolvedDirectoryPath = resolve4(cwd, source.path);
9903
+ const resolvedRelativeRoot = resolve4(cwd, source.relativeRoot ?? source.path);
9621
9904
  await ensureExistingDirectory(resolvedDirectoryPath, "Directory source");
9622
9905
  await ensureExistingDirectory(resolvedRelativeRoot, "Directory relativeRoot");
9623
9906
  if (!isContainedWithin({ childPath: resolvedDirectoryPath, parentPath: resolvedRelativeRoot })) {
@@ -9656,8 +9939,8 @@ var resolveUrlSource = async ({
9656
9939
  throw createSourceResolutionError(`URL source "${source.url}" must use http or https.`);
9657
9940
  }
9658
9941
  const relativePath = normalizeRelativePath(source.relativePath ?? deriveRelativePathFromUrl(parsedUrl));
9659
- const downloadDirectoryPath = await mkdtemp(resolve3(tmpdir(), "chalksurf-cli-source-"));
9660
- const downloadedFilePath = resolve3(downloadDirectoryPath, basename(relativePath));
9942
+ const downloadDirectoryPath = await mkdtemp(resolve4(tmpdir(), "chalksurf-cli-source-"));
9943
+ const downloadedFilePath = resolve4(downloadDirectoryPath, basename(relativePath));
9661
9944
  try {
9662
9945
  const response = await fetch(source.url);
9663
9946
  if (!response.ok) {
@@ -9979,9 +10262,6 @@ var formatUploadExerciseFigureOutput = ({ figure, receipt, review }) => [
9979
10262
  `figure: ${figure.type} ${figure.url}`,
9980
10263
  `image: ${review.pixelWidth ?? "?"}x${review.pixelHeight ?? "?"} ${review.byteLength} bytes`
9981
10264
  ].join("\n");
9982
- var formatAttachExerciseFiguresOutput = ({ receipt }) => {
9983
- return formatAgentWriteReceiptOutput({ receipt });
9984
- };
9985
10265
  var getExerciseTranslationJobIds = ({ jobs }) => jobs.map((job) => job.jobId);
9986
10266
  var formatExerciseTranslationOutput = ({ jobs }) => {
9987
10267
  if (jobs.length === 0) {
@@ -9998,16 +10278,6 @@ var resolveVisibilityFlag = ({ privateFlag, publicFlag }) => {
9998
10278
  }
9999
10279
  return Boolean(publicFlag);
10000
10280
  };
10001
- var parseJsonFlagValue = ({ label, value }) => {
10002
- if (value === void 0) {
10003
- throw new CliCommandError(`${label} is required.`, 2);
10004
- }
10005
- try {
10006
- return JSON.parse(value);
10007
- } catch {
10008
- throw new CliCommandError(`${label} must be valid JSON.`, 2);
10009
- }
10010
- };
10011
10281
  var resolveExerciseFigureType = ({
10012
10282
  label,
10013
10283
  required,
@@ -10024,13 +10294,6 @@ var resolveExerciseFigureType = ({
10024
10294
  }
10025
10295
  return value;
10026
10296
  };
10027
- var resolveAttachFigureMode = (value) => {
10028
- const mode = value ?? "append";
10029
- if (mode !== "append" && mode !== "replace_type" && mode !== "replace_all") {
10030
- throw new CliCommandError("--mode must be one of: append, replace_type, replace_all.", 2);
10031
- }
10032
- return mode;
10033
- };
10034
10297
  var parseUploadExerciseFigureInput = (input) => {
10035
10298
  const parsedInput = uploadExerciseFigureAgentInputSchema.safeParse(input);
10036
10299
  if (!parsedInput.success) {
@@ -10038,22 +10301,7 @@ var parseUploadExerciseFigureInput = (input) => {
10038
10301
  }
10039
10302
  return parsedInput.data;
10040
10303
  };
10041
- var parseAttachExerciseFiguresInput = (input) => {
10042
- const parsedInput = attachExerciseFiguresAgentInputSchema.safeParse(input);
10043
- if (!parsedInput.success) {
10044
- throw new CliCommandError(`Invalid figure attach input: ${formatZodIssues(parsedInput.error.issues)}`, 2);
10045
- }
10046
- return parsedInput.data;
10047
- };
10048
- var parseFigureJsonList = (value) => {
10049
- const parsedValue = parseJsonFlagValue({ label: "--figure-json", value });
10050
- const figures = Array.isArray(parsedValue) ? parsedValue : [parsedValue];
10051
- if (!figures.every((figure) => typeof figure === "object" && figure !== null && !Array.isArray(figure))) {
10052
- throw new CliCommandError("--figure-json must be a figure object or an array of figure objects.", 2);
10053
- }
10054
- return figures;
10055
- };
10056
- var resolveLocalPath = ({ cwd, path }) => isAbsolute2(path) ? path : resolve4(cwd, path);
10304
+ var resolveLocalPath = ({ cwd, path }) => isAbsolute2(path) ? path : resolve5(cwd, path);
10057
10305
  var getMimeTypeFromFileName = (fileName) => mimeTypesByExtension[extname2(fileName).toLowerCase()] ?? "";
10058
10306
  var buildLocalFigureUploadFormData = async ({
10059
10307
  context,
@@ -10062,7 +10310,7 @@ var buildLocalFigureUploadFormData = async ({
10062
10310
  }) => {
10063
10311
  const resolvedSourcePath = resolveLocalPath({ cwd: context.cwd, path: source });
10064
10312
  const fileName = input.originalFileName ?? basename2(resolvedSourcePath);
10065
- const fileBuffer = await readFile3(resolvedSourcePath);
10313
+ const fileBuffer = await readFile4(resolvedSourcePath);
10066
10314
  const file = new File([new Uint8Array(fileBuffer)], fileName, { type: getMimeTypeFromFileName(fileName) });
10067
10315
  const formData = new FormData();
10068
10316
  formData.set("payload", JSON.stringify(input));
@@ -10442,11 +10690,14 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10442
10690
  }
10443
10691
  ).command(
10444
10692
  "figure <subcommand>",
10445
- "Exercise figure upload and attachment commands",
10693
+ "Exercise figure upload commands",
10446
10694
  (figureYargs) => figureYargs.command(
10447
- "upload [source]",
10448
- "Upload a reviewed figure image to exercise-image storage",
10449
- (uploadYargs) => uploadYargs.positional("source", {
10695
+ "upload <exerciseId> [source]",
10696
+ "Upload and attach a reviewed figure image to an exercise",
10697
+ (uploadYargs) => uploadYargs.positional("exerciseId", {
10698
+ type: "string",
10699
+ describe: "Exercise ID to attach the figure to"
10700
+ }).positional("source", {
10450
10701
  type: "string",
10451
10702
  describe: "Local image file path. Omit when using --input-json for HTTPS/base64/rendered sources."
10452
10703
  }).option("input-json", {
@@ -10467,15 +10718,21 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10467
10718
  default: false,
10468
10719
  describe: "Allow uploading a rendered figure whose quality status is needs_revision"
10469
10720
  }).example(
10470
- "chalksurf exercise figure upload ./triangle.png --figure-type text --json",
10721
+ "chalksurf exercise figure upload 00000000-0000-4000-8000-000000000001 ./triangle.png --figure-type text --json",
10471
10722
  "Upload a local figure image"
10472
10723
  ).example(
10473
- `chalksurf exercise figure upload --input-json '{"figureType":"text","source":{"kind":"https_url","url":"https://example.com/figure.png"}}' --json`,
10724
+ `chalksurf exercise figure upload 00000000-0000-4000-8000-000000000001 --input-json '{"exerciseId":"00000000-0000-4000-8000-000000000001","figureType":"text","source":{"kind":"https_url","url":"https://example.com/figure.png"}}' --json`,
10474
10725
  "Upload a figure from an HTTPS image URL"
10475
10726
  ),
10476
10727
  async (argv) => {
10477
10728
  const source = argv.source;
10729
+ const exerciseId = resolveRequestedUuid({
10730
+ fallbackValue: argv.exerciseId,
10731
+ label: "exerciseId",
10732
+ required: true
10733
+ });
10478
10734
  const input = argv.inputJson === void 0 ? parseUploadExerciseFigureInput({
10735
+ exerciseId,
10479
10736
  figureType: resolveExerciseFigureType({
10480
10737
  label: "--figure-type",
10481
10738
  required: true,
@@ -10485,9 +10742,10 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10485
10742
  ...argv.widthCm === void 0 ? {} : { widthInCm: argv.widthCm },
10486
10743
  ...argv.originalFileName === void 0 ? {} : { originalFileName: argv.originalFileName },
10487
10744
  allowNeedsRevision: argv.allowNeedsRevision ?? false
10488
- }) : parseUploadExerciseFigureInput(
10489
- parseJsonObjectFlag({ label: "--input-json", value: argv.inputJson })
10490
- );
10745
+ }) : parseUploadExerciseFigureInput({
10746
+ ...parseJsonObjectFlag({ label: "--input-json", value: argv.inputJson }),
10747
+ exerciseId
10748
+ });
10491
10749
  if (argv.inputJson === void 0 && !source) {
10492
10750
  throw new CliCommandError("source is required unless --input-json is provided.", 2);
10493
10751
  }
@@ -10527,91 +10785,6 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10527
10785
  }
10528
10786
  );
10529
10787
  }
10530
- ).command(
10531
- "attach [exerciseId]",
10532
- "Attach one or more uploaded figures to an exercise",
10533
- (attachYargs) => attachYargs.positional("exerciseId", {
10534
- type: "string",
10535
- describe: "Exercise ID to update"
10536
- }).option("expected-updated-at", {
10537
- type: "string",
10538
- demandOption: true,
10539
- describe: "Expected current exercise updated_at ISO timestamp"
10540
- }).option("figure-json", {
10541
- type: "string",
10542
- demandOption: true,
10543
- describe: "Figure object or array of figure objects, usually from exercise figure upload output"
10544
- }).option("mode", {
10545
- type: "string",
10546
- default: "append",
10547
- choices: ["append", "replace_type", "replace_all"],
10548
- describe: "How to merge figures into the exercise figure collection"
10549
- }).option("figure-type", {
10550
- type: "string",
10551
- choices: [...exerciseFigureTypes],
10552
- describe: "Required for --mode replace_type"
10553
- }).option("target-sheet-id", {
10554
- type: "string",
10555
- describe: "Sheet ID whose variant workflow this update targets"
10556
- }).option("allow-shared-exercise-update", {
10557
- type: "boolean",
10558
- default: false,
10559
- describe: "Acknowledge updating an exercise used outside --target-sheet-id"
10560
- }).example(
10561
- `chalksurf exercise figure attach 00000000-0000-4000-8000-000000000001 --expected-updated-at 2026-01-01T00:00:00.000Z --figure-json '{"type":"text","url":"https://...","widthInCm":6}' --json`,
10562
- "Append one uploaded figure to an exercise"
10563
- ),
10564
- async (argv) => {
10565
- const exerciseId = resolveRequestedUuid({
10566
- fallbackValue: argv.exerciseId,
10567
- label: "exerciseId",
10568
- required: true
10569
- });
10570
- const targetSheetId = resolveRequestedUuid({
10571
- flagValue: argv.targetSheetId,
10572
- label: "--target-sheet-id"
10573
- });
10574
- const mode = resolveAttachFigureMode(argv.mode);
10575
- const figureType = resolveExerciseFigureType({
10576
- label: "--figure-type",
10577
- required: mode === "replace_type",
10578
- value: argv.figureType
10579
- });
10580
- const input = parseAttachExerciseFiguresInput({
10581
- exerciseId,
10582
- expectedUpdatedAt: argv.expectedUpdatedAt,
10583
- ...targetSheetId === void 0 ? {} : { targetSheetId },
10584
- allowSharedExerciseUpdate: argv.allowSharedExerciseUpdate ?? false,
10585
- mode,
10586
- ...figureType === void 0 ? {} : { figureType },
10587
- figures: parseFigureJsonList(argv.figureJson)
10588
- });
10589
- const { apiClient, organizationId } = await createResolvedApiClient({
10590
- context,
10591
- baseUrlFlagValue: argv.baseUrl,
10592
- organizationFlagValue: argv.organization,
10593
- profileName: argv.profile
10594
- });
10595
- let result;
10596
- try {
10597
- result = await apiClient.agentAttachExerciseFigures(input);
10598
- } catch (error) {
10599
- throw mapApiErrorToCliError(error);
10600
- }
10601
- context.output.print(
10602
- {
10603
- request: {
10604
- ...input,
10605
- organizationId: organizationId ?? null
10606
- },
10607
- receipt: result.receipt
10608
- },
10609
- (output) => formatAttachExerciseFiguresOutput({ receipt: output.receipt }),
10610
- {
10611
- command: "exercise figure attach"
10612
- }
10613
- );
10614
- }
10615
10788
  ).demandCommand(1).strict(),
10616
10789
  () => {
10617
10790
  }
@@ -11511,7 +11684,7 @@ var registerFeedbackCommands = (feedbackYargs, context) => {
11511
11684
 
11512
11685
  // src/commands/figure.ts
11513
11686
  import { writeFile as writeFile2 } from "node:fs/promises";
11514
- import { isAbsolute as isAbsolute3, resolve as resolve5 } from "node:path";
11687
+ import { isAbsolute as isAbsolute3, resolve as resolve6 } from "node:path";
11515
11688
  var parseFigureRenderDraftInput = (value) => {
11516
11689
  const parsedInput = figureRenderDraftInputSchema.safeParse(parseJsonObjectFlag({ label: "--input-json", value }));
11517
11690
  if (!parsedInput.success) {
@@ -11519,7 +11692,7 @@ var parseFigureRenderDraftInput = (value) => {
11519
11692
  }
11520
11693
  return parsedInput.data;
11521
11694
  };
11522
- var resolveOutputPath = ({ cwd, outputPath }) => isAbsolute3(outputPath) ? outputPath : resolve5(cwd, outputPath);
11695
+ var resolveOutputPath = ({ cwd, outputPath }) => isAbsolute3(outputPath) ? outputPath : resolve6(cwd, outputPath);
11523
11696
  var writeFigurePng = async ({
11524
11697
  cwd,
11525
11698
  outputPath,
@@ -12045,11 +12218,923 @@ var registerProfileCommands = (profileYargs, context) => {
12045
12218
  }
12046
12219
  );
12047
12220
  }
12048
- ).demandCommand(1).strict();
12221
+ ).demandCommand(1).strict();
12222
+ };
12223
+
12224
+ // src/commands/quality-issue/index.ts
12225
+ import { z as z15 } from "zod";
12226
+
12227
+ // src/commands/quality-issue/artifact-integrity.ts
12228
+ import { createHash } from "node:crypto";
12229
+ import { readFile as readFile5 } from "node:fs/promises";
12230
+ import { join as join2 } from "node:path";
12231
+
12232
+ // src/commands/quality-issue/artifact-schemas.ts
12233
+ import { z as z14 } from "zod";
12234
+ var schemaVersion = z14.literal("v1");
12235
+ var uuid = z14.uuid();
12236
+ var commonShape = {
12237
+ schemaVersion,
12238
+ runId: z14.string().trim().min(1),
12239
+ organizationId: uuid
12240
+ };
12241
+ var qualityIssueScopeSchema = z14.object({
12242
+ ...commonShape,
12243
+ request: z14.string().trim().min(1),
12244
+ environment: z14.string().trim().min(1),
12245
+ profile: z14.string().trim().min(1),
12246
+ mode: z14.enum(["dry_run", "apply"]),
12247
+ discovery: z14.object({}).catchall(z14.unknown())
12248
+ }).strict();
12249
+ var qualityIssueInventoryRowSchema = z14.object({
12250
+ ...commonShape,
12251
+ issueId: uuid,
12252
+ exerciseId: uuid,
12253
+ exerciseUpdatedAt: z14.string().trim().min(1),
12254
+ severity: z14.enum(["warning", "error", "critical"]),
12255
+ subject: z14.enum(["math", "physics", "chemistry"]),
12256
+ evidence: z14.object({}).catchall(z14.unknown())
12257
+ }).strict();
12258
+ var qualityIssueDiscoveryRowSchema = z14.object({
12259
+ ...commonShape,
12260
+ tool: z14.string().trim().min(1),
12261
+ evidence: z14.object({}).catchall(z14.unknown())
12262
+ }).strict();
12263
+ var qualityIssueCandidateExerciseRowSchema = z14.object({
12264
+ ...commonShape,
12265
+ exerciseId: uuid,
12266
+ evidence: z14.object({}).catchall(z14.unknown())
12267
+ }).strict();
12268
+ var patchSchema = z14.object({
12269
+ path: z14.string().trim().min(1),
12270
+ before: z14.unknown(),
12271
+ after: z14.unknown()
12272
+ }).strict().refine((patch) => Object.hasOwn(patch, "before") && Object.hasOwn(patch, "after"), {
12273
+ message: "Patch before and after values are required; use null for an intentionally empty value."
12274
+ });
12275
+ var classificationBaseShape = {
12276
+ ...commonShape,
12277
+ issueId: uuid,
12278
+ reasonCode: z14.string().trim().min(1),
12279
+ rationale: z14.string().trim().min(1)
12280
+ };
12281
+ var qualityIssueClassificationRowSchema = z14.discriminatedUnion("disposition", [
12282
+ z14.object({
12283
+ ...classificationBaseShape,
12284
+ disposition: z14.literal("proposed_fix"),
12285
+ patches: z14.array(patchSchema).min(1)
12286
+ }).strict(),
12287
+ z14.object({ ...classificationBaseShape, disposition: z14.literal("proposed_dismissal") }).strict(),
12288
+ z14.object({ ...classificationBaseShape, disposition: z14.literal("needs_human") }).strict(),
12289
+ z14.object({ ...classificationBaseShape, disposition: z14.literal("stale") }).strict(),
12290
+ z14.object({ ...classificationBaseShape, disposition: z14.literal("failed") }).strict(),
12291
+ z14.object({ ...classificationBaseShape, disposition: z14.literal("skipped") }).strict()
12292
+ ]);
12293
+ var actionIdentityShape = {
12294
+ actionId: z14.string().trim().min(1),
12295
+ exerciseId: uuid
12296
+ };
12297
+ var updateActionSchema = z14.object({
12298
+ ...actionIdentityShape,
12299
+ type: z14.literal("update_exercise"),
12300
+ expectedExerciseUpdatedAt: z14.string().trim().min(1),
12301
+ issueIds: z14.array(uuid).min(1),
12302
+ patches: z14.array(patchSchema).min(1)
12303
+ }).strict();
12304
+ var resolveActionSchema = z14.object({
12305
+ ...actionIdentityShape,
12306
+ type: z14.literal("resolve_quality_issue"),
12307
+ issueId: uuid,
12308
+ expectedExerciseUpdatedAtFromActionId: z14.string().trim().min(1)
12309
+ }).strict();
12310
+ var dismissActionSchema = z14.object({
12311
+ ...actionIdentityShape,
12312
+ type: z14.literal("dismiss_quality_issue"),
12313
+ issueId: uuid,
12314
+ expectedExerciseUpdatedAt: z14.string().trim().min(1)
12315
+ }).strict();
12316
+ var qualityIssueActionPlanSchema = z14.object({
12317
+ ...commonShape,
12318
+ inventory: z14.array(
12319
+ z14.object({
12320
+ issueId: uuid,
12321
+ exerciseId: uuid,
12322
+ exerciseUpdatedAt: z14.string().trim().min(1)
12323
+ }).strict()
12324
+ ),
12325
+ actions: z14.array(z14.discriminatedUnion("type", [updateActionSchema, resolveActionSchema, dismissActionSchema])),
12326
+ exclusions: z14.array(
12327
+ z14.object({
12328
+ issueId: uuid,
12329
+ disposition: z14.enum(["needs_human", "stale", "failed", "skipped"]),
12330
+ reasonCode: z14.string().trim().min(1)
12331
+ }).strict()
12332
+ )
12333
+ }).strict();
12334
+ var resultBaseShape = {
12335
+ ...commonShape,
12336
+ actionId: z14.string().trim().min(1)
12337
+ };
12338
+ var terminalErrorSchema = z14.object({
12339
+ code: z14.string().trim().min(1),
12340
+ message: z14.string().trim().min(1),
12341
+ recovery: z14.string().trim().min(1)
12342
+ }).strict();
12343
+ var terminalResultSchema = (outcome) => z14.object({
12344
+ ...resultBaseShape,
12345
+ outcome: z14.literal(outcome),
12346
+ error: terminalErrorSchema
12347
+ }).strict();
12348
+ var qualityIssueApplicationResultRowSchema = z14.discriminatedUnion("outcome", [
12349
+ z14.object({
12350
+ ...resultBaseShape,
12351
+ outcome: z14.literal("success"),
12352
+ receipt: agentWriteReceiptSchema,
12353
+ resultingExerciseUpdatedAt: z14.iso.datetime({ offset: true }).optional()
12354
+ }).strict(),
12355
+ terminalResultSchema("failed"),
12356
+ terminalResultSchema("stale"),
12357
+ terminalResultSchema("skipped")
12358
+ ]);
12359
+
12360
+ // src/commands/quality-issue/artifact-integrity.ts
12361
+ var readJson = async (path, schema) => {
12362
+ try {
12363
+ return schema.parse(JSON.parse(await readFile5(path, "utf8")));
12364
+ } catch (error) {
12365
+ throw new CliCommandError(`Artifact validation failed for ${path}: ${String(error)}`, 2, true, {
12366
+ code: "artifact_validation_failed",
12367
+ retryable: false
12368
+ });
12369
+ }
12370
+ };
12371
+ var readJsonLines = async (path, schema, optional = false) => {
12372
+ let contents;
12373
+ try {
12374
+ contents = await readFile5(path, "utf8");
12375
+ } catch (error) {
12376
+ if (optional && typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
12377
+ return [];
12378
+ }
12379
+ throw new CliCommandError(`Artifact validation failed for ${path}: ${String(error)}`, 2, true, {
12380
+ code: "artifact_validation_failed",
12381
+ retryable: false
12382
+ });
12383
+ }
12384
+ return contents.split(/\r?\n/u).filter((line) => line.trim().length > 0).map((line, index) => {
12385
+ try {
12386
+ return schema.parse(JSON.parse(line));
12387
+ } catch (error) {
12388
+ throw new CliCommandError(`Artifact validation failed for ${path}:${index + 1}: ${String(error)}`, 2, true, {
12389
+ code: "artifact_validation_failed",
12390
+ retryable: false
12391
+ });
12392
+ }
12393
+ });
12394
+ };
12395
+ var assertUnique = (values, label) => {
12396
+ const seen = /* @__PURE__ */ new Set();
12397
+ for (const value of values) {
12398
+ if (seen.has(value)) {
12399
+ throw new CliCommandError(`Artifact validation failed: duplicate ${label} ${value}`, 2, true, {
12400
+ code: "artifact_validation_failed",
12401
+ retryable: false
12402
+ });
12403
+ }
12404
+ seen.add(value);
12405
+ }
12406
+ };
12407
+ var canonicalize = (value) => {
12408
+ if (Array.isArray(value)) {
12409
+ return value.map(canonicalize);
12410
+ }
12411
+ if (value !== null && typeof value === "object") {
12412
+ return Object.fromEntries(
12413
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nestedValue]) => [key, canonicalize(nestedValue)])
12414
+ );
12415
+ }
12416
+ return value;
12417
+ };
12418
+ var canonicalJson = (value) => JSON.stringify(canonicalize(value));
12419
+ var assertPartitionIdentity = (scope, rows) => {
12420
+ if (rows.some((row) => row.runId !== scope.runId || row.organizationId !== scope.organizationId)) {
12421
+ throw new CliCommandError("Artifact validation failed: partition identity mismatch", 2, true, {
12422
+ code: "artifact_validation_failed",
12423
+ retryable: false
12424
+ });
12425
+ }
12426
+ };
12427
+ var validateQualityIssueRun = async (runDirectory) => {
12428
+ const scope = await readJson(join2(runDirectory, "scope.json"), qualityIssueScopeSchema);
12429
+ const discovery = await readJsonLines(join2(runDirectory, "discovery.jsonl"), qualityIssueDiscoveryRowSchema);
12430
+ const candidates = await readJsonLines(
12431
+ join2(runDirectory, "candidate-exercises.jsonl"),
12432
+ qualityIssueCandidateExerciseRowSchema
12433
+ );
12434
+ const inventory = await readJsonLines(join2(runDirectory, "inventory.jsonl"), qualityIssueInventoryRowSchema);
12435
+ const classifications = await readJsonLines(
12436
+ join2(runDirectory, "classifications.jsonl"),
12437
+ qualityIssueClassificationRowSchema
12438
+ );
12439
+ const actionPlan = await readJson(join2(runDirectory, "action-plan.json"), qualityIssueActionPlanSchema);
12440
+ const results = await readJsonLines(
12441
+ join2(runDirectory, "application-results.jsonl"),
12442
+ qualityIssueApplicationResultRowSchema,
12443
+ true
12444
+ );
12445
+ assertPartitionIdentity(scope, [
12446
+ ...discovery,
12447
+ ...candidates,
12448
+ ...inventory,
12449
+ ...classifications,
12450
+ actionPlan,
12451
+ ...results
12452
+ ]);
12453
+ assertUnique(
12454
+ candidates.map((row) => row.exerciseId),
12455
+ "candidate exerciseId"
12456
+ );
12457
+ assertUnique(
12458
+ inventory.map((row) => row.issueId),
12459
+ "inventory issueId"
12460
+ );
12461
+ assertUnique(
12462
+ classifications.map((row) => row.issueId),
12463
+ "classification issueId"
12464
+ );
12465
+ assertUnique(
12466
+ actionPlan.actions.map((action) => action.actionId),
12467
+ "actionId"
12468
+ );
12469
+ const inventoryIds = new Set(inventory.map((row) => row.issueId));
12470
+ const classificationIds = new Set(classifications.map((row) => row.issueId));
12471
+ if (inventoryIds.size !== classificationIds.size || [...inventoryIds].some((issueId) => !classificationIds.has(issueId))) {
12472
+ throw new CliCommandError(
12473
+ "Artifact validation failed: every inventory issue needs exactly one classification",
12474
+ 2,
12475
+ true,
12476
+ {
12477
+ code: "artifact_validation_failed",
12478
+ retryable: false
12479
+ }
12480
+ );
12481
+ }
12482
+ const planInventoryIds = new Set(actionPlan.inventory.map((row) => row.issueId));
12483
+ assertUnique(
12484
+ actionPlan.inventory.map((row) => row.issueId),
12485
+ "action-plan inventory issueId"
12486
+ );
12487
+ if (inventoryIds.size !== planInventoryIds.size || [...inventoryIds].some((issueId) => !planInventoryIds.has(issueId))) {
12488
+ throw new CliCommandError(
12489
+ "Artifact validation failed: action-plan inventory does not match inventory.jsonl",
12490
+ 2,
12491
+ true,
12492
+ {
12493
+ code: "artifact_validation_failed",
12494
+ retryable: false
12495
+ }
12496
+ );
12497
+ }
12498
+ const candidateExerciseIds = new Set(candidates.map((row) => row.exerciseId));
12499
+ if (inventory.some((row) => !candidateExerciseIds.has(row.exerciseId))) {
12500
+ throw new CliCommandError("Artifact validation failed: inventory exercise is missing from candidates", 2, true, {
12501
+ code: "artifact_validation_failed",
12502
+ retryable: false
12503
+ });
12504
+ }
12505
+ const inventoryByIssueId = new Map(inventory.map((row) => [row.issueId, row]));
12506
+ const classificationByIssueId = new Map(classifications.map((row) => [row.issueId, row]));
12507
+ const patchKeys = actionPlan.actions.flatMap(
12508
+ (action) => action.type === "update_exercise" ? action.patches.map((patch) => `${action.exerciseId}\0${patch.path}`) : []
12509
+ );
12510
+ assertUnique(patchKeys, "exercise patch path");
12511
+ assertUnique(
12512
+ classifications.flatMap((classification) => {
12513
+ if (classification.disposition !== "proposed_fix") return [];
12514
+ const inventoryRow = inventoryByIssueId.get(classification.issueId);
12515
+ return classification.patches.map((patch) => `${inventoryRow?.exerciseId ?? ""}\0${patch.path}`);
12516
+ }),
12517
+ "classified exercise patch path"
12518
+ );
12519
+ for (const plannedIssue of actionPlan.inventory) {
12520
+ const source = inventoryByIssueId.get(plannedIssue.issueId);
12521
+ if (!source || source.exerciseId !== plannedIssue.exerciseId || source.exerciseUpdatedAt !== plannedIssue.exerciseUpdatedAt) {
12522
+ throw new CliCommandError("Artifact validation failed: action-plan evidence differs from inventory", 2, true, {
12523
+ code: "artifact_validation_failed",
12524
+ retryable: false
12525
+ });
12526
+ }
12527
+ }
12528
+ const updateActionsByExerciseId = /* @__PURE__ */ new Map();
12529
+ const updateActionsByIssueId = /* @__PURE__ */ new Map();
12530
+ const resolveActionsByIssueId = /* @__PURE__ */ new Map();
12531
+ const dismissActionsByIssueId = /* @__PURE__ */ new Map();
12532
+ const actionIndexById = new Map(actionPlan.actions.map((action, index) => [action.actionId, index]));
12533
+ for (const action of actionPlan.actions) {
12534
+ if (action.type === "update_exercise") {
12535
+ assertUnique(action.issueIds, `update action ${action.actionId} issueId`);
12536
+ if (updateActionsByExerciseId.has(action.exerciseId)) {
12537
+ throw new CliCommandError(
12538
+ `Artifact validation failed: exercise ${action.exerciseId} has more than one update action`,
12539
+ 2,
12540
+ true,
12541
+ { code: "artifact_validation_failed", retryable: false }
12542
+ );
12543
+ }
12544
+ updateActionsByExerciseId.set(action.exerciseId, action);
12545
+ const expectedPatches = action.issueIds.flatMap((actionIssueId) => {
12546
+ const source2 = inventoryByIssueId.get(actionIssueId);
12547
+ const classification = classificationByIssueId.get(actionIssueId);
12548
+ if (!source2 || source2.exerciseId !== action.exerciseId || source2.exerciseUpdatedAt !== action.expectedExerciseUpdatedAt || classification?.disposition !== "proposed_fix") {
12549
+ throw new CliCommandError(
12550
+ "Artifact validation failed: update action evidence differs from inventory",
12551
+ 2,
12552
+ true,
12553
+ {
12554
+ code: "artifact_validation_failed",
12555
+ retryable: false
12556
+ }
12557
+ );
12558
+ }
12559
+ updateActionsByIssueId.set(actionIssueId, [...updateActionsByIssueId.get(actionIssueId) ?? [], action]);
12560
+ return classification.patches;
12561
+ });
12562
+ const sortPatches = (patches) => [...patches].sort((left, right) => left.path.localeCompare(right.path));
12563
+ if (canonicalJson(sortPatches(action.patches)) !== canonicalJson(sortPatches(expectedPatches))) {
12564
+ throw new CliCommandError(
12565
+ `Artifact validation failed: update action patches do not match classifications for ${action.exerciseId}`,
12566
+ 2,
12567
+ true,
12568
+ { code: "artifact_validation_failed", retryable: false }
12569
+ );
12570
+ }
12571
+ continue;
12572
+ }
12573
+ const source = inventoryByIssueId.get(action.issueId);
12574
+ if (!source || source.exerciseId !== action.exerciseId) {
12575
+ throw new CliCommandError("Artifact validation failed: triage action evidence differs from inventory", 2, true, {
12576
+ code: "artifact_validation_failed",
12577
+ retryable: false
12578
+ });
12579
+ }
12580
+ if (action.type === "resolve_quality_issue") {
12581
+ resolveActionsByIssueId.set(action.issueId, [...resolveActionsByIssueId.get(action.issueId) ?? [], action]);
12582
+ } else {
12583
+ if (source.exerciseUpdatedAt !== action.expectedExerciseUpdatedAt) {
12584
+ throw new CliCommandError("Artifact validation failed: dismissal evidence differs from inventory", 2, true, {
12585
+ code: "artifact_validation_failed",
12586
+ retryable: false
12587
+ });
12588
+ }
12589
+ dismissActionsByIssueId.set(action.issueId, [...dismissActionsByIssueId.get(action.issueId) ?? [], action]);
12590
+ }
12591
+ }
12592
+ const exclusionsByIssueId = new Map(actionPlan.exclusions.map((exclusion) => [exclusion.issueId, exclusion]));
12593
+ assertUnique(
12594
+ actionPlan.exclusions.map((exclusion) => exclusion.issueId),
12595
+ "exclusion issueId"
12596
+ );
12597
+ for (const classification of classifications) {
12598
+ const source = inventoryByIssueId.get(classification.issueId);
12599
+ const updates = updateActionsByIssueId.get(classification.issueId) ?? [];
12600
+ const resolves = resolveActionsByIssueId.get(classification.issueId) ?? [];
12601
+ const dismissals = dismissActionsByIssueId.get(classification.issueId) ?? [];
12602
+ const exclusion = exclusionsByIssueId.get(classification.issueId);
12603
+ const exerciseUpdate = source ? updateActionsByExerciseId.get(source.exerciseId) : void 0;
12604
+ if (classification.disposition === "proposed_dismissal" && dismissals.length === 1 && exerciseUpdate && (actionIndexById.get(dismissals[0].actionId) ?? Number.POSITIVE_INFINITY) > (actionIndexById.get(exerciseUpdate.actionId) ?? Number.NEGATIVE_INFINITY)) {
12605
+ throw new CliCommandError(
12606
+ `Artifact validation failed: dismissal ${dismissals[0].actionId} must precede update ${exerciseUpdate.actionId}`,
12607
+ 2,
12608
+ true,
12609
+ { code: "artifact_validation_failed", retryable: false }
12610
+ );
12611
+ }
12612
+ const valid = classification.disposition === "proposed_fix" && !exclusion && updates.length === 1 && resolves.length === 1 && dismissals.length === 0 && resolves[0]?.exerciseId === source?.exerciseId && resolves[0]?.expectedExerciseUpdatedAtFromActionId === updates[0]?.actionId && (actionIndexById.get(updates[0]?.actionId ?? "") ?? Number.POSITIVE_INFINITY) < (actionIndexById.get(resolves[0]?.actionId ?? "") ?? Number.NEGATIVE_INFINITY) || classification.disposition === "proposed_dismissal" && !exclusion && updates.length === 0 && resolves.length === 0 && dismissals.length === 1 || ["needs_human", "stale", "failed", "skipped"].includes(classification.disposition) && updates.length === 0 && resolves.length === 0 && dismissals.length === 0 && exclusion?.disposition === classification.disposition && exclusion.reasonCode === classification.reasonCode;
12613
+ if (!valid) {
12614
+ throw new CliCommandError(
12615
+ `Artifact validation failed: action plan does not exactly implement ${classification.disposition} for ${classification.issueId}`,
12616
+ 2,
12617
+ true,
12618
+ { code: "artifact_validation_failed", retryable: false }
12619
+ );
12620
+ }
12621
+ }
12622
+ if ([...exclusionsByIssueId].some(([excludedIssueId]) => !classificationByIssueId.has(excludedIssueId))) {
12623
+ throw new CliCommandError("Artifact validation failed: action plan contains an unexpected exclusion", 2, true, {
12624
+ code: "artifact_validation_failed",
12625
+ retryable: false
12626
+ });
12627
+ }
12628
+ return { scope, discovery, candidates, inventory, classifications, actionPlan, results };
12629
+ };
12630
+ var digestQualityIssueRun = (run) => createHash("sha256").update(
12631
+ JSON.stringify(
12632
+ canonicalize({
12633
+ scope: run.scope,
12634
+ discovery: run.discovery,
12635
+ candidates: run.candidates,
12636
+ inventory: run.inventory,
12637
+ classifications: run.classifications,
12638
+ actionPlan: run.actionPlan
12639
+ })
12640
+ )
12641
+ ).digest("hex");
12642
+ var reconcileQualityIssueRun = ({
12643
+ actionPlan,
12644
+ results
12645
+ }) => {
12646
+ const planned = new Set(actionPlan.actions.map((action) => action.actionId));
12647
+ const counts = /* @__PURE__ */ new Map();
12648
+ for (const result of results) counts.set(result.actionId, (counts.get(result.actionId) ?? 0) + 1);
12649
+ const missingActionIds = [...planned].filter((actionId) => !counts.has(actionId));
12650
+ const duplicateActionIds = [...counts].filter(([, count]) => count > 1).map(([actionId]) => actionId);
12651
+ const unexpectedActionIds = [...counts].filter(([actionId]) => !planned.has(actionId)).map(([actionId]) => actionId);
12652
+ const resultByActionId = new Map(
12653
+ results.filter((result) => counts.get(result.actionId) === 1).map((result) => [result.actionId, result])
12654
+ );
12655
+ const resultIndexByActionId = new Map(results.map((result, index) => [result.actionId, index]));
12656
+ const sameStrings = (left, right) => canonicalJson([...left].sort()) === canonicalJson([...right].sort());
12657
+ const semanticInvalidActionIds = actionPlan.actions.flatMap((action) => {
12658
+ const result = resultByActionId.get(action.actionId);
12659
+ if (!result || result.outcome !== "success") return [];
12660
+ const receipt = result.receipt;
12661
+ if (receipt.resource.type !== "exercise" || receipt.resource.id !== action.exerciseId || receipt.operation !== action.type || receipt.precondition.status !== "matched") {
12662
+ return [action.actionId];
12663
+ }
12664
+ if (action.type === "update_exercise") {
12665
+ const valid2 = result.resultingExerciseUpdatedAt !== void 0 && receipt.precondition.expectedUpdatedAt === action.expectedExerciseUpdatedAt && receipt.precondition.actualUpdatedAt === action.expectedExerciseUpdatedAt && sameStrings(
12666
+ receipt.changedPaths,
12667
+ action.patches.map((patch) => patch.path)
12668
+ );
12669
+ return valid2 ? [] : [action.actionId];
12670
+ }
12671
+ const expectedChangedPaths = [`qualityIssues.${action.issueId}.status`];
12672
+ const details = receipt.details;
12673
+ const expectedStatus = action.type === "resolve_quality_issue" ? "resolved" : "dismissed";
12674
+ if (!sameStrings(receipt.changedPaths, expectedChangedPaths) || typeof details !== "object" || details === null || !("issueId" in details) || details.issueId !== action.issueId || !("status" in details) || details.status !== expectedStatus) {
12675
+ return [action.actionId];
12676
+ }
12677
+ if (action.type === "dismiss_quality_issue") {
12678
+ const valid2 = receipt.precondition.expectedUpdatedAt === action.expectedExerciseUpdatedAt && receipt.precondition.actualUpdatedAt === action.expectedExerciseUpdatedAt;
12679
+ return valid2 ? [] : [action.actionId];
12680
+ }
12681
+ const dependencyResult = resultByActionId.get(action.expectedExerciseUpdatedAtFromActionId);
12682
+ const dependencyIndex = resultIndexByActionId.get(action.expectedExerciseUpdatedAtFromActionId);
12683
+ const resultIndex = resultIndexByActionId.get(action.actionId);
12684
+ const expectedUpdatedAt = dependencyResult?.outcome === "success" ? dependencyResult.resultingExerciseUpdatedAt : void 0;
12685
+ const valid = dependencyResult?.outcome === "success" && expectedUpdatedAt !== void 0 && dependencyIndex !== void 0 && resultIndex !== void 0 && dependencyIndex < resultIndex && receipt.precondition.expectedUpdatedAt === expectedUpdatedAt && receipt.precondition.actualUpdatedAt === expectedUpdatedAt;
12686
+ return valid ? [] : [action.actionId];
12687
+ });
12688
+ const orderInvalidActionIds = missingActionIds.length === 0 && duplicateActionIds.length === 0 && unexpectedActionIds.length === 0 ? actionPlan.actions.flatMap(
12689
+ (action, index) => results[index]?.actionId === action.actionId ? [] : [action.actionId]
12690
+ ) : [];
12691
+ const invalidActionIds = [.../* @__PURE__ */ new Set([...semanticInvalidActionIds, ...orderInvalidActionIds])];
12692
+ return {
12693
+ complete: missingActionIds.length === 0 && duplicateActionIds.length === 0 && unexpectedActionIds.length === 0 && invalidActionIds.length === 0,
12694
+ missingActionIds,
12695
+ duplicateActionIds,
12696
+ unexpectedActionIds,
12697
+ invalidActionIds
12698
+ };
12699
+ };
12700
+ var verifyQualityIssueRun = (run, approvedDigest) => {
12701
+ const actualDigest = digestQualityIssueRun(run);
12702
+ if (actualDigest !== approvedDigest) {
12703
+ throw new CliCommandError("Approved digest does not match the current action plan", 2, true, {
12704
+ code: "approval_digest_mismatch",
12705
+ retryable: false
12706
+ });
12707
+ }
12708
+ return actualDigest;
12709
+ };
12710
+
12711
+ // src/commands/quality-issue/index.ts
12712
+ var parseUuid = (value, label) => {
12713
+ const result = z15.uuid().safeParse(value);
12714
+ if (!result.success) throw new CliCommandError(`${label} must be a UUID.`, 2);
12715
+ return result.data;
12716
+ };
12717
+ var parseUuidArray = (values, label) => {
12718
+ if (values === void 0) return void 0;
12719
+ const result = z15.array(z15.uuid()).min(1).max(100).safeParse(values);
12720
+ if (!result.success) throw new CliCommandError(`${label} must contain 1 to 100 UUIDs.`, 2);
12721
+ return result.data;
12722
+ };
12723
+ var registerQualityIssueCommands = (yargs2, context) => yargs2.command(
12724
+ "list",
12725
+ "List active quality issues for organization-owned exercises",
12726
+ (listYargs) => listYargs.option("exercise-id", { type: "string", array: true, describe: "Restrict to exercise IDs" }).option("issue-id", { type: "string", array: true, describe: "Restrict to quality issue IDs" }).option("severity", {
12727
+ type: "string",
12728
+ array: true,
12729
+ choices: ["warning", "error", "critical"]
12730
+ }).option("limit", { type: "number", default: 50 }).option("cursor", { type: "string" }),
12731
+ async (argv) => {
12732
+ const common = argv;
12733
+ if (argv.exerciseId && argv.issueId) {
12734
+ throw new CliCommandError("--exercise-id and --issue-id are mutually exclusive.", 2);
12735
+ }
12736
+ const input = {
12737
+ exerciseIds: parseUuidArray(argv.exerciseId, "exercise-id"),
12738
+ issueIds: parseUuidArray(argv.issueId, "issue-id"),
12739
+ severities: argv.severity,
12740
+ limit: normalizeLimit(argv.limit),
12741
+ cursor: argv.cursor
12742
+ };
12743
+ const { apiClient, organizationId } = await createResolvedApiClient({
12744
+ context,
12745
+ baseUrlFlagValue: common.baseUrl,
12746
+ organizationFlagValue: common.organization,
12747
+ profileName: common.profile
12748
+ });
12749
+ try {
12750
+ const result = await apiClient.agentListQualityIssues(input);
12751
+ context.output.print(
12752
+ { request: { organizationId, ...input }, ...result },
12753
+ `${result.issues.length} active quality issue(s)${result.nextCursor ? "; more available" : ""}`,
12754
+ { command: "quality-issue list" }
12755
+ );
12756
+ } catch (error) {
12757
+ throw mapApiErrorToCliError(error);
12758
+ }
12759
+ }
12760
+ ).command(
12761
+ "resolve <issueId>",
12762
+ "Resolve one quality issue using the approved exercise timestamp",
12763
+ (commandYargs) => commandYargs.positional("issueId", { type: "string", demandOption: true }).option("expected-exercise-updated-at", { type: "string", demandOption: true }),
12764
+ async (argv) => {
12765
+ const common = argv;
12766
+ const input = {
12767
+ issueId: parseUuid(argv.issueId, "issueId"),
12768
+ expectedExerciseUpdatedAt: argv.expectedExerciseUpdatedAt
12769
+ };
12770
+ const { apiClient } = await createResolvedApiClient({
12771
+ context,
12772
+ baseUrlFlagValue: common.baseUrl,
12773
+ organizationFlagValue: common.organization,
12774
+ profileName: common.profile
12775
+ });
12776
+ try {
12777
+ const result = await apiClient.agentResolveQualityIssue(input);
12778
+ context.output.print(result, formatAgentWriteReceiptOutput, { command: "quality-issue resolve" });
12779
+ } catch (error) {
12780
+ throw mapApiErrorToCliError(error);
12781
+ }
12782
+ }
12783
+ ).command(
12784
+ "dismiss <issueId>",
12785
+ "Dismiss one quality issue using the approved exercise timestamp",
12786
+ (commandYargs) => commandYargs.positional("issueId", { type: "string", demandOption: true }).option("expected-exercise-updated-at", { type: "string", demandOption: true }),
12787
+ async (argv) => {
12788
+ const common = argv;
12789
+ const input = {
12790
+ issueId: parseUuid(argv.issueId, "issueId"),
12791
+ expectedExerciseUpdatedAt: argv.expectedExerciseUpdatedAt
12792
+ };
12793
+ const { apiClient } = await createResolvedApiClient({
12794
+ context,
12795
+ baseUrlFlagValue: common.baseUrl,
12796
+ organizationFlagValue: common.organization,
12797
+ profileName: common.profile
12798
+ });
12799
+ try {
12800
+ const result = await apiClient.agentDismissQualityIssue(input);
12801
+ context.output.print(result, formatAgentWriteReceiptOutput, { command: "quality-issue dismiss" });
12802
+ } catch (error) {
12803
+ throw mapApiErrorToCliError(error);
12804
+ }
12805
+ }
12806
+ ).command(
12807
+ "artifact <subcommand>",
12808
+ "Validate and reconcile local quality-issue run artifacts",
12809
+ (artifactYargs) => artifactYargs.command(
12810
+ "validate",
12811
+ "Validate a quality-issue run partition",
12812
+ (commandYargs) => commandYargs.option("run-dir", { type: "string", demandOption: true }),
12813
+ async (argv) => {
12814
+ const run = await validateQualityIssueRun(argv.runDir);
12815
+ context.output.print(
12816
+ {
12817
+ runId: run.scope.runId,
12818
+ organizationId: run.scope.organizationId,
12819
+ inventoryCount: run.inventory.length,
12820
+ classificationCount: run.classifications.length,
12821
+ actionCount: run.actionPlan.actions.length,
12822
+ resultCount: run.results.length
12823
+ },
12824
+ "Quality-issue run artifacts are valid.",
12825
+ { command: "quality-issue artifact validate" }
12826
+ );
12827
+ }
12828
+ ).command(
12829
+ "digest",
12830
+ "Compute the canonical action-plan digest",
12831
+ (commandYargs) => commandYargs.option("run-dir", { type: "string", demandOption: true }),
12832
+ async (argv) => {
12833
+ const run = await validateQualityIssueRun(argv.runDir);
12834
+ const digest = digestQualityIssueRun(run);
12835
+ context.output.print({ digest }, digest, { command: "quality-issue artifact digest" });
12836
+ }
12837
+ ).command(
12838
+ "verify",
12839
+ "Verify an approved action-plan digest before writes",
12840
+ (commandYargs) => commandYargs.option("run-dir", { type: "string", demandOption: true }).option("approved-digest", { type: "string", demandOption: true }),
12841
+ async (argv) => {
12842
+ const run = await validateQualityIssueRun(argv.runDir);
12843
+ const digest = verifyQualityIssueRun(run, argv.approvedDigest);
12844
+ context.output.print({ digest, verified: true }, "Approved digest verified.", {
12845
+ command: "quality-issue artifact verify"
12846
+ });
12847
+ }
12848
+ ).command(
12849
+ "reconcile",
12850
+ "Reconcile planned actions with terminal result rows",
12851
+ (commandYargs) => commandYargs.option("run-dir", { type: "string", demandOption: true }),
12852
+ async (argv) => {
12853
+ const run = await validateQualityIssueRun(argv.runDir);
12854
+ const reconciliation = reconcileQualityIssueRun(run);
12855
+ context.output.print(
12856
+ reconciliation,
12857
+ reconciliation.complete ? "Reconciliation complete." : "Reconciliation incomplete.",
12858
+ {
12859
+ command: "quality-issue artifact reconcile",
12860
+ ok: reconciliation.complete
12861
+ }
12862
+ );
12863
+ if (!reconciliation.complete) throwSilentExitCode(2);
12864
+ }
12865
+ ).demandCommand(1),
12866
+ () => {
12867
+ }
12868
+ ).demandCommand(1);
12869
+
12870
+ // src/commands/sheet.ts
12871
+ import { z as z16 } from "zod";
12872
+
12873
+ // src/lib/result-artifact.ts
12874
+ import { randomUUID } from "node:crypto";
12875
+ import { link, open as open2, unlink } from "node:fs/promises";
12876
+ import { basename as basename3, dirname as dirname2, join as join3, resolve as resolve7 } from "node:path";
12877
+
12878
+ // src/lib/output.ts
12879
+ var cliJsonSchemaVersion = "v1";
12880
+ var ensureTrailingNewline = (value) => {
12881
+ return value.endsWith("\n") ? value : `${value}
12882
+ `;
12883
+ };
12884
+ var writeLine = (writer, value) => {
12885
+ writer.write(ensureTrailingNewline(value));
12886
+ };
12887
+ var serializeCliOutputEnvelope = (envelope) => JSON.stringify(envelope, null, 2);
12888
+ var createCliOutputEnvelope = ({
12889
+ command,
12890
+ error,
12891
+ ok,
12892
+ result,
12893
+ warnings
12894
+ }) => ({
12895
+ schemaVersion: cliJsonSchemaVersion,
12896
+ command,
12897
+ ok,
12898
+ ...result === void 0 ? {} : { result },
12899
+ ...error ? { error } : {},
12900
+ warnings
12901
+ });
12902
+ var createOutput = ({
12903
+ json,
12904
+ stdout,
12905
+ stderr
12906
+ }) => {
12907
+ let warnings = [];
12908
+ const consumeWarnings = () => {
12909
+ const nextWarnings = warnings;
12910
+ warnings = [];
12911
+ return nextWarnings;
12912
+ };
12913
+ return {
12914
+ json,
12915
+ print: (value, humanFormatter, options) => {
12916
+ if (json) {
12917
+ writeLine(
12918
+ stdout,
12919
+ serializeCliOutputEnvelope(
12920
+ createCliOutputEnvelope({
12921
+ command: options.command,
12922
+ error: options.error,
12923
+ ok: options.ok ?? true,
12924
+ result: value,
12925
+ warnings: consumeWarnings()
12926
+ })
12927
+ )
12928
+ );
12929
+ return;
12930
+ }
12931
+ const message = typeof humanFormatter === "function" ? humanFormatter(value) : humanFormatter;
12932
+ writeLine(stdout, message);
12933
+ },
12934
+ printWithResultArtifact: async (value, compactValue, humanFormatter, options) => {
12935
+ const currentWarnings = [...warnings];
12936
+ await options.publishArtifact(
12937
+ createCliOutputEnvelope({
12938
+ command: options.command,
12939
+ error: options.error,
12940
+ ok: options.ok ?? true,
12941
+ result: value,
12942
+ warnings: currentWarnings
12943
+ })
12944
+ );
12945
+ warnings = warnings.slice(currentWarnings.length);
12946
+ if (json) {
12947
+ writeLine(
12948
+ stdout,
12949
+ serializeCliOutputEnvelope(
12950
+ createCliOutputEnvelope({
12951
+ command: options.command,
12952
+ error: options.error,
12953
+ ok: options.ok ?? true,
12954
+ result: compactValue,
12955
+ warnings: currentWarnings
12956
+ })
12957
+ )
12958
+ );
12959
+ return;
12960
+ }
12961
+ const message = typeof humanFormatter === "function" ? humanFormatter(compactValue) : humanFormatter;
12962
+ writeLine(stdout, message);
12963
+ },
12964
+ printError: ({ command, error, result }) => {
12965
+ if (json) {
12966
+ writeLine(
12967
+ stdout,
12968
+ serializeCliOutputEnvelope(
12969
+ createCliOutputEnvelope({
12970
+ command,
12971
+ ok: false,
12972
+ result,
12973
+ error,
12974
+ warnings: consumeWarnings()
12975
+ })
12976
+ )
12977
+ );
12978
+ return;
12979
+ }
12980
+ writeLine(stderr, formatSerializableCliErrorForHuman(error));
12981
+ },
12982
+ info: (message) => {
12983
+ if (json) {
12984
+ warnings.push(message);
12985
+ return;
12986
+ }
12987
+ writeLine(stderr, message);
12988
+ },
12989
+ error: (message) => {
12990
+ writeLine(stderr, message);
12991
+ }
12992
+ };
12993
+ };
12994
+
12995
+ // src/lib/result-artifact.ts
12996
+ var resolveCliResultFilePath = ({ cwd, inputPath }) => resolve7(cwd, inputPath);
12997
+ var getFilesystemErrorCode = (error) => error.code;
12998
+ var publishCliResultArtifact = async ({
12999
+ destinationPath,
13000
+ envelope
13001
+ }) => {
13002
+ const temporaryPath = join3(dirname2(destinationPath), `.${basename3(destinationPath)}.${randomUUID()}.tmp`);
13003
+ let temporaryFile;
13004
+ try {
13005
+ temporaryFile = await open2(temporaryPath, "wx", 384);
13006
+ await temporaryFile.writeFile(`${serializeCliOutputEnvelope(envelope)}
13007
+ `, "utf8");
13008
+ await temporaryFile.sync();
13009
+ await temporaryFile.close();
13010
+ temporaryFile = void 0;
13011
+ await link(temporaryPath, destinationPath);
13012
+ } catch (error) {
13013
+ if (getFilesystemErrorCode(error) === "EEXIST") {
13014
+ throw new CliCommandError(`Result file "${destinationPath}" already exists; choose another path.`, 2);
13015
+ }
13016
+ const message = error instanceof Error ? error.message : "Unknown filesystem error";
13017
+ throw new CliCommandError(`Could not write result file "${destinationPath}": ${message}`, 1);
13018
+ } finally {
13019
+ await temporaryFile?.close().catch(() => {
13020
+ });
13021
+ await unlink(temporaryPath).catch(() => {
13022
+ });
13023
+ }
13024
+ };
13025
+
13026
+ // src/lib/sheet-list-all.ts
13027
+ var pageSize = 100;
13028
+ var collectAllSheets = async ({
13029
+ fetchPage
13030
+ }) => {
13031
+ const sheets = [];
13032
+ const seenSheetIds = /* @__PURE__ */ new Set();
13033
+ const duplicateSheetIds = /* @__PURE__ */ new Set();
13034
+ const seenPageSignatures = /* @__PURE__ */ new Set();
13035
+ const observedTotalCounts = [];
13036
+ const observedInventoryTokens = [];
13037
+ const issues = [];
13038
+ const warnings = [];
13039
+ let afterId;
13040
+ let pagesFetched = 0;
13041
+ let rowsFetched = 0;
13042
+ const addIssue = (issue, warning) => {
13043
+ if (issues.includes(issue)) {
13044
+ return;
13045
+ }
13046
+ issues.push(issue);
13047
+ warnings.push(warning);
13048
+ };
13049
+ while (true) {
13050
+ const page = await fetchPage({ limit: pageSize, ...afterId ? { afterId } : {} });
13051
+ pagesFetched += 1;
13052
+ rowsFetched += page.sheets.length;
13053
+ observedTotalCounts.push(page.totalCount);
13054
+ observedInventoryTokens.push(page.inventoryToken);
13055
+ const firstObservedTotal = observedTotalCounts[0];
13056
+ if (page.totalCount !== firstObservedTotal) {
13057
+ addIssue(
13058
+ "total_count_changed",
13059
+ `Sheet inventory total changed during collection (${firstObservedTotal} -> ${page.totalCount}).`
13060
+ );
13061
+ }
13062
+ const firstInventoryToken = observedInventoryTokens[0];
13063
+ if (page.inventoryToken !== firstInventoryToken) {
13064
+ addIssue(
13065
+ "inventory_changed",
13066
+ `Sheet inventory membership changed during collection (${firstInventoryToken} -> ${page.inventoryToken}).`
13067
+ );
13068
+ }
13069
+ if (page.sheets.length === 0) {
13070
+ if (sheets.length < page.totalCount) {
13071
+ addIssue(
13072
+ "early_empty_page",
13073
+ `Sheet inventory returned an empty page${afterId ? ` after cursor ${afterId}` : ""} before the observed total of ${page.totalCount}.`
13074
+ );
13075
+ }
13076
+ break;
13077
+ }
13078
+ const pageSignature = page.sheets.map(({ id }) => id).join("\n");
13079
+ if (seenPageSignatures.has(pageSignature)) {
13080
+ addIssue(
13081
+ "repeated_page",
13082
+ `Sheet inventory page${afterId ? ` after cursor ${afterId}` : ""} repeated an earlier page; collection stopped.`
13083
+ );
13084
+ break;
13085
+ }
13086
+ seenPageSignatures.add(pageSignature);
13087
+ for (const sheet of page.sheets) {
13088
+ if (seenSheetIds.has(sheet.id)) {
13089
+ duplicateSheetIds.add(sheet.id);
13090
+ continue;
13091
+ }
13092
+ seenSheetIds.add(sheet.id);
13093
+ sheets.push(sheet);
13094
+ }
13095
+ if (duplicateSheetIds.size > 0) {
13096
+ addIssue(
13097
+ "duplicate_sheet_ids",
13098
+ `Sheet inventory pages repeated ${duplicateSheetIds.size} sheet ID${duplicateSheetIds.size === 1 ? "" : "s"}; the snapshot may be incomplete.`
13099
+ );
13100
+ }
13101
+ if (rowsFetched >= page.totalCount) {
13102
+ break;
13103
+ }
13104
+ if (page.sheets.length < pageSize) {
13105
+ addIssue(
13106
+ "early_short_page",
13107
+ `Sheet inventory returned only ${page.sheets.length} sheets${afterId ? ` after cursor ${afterId}` : ""} before the observed total of ${page.totalCount}.`
13108
+ );
13109
+ break;
13110
+ }
13111
+ afterId = page.sheets.at(-1).id;
13112
+ }
13113
+ const totalCount = observedTotalCounts.at(-1) ?? 0;
13114
+ if (sheets.length !== totalCount) {
13115
+ addIssue(
13116
+ "sheet_count_mismatch",
13117
+ `Collected ${sheets.length} unique sheets but the final observed total was ${totalCount}.`
13118
+ );
13119
+ }
13120
+ return {
13121
+ sheets,
13122
+ totalCount,
13123
+ snapshot: {
13124
+ complete: issues.length === 0,
13125
+ pageSize,
13126
+ pagesFetched,
13127
+ observedTotalCounts,
13128
+ observedInventoryTokens,
13129
+ uniqueSheetCount: sheets.length,
13130
+ duplicateSheetIds: [...duplicateSheetIds],
13131
+ issues
13132
+ },
13133
+ warnings
13134
+ };
12049
13135
  };
12050
13136
 
12051
13137
  // src/commands/sheet.ts
12052
- import { z as z14 } from "zod";
12053
13138
  var readinessIssueTypeLabels = {
12054
13139
  missing_translation: "missing translations",
12055
13140
  missing_solution: "missing solutions",
@@ -12074,6 +13159,33 @@ var normalizeReadinessFilters = ({
12074
13159
  }) ?? []
12075
13160
  };
12076
13161
  };
13162
+ var normalizeSheetSeriesPath = (value) => {
13163
+ const normalizedValue = normalizeOptionalText(value);
13164
+ if (normalizedValue === null) {
13165
+ return null;
13166
+ }
13167
+ const parsedValue = sheetSeriesPathSchema.safeParse(normalizedValue);
13168
+ if (!parsedValue.success) {
13169
+ throw new CliCommandError(
13170
+ '--series-path is unknown. Run "chalksurf sheet series list --json" to inspect valid paths.',
13171
+ 2
13172
+ );
13173
+ }
13174
+ return parsedValue.data;
13175
+ };
13176
+ var normalizeSheetSeriesYearFilter = ({ fromYear, toYear }) => {
13177
+ if (fromYear === void 0 && toYear === void 0) {
13178
+ return null;
13179
+ }
13180
+ if (fromYear !== void 0 && toYear !== void 0 && fromYear > toYear) {
13181
+ throw new CliCommandError("--from-year must be less than or equal to --to-year.", 2);
13182
+ }
13183
+ const parsedFilter = sheetSeriesYearFilterSchema.safeParse({ fromYear, toYear });
13184
+ if (!parsedFilter.success) {
13185
+ throw new CliCommandError("--from-year and --to-year must be four-digit years.", 2);
13186
+ }
13187
+ return parsedFilter.data;
13188
+ };
12077
13189
  var hasUniqueItems2 = (values) => new Set(values).size === values.length;
12078
13190
  var sheetImportComponentIdPattern3 = /^[a-zA-Z0-9_-]{1,80}$/;
12079
13191
  var isValidTargetFolderPath2 = (value) => {
@@ -12174,7 +13286,7 @@ var resolveRequestedUuid2 = ({
12174
13286
  }
12175
13287
  return void 0;
12176
13288
  }
12177
- const parsedValue = z14.uuid().safeParse(resolvedValue);
13289
+ const parsedValue = z16.uuid().safeParse(resolvedValue);
12178
13290
  if (!parsedValue.success) {
12179
13291
  throw new CliCommandError(`${label} must be a valid UUID.`, 2);
12180
13292
  }
@@ -12185,7 +13297,7 @@ var resolveRequestedUuidList = ({ label, values }) => {
12185
13297
  throw new CliCommandError(`${label} is required at least once.`, 2);
12186
13298
  }
12187
13299
  return values.map((value, index) => {
12188
- const parsedValue = z14.uuid().safeParse(value.trim());
13300
+ const parsedValue = z16.uuid().safeParse(value.trim());
12189
13301
  if (!parsedValue.success) {
12190
13302
  throw new CliCommandError(`${label}[${index}] must be a valid UUID.`, 2);
12191
13303
  }
@@ -12501,6 +13613,19 @@ var formatSheetSearchOutput = ({
12501
13613
  )
12502
13614
  ].join("\n");
12503
13615
  };
13616
+ var formatSheetListOutput = ({ sheets, totalCount }) => {
13617
+ if (sheets.length === 0) {
13618
+ return "No sheets found.";
13619
+ }
13620
+ return [
13621
+ `${sheets.length} of ${totalCount} sheet${totalCount === 1 ? "" : "s"} returned.`,
13622
+ ...sheets.map((sheet) => {
13623
+ const folderPath = sheet.folderPath.map(({ name }) => name).join(" / ") || "(root)";
13624
+ const assignments = sheet.seriesAssignments.map(({ editionYear, seriesPath }) => `${seriesPath}@${editionYear}`).join(", ") || "unassigned";
13625
+ return `${sheet.id} ${sheet.name} ${folderPath} ${assignments}`;
13626
+ })
13627
+ ].join("\n");
13628
+ };
12504
13629
  var formatSheetReadinessSummary = (readinessSummary) => {
12505
13630
  if (!readinessSummary.hasIssues) {
12506
13631
  return "OK";
@@ -12581,22 +13706,26 @@ var formatDeleteSheetOutput = ({ receipt }) => {
12581
13706
  var formatUpdateSheetOutput = ({ receipt }) => {
12582
13707
  return formatAgentWriteReceiptOutput({ receipt });
12583
13708
  };
12584
- var formatBulkUpdateSheetsOutput = ({
12585
- confirmationDigest,
12586
- mode,
12587
- receipt,
12588
- results
12589
- }) => {
12590
- const summary = results.reduce(
13709
+ var getBulkUpdateSheetsCounts = ({ results }) => {
13710
+ const statusCounts = results.reduce(
12591
13711
  (counts, result) => ({
12592
13712
  ...counts,
12593
13713
  [result.status]: counts[result.status] + 1
12594
13714
  }),
12595
13715
  { ready: 0, unchanged: 0, updated: 0, error: 0 }
12596
13716
  );
13717
+ return { total: results.length, ...statusCounts };
13718
+ };
13719
+ var formatBulkUpdateSheetsOutput = ({
13720
+ confirmationDigest,
13721
+ mode,
13722
+ receipt,
13723
+ results
13724
+ }) => {
13725
+ const counts = getBulkUpdateSheetsCounts({ confirmationDigest, mode, receipt, results });
12597
13726
  const lines = [
12598
- `${mode === "dry_run" ? "Validated" : "Applied"} ${results.length} sheet update${results.length === 1 ? "" : "s"}.`,
12599
- `ready: ${summary.ready} updated: ${summary.updated} unchanged: ${summary.unchanged} errors: ${summary.error}`,
13727
+ `${mode === "dry_run" ? "Validated" : "Applied"} ${counts.total} sheet update${counts.total === 1 ? "" : "s"}.`,
13728
+ `ready: ${counts.ready} updated: ${counts.updated} unchanged: ${counts.unchanged} errors: ${counts.error}`,
12600
13729
  `confirmation digest: ${confirmationDigest}`
12601
13730
  ];
12602
13731
  if (receipt) {
@@ -12604,6 +13733,161 @@ var formatBulkUpdateSheetsOutput = ({
12604
13733
  }
12605
13734
  return lines.join("\n");
12606
13735
  };
13736
+ var formatVerifyBulkSheetUpdatesOutput = ({ counts, state }) => [
13737
+ `Verification state: ${state}.`,
13738
+ `total: ${counts.total} applied: ${counts.applied} pending: ${counts.pending} conflicting: ${counts.conflicting} errors: ${counts.error}`
13739
+ ].join("\n");
13740
+ var printWithOptionalResultArtifact = async ({
13741
+ command,
13742
+ compactHumanFormatter,
13743
+ compactValue,
13744
+ context,
13745
+ fullHumanFormatter,
13746
+ fullValue,
13747
+ resultFileInput
13748
+ }) => {
13749
+ if (resultFileInput === void 0) {
13750
+ context.output.print(fullValue, fullHumanFormatter, { command });
13751
+ return;
13752
+ }
13753
+ if (!resultFileInput.trim()) {
13754
+ throw new CliCommandError("--result-file must be a non-empty path.", 2);
13755
+ }
13756
+ const resultFile = resolveCliResultFilePath({ cwd: context.cwd, inputPath: resultFileInput });
13757
+ await context.output.printWithResultArtifact(fullValue, { ...compactValue, resultFile }, compactHumanFormatter, {
13758
+ command,
13759
+ publishArtifact: async (envelope) => await publishCliResultArtifact({
13760
+ destinationPath: resultFile,
13761
+ envelope
13762
+ })
13763
+ });
13764
+ };
13765
+ var configureBulkJsonInputOptions = (commandYargs, description) => commandYargs.option("input", {
13766
+ type: "string",
13767
+ nargs: 1,
13768
+ describe: 'Read the JSON object from a UTF-8 file, or "-" for stdin'
13769
+ }).option("input-json", {
13770
+ type: "string",
13771
+ nargs: 1,
13772
+ describe: description
13773
+ }).option("result-file", {
13774
+ type: "string",
13775
+ nargs: 1,
13776
+ describe: "Publish the complete JSON result envelope to a new private file"
13777
+ });
13778
+ var runBulkUpdateCommand = async (argv, context) => {
13779
+ const args = argv;
13780
+ if (args.resultFile !== void 0 && !args.resultFile.trim()) {
13781
+ throw new CliCommandError("--result-file must be a non-empty path.", 2);
13782
+ }
13783
+ const input = parseBulkUpdateSheetsAgentInput(
13784
+ await loadJsonObjectInput({
13785
+ cwd: context.cwd,
13786
+ inputJson: args.inputJson,
13787
+ inputPath: args.input,
13788
+ stdin: context.stdin
13789
+ })
13790
+ );
13791
+ const { apiClient, organizationId } = await createResolvedApiClient({
13792
+ context,
13793
+ baseUrlFlagValue: args.baseUrl,
13794
+ organizationFlagValue: args.organization,
13795
+ profileName: args.profile
13796
+ });
13797
+ let result;
13798
+ try {
13799
+ result = await apiClient.agentBulkUpdateSheets(input);
13800
+ } catch (error) {
13801
+ throw mapApiErrorToCliError(error);
13802
+ }
13803
+ const fullValue = {
13804
+ request: {
13805
+ ...input,
13806
+ organizationId: organizationId ?? null
13807
+ },
13808
+ ...result
13809
+ };
13810
+ const counts = getBulkUpdateSheetsCounts(result);
13811
+ try {
13812
+ await printWithOptionalResultArtifact({
13813
+ command: "sheet bulk-update",
13814
+ compactHumanFormatter: ({ confirmationDigest, counts: counts2, mode, receiptPresent, resultFile }) => [
13815
+ `${mode === "dry_run" ? "Validated" : "Applied"} ${counts2.total} sheet update${counts2.total === 1 ? "" : "s"}.`,
13816
+ `ready: ${counts2.ready} updated: ${counts2.updated} unchanged: ${counts2.unchanged} errors: ${counts2.error}`,
13817
+ `confirmation digest: ${confirmationDigest}`,
13818
+ `receipt: ${receiptPresent ? "present" : "none"}`,
13819
+ `full result: ${resultFile}`
13820
+ ].join("\n"),
13821
+ compactValue: {
13822
+ mode: result.mode,
13823
+ counts,
13824
+ confirmationDigest: result.confirmationDigest,
13825
+ receiptPresent: result.receipt !== null
13826
+ },
13827
+ context,
13828
+ fullHumanFormatter: (output) => formatBulkUpdateSheetsOutput(output),
13829
+ fullValue,
13830
+ resultFileInput: args.resultFile
13831
+ });
13832
+ } catch (error) {
13833
+ if (input.mode !== "apply") {
13834
+ throw error;
13835
+ }
13836
+ const message = error instanceof Error ? error.message : "Unknown result artifact error";
13837
+ context.output.info(
13838
+ `The server confirmed the bulk apply, but the result artifact could not be published: ${message} Do not retry the apply; preserve this stdout response and use "sheet bulk-update verify" with the original manifest if independent recovery is needed.`
13839
+ );
13840
+ context.output.print(fullValue, (output) => formatBulkUpdateSheetsOutput(output), {
13841
+ command: "sheet bulk-update"
13842
+ });
13843
+ }
13844
+ };
13845
+ var runVerifyBulkSheetUpdatesCommand = async (argv, context) => {
13846
+ const args = argv;
13847
+ const input = parseVerifyBulkSheetUpdatesAgentInput(
13848
+ await loadJsonObjectInput({
13849
+ cwd: context.cwd,
13850
+ inputJson: args.inputJson,
13851
+ inputPath: args.input,
13852
+ stdin: context.stdin
13853
+ })
13854
+ );
13855
+ const { apiClient, organizationId } = await createResolvedApiClient({
13856
+ context,
13857
+ baseUrlFlagValue: args.baseUrl,
13858
+ organizationFlagValue: args.organization,
13859
+ profileName: args.profile
13860
+ });
13861
+ let result;
13862
+ try {
13863
+ result = await apiClient.agentVerifyBulkSheetUpdates(input);
13864
+ } catch (error) {
13865
+ throw mapApiErrorToCliError(error);
13866
+ }
13867
+ const fullValue = {
13868
+ request: {
13869
+ ...input,
13870
+ organizationId: organizationId ?? null
13871
+ },
13872
+ ...result
13873
+ };
13874
+ await printWithOptionalResultArtifact({
13875
+ command: "sheet bulk-update verify",
13876
+ compactHumanFormatter: ({ counts, resultFile, state }) => [
13877
+ `Verification state: ${state}.`,
13878
+ `total: ${counts.total} applied: ${counts.applied} pending: ${counts.pending} conflicting: ${counts.conflicting} errors: ${counts.error}`,
13879
+ `full result: ${resultFile}`
13880
+ ].join("\n"),
13881
+ compactValue: {
13882
+ state: result.state,
13883
+ counts: result.counts
13884
+ },
13885
+ context,
13886
+ fullHumanFormatter: (output) => formatVerifyBulkSheetUpdatesOutput(output),
13887
+ fullValue,
13888
+ resultFileInput: args.resultFile
13889
+ });
13890
+ };
12607
13891
  var formatSheetSeriesOutput = ({ series }) => series.map(({ depth, names, path }) => `${" ".repeat(depth)}${names.english} ${path}`).join("\n");
12608
13892
  var formatSetSheetVisibilityOutput = ({ receipt }) => {
12609
13893
  return formatAgentWriteReceiptOutput({ receipt });
@@ -12649,6 +13933,128 @@ var resolveVisibilityFlag2 = ({ privateFlag, publicFlag }) => {
12649
13933
  };
12650
13934
  var registerSheetCommands = (sheetYargs, context) => {
12651
13935
  return sheetYargs.command(
13936
+ "list",
13937
+ "List exercise sheets owned by the selected organization",
13938
+ (listYargs) => listYargs.option("folder-id", {
13939
+ type: "string",
13940
+ describe: "Only include sheets placed directly in this folder"
13941
+ }).option("include-descendants", {
13942
+ type: "boolean",
13943
+ default: false,
13944
+ describe: "Include sheets in descendant folders; requires --folder-id"
13945
+ }).option("limit", {
13946
+ type: "number",
13947
+ describe: "Maximum number of sheets to return"
13948
+ }).option("offset", {
13949
+ type: "number",
13950
+ describe: "Number of organization-owned sheets to skip"
13951
+ }).option("after-id", {
13952
+ type: "string",
13953
+ describe: "Return sheets with IDs after this keyset cursor; cannot be combined with --offset"
13954
+ }).option("all", {
13955
+ type: "boolean",
13956
+ default: false,
13957
+ describe: "Collect every organization-owned sheet in sequential pages of 100"
13958
+ }).option("result-file", {
13959
+ type: "string",
13960
+ nargs: 1,
13961
+ describe: "Publish the complete JSON result envelope to a new private file"
13962
+ }).example("chalksurf --profile prod-codex sheet list --json", "List sheets owned by the selected organization").example(
13963
+ "chalksurf --profile prod-codex sheet list --all --result-file inventory.json --json",
13964
+ "Save a complete reviewed inventory"
13965
+ ).example(
13966
+ "chalksurf --profile prod-codex sheet list --folder-id 00000000-0000-4000-8000-000000000001 --include-descendants --json",
13967
+ "List a folder subtree"
13968
+ ),
13969
+ async (argv) => {
13970
+ const folderId = resolveRequestedUuid2({
13971
+ flagValue: argv.folderId,
13972
+ label: "--folder-id"
13973
+ });
13974
+ const includeDescendants = Boolean(argv.includeDescendants);
13975
+ if (includeDescendants && !folderId) {
13976
+ throw new CliCommandError("--folder-id is required with --include-descendants.", 2);
13977
+ }
13978
+ const collectAll = Boolean(argv.all);
13979
+ const afterId = resolveRequestedUuid2({
13980
+ flagValue: argv.afterId,
13981
+ label: "--after-id"
13982
+ });
13983
+ const limitArgument = argv.limit;
13984
+ const offsetArgument = argv.offset;
13985
+ if (collectAll && (limitArgument !== void 0 || offsetArgument !== void 0 || afterId !== void 0)) {
13986
+ throw new CliCommandError("--all cannot be combined with --limit, --offset, or --after-id.", 2);
13987
+ }
13988
+ if (afterId !== void 0 && offsetArgument !== void 0 && offsetArgument !== 0) {
13989
+ throw new CliCommandError("--after-id cannot be combined with a non-zero --offset.", 2);
13990
+ }
13991
+ const limit = collectAll ? null : normalizeLimit(limitArgument ?? 50);
13992
+ const offset = collectAll ? null : normalizeOffset(offsetArgument);
13993
+ const { apiClient, organizationId } = await createResolvedApiClient({
13994
+ context,
13995
+ baseUrlFlagValue: argv.baseUrl,
13996
+ organizationFlagValue: argv.organization,
13997
+ profileName: argv.profile
13998
+ });
13999
+ let result;
14000
+ try {
14001
+ if (collectAll) {
14002
+ const collected = await collectAllSheets({
14003
+ fetchPage: async ({ afterId: pageAfterId, limit: pageLimit }) => await apiClient.agentListSheets({
14004
+ afterId: pageAfterId,
14005
+ folderId,
14006
+ includeDescendants,
14007
+ limit: pageLimit,
14008
+ offset: 0
14009
+ })
14010
+ });
14011
+ for (const warning of collected.warnings) {
14012
+ context.output.info(warning);
14013
+ }
14014
+ result = {
14015
+ sheets: collected.sheets,
14016
+ totalCount: collected.totalCount,
14017
+ snapshot: collected.snapshot
14018
+ };
14019
+ } else {
14020
+ result = await apiClient.agentListSheets({
14021
+ afterId,
14022
+ folderId,
14023
+ includeDescendants,
14024
+ limit,
14025
+ offset
14026
+ });
14027
+ }
14028
+ } catch (error) {
14029
+ throw mapApiErrorToCliError(error);
14030
+ }
14031
+ const fullValue = {
14032
+ request: {
14033
+ ...collectAll ? { all: true } : {},
14034
+ ...afterId ? { afterId } : {},
14035
+ folderId: folderId ?? null,
14036
+ includeDescendants,
14037
+ limit,
14038
+ offset,
14039
+ organizationId: organizationId ?? null
14040
+ },
14041
+ ...result
14042
+ };
14043
+ await printWithOptionalResultArtifact({
14044
+ command: "sheet list",
14045
+ compactHumanFormatter: ({ resultFile, sheetCount, snapshotComplete }) => `Saved ${sheetCount} sheet${sheetCount === 1 ? "" : "s"} to ${resultFile}.${snapshotComplete === false ? " Snapshot is incomplete." : ""}`,
14046
+ compactValue: {
14047
+ sheetCount: result.sheets.length,
14048
+ totalCount: result.totalCount,
14049
+ snapshotComplete: "snapshot" in result ? result.snapshot.complete : null
14050
+ },
14051
+ context,
14052
+ fullHumanFormatter: (output) => formatSheetListOutput(output),
14053
+ fullValue,
14054
+ resultFileInput: argv.resultFile
14055
+ });
14056
+ }
14057
+ ).command(
12652
14058
  "search",
12653
14059
  "Search exercise sheets visible to the selected organization",
12654
14060
  (searchYargs) => searchYargs.option("text", {
@@ -12661,6 +14067,15 @@ var registerSheetCommands = (sheetYargs, context) => {
12661
14067
  }).option("subject", {
12662
14068
  type: "string",
12663
14069
  describe: `Only include sheets in this subject (${exerciseSubjects.join(", ")})`
14070
+ }).option("series-path", {
14071
+ type: "string",
14072
+ describe: "Only include sheets with an assignment at or below this authoritative series path"
14073
+ }).option("from-year", {
14074
+ type: "number",
14075
+ describe: "Only include matching series assignments from this edition year"
14076
+ }).option("to-year", {
14077
+ type: "number",
14078
+ describe: "Only include matching series assignments through this edition year"
12664
14079
  }).option("limit", {
12665
14080
  type: "number",
12666
14081
  default: 20,
@@ -12696,6 +14111,11 @@ var registerSheetCommands = (sheetYargs, context) => {
12696
14111
  label: "--subject",
12697
14112
  value: argv.subject
12698
14113
  });
14114
+ const seriesPath = normalizeSheetSeriesPath(argv.seriesPath);
14115
+ const yearFilter = normalizeSheetSeriesYearFilter({
14116
+ fromYear: argv.fromYear,
14117
+ toYear: argv.toYear
14118
+ });
12699
14119
  const limit = normalizeLimit(argv.limit);
12700
14120
  const offset = normalizeOffset(argv.offset);
12701
14121
  const readinessFilters = normalizeReadinessFilters({
@@ -12716,6 +14136,8 @@ var registerSheetCommands = (sheetYargs, context) => {
12716
14136
  issueTypes: readinessFilters.issueTypes,
12717
14137
  text,
12718
14138
  subject,
14139
+ seriesPath,
14140
+ yearFilter,
12719
14141
  ownership: toApiOwnership(ownership),
12720
14142
  limit,
12721
14143
  offset
@@ -12728,6 +14150,8 @@ var registerSheetCommands = (sheetYargs, context) => {
12728
14150
  request: {
12729
14151
  text,
12730
14152
  subject,
14153
+ seriesPath,
14154
+ yearFilter,
12731
14155
  ownership,
12732
14156
  hasIssues: readinessFilters.hasIssues,
12733
14157
  issueTypes: readinessFilters.issueTypes,
@@ -12934,8 +14358,16 @@ var registerSheetCommands = (sheetYargs, context) => {
12934
14358
  (seriesYargs) => seriesYargs.command(
12935
14359
  "list",
12936
14360
  "List valid sheet-series paths",
12937
- (listYargs) => listYargs.example("chalksurf sheet series list --json", "Inspect valid paths before assigning sheets"),
14361
+ (listYargs) => listYargs.option("subject", {
14362
+ type: "string",
14363
+ describe: `Only include series paths valid for this subject (${exerciseSubjects.join(", ")})`
14364
+ }).example("chalksurf sheet series list --json", "Inspect valid paths before assigning sheets").example("chalksurf sheet series list --subject math --json", "Inspect mathematics series paths"),
12938
14365
  async (argv) => {
14366
+ const subject = normalizeChoiceValue({
14367
+ allowedValues: exerciseSubjects,
14368
+ label: "--subject",
14369
+ value: argv.subject
14370
+ });
12939
14371
  const { apiClient, organizationId } = await createResolvedApiClient({
12940
14372
  context,
12941
14373
  baseUrlFlagValue: argv.baseUrl,
@@ -12944,13 +14376,15 @@ var registerSheetCommands = (sheetYargs, context) => {
12944
14376
  });
12945
14377
  let result;
12946
14378
  try {
12947
- result = await apiClient.agentListSheetSeries();
14379
+ result = await apiClient.agentListSheetSeries({
14380
+ subject: subject ?? void 0
14381
+ });
12948
14382
  } catch (error) {
12949
14383
  throw mapApiErrorToCliError(error);
12950
14384
  }
12951
14385
  context.output.print(
12952
14386
  {
12953
- request: { organizationId: organizationId ?? null },
14387
+ request: { organizationId: organizationId ?? null, subject },
12954
14388
  series: result.series
12955
14389
  },
12956
14390
  (output) => formatSheetSeriesOutput({ series: output.series }),
@@ -13610,43 +15044,35 @@ var registerSheetCommands = (sheetYargs, context) => {
13610
15044
  );
13611
15045
  }
13612
15046
  ).command(
13613
- "bulk-update",
13614
- "Dry-run or apply up to 100 sheet patches",
13615
- (bulkUpdateYargs) => bulkUpdateYargs.option("input-json", {
13616
- type: "string",
13617
- demandOption: true,
13618
- describe: "JSON matching the bulk update contract: mode, updates, and confirmationDigest for apply mode"
13619
- }).example(
13620
- `chalksurf sheet bulk-update --input-json '{"mode":"dry_run","updates":[{"id":"00000000-0000-4000-8000-000000000001","expectedUpdatedAt":"2026-01-01T00:00:00.000Z","patch":{"seriesAssignments":[{"seriesPath":"international.imo","editionYear":2025}]}}]}' --json`,
13621
- "Validate sheet-series assignments and obtain a confirmation digest"
15047
+ "bulk-update [subcommand]",
15048
+ "Bulk sheet update and recovery commands",
15049
+ (bulkUpdateYargs) => bulkUpdateYargs.command(
15050
+ "$0",
15051
+ "Dry-run or apply up to 100 sheet patches",
15052
+ (runYargs) => configureBulkJsonInputOptions(
15053
+ runYargs,
15054
+ "Inline JSON with mode, updates, and confirmationDigest for apply mode"
15055
+ ).example(
15056
+ "chalksurf sheet bulk-update --input batch-001.json --result-file dry-run.json --json",
15057
+ "Validate a reviewed batch and save the complete result"
15058
+ ).example(
15059
+ "cat batch-001.json | chalksurf sheet bulk-update --input - --json",
15060
+ "Validate a reviewed batch from stdin"
15061
+ ).example('chalksurf sheet bulk-update --input-json "$BATCH_JSON" --json', "Validate an inline batch"),
15062
+ async (argv) => await runBulkUpdateCommand(argv, context)
15063
+ ).command(
15064
+ "verify",
15065
+ "Verify current targets after an uncertain bulk apply response",
15066
+ (verifyYargs) => configureBulkJsonInputOptions(
15067
+ verifyYargs,
15068
+ "Inline JSON containing updates, or a prior dry-run/apply manifest"
15069
+ ).example(
15070
+ "chalksurf sheet bulk-update verify --input batch-001.json --result-file verification.json --json",
15071
+ "Classify the current state of every target in a batch"
15072
+ ),
15073
+ async (argv) => await runVerifyBulkSheetUpdatesCommand(argv, context)
13622
15074
  ),
13623
- async (argv) => {
13624
- const input = parseBulkUpdateSheetsAgentInput(
13625
- parseJsonObjectFlag({ label: "--input-json", value: argv.inputJson })
13626
- );
13627
- const { apiClient, organizationId } = await createResolvedApiClient({
13628
- context,
13629
- baseUrlFlagValue: argv.baseUrl,
13630
- organizationFlagValue: argv.organization,
13631
- profileName: argv.profile
13632
- });
13633
- let result;
13634
- try {
13635
- result = await apiClient.agentBulkUpdateSheets(input);
13636
- } catch (error) {
13637
- throw mapApiErrorToCliError(error);
13638
- }
13639
- context.output.print(
13640
- {
13641
- request: {
13642
- ...input,
13643
- organizationId: organizationId ?? null
13644
- },
13645
- ...result
13646
- },
13647
- (output) => formatBulkUpdateSheetsOutput(output),
13648
- { command: "sheet bulk-update" }
13649
- );
15075
+ () => {
13650
15076
  }
13651
15077
  ).command(
13652
15078
  "import [sources..]",
@@ -14036,84 +15462,6 @@ var registerSheetCommands = (sheetYargs, context) => {
14036
15462
  ).demandCommand(1).strict();
14037
15463
  };
14038
15464
 
14039
- // src/lib/output.ts
14040
- var cliJsonSchemaVersion = "v1";
14041
- var ensureTrailingNewline = (value) => {
14042
- return value.endsWith("\n") ? value : `${value}
14043
- `;
14044
- };
14045
- var writeLine = (writer, value) => {
14046
- writer.write(ensureTrailingNewline(value));
14047
- };
14048
- var createOutput = ({
14049
- json,
14050
- stdout,
14051
- stderr
14052
- }) => {
14053
- let warnings = [];
14054
- const consumeWarnings = () => {
14055
- const nextWarnings = warnings;
14056
- warnings = [];
14057
- return nextWarnings;
14058
- };
14059
- return {
14060
- json,
14061
- print: (value, humanFormatter, options) => {
14062
- if (json) {
14063
- writeLine(
14064
- stdout,
14065
- JSON.stringify(
14066
- {
14067
- schemaVersion: cliJsonSchemaVersion,
14068
- command: options.command,
14069
- ok: options.ok ?? true,
14070
- result: value,
14071
- ...options.error ? { error: options.error } : {},
14072
- warnings: consumeWarnings()
14073
- },
14074
- null,
14075
- 2
14076
- )
14077
- );
14078
- return;
14079
- }
14080
- const message = typeof humanFormatter === "function" ? humanFormatter(value) : humanFormatter;
14081
- writeLine(stdout, message);
14082
- },
14083
- printError: ({ command, error, result }) => {
14084
- if (json) {
14085
- writeLine(
14086
- stdout,
14087
- JSON.stringify(
14088
- {
14089
- schemaVersion: cliJsonSchemaVersion,
14090
- command,
14091
- ok: false,
14092
- ...result === void 0 ? {} : { result },
14093
- error,
14094
- warnings: consumeWarnings()
14095
- },
14096
- null,
14097
- 2
14098
- )
14099
- );
14100
- return;
14101
- }
14102
- writeLine(stderr, formatSerializableCliErrorForHuman(error));
14103
- },
14104
- info: (message) => {
14105
- if (json) {
14106
- warnings.push(message);
14107
- return;
14108
- }
14109
- writeLine(stderr, message);
14110
- },
14111
- error: (message) => {
14112
- writeLine(stderr, message);
14113
- }
14114
- };
14115
- };
14116
-
14117
15465
  // src/lib/prompt-secret.ts
14118
15466
  import { createInterface } from "node:readline/promises";
14119
15467
  import { Writable } from "node:stream";
@@ -14151,11 +15499,13 @@ var createPromptSecret = ({
14151
15499
  };
14152
15500
 
14153
15501
  // src/bin/chalksurf.ts
15502
+ var getHelpWidth = (stdout) => stdout.columns === void 0 ? null : Math.min(120, Math.max(80, stdout.columns));
14154
15503
  var packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
14155
15504
  var createCli = ({
14156
15505
  cwd,
14157
15506
  configPath,
14158
15507
  env,
15508
+ helpWidth,
14159
15509
  now,
14160
15510
  output,
14161
15511
  sleep,
@@ -14172,7 +15522,7 @@ var createCli = ({
14172
15522
  sleep,
14173
15523
  stdin
14174
15524
  };
14175
- return yargs().scriptName("chalksurf").usage("$0 <command> [options]").version(false).option("json", {
15525
+ return yargs().wrap(helpWidth).scriptName("chalksurf").usage("$0 <command> [options]").version(false).option("json", {
14176
15526
  type: "boolean",
14177
15527
  default: false,
14178
15528
  describe: "Write machine-readable JSON to stdout"
@@ -14216,6 +15566,12 @@ var createCli = ({
14216
15566
  (exerciseYargs) => registerExerciseCommands(exerciseYargs, commandContext),
14217
15567
  () => {
14218
15568
  }
15569
+ ).command(
15570
+ "quality-issue <subcommand>",
15571
+ "Exercise quality issue review commands",
15572
+ (qualityIssueYargs) => registerQualityIssueCommands(qualityIssueYargs, commandContext),
15573
+ () => {
15574
+ }
14219
15575
  ).command(
14220
15576
  "feedback <subcommand>",
14221
15577
  "Feedback commands",
@@ -14255,7 +15611,7 @@ var parseCliWithCapturedOutput = async ({
14255
15611
  argv,
14256
15612
  stdout
14257
15613
  }) => {
14258
- await new Promise((resolve6, reject) => {
15614
+ await new Promise((resolve8, reject) => {
14259
15615
  cli.parse(argv, (error, _parsedArgv, output) => {
14260
15616
  if (output) {
14261
15617
  stdout.write(output);
@@ -14264,14 +15620,24 @@ var parseCliWithCapturedOutput = async ({
14264
15620
  reject(error);
14265
15621
  return;
14266
15622
  }
14267
- resolve6();
15623
+ resolve8();
14268
15624
  });
14269
15625
  });
14270
15626
  };
14271
15627
  var hasFlag = (argv, flags) => {
14272
15628
  return argv.some((argument) => flags.includes(argument));
14273
15629
  };
14274
- var topLevelCommands = /* @__PURE__ */ new Set(["auth", "org", "profile", "exercise", "feedback", "figure", "sheet", "job"]);
15630
+ var topLevelCommands = /* @__PURE__ */ new Set([
15631
+ "auth",
15632
+ "org",
15633
+ "profile",
15634
+ "exercise",
15635
+ "quality-issue",
15636
+ "feedback",
15637
+ "figure",
15638
+ "sheet",
15639
+ "job"
15640
+ ]);
14275
15641
  var hasTopLevelCommand = (argv) => {
14276
15642
  return argv.some((argument) => topLevelCommands.has(argument));
14277
15643
  };
@@ -14281,7 +15647,17 @@ var resolveCommandName = (argv) => {
14281
15647
  if (!topLevelCommands.has(argument)) {
14282
15648
  continue;
14283
15649
  }
14284
- const subcommand = argv.slice(index + 1).find((candidate) => candidate.length > 0 && !candidate.startsWith("-") && !candidate.startsWith("http"));
15650
+ const commandArguments = argv.slice(index + 1);
15651
+ const subcommandIndex = commandArguments.findIndex(
15652
+ (candidate) => candidate.length > 0 && !candidate.startsWith("-") && !candidate.startsWith("http")
15653
+ );
15654
+ const subcommand = commandArguments[subcommandIndex];
15655
+ if (argument === "sheet" && subcommand === "bulk-update") {
15656
+ const nestedSubcommand = commandArguments.slice(subcommandIndex + 1).find((candidate) => candidate.length > 0 && !candidate.startsWith("-"));
15657
+ if (nestedSubcommand === "verify") {
15658
+ return "sheet bulk-update verify";
15659
+ }
15660
+ }
14285
15661
  return subcommand ? `${argument} ${subcommand}` : argument;
14286
15662
  }
14287
15663
  if (hasFlag(argv, ["--version", "-v"])) {
@@ -14295,7 +15671,7 @@ var runCli = async ({
14295
15671
  configPath,
14296
15672
  env = process.env,
14297
15673
  nowImpl = Date.now,
14298
- sleepImpl = (milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)),
15674
+ sleepImpl = (milliseconds) => new Promise((resolve8) => setTimeout(resolve8, milliseconds)),
14299
15675
  stdin = process.stdin,
14300
15676
  stdout = process.stdout,
14301
15677
  stderr = process.stderr
@@ -14310,6 +15686,7 @@ var runCli = async ({
14310
15686
  cwd,
14311
15687
  configPath,
14312
15688
  env,
15689
+ helpWidth: getHelpWidth(stdout),
14313
15690
  now: nowImpl,
14314
15691
  output,
14315
15692
  sleep: sleepImpl,