@mytegroupinc/myte-core 0.0.49 → 0.0.50

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.
@@ -2,14 +2,15 @@
2
2
  "use strict";
3
3
 
4
4
  const fs = require("node:fs");
5
- const os = require("node:os");
6
- const path = require("node:path");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
7
  const { spawnSync } = require("node:child_process");
8
8
  const {
9
9
  createCertificationManifest,
10
10
  recordCertificationArtifact,
11
11
  validateCertificationManifest,
12
12
  } = require("../lib/certification-manifest");
13
+ const { readYamlFile } = require("../cli");
13
14
 
14
15
  const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
15
16
  const LIVE_SCENARIO_MATRIX = [
@@ -20,6 +21,15 @@ const LIVE_SCENARIO_MATRIX = [
20
21
  { id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
21
22
  { id: "feedback-get-history", kind: "read", mutation: "none", required: true },
22
23
  { id: "feedback-comment", kind: "write", mutation: "test_comment", required: true },
24
+ { id: "feedback-comment-sync-roundtrip", kind: "read/local-sync", mutation: "local_temp_only", required: true },
25
+ {
26
+ id: "feedback-comment-attachment-sync",
27
+ kind: "web-write/read/local-sync",
28
+ mutation: "test_comment_attachment",
29
+ required: "when --require-comment-attachment-sync is set",
30
+ },
31
+ { id: "feedback-stale-edit-guard", kind: "conflict-safety", mutation: "test_feedback", required: true },
32
+ { id: "feedback-forced-stale-override", kind: "owner-conflict-override", mutation: "test_feedback", required: true },
23
33
  { id: "feedback-move-undo", kind: "governed-write", mutation: "test_feedback", required: true },
24
34
  { id: "feedback-validate-submit", kind: "review", mutation: "test_review", required: true },
25
35
  { id: "feedback-review-list-detail", kind: "read", mutation: "none", required: true },
@@ -74,6 +84,10 @@ function requireConfirm(args) {
74
84
 
75
85
  function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
76
86
  const env = { ...process.env };
87
+ delete env.MYTE_OWNER_API_KEY;
88
+ delete env.MYTE_DELEGATE_API_KEY;
89
+ delete env.MYTE_COLLABORATOR_API_KEY;
90
+ delete env.MYTE_WEB_ACCESS_TOKEN;
77
91
  if (apiKey) {
78
92
  env.MYTE_API_KEY = apiKey;
79
93
  env.MYTE_PROJECT_API_KEY = apiKey;
@@ -99,8 +113,75 @@ function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
99
113
  result.stderr || stdout || "(no output)",
100
114
  ].join("\n"));
101
115
  }
102
- return { status: result.status, stdout: parsed, stderr: String(result.stderr || "").trim() };
103
- }
116
+ return { status: result.status, stdout: parsed, stderr: String(result.stderr || "").trim() };
117
+ }
118
+
119
+ function resolveHarnessApiBase(args) {
120
+ const raw = String(
121
+ args["base-url"]
122
+ || process.env.MYTE_API_BASE
123
+ || "https://api.myte.dev",
124
+ ).trim().replace(/\/+$/, "");
125
+ return raw.endsWith("/api") ? raw : `${raw}/api`;
126
+ }
127
+
128
+ async function createWebFeedbackCommentWithAttachment({
129
+ apiBase,
130
+ accessToken,
131
+ feedbackId,
132
+ projectId,
133
+ namespace,
134
+ timeoutMs,
135
+ }) {
136
+ const attachmentName = `${namespace}-comment-context.md`;
137
+ const attachmentContent = [
138
+ `# ${namespace} Comment Attachment`,
139
+ "",
140
+ "This disposable document proves that a web-created Feedback comment attachment",
141
+ "remains associated with its comment and becomes readable after Project Assistant sync.",
142
+ "",
143
+ ].join("\n");
144
+ const controller = new AbortController();
145
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
146
+ let response;
147
+ let body;
148
+ try {
149
+ response = await fetch(`${apiBase}/feedbacks/${feedbackId}/comments`, {
150
+ method: "POST",
151
+ headers: {
152
+ Authorization: `Bearer ${accessToken}`,
153
+ "Content-Type": "application/json",
154
+ },
155
+ body: JSON.stringify({
156
+ project_id: projectId,
157
+ content: `${namespace} web attachment sync certification comment.`,
158
+ attachments: [
159
+ {
160
+ name: attachmentName,
161
+ data: `data:text/markdown;base64,${Buffer.from(attachmentContent, "utf8").toString("base64")}`,
162
+ size: Buffer.byteLength(attachmentContent, "utf8"),
163
+ type: "text/markdown",
164
+ },
165
+ ],
166
+ }),
167
+ signal: controller.signal,
168
+ });
169
+ body = await response.json();
170
+ } finally {
171
+ clearTimeout(timeout);
172
+ }
173
+ if (!response.ok || body?.status !== "success" || !body?.data?.comment_id) {
174
+ throw new Error(
175
+ `Web Feedback comment attachment fixture failed (${response.status}): ${body?.message || "unknown response"}`,
176
+ );
177
+ }
178
+ return {
179
+ commentId: String(body.data.comment_id),
180
+ commentContent: String(body.data.content || ""),
181
+ attachmentName,
182
+ attachmentContent,
183
+ };
184
+ }
104
185
 
105
186
  function assert(condition, message) {
106
187
  if (!condition) {
@@ -221,22 +302,23 @@ function writeCertificationManifest(manifestPath, manifest) {
221
302
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
222
303
  }
223
304
 
224
- function loadFeedbackManifest(workspace) {
305
+ function loadFeedbackManifest(workspace) {
225
306
  const manifestPath = path.join(workspace, "MyteCommandCenter", "data", "feedback.yml");
226
307
  if (!fs.existsSync(manifestPath)) {
227
308
  return { items: [] };
228
- }
229
- const text = fs.readFileSync(manifestPath, "utf8");
230
- try {
231
- const parsed = JSON.parse(text);
232
- const items = []
233
- .concat(Array.isArray(parsed.items) ? parsed.items : [])
234
- .concat(Array.isArray(parsed.queue) ? parsed.queue : []);
235
- return { items };
236
- } catch (_err) {
237
- // Older local files may still be YAML-like. Keep a small fallback parser so
238
- // the harness can validate either shape without adding runtime deps.
239
- }
309
+ }
310
+ try {
311
+ const parsed = readYamlFile(manifestPath);
312
+ if (parsed && typeof parsed === "object") {
313
+ return {
314
+ ...parsed,
315
+ items: Array.isArray(parsed.items) ? parsed.items : [],
316
+ };
317
+ }
318
+ } catch (_err) {
319
+ // Keep a small fallback parser for a partially written legacy snapshot.
320
+ }
321
+ const text = fs.readFileSync(manifestPath, "utf8");
240
322
  const items = [];
241
323
  let current = null;
242
324
  for (const line of text.split(/\r?\n/)) {
@@ -258,8 +340,62 @@ function loadFeedbackManifest(workspace) {
258
340
  }
259
341
  }
260
342
  if (current) items.push(current);
261
- return { items };
262
- }
343
+ return { items };
344
+ }
345
+
346
+ function findFeedbackItem(workspace, feedbackId) {
347
+ const manifest = loadFeedbackManifest(workspace);
348
+ return (manifest.items || []).find(
349
+ (item) => String(item?.feedback_id || item?.id || "") === String(feedbackId),
350
+ ) || null;
351
+ }
352
+
353
+ function assertSynchronizedComment({
354
+ workspace,
355
+ feedbackId,
356
+ commentId,
357
+ expectedContent,
358
+ expectedAttachment,
359
+ }) {
360
+ const item = findFeedbackItem(workspace, feedbackId);
361
+ assert(item, `Feedback ${feedbackId} was missing while validating synchronized comments.`);
362
+ const turn = (item.conversation_turns || []).find(
363
+ (candidate) => String(candidate?.comment_id || "") === String(commentId),
364
+ );
365
+ assert(turn, `Comment ${commentId} was not linked to Feedback ${feedbackId} after sync.`);
366
+ assert(
367
+ String(turn.content || "").includes(expectedContent),
368
+ `Comment ${commentId} did not preserve its expected content after sync.`,
369
+ );
370
+
371
+ if (!expectedAttachment) return;
372
+ const attachment = (turn.attachments || []).find(
373
+ (candidate) => String(candidate?.name || "") === expectedAttachment.name,
374
+ );
375
+ assert(
376
+ attachment,
377
+ `Comment ${commentId} did not preserve attachment ${expectedAttachment.name}.`,
378
+ );
379
+ assert(
380
+ attachment.context_status === "readable",
381
+ `Comment attachment ${expectedAttachment.name} was not synchronized as readable context.`,
382
+ );
383
+ assert(
384
+ attachment.local_file,
385
+ `Comment attachment ${expectedAttachment.name} did not receive a local file reference.`,
386
+ );
387
+ const commandCenterRoot = path.resolve(workspace, "MyteCommandCenter");
388
+ const localPath = path.resolve(commandCenterRoot, String(attachment.local_file));
389
+ assert(
390
+ localPath.startsWith(`${commandCenterRoot}${path.sep}`),
391
+ `Comment attachment local path escaped MyteCommandCenter: ${attachment.local_file}`,
392
+ );
393
+ assert(fs.existsSync(localPath), `Synchronized comment attachment file is missing: ${localPath}`);
394
+ assert(
395
+ fs.readFileSync(localPath, "utf8").includes(expectedAttachment.contentMarker),
396
+ `Synchronized comment attachment ${expectedAttachment.name} lost its readable content.`,
397
+ );
398
+ }
263
399
 
264
400
  function findFeedbackState(workspace, feedbackId) {
265
401
  const manifest = loadFeedbackManifest(workspace);
@@ -294,7 +430,7 @@ function assertFeedbackAbsent(workspace, feedbackIds) {
294
430
  }
295
431
  }
296
432
 
297
- function main() {
433
+ async function main() {
298
434
  const args = parseArgs(process.argv.slice(2));
299
435
  if (!args["confirm-live"]) {
300
436
  console.log(JSON.stringify({
@@ -303,11 +439,13 @@ function main() {
303
439
  message: "No network, project mutation, or local certification artifact was created.",
304
440
  required_live_flag: "--confirm-live",
305
441
  optional_strict_role_flag: "--require-role-matrix",
442
+ optional_comment_attachment_flag: "--require-comment-attachment-sync",
306
443
  required_role_environment: [
307
444
  "MYTE_OWNER_API_KEY",
308
445
  "MYTE_DELEGATE_API_KEY",
309
446
  "MYTE_COLLABORATOR_API_KEY",
310
447
  ],
448
+ comment_attachment_environment: "MYTE_WEB_ACCESS_TOKEN",
311
449
  scenario_matrix: LIVE_SCENARIO_MATRIX,
312
450
  }, null, 2));
313
451
  return;
@@ -322,6 +460,7 @@ function main() {
322
460
  ).trim();
323
461
  const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
324
462
  const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
463
+ const webAccessToken = String(process.env.MYTE_WEB_ACCESS_TOKEN || "").trim();
325
464
  process.env.MYTE_API_KEY = ownerApiKey;
326
465
  process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
327
466
  if (
@@ -332,6 +471,11 @@ function main() {
332
471
  "--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
333
472
  );
334
473
  }
474
+ if (args["require-comment-attachment-sync"] && !webAccessToken) {
475
+ throw new Error(
476
+ "--require-comment-attachment-sync requires an ephemeral MYTE_WEB_ACCESS_TOKEN for a user assigned to the target project.",
477
+ );
478
+ }
335
479
 
336
480
  const workspace = path.resolve(
337
481
  args.workspace || fs.mkdtempSync(path.join(os.tmpdir(), "myte-feedback-full-harness-")),
@@ -359,8 +503,13 @@ function main() {
359
503
  collaborator: Boolean(collaboratorApiKey),
360
504
  same_project: true,
361
505
  collaborator_review_denied: null,
506
+ collaborator_direct_apply_denied: null,
507
+ collaborator_archive_denied: null,
508
+ collaborator_writes_used: false,
509
+ owner_review_used: false,
362
510
  delegate_review_used: false,
363
511
  };
512
+ let commentAttachmentSyncVerified = false;
364
513
  const manifestPath = path.resolve(
365
514
  args.manifest
366
515
  || path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
@@ -507,6 +656,7 @@ function main() {
507
656
 
508
657
  const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
509
658
  const operationalReviewKey = delegateApiKey || ownerApiKey;
659
+ roleMatrix.collaborator_writes_used = Boolean(collaboratorApiKey);
510
660
  const commentApprovalPath = writeCommentApproval(
511
661
  workspace,
512
662
  runId,
@@ -535,6 +685,204 @@ function main() {
535
685
  cleanup: "retained_with_archived_parent",
536
686
  },
537
687
  });
688
+ let webAttachmentFixture = null;
689
+ if (webAccessToken) {
690
+ webAttachmentFixture = await createWebFeedbackCommentWithAttachment({
691
+ apiBase: resolveHarnessApiBase(args),
692
+ accessToken: webAccessToken,
693
+ feedbackId: createdFeedbackIds[0],
694
+ projectId: manifest.project_id,
695
+ namespace,
696
+ timeoutMs: Number(args["timeout-ms"] || 60000),
697
+ });
698
+ recordCertificationArtifact(manifest, {
699
+ kind: "comment",
700
+ id: webAttachmentFixture.commentId,
701
+ metadata: {
702
+ feedback_id: createdFeedbackIds[0],
703
+ attachment_name: webAttachmentFixture.attachmentName,
704
+ cleanup: "retained_with_archived_parent",
705
+ },
706
+ });
707
+ }
708
+
709
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
710
+ assertSynchronizedComment({
711
+ workspace,
712
+ feedbackId: createdFeedbackIds[0],
713
+ commentId: String(comment.comment_id),
714
+ expectedContent: runId,
715
+ });
716
+ if (webAttachmentFixture) {
717
+ assertSynchronizedComment({
718
+ workspace,
719
+ feedbackId: createdFeedbackIds[0],
720
+ commentId: webAttachmentFixture.commentId,
721
+ expectedContent: namespace,
722
+ expectedAttachment: {
723
+ name: webAttachmentFixture.attachmentName,
724
+ contentMarker: `${namespace} Comment Attachment`,
725
+ },
726
+ });
727
+ commentAttachmentSyncVerified = true;
728
+ }
729
+
730
+ const staleGuardFeedbackId = createdFeedbackIds[4];
731
+ const staleDraft = runCli([
732
+ "feedback",
733
+ "edit",
734
+ "--feedback-id",
735
+ staleGuardFeedbackId,
736
+ "--title",
737
+ `${namespace} stale local proposal`,
738
+ "--reason",
739
+ "Harness creates this artifact before a remote revision.",
740
+ "--json",
741
+ ...baseArgs,
742
+ ], workspace).stdout;
743
+ assert(staleDraft.artifact_path, "Stale-edit guard did not create its initial local artifact.");
744
+
745
+ const staleRemoteMove = runCli([
746
+ "feedback",
747
+ "move",
748
+ "--feedback-id",
749
+ staleGuardFeedbackId,
750
+ "--from-state",
751
+ "todo",
752
+ "--to-state",
753
+ "in_progress",
754
+ "--reason",
755
+ "Harness advances the server revision after the local artifact was created.",
756
+ "--no-sync",
757
+ "--json",
758
+ ...baseArgs,
759
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
760
+ const staleRemoteMoveEventId = responseEventId(staleRemoteMove);
761
+ assert(staleRemoteMoveEventId, "Stale-edit guard move did not return an event id.");
762
+
763
+ const staleValidation = runCli([
764
+ "feedback",
765
+ "validate",
766
+ "--file",
767
+ staleDraft.artifact_path,
768
+ "--json",
769
+ ...baseArgs,
770
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
771
+ assert(staleValidation.ok === true && staleValidation.valid === false, "A stale local Feedback artifact unexpectedly validated.");
772
+ assert(
773
+ (staleValidation.errors || []).some((error) => error?.code === "snapshot_mismatch"),
774
+ `Stale artifact did not return snapshot_mismatch: ${JSON.stringify(staleValidation, null, 2)}`,
775
+ );
776
+
777
+ const staleGuardUndo = runCli([
778
+ "feedback",
779
+ "undo",
780
+ "--feedback-id",
781
+ staleGuardFeedbackId,
782
+ "--event-id",
783
+ staleRemoteMoveEventId,
784
+ "--reason",
785
+ "Harness restores the disposable card after stale-artifact certification.",
786
+ "--no-sync",
787
+ "--json",
788
+ ...baseArgs,
789
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
790
+ assert(
791
+ String(staleGuardUndo.feedback_state || "") === "todo",
792
+ `Stale-edit guard cleanup did not restore todo: ${JSON.stringify(staleGuardUndo, null, 2)}`,
793
+ );
794
+
795
+ const forcedStaleApply = runCli([
796
+ "feedback",
797
+ "apply",
798
+ "--file",
799
+ staleDraft.artifact_path,
800
+ "--force",
801
+ "--no-sync",
802
+ "--json",
803
+ ...baseArgs,
804
+ ], workspace, { apiKey: ownerApiKey }).stdout;
805
+ assert(
806
+ String(forcedStaleApply.feedback?.title || "") === `${namespace} stale proposal`,
807
+ `Owner forced stale override did not apply the intended title: ${JSON.stringify(forcedStaleApply, null, 2)}`,
808
+ );
809
+ assert(
810
+ (forcedStaleApply.warnings || []).some(
811
+ (warning) => warning?.code === "snapshot_mismatch_forced",
812
+ ),
813
+ `Owner forced stale override did not report snapshot_mismatch_forced: ${JSON.stringify(forcedStaleApply, null, 2)}`,
814
+ );
815
+ assert(
816
+ forcedStaleApply.history?.force === true
817
+ && Boolean(String(forcedStaleApply.history?.reason || "").trim()),
818
+ `Owner forced stale override was not explicitly audited with a reason: ${JSON.stringify(forcedStaleApply, null, 2)}`,
819
+ );
820
+
821
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
822
+ const freshDraft = runCli([
823
+ "feedback",
824
+ "edit",
825
+ "--feedback-id",
826
+ staleGuardFeedbackId,
827
+ "--title",
828
+ `${namespace} fresh proposal after resync`,
829
+ "--reason",
830
+ "Harness confirms a fresh artifact can replace the rejected stale proposal.",
831
+ "--json",
832
+ ...baseArgs,
833
+ ], workspace).stdout;
834
+ const freshValidation = runCli([
835
+ "feedback",
836
+ "validate",
837
+ "--file",
838
+ freshDraft.artifact_path,
839
+ "--json",
840
+ ...baseArgs,
841
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
842
+ assert(
843
+ freshValidation.ok === true && freshValidation.valid !== false,
844
+ `Fresh artifact did not validate after resync: ${JSON.stringify(freshValidation, null, 2)}`,
845
+ );
846
+
847
+ if (collaboratorApiKey) {
848
+ const directApplyDenied = runCli([
849
+ "feedback",
850
+ "apply",
851
+ "--file",
852
+ freshDraft.artifact_path,
853
+ "--no-sync",
854
+ "--json",
855
+ ...baseArgs,
856
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
857
+ roleMatrix.collaborator_direct_apply_denied =
858
+ directApplyDenied.ok === false && directApplyDenied.status === 403;
859
+ assert(
860
+ roleMatrix.collaborator_direct_apply_denied,
861
+ `Regular collaborator direct apply was not denied: ${JSON.stringify(directApplyDenied, null, 2)}`,
862
+ );
863
+
864
+ const collaboratorArchiveDenied = runCli([
865
+ "feedback",
866
+ "move",
867
+ "--feedback-id",
868
+ staleGuardFeedbackId,
869
+ "--from-state",
870
+ "todo",
871
+ "--to-state",
872
+ "archived",
873
+ "--reason",
874
+ "Harness expects regular collaborator archive to be denied.",
875
+ "--no-sync",
876
+ "--json",
877
+ ...baseArgs,
878
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
879
+ roleMatrix.collaborator_archive_denied =
880
+ collaboratorArchiveDenied.ok === false && collaboratorArchiveDenied.status === 403;
881
+ assert(
882
+ roleMatrix.collaborator_archive_denied,
883
+ `Regular collaborator archive was not denied: ${JSON.stringify(collaboratorArchiveDenied, null, 2)}`,
884
+ );
885
+ }
538
886
 
539
887
  const directMove = runCli([
540
888
  "feedback",
@@ -819,11 +1167,38 @@ function main() {
819
1167
  assert(blocked.status === 409, `Expected 409 pending review block, got ${blocked.status}.`);
820
1168
  assert(blocked.data?.code === "pending_feedback_review", `Expected pending_feedback_review, got ${JSON.stringify(blocked, null, 2)}.`);
821
1169
 
1170
+ const ownerApproval = runCli([
1171
+ "feedback",
1172
+ "review",
1173
+ "--request-id",
1174
+ reviewRequestIds[0],
1175
+ "--action",
1176
+ "approve",
1177
+ "--reason",
1178
+ "Harness owner approval.",
1179
+ "--verification-evidence",
1180
+ "Stale-state, validation, role denial, request-changes, and revision checks passed.",
1181
+ "--deployment-evidence",
1182
+ "Dev post-deploy certification run; artifacts will be archived.",
1183
+ "--no-sync",
1184
+ "--json",
1185
+ ...baseArgs,
1186
+ ], workspace, { apiKey: ownerApiKey }).stdout;
1187
+ roleMatrix.owner_review_used = true;
1188
+ assert(
1189
+ String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
1190
+ `Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
1191
+ );
1192
+
1193
+ const batchApprovalRequestIds = [
1194
+ reviewRequestIds[3],
1195
+ reviewRequestIds[4],
1196
+ ];
822
1197
  const review = runCli([
823
1198
  "feedback",
824
1199
  "review",
825
1200
  "--request-ids",
826
- pendingApprovalRequestIds.join(","),
1201
+ batchApprovalRequestIds.join(","),
827
1202
  "--action",
828
1203
  "approve",
829
1204
  "--reason",
@@ -836,7 +1211,7 @@ function main() {
836
1211
  ...baseArgs,
837
1212
  ], workspace, { apiKey: operationalReviewKey }).stdout;
838
1213
  roleMatrix.delegate_review_used = Boolean(delegateApiKey);
839
- assert(review.ok !== false && review.updated_count === 3, `batch review failed: ${JSON.stringify(review, null, 2)}`);
1214
+ assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
840
1215
 
841
1216
  runCli(["feedback-sync", "--json", ...baseArgs], workspace);
842
1217
  assertFeedbackPresent(
@@ -1086,14 +1461,35 @@ function main() {
1086
1461
 
1087
1462
  if (roleMatrix.required) {
1088
1463
  assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
1464
+ assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
1089
1465
  assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
1466
+ assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
1467
+ assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
1468
+ assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
1090
1469
  assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
1091
1470
  }
1471
+ if (args["require-comment-attachment-sync"]) {
1472
+ assert(
1473
+ commentAttachmentSyncVerified,
1474
+ "Web-created Feedback comment attachment did not survive Project Assistant sync.",
1475
+ );
1476
+ }
1092
1477
  manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
1093
1478
  id: scenario.id,
1094
- status:
1095
- scenario.id === "owner-delegate-collaborator-matrix" && !roleMatrix.required
1096
- ? "optional_not_requested"
1479
+ status: scenario.id === "owner-delegate-collaborator-matrix"
1480
+ ? (
1481
+ roleMatrix.required
1482
+ ? "passed"
1483
+ : delegateApiKey && collaboratorApiKey
1484
+ ? "passed_optional"
1485
+ : "optional_not_requested"
1486
+ )
1487
+ : scenario.id === "feedback-comment-attachment-sync"
1488
+ ? (
1489
+ commentAttachmentSyncVerified
1490
+ ? "passed"
1491
+ : "optional_not_requested"
1492
+ )
1097
1493
  : "passed",
1098
1494
  checked_at: new Date().toISOString(),
1099
1495
  }));
@@ -1114,6 +1510,10 @@ function main() {
1114
1510
  review_request_ids: reviewRequestIds,
1115
1511
  role_matrix: roleMatrix,
1116
1512
  comment_verified: true,
1513
+ comment_sync_roundtrip_verified: true,
1514
+ comment_attachment_sync_verified: commentAttachmentSyncVerified,
1515
+ stale_edit_rejection_and_resync_verified: true,
1516
+ forced_stale_override_verified: true,
1117
1517
  move_undo_history_verified: true,
1118
1518
  validation_verified: true,
1119
1519
  request_changes_revision_verified: true,
@@ -1180,4 +1580,10 @@ function main() {
1180
1580
  }
1181
1581
  }
1182
1582
 
1183
- main();
1583
+ main().catch((err) => {
1584
+ console.error(JSON.stringify({
1585
+ ok: false,
1586
+ message: err?.message || String(err),
1587
+ }, null, 2));
1588
+ process.exitCode = 1;
1589
+ });