@chalksurf/cli 0.3.4 → 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.
@@ -151,6 +151,15 @@ var createApiClient = ({
151
151
  agentGetExerciseUsage: async (id) => {
152
152
  return await query("agentGetExerciseUsage", { id });
153
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
+ },
154
163
  agentListExerciseLabels: async () => {
155
164
  return await query("agentListExerciseLabels", {});
156
165
  },
@@ -211,6 +220,9 @@ var createApiClient = ({
211
220
  agentBulkUpdateSheets: async (input) => {
212
221
  return await mutation("agentBulkUpdateSheets", input);
213
222
  },
223
+ agentVerifyBulkSheetUpdates: async (input) => {
224
+ return await mutation("agentVerifyBulkSheetUpdates", input);
225
+ },
214
226
  agentListSheets: async (input) => {
215
227
  return await query("agentListSheets", input);
216
228
  },
@@ -887,7 +899,9 @@ var Constants = {
887
899
  "generate_sheet_translation_completed",
888
900
  "upload_exercise_figure",
889
901
  "attach_exercise_figures",
890
- "bulk_update_sheets"
902
+ "bulk_update_sheets",
903
+ "resolve_quality_issue",
904
+ "dismiss_quality_issue"
891
905
  ],
892
906
  agent_audit_outcome: ["started", "success", "failed", "denied"],
893
907
  agent_audit_resource_type: ["exercise", "sheet", "folder", "job", "event", "figure_asset"],
@@ -928,7 +942,6 @@ var Constants = {
928
942
  "figure_update",
929
943
  "automatic_labeling"
930
944
  ],
931
- figure_asset_source_kind: ["document_crop", "rendered_figure"],
932
945
  job_type: [
933
946
  "exercise_sheet_import",
934
947
  "exercise_import",
@@ -1408,6 +1421,18 @@ var agentWriteOperationRegistry = {
1408
1421
  resourceType: "exercise",
1409
1422
  sync: true
1410
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
+ },
1411
1436
  update_sheet: {
1412
1437
  agentToolName: "update_sheet",
1413
1438
  receiptOperation: "update_sheet",
@@ -1735,6 +1760,7 @@ var folderPathSegmentSchema = strictObject({
1735
1760
  name: z3.string().trim().min(1)
1736
1761
  });
1737
1762
  var listSheetsAgentInputSchema = strictObject({
1763
+ afterId: uuidSchema.optional(),
1738
1764
  folderId: uuidSchema.optional(),
1739
1765
  includeDescendants: z3.boolean().optional().default(false),
1740
1766
  limit: z3.number().int().min(1).max(100).optional().default(50),
@@ -1742,6 +1768,9 @@ var listSheetsAgentInputSchema = strictObject({
1742
1768
  }).refine(({ folderId, includeDescendants }) => folderId !== void 0 || !includeDescendants, {
1743
1769
  message: "folderId is required when includeDescendants is true",
1744
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"]
1745
1774
  });
1746
1775
  var listSheetsAgentOutputSchema = strictObject({
1747
1776
  sheets: z3.array(
@@ -1756,6 +1785,7 @@ var listSheetsAgentOutputSchema = strictObject({
1756
1785
  seriesAssignments: sheetSeriesAssignmentsSchema
1757
1786
  })
1758
1787
  ),
1788
+ inventoryToken: z3.string().trim().min(1),
1759
1789
  totalCount: z3.number().int().min(0)
1760
1790
  });
1761
1791
  var exerciseLabelSchema = strictObject({
@@ -2008,6 +2038,41 @@ var getExerciseUsageAgentInputSchema = strictObject({
2008
2038
  var getExerciseUsageAgentOutputSchema = strictObject({
2009
2039
  usage: exerciseUsageSchema
2010
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;
2011
2076
  var agentWriteReceiptSchema = strictObject({
2012
2077
  resource: strictObject({
2013
2078
  type: z3.enum(["exercise", "sheet", "folder"]),
@@ -2033,6 +2098,8 @@ var agentWriteReceiptSchema = strictObject({
2033
2098
  var agentWriteReceiptOutputSchema = strictObject({
2034
2099
  receipt: agentWriteReceiptSchema
2035
2100
  });
2101
+ var resolveQualityIssueAgentOutputSchema = agentWriteReceiptOutputSchema;
2102
+ var dismissQualityIssueAgentOutputSchema = agentWriteReceiptOutputSchema;
2036
2103
  var copyExerciseAgentInputSchema = strictObject({
2037
2104
  id: uuidSchema
2038
2105
  });
@@ -2218,6 +2285,34 @@ var bulkUpdateSheetsAgentOutputSchema = strictObject({
2218
2285
  results: z3.array(bulkUpdateSheetResultSchema),
2219
2286
  receipt: agentWriteReceiptSchema.nullable()
2220
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
+ });
2221
2316
  var createSheetScoredExerciseSchema = strictObject({
2222
2317
  id: uuidSchema,
2223
2318
  scorePoints: z3.number().min(0).max(99999.9).refine((value) => Number.isInteger(value * 10), "Score can have at most 1 decimal place").nullable()
@@ -2611,6 +2706,18 @@ var getExerciseUsageInputSchema = getExerciseUsageAgentInputSchema.extend({
2611
2706
  organizationId: organizationIdSchema
2612
2707
  });
2613
2708
  var getExerciseUsageOutputSchema = getExerciseUsageAgentOutputSchema;
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({
2718
+ organizationId: organizationIdSchema
2719
+ });
2720
+ var dismissQualityIssueOutputSchema = dismissQualityIssueAgentOutputSchema;
2614
2721
  var listExerciseLabelsInputSchema = listExerciseLabelsAgentInputSchema;
2615
2722
  var listExerciseLabelsOutputSchema = listExerciseLabelsAgentOutputSchema;
2616
2723
  var copyExerciseInputSchema = copyExerciseAgentInputSchema.extend({
@@ -2687,7 +2794,7 @@ var searchSheetsOutputSchema = strictObject2({
2687
2794
  var listSheetsInputSchema = listSheetsAgentInputSchema.extend({
2688
2795
  organizationId: organizationIdSchema
2689
2796
  });
2690
- var listSheetsOutputSchema = listSheetsAgentOutputSchema;
2797
+ var listSheetsOutputSchema = listSheetsAgentOutputSchema.omit({ inventoryToken: true });
2691
2798
  var getSheetInputSchema = getSheetAgentInputSchema.extend({
2692
2799
  organizationId: organizationIdSchema
2693
2800
  });
@@ -2726,6 +2833,10 @@ var bulkUpdateSheetsInputSchema = bulkUpdateSheetsAgentInputSchema.extend({
2726
2833
  organizationId: organizationIdSchema
2727
2834
  });
2728
2835
  var bulkUpdateSheetsOutputSchema = bulkUpdateSheetsAgentOutputSchema;
2836
+ var verifyBulkSheetUpdatesInputSchema = verifyBulkSheetUpdatesAgentInputSchema.extend({
2837
+ organizationId: organizationIdSchema
2838
+ });
2839
+ var verifyBulkSheetUpdatesOutputSchema = verifyBulkSheetUpdatesAgentOutputSchema;
2729
2840
  var setSheetVisibilityInputSchema = setSheetVisibilityAgentInputSchema.extend({
2730
2841
  organizationId: organizationIdSchema
2731
2842
  });
@@ -3587,6 +3698,16 @@ var chalksurfMcpToolCapabilities = {
3587
3698
  requiredPermission: "read",
3588
3699
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.read] }
3589
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
+ },
3590
3711
  list_exercise_labels: {
3591
3712
  name: "list_exercise_labels",
3592
3713
  title: "List exercise labels",
@@ -3637,6 +3758,26 @@ var chalksurfMcpToolCapabilities = {
3637
3758
  requiredPermission: "write",
3638
3759
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3639
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
+ },
3640
3781
  set_exercise_visibility: {
3641
3782
  name: "set_exercise_visibility",
3642
3783
  title: "Set exercise visibility",
@@ -3801,6 +3942,16 @@ var chalksurfMcpToolCapabilities = {
3801
3942
  requiredPermission: "write",
3802
3943
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3803
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
+ },
3804
3955
  set_sheet_visibility: {
3805
3956
  name: "set_sheet_visibility",
3806
3957
  title: "Set sheet visibility",
@@ -8873,10 +9024,46 @@ var parseBulkUpdateSheetsAgentInput = (input) => {
8873
9024
  }
8874
9025
  return parsedInput.data;
8875
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
+ };
8876
9050
 
8877
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
+ };
8878
9058
  var formatAgentWriteReceiptOutput = ({ receipt }) => {
8879
- 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`;
8880
9067
  };
8881
9068
 
8882
9069
  // src/lib/command-options.ts
@@ -12034,135 +12221,1047 @@ var registerProfileCommands = (profileYargs, context) => {
12034
12221
  ).demandCommand(1).strict();
12035
12222
  };
12036
12223
 
12037
- // src/commands/sheet.ts
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
12038
12233
  import { z as z14 } from "zod";
12039
- var readinessIssueTypeLabels = {
12040
- missing_translation: "missing translations",
12041
- missing_solution: "missing solutions",
12042
- invalid_latex: "invalid LaTeX",
12043
- missing_score: "missing scores",
12044
- exercise_quality_issue: "exercise quality issues"
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
+ }
12045
12370
  };
12046
- var normalizeReadinessFilters = ({
12047
- issueTypes,
12048
- needsWork,
12049
- ready
12050
- }) => {
12051
- if (needsWork && ready) {
12052
- throw new CliCommandError("--needs-work and --ready cannot be used together.", 2);
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
+ });
12053
12383
  }
12054
- return {
12055
- hasIssues: needsWork ? true : ready ? false : null,
12056
- issueTypes: normalizeChoiceValues({
12057
- allowedValues: sheetReadinessIssueTypes,
12058
- label: "--issue-type",
12059
- values: issueTypes
12060
- }) ?? []
12061
- };
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
+ });
12062
12394
  };
12063
- var normalizeSheetSeriesPath = (value) => {
12064
- const normalizedValue = normalizeOptionalText(value);
12065
- if (normalizedValue === null) {
12066
- return null;
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);
12067
12405
  }
12068
- const parsedValue = sheetSeriesPathSchema.safeParse(normalizedValue);
12069
- if (!parsedValue.success) {
12070
- throw new CliCommandError(
12071
- '--series-path is unknown. Run "chalksurf sheet series list --json" to inspect valid paths.',
12072
- 2
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)])
12073
12414
  );
12074
12415
  }
12075
- return parsedValue.data;
12416
+ return value;
12076
12417
  };
12077
- var normalizeSheetSeriesYearFilter = ({ fromYear, toYear }) => {
12078
- if (fromYear === void 0 && toYear === void 0) {
12079
- return null;
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
+ });
12080
12425
  }
12081
- if (fromYear !== void 0 && toYear !== void 0 && fromYear > toYear) {
12082
- throw new CliCommandError("--from-year must be less than or equal to --to-year.", 2);
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
+ );
12083
12481
  }
12084
- const parsedFilter = sheetSeriesYearFilterSchema.safeParse({ fromYear, toYear });
12085
- if (!parsedFilter.success) {
12086
- throw new CliCommandError("--from-year and --to-year must be four-digit years.", 2);
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
+ );
12087
12497
  }
12088
- return parsedFilter.data;
12089
- };
12090
- var hasUniqueItems2 = (values) => new Set(values).size === values.length;
12091
- var sheetImportComponentIdPattern3 = /^[a-zA-Z0-9_-]{1,80}$/;
12092
- var isValidTargetFolderPath2 = (value) => {
12093
- const normalizedSegments = value.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0);
12094
- return normalizedSegments.length > 0 && normalizedSegments.every((segment) => segment !== "." && segment !== "..");
12095
- };
12096
- var assertValidSheetImportPlan = (plan) => {
12097
- if (plan.sheets.length === 0) {
12098
- throw new CliCommandError("Invalid sheet import plan: sheets must include at least one sheet.", 2);
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
+ });
12099
12504
  }
12100
- plan.sheets.forEach((sheet) => {
12101
- if (sheet.sourceIndexes.length === 0) {
12102
- throw new CliCommandError("Invalid sheet import plan: sourceIndexes must include at least one source.", 2);
12103
- }
12104
- if (!sheet.sourceIndexes.every((sourceIndex) => Number.isInteger(sourceIndex) && sourceIndex >= 0)) {
12105
- throw new CliCommandError("Invalid sheet import plan: sourceIndexes must contain only non-negative integers.", 2);
12106
- }
12107
- if (!hasUniqueItems2(sheet.sourceIndexes)) {
12108
- throw new CliCommandError("Invalid sheet import plan: sourceIndexes must be unique within a sheet.", 2);
12109
- }
12110
- if (sheet.components) {
12111
- if (sheet.targetFolderPath !== void 0 || sheet.titleOverride !== void 0 || sheet.translateToLanguages) {
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)) {
12112
12537
  throw new CliCommandError(
12113
- "Invalid sheet import plan: targetFolderPath, titleOverride, and translateToLanguages must be set on components when components are provided.",
12114
- 2
12538
+ `Artifact validation failed: exercise ${action.exerciseId} has more than one update action`,
12539
+ 2,
12540
+ true,
12541
+ { code: "artifact_validation_failed", retryable: false }
12115
12542
  );
12116
12543
  }
12117
- if (!hasUniqueItems2(sheet.components.map((component) => component.componentId))) {
12118
- throw new CliCommandError("Invalid sheet import plan: componentId values must be unique within a sheet.", 2);
12119
- }
12120
- sheet.components.forEach((component) => {
12121
- if (!sheetImportComponentIdPattern3.test(component.componentId)) {
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") {
12122
12549
  throw new CliCommandError(
12123
- "Invalid sheet import plan: componentId must be 1-80 letters, numbers, underscores, or hyphens.",
12124
- 2
12125
- );
12126
- }
12127
- if (!component.description.trim()) {
12128
- throw new CliCommandError("Invalid sheet import plan: component descriptions must be non-empty.", 2);
12129
- }
12130
- if (component.targetFolderPath != null && !isValidTargetFolderPath2(component.targetFolderPath)) {
12131
- throw new CliCommandError(
12132
- 'Invalid sheet import plan: component targetFolderPath must be a normalized folder path without "." or ".." segments.',
12133
- 2
12550
+ "Artifact validation failed: update action evidence differs from inventory",
12551
+ 2,
12552
+ true,
12553
+ {
12554
+ code: "artifact_validation_failed",
12555
+ retryable: false
12556
+ }
12134
12557
  );
12135
12558
  }
12136
- if (component.translateToLanguages && !hasUniqueItems2(component.translateToLanguages)) {
12137
- throw new CliCommandError("Invalid sheet import plan: component translateToLanguages must be unique.", 2);
12138
- }
12559
+ updateActionsByIssueId.set(actionIssueId, [...updateActionsByIssueId.get(actionIssueId) ?? [], action]);
12560
+ return classification.patches;
12139
12561
  });
12140
- return;
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;
12141
12572
  }
12142
- if (sheet.targetFolderPath === void 0) {
12143
- throw new CliCommandError("Invalid sheet import plan: targetFolderPath is required.", 2);
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
+ });
12144
12579
  }
12145
- if (sheet.targetFolderPath !== null && !isValidTargetFolderPath2(sheet.targetFolderPath)) {
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)) {
12146
12605
  throw new CliCommandError(
12147
- 'Invalid sheet import plan: targetFolderPath must be a normalized folder path without "." or ".." segments.',
12148
- 2
12606
+ `Artifact validation failed: dismissal ${dismissals[0].actionId} must precede update ${exerciseUpdate.actionId}`,
12607
+ 2,
12608
+ true,
12609
+ { code: "artifact_validation_failed", retryable: false }
12149
12610
  );
12150
12611
  }
12151
- if (sheet.translateToLanguages && !hasUniqueItems2(sheet.translateToLanguages)) {
12152
- throw new CliCommandError("Invalid sheet import plan: translateToLanguages must be unique.", 2);
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
+ );
12153
12620
  }
12154
- });
12155
- };
12156
- var normalizeOptionalTitle = (value) => {
12157
- const normalizedValue = value?.trim();
12158
- if (!normalizedValue) {
12159
- return void 0;
12160
12621
  }
12161
- return normalizedValue;
12162
- };
12163
- var normalizeRequiredText = ({ label, value }) => {
12164
- const normalizedValue = value?.trim();
12165
- if (!normalizedValue) {
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
+ };
13135
+ };
13136
+
13137
+ // src/commands/sheet.ts
13138
+ var readinessIssueTypeLabels = {
13139
+ missing_translation: "missing translations",
13140
+ missing_solution: "missing solutions",
13141
+ invalid_latex: "invalid LaTeX",
13142
+ missing_score: "missing scores",
13143
+ exercise_quality_issue: "exercise quality issues"
13144
+ };
13145
+ var normalizeReadinessFilters = ({
13146
+ issueTypes,
13147
+ needsWork,
13148
+ ready
13149
+ }) => {
13150
+ if (needsWork && ready) {
13151
+ throw new CliCommandError("--needs-work and --ready cannot be used together.", 2);
13152
+ }
13153
+ return {
13154
+ hasIssues: needsWork ? true : ready ? false : null,
13155
+ issueTypes: normalizeChoiceValues({
13156
+ allowedValues: sheetReadinessIssueTypes,
13157
+ label: "--issue-type",
13158
+ values: issueTypes
13159
+ }) ?? []
13160
+ };
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
+ };
13189
+ var hasUniqueItems2 = (values) => new Set(values).size === values.length;
13190
+ var sheetImportComponentIdPattern3 = /^[a-zA-Z0-9_-]{1,80}$/;
13191
+ var isValidTargetFolderPath2 = (value) => {
13192
+ const normalizedSegments = value.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0);
13193
+ return normalizedSegments.length > 0 && normalizedSegments.every((segment) => segment !== "." && segment !== "..");
13194
+ };
13195
+ var assertValidSheetImportPlan = (plan) => {
13196
+ if (plan.sheets.length === 0) {
13197
+ throw new CliCommandError("Invalid sheet import plan: sheets must include at least one sheet.", 2);
13198
+ }
13199
+ plan.sheets.forEach((sheet) => {
13200
+ if (sheet.sourceIndexes.length === 0) {
13201
+ throw new CliCommandError("Invalid sheet import plan: sourceIndexes must include at least one source.", 2);
13202
+ }
13203
+ if (!sheet.sourceIndexes.every((sourceIndex) => Number.isInteger(sourceIndex) && sourceIndex >= 0)) {
13204
+ throw new CliCommandError("Invalid sheet import plan: sourceIndexes must contain only non-negative integers.", 2);
13205
+ }
13206
+ if (!hasUniqueItems2(sheet.sourceIndexes)) {
13207
+ throw new CliCommandError("Invalid sheet import plan: sourceIndexes must be unique within a sheet.", 2);
13208
+ }
13209
+ if (sheet.components) {
13210
+ if (sheet.targetFolderPath !== void 0 || sheet.titleOverride !== void 0 || sheet.translateToLanguages) {
13211
+ throw new CliCommandError(
13212
+ "Invalid sheet import plan: targetFolderPath, titleOverride, and translateToLanguages must be set on components when components are provided.",
13213
+ 2
13214
+ );
13215
+ }
13216
+ if (!hasUniqueItems2(sheet.components.map((component) => component.componentId))) {
13217
+ throw new CliCommandError("Invalid sheet import plan: componentId values must be unique within a sheet.", 2);
13218
+ }
13219
+ sheet.components.forEach((component) => {
13220
+ if (!sheetImportComponentIdPattern3.test(component.componentId)) {
13221
+ throw new CliCommandError(
13222
+ "Invalid sheet import plan: componentId must be 1-80 letters, numbers, underscores, or hyphens.",
13223
+ 2
13224
+ );
13225
+ }
13226
+ if (!component.description.trim()) {
13227
+ throw new CliCommandError("Invalid sheet import plan: component descriptions must be non-empty.", 2);
13228
+ }
13229
+ if (component.targetFolderPath != null && !isValidTargetFolderPath2(component.targetFolderPath)) {
13230
+ throw new CliCommandError(
13231
+ 'Invalid sheet import plan: component targetFolderPath must be a normalized folder path without "." or ".." segments.',
13232
+ 2
13233
+ );
13234
+ }
13235
+ if (component.translateToLanguages && !hasUniqueItems2(component.translateToLanguages)) {
13236
+ throw new CliCommandError("Invalid sheet import plan: component translateToLanguages must be unique.", 2);
13237
+ }
13238
+ });
13239
+ return;
13240
+ }
13241
+ if (sheet.targetFolderPath === void 0) {
13242
+ throw new CliCommandError("Invalid sheet import plan: targetFolderPath is required.", 2);
13243
+ }
13244
+ if (sheet.targetFolderPath !== null && !isValidTargetFolderPath2(sheet.targetFolderPath)) {
13245
+ throw new CliCommandError(
13246
+ 'Invalid sheet import plan: targetFolderPath must be a normalized folder path without "." or ".." segments.',
13247
+ 2
13248
+ );
13249
+ }
13250
+ if (sheet.translateToLanguages && !hasUniqueItems2(sheet.translateToLanguages)) {
13251
+ throw new CliCommandError("Invalid sheet import plan: translateToLanguages must be unique.", 2);
13252
+ }
13253
+ });
13254
+ };
13255
+ var normalizeOptionalTitle = (value) => {
13256
+ const normalizedValue = value?.trim();
13257
+ if (!normalizedValue) {
13258
+ return void 0;
13259
+ }
13260
+ return normalizedValue;
13261
+ };
13262
+ var normalizeRequiredText = ({ label, value }) => {
13263
+ const normalizedValue = value?.trim();
13264
+ if (!normalizedValue) {
12166
13265
  throw new CliCommandError(`${label} is required.`, 2);
12167
13266
  }
12168
13267
  return normalizedValue;
@@ -12187,7 +13286,7 @@ var resolveRequestedUuid2 = ({
12187
13286
  }
12188
13287
  return void 0;
12189
13288
  }
12190
- const parsedValue = z14.uuid().safeParse(resolvedValue);
13289
+ const parsedValue = z16.uuid().safeParse(resolvedValue);
12191
13290
  if (!parsedValue.success) {
12192
13291
  throw new CliCommandError(`${label} must be a valid UUID.`, 2);
12193
13292
  }
@@ -12198,7 +13297,7 @@ var resolveRequestedUuidList = ({ label, values }) => {
12198
13297
  throw new CliCommandError(`${label} is required at least once.`, 2);
12199
13298
  }
12200
13299
  return values.map((value, index) => {
12201
- const parsedValue = z14.uuid().safeParse(value.trim());
13300
+ const parsedValue = z16.uuid().safeParse(value.trim());
12202
13301
  if (!parsedValue.success) {
12203
13302
  throw new CliCommandError(`${label}[${index}] must be a valid UUID.`, 2);
12204
13303
  }
@@ -12607,22 +13706,26 @@ var formatDeleteSheetOutput = ({ receipt }) => {
12607
13706
  var formatUpdateSheetOutput = ({ receipt }) => {
12608
13707
  return formatAgentWriteReceiptOutput({ receipt });
12609
13708
  };
12610
- var formatBulkUpdateSheetsOutput = ({
12611
- confirmationDigest,
12612
- mode,
12613
- receipt,
12614
- results
12615
- }) => {
12616
- const summary = results.reduce(
13709
+ var getBulkUpdateSheetsCounts = ({ results }) => {
13710
+ const statusCounts = results.reduce(
12617
13711
  (counts, result) => ({
12618
13712
  ...counts,
12619
13713
  [result.status]: counts[result.status] + 1
12620
13714
  }),
12621
13715
  { ready: 0, unchanged: 0, updated: 0, error: 0 }
12622
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 });
12623
13726
  const lines = [
12624
- `${mode === "dry_run" ? "Validated" : "Applied"} ${results.length} sheet update${results.length === 1 ? "" : "s"}.`,
12625
- `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}`,
12626
13729
  `confirmation digest: ${confirmationDigest}`
12627
13730
  ];
12628
13731
  if (receipt) {
@@ -12630,6 +13733,161 @@ var formatBulkUpdateSheetsOutput = ({
12630
13733
  }
12631
13734
  return lines.join("\n");
12632
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
+ };
12633
13891
  var formatSheetSeriesOutput = ({ series }) => series.map(({ depth, names, path }) => `${" ".repeat(depth)}${names.english} ${path}`).join("\n");
12634
13892
  var formatSetSheetVisibilityOutput = ({ receipt }) => {
12635
13893
  return formatAgentWriteReceiptOutput({ receipt });
@@ -12686,13 +13944,25 @@ var registerSheetCommands = (sheetYargs, context) => {
12686
13944
  describe: "Include sheets in descendant folders; requires --folder-id"
12687
13945
  }).option("limit", {
12688
13946
  type: "number",
12689
- default: 50,
12690
13947
  describe: "Maximum number of sheets to return"
12691
13948
  }).option("offset", {
12692
13949
  type: "number",
12693
- default: 0,
12694
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"
12695
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(
12696
13966
  "chalksurf --profile prod-codex sheet list --folder-id 00000000-0000-4000-8000-000000000001 --include-descendants --json",
12697
13967
  "List a folder subtree"
12698
13968
  ),
@@ -12705,8 +13975,21 @@ var registerSheetCommands = (sheetYargs, context) => {
12705
13975
  if (includeDescendants && !folderId) {
12706
13976
  throw new CliCommandError("--folder-id is required with --include-descendants.", 2);
12707
13977
  }
12708
- const limit = normalizeLimit(argv.limit);
12709
- const offset = normalizeOffset(argv.offset);
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);
12710
13993
  const { apiClient, organizationId } = await createResolvedApiClient({
12711
13994
  context,
12712
13995
  baseUrlFlagValue: argv.baseUrl,
@@ -12715,31 +13998,61 @@ var registerSheetCommands = (sheetYargs, context) => {
12715
13998
  });
12716
13999
  let result;
12717
14000
  try {
12718
- result = await apiClient.agentListSheets({
12719
- folderId,
12720
- includeDescendants,
12721
- limit,
12722
- offset
12723
- });
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
+ }
12724
14028
  } catch (error) {
12725
14029
  throw mapApiErrorToCliError(error);
12726
14030
  }
12727
- context.output.print(
12728
- {
12729
- request: {
12730
- folderId: folderId ?? null,
12731
- includeDescendants,
12732
- limit,
12733
- offset,
12734
- organizationId: organizationId ?? null
12735
- },
12736
- ...result
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
12737
14040
  },
12738
- (output) => formatSheetListOutput(output),
12739
- {
12740
- command: "sheet list"
12741
- }
12742
- );
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
+ });
12743
14056
  }
12744
14057
  ).command(
12745
14058
  "search",
@@ -13731,58 +15044,35 @@ var registerSheetCommands = (sheetYargs, context) => {
13731
15044
  );
13732
15045
  }
13733
15046
  ).command(
13734
- "bulk-update",
13735
- "Dry-run or apply up to 100 sheet patches",
13736
- (bulkUpdateYargs) => bulkUpdateYargs.option("input", {
13737
- type: "string",
13738
- nargs: 1,
13739
- describe: 'Read the bulk update JSON object from a UTF-8 file, or "-" for stdin'
13740
- }).option("input-json", {
13741
- type: "string",
13742
- nargs: 1,
13743
- describe: "Inline JSON matching the bulk update contract: mode, updates, and confirmationDigest for apply mode"
13744
- }).example(
13745
- "chalksurf sheet bulk-update --input batch-001.json --json",
13746
- "Validate a reviewed bulk-update batch from a file"
13747
- ).example(
13748
- "cat batch-001.json | chalksurf sheet bulk-update --input - --json",
13749
- "Validate a reviewed bulk-update batch from stdin"
13750
- ).example(
13751
- `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`,
13752
- "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)
13753
15074
  ),
13754
- async (argv) => {
13755
- const input = parseBulkUpdateSheetsAgentInput(
13756
- await loadJsonObjectInput({
13757
- cwd: context.cwd,
13758
- inputJson: argv.inputJson,
13759
- inputPath: argv.input,
13760
- stdin: context.stdin
13761
- })
13762
- );
13763
- const { apiClient, organizationId } = await createResolvedApiClient({
13764
- context,
13765
- baseUrlFlagValue: argv.baseUrl,
13766
- organizationFlagValue: argv.organization,
13767
- profileName: argv.profile
13768
- });
13769
- let result;
13770
- try {
13771
- result = await apiClient.agentBulkUpdateSheets(input);
13772
- } catch (error) {
13773
- throw mapApiErrorToCliError(error);
13774
- }
13775
- context.output.print(
13776
- {
13777
- request: {
13778
- ...input,
13779
- organizationId: organizationId ?? null
13780
- },
13781
- ...result
13782
- },
13783
- (output) => formatBulkUpdateSheetsOutput(output),
13784
- { command: "sheet bulk-update" }
13785
- );
15075
+ () => {
13786
15076
  }
13787
15077
  ).command(
13788
15078
  "import [sources..]",
@@ -14172,84 +15462,6 @@ var registerSheetCommands = (sheetYargs, context) => {
14172
15462
  ).demandCommand(1).strict();
14173
15463
  };
14174
15464
 
14175
- // src/lib/output.ts
14176
- var cliJsonSchemaVersion = "v1";
14177
- var ensureTrailingNewline = (value) => {
14178
- return value.endsWith("\n") ? value : `${value}
14179
- `;
14180
- };
14181
- var writeLine = (writer, value) => {
14182
- writer.write(ensureTrailingNewline(value));
14183
- };
14184
- var createOutput = ({
14185
- json,
14186
- stdout,
14187
- stderr
14188
- }) => {
14189
- let warnings = [];
14190
- const consumeWarnings = () => {
14191
- const nextWarnings = warnings;
14192
- warnings = [];
14193
- return nextWarnings;
14194
- };
14195
- return {
14196
- json,
14197
- print: (value, humanFormatter, options) => {
14198
- if (json) {
14199
- writeLine(
14200
- stdout,
14201
- JSON.stringify(
14202
- {
14203
- schemaVersion: cliJsonSchemaVersion,
14204
- command: options.command,
14205
- ok: options.ok ?? true,
14206
- result: value,
14207
- ...options.error ? { error: options.error } : {},
14208
- warnings: consumeWarnings()
14209
- },
14210
- null,
14211
- 2
14212
- )
14213
- );
14214
- return;
14215
- }
14216
- const message = typeof humanFormatter === "function" ? humanFormatter(value) : humanFormatter;
14217
- writeLine(stdout, message);
14218
- },
14219
- printError: ({ command, error, result }) => {
14220
- if (json) {
14221
- writeLine(
14222
- stdout,
14223
- JSON.stringify(
14224
- {
14225
- schemaVersion: cliJsonSchemaVersion,
14226
- command,
14227
- ok: false,
14228
- ...result === void 0 ? {} : { result },
14229
- error,
14230
- warnings: consumeWarnings()
14231
- },
14232
- null,
14233
- 2
14234
- )
14235
- );
14236
- return;
14237
- }
14238
- writeLine(stderr, formatSerializableCliErrorForHuman(error));
14239
- },
14240
- info: (message) => {
14241
- if (json) {
14242
- warnings.push(message);
14243
- return;
14244
- }
14245
- writeLine(stderr, message);
14246
- },
14247
- error: (message) => {
14248
- writeLine(stderr, message);
14249
- }
14250
- };
14251
- };
14252
-
14253
15465
  // src/lib/prompt-secret.ts
14254
15466
  import { createInterface } from "node:readline/promises";
14255
15467
  import { Writable } from "node:stream";
@@ -14287,11 +15499,13 @@ var createPromptSecret = ({
14287
15499
  };
14288
15500
 
14289
15501
  // src/bin/chalksurf.ts
15502
+ var getHelpWidth = (stdout) => stdout.columns === void 0 ? null : Math.min(120, Math.max(80, stdout.columns));
14290
15503
  var packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
14291
15504
  var createCli = ({
14292
15505
  cwd,
14293
15506
  configPath,
14294
15507
  env,
15508
+ helpWidth,
14295
15509
  now,
14296
15510
  output,
14297
15511
  sleep,
@@ -14308,7 +15522,7 @@ var createCli = ({
14308
15522
  sleep,
14309
15523
  stdin
14310
15524
  };
14311
- 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", {
14312
15526
  type: "boolean",
14313
15527
  default: false,
14314
15528
  describe: "Write machine-readable JSON to stdout"
@@ -14352,6 +15566,12 @@ var createCli = ({
14352
15566
  (exerciseYargs) => registerExerciseCommands(exerciseYargs, commandContext),
14353
15567
  () => {
14354
15568
  }
15569
+ ).command(
15570
+ "quality-issue <subcommand>",
15571
+ "Exercise quality issue review commands",
15572
+ (qualityIssueYargs) => registerQualityIssueCommands(qualityIssueYargs, commandContext),
15573
+ () => {
15574
+ }
14355
15575
  ).command(
14356
15576
  "feedback <subcommand>",
14357
15577
  "Feedback commands",
@@ -14391,7 +15611,7 @@ var parseCliWithCapturedOutput = async ({
14391
15611
  argv,
14392
15612
  stdout
14393
15613
  }) => {
14394
- await new Promise((resolve7, reject) => {
15614
+ await new Promise((resolve8, reject) => {
14395
15615
  cli.parse(argv, (error, _parsedArgv, output) => {
14396
15616
  if (output) {
14397
15617
  stdout.write(output);
@@ -14400,14 +15620,24 @@ var parseCliWithCapturedOutput = async ({
14400
15620
  reject(error);
14401
15621
  return;
14402
15622
  }
14403
- resolve7();
15623
+ resolve8();
14404
15624
  });
14405
15625
  });
14406
15626
  };
14407
15627
  var hasFlag = (argv, flags) => {
14408
15628
  return argv.some((argument) => flags.includes(argument));
14409
15629
  };
14410
- 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
+ ]);
14411
15641
  var hasTopLevelCommand = (argv) => {
14412
15642
  return argv.some((argument) => topLevelCommands.has(argument));
14413
15643
  };
@@ -14417,7 +15647,17 @@ var resolveCommandName = (argv) => {
14417
15647
  if (!topLevelCommands.has(argument)) {
14418
15648
  continue;
14419
15649
  }
14420
- 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
+ }
14421
15661
  return subcommand ? `${argument} ${subcommand}` : argument;
14422
15662
  }
14423
15663
  if (hasFlag(argv, ["--version", "-v"])) {
@@ -14431,7 +15671,7 @@ var runCli = async ({
14431
15671
  configPath,
14432
15672
  env = process.env,
14433
15673
  nowImpl = Date.now,
14434
- sleepImpl = (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
15674
+ sleepImpl = (milliseconds) => new Promise((resolve8) => setTimeout(resolve8, milliseconds)),
14435
15675
  stdin = process.stdin,
14436
15676
  stdout = process.stdout,
14437
15677
  stderr = process.stderr
@@ -14446,6 +15686,7 @@ var runCli = async ({
14446
15686
  cwd,
14447
15687
  configPath,
14448
15688
  env,
15689
+ helpWidth: getHelpWidth(stdout),
14449
15690
  now: nowImpl,
14450
15691
  output,
14451
15692
  sleep: sleepImpl,