@mytegroupinc/myte-core 0.0.49 → 0.0.51

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,205 @@ 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 staleProposalTitle = `${namespace} stale local proposal`;
732
+ const staleDraft = runCli([
733
+ "feedback",
734
+ "edit",
735
+ "--feedback-id",
736
+ staleGuardFeedbackId,
737
+ "--title",
738
+ staleProposalTitle,
739
+ "--reason",
740
+ "Harness creates this artifact before a remote revision.",
741
+ "--json",
742
+ ...baseArgs,
743
+ ], workspace).stdout;
744
+ assert(staleDraft.artifact_path, "Stale-edit guard did not create its initial local artifact.");
745
+
746
+ const staleRemoteMove = runCli([
747
+ "feedback",
748
+ "move",
749
+ "--feedback-id",
750
+ staleGuardFeedbackId,
751
+ "--from-state",
752
+ "todo",
753
+ "--to-state",
754
+ "in_progress",
755
+ "--reason",
756
+ "Harness advances the server revision after the local artifact was created.",
757
+ "--no-sync",
758
+ "--json",
759
+ ...baseArgs,
760
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
761
+ const staleRemoteMoveEventId = responseEventId(staleRemoteMove);
762
+ assert(staleRemoteMoveEventId, "Stale-edit guard move did not return an event id.");
763
+
764
+ const staleValidation = runCli([
765
+ "feedback",
766
+ "validate",
767
+ "--file",
768
+ staleDraft.artifact_path,
769
+ "--json",
770
+ ...baseArgs,
771
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
772
+ assert(staleValidation.ok === true && staleValidation.valid === false, "A stale local Feedback artifact unexpectedly validated.");
773
+ assert(
774
+ (staleValidation.errors || []).some((error) => error?.code === "snapshot_mismatch"),
775
+ `Stale artifact did not return snapshot_mismatch: ${JSON.stringify(staleValidation, null, 2)}`,
776
+ );
777
+
778
+ const staleGuardUndo = runCli([
779
+ "feedback",
780
+ "undo",
781
+ "--feedback-id",
782
+ staleGuardFeedbackId,
783
+ "--event-id",
784
+ staleRemoteMoveEventId,
785
+ "--reason",
786
+ "Harness restores the disposable card after stale-artifact certification.",
787
+ "--no-sync",
788
+ "--json",
789
+ ...baseArgs,
790
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
791
+ assert(
792
+ String(staleGuardUndo.feedback_state || "") === "todo",
793
+ `Stale-edit guard cleanup did not restore todo: ${JSON.stringify(staleGuardUndo, null, 2)}`,
794
+ );
795
+
796
+ const forcedStaleApply = runCli([
797
+ "feedback",
798
+ "apply",
799
+ "--file",
800
+ staleDraft.artifact_path,
801
+ "--force",
802
+ "--no-sync",
803
+ "--json",
804
+ ...baseArgs,
805
+ ], workspace, { apiKey: ownerApiKey }).stdout;
806
+ assert(
807
+ String(forcedStaleApply.feedback?.title || "") === staleProposalTitle,
808
+ `Owner forced stale override did not apply the intended title: ${JSON.stringify(forcedStaleApply, null, 2)}`,
809
+ );
810
+ assert(
811
+ (forcedStaleApply.warnings || []).some(
812
+ (warning) => warning?.code === "snapshot_mismatch_forced",
813
+ ),
814
+ `Owner forced stale override did not report snapshot_mismatch_forced: ${JSON.stringify(forcedStaleApply, null, 2)}`,
815
+ );
816
+ assert(
817
+ forcedStaleApply.history?.force === true
818
+ && Boolean(String(forcedStaleApply.history?.reason || "").trim()),
819
+ `Owner forced stale override was not explicitly audited with a reason: ${JSON.stringify(forcedStaleApply, null, 2)}`,
820
+ );
821
+
822
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
823
+ const freshDraft = runCli([
824
+ "feedback",
825
+ "edit",
826
+ "--feedback-id",
827
+ staleGuardFeedbackId,
828
+ "--title",
829
+ `${namespace} fresh proposal after resync`,
830
+ "--reason",
831
+ "Harness confirms a fresh artifact can replace the rejected stale proposal.",
832
+ "--json",
833
+ ...baseArgs,
834
+ ], workspace).stdout;
835
+ const freshValidation = runCli([
836
+ "feedback",
837
+ "validate",
838
+ "--file",
839
+ freshDraft.artifact_path,
840
+ "--json",
841
+ ...baseArgs,
842
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
843
+ assert(
844
+ freshValidation.ok === true && freshValidation.valid !== false,
845
+ `Fresh artifact did not validate after resync: ${JSON.stringify(freshValidation, null, 2)}`,
846
+ );
847
+
848
+ if (collaboratorApiKey) {
849
+ const directApplyDenied = runCli([
850
+ "feedback",
851
+ "apply",
852
+ "--file",
853
+ freshDraft.artifact_path,
854
+ "--no-sync",
855
+ "--json",
856
+ ...baseArgs,
857
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
858
+ roleMatrix.collaborator_direct_apply_denied =
859
+ directApplyDenied.ok === false && directApplyDenied.status === 403;
860
+ assert(
861
+ roleMatrix.collaborator_direct_apply_denied,
862
+ `Regular collaborator direct apply was not denied: ${JSON.stringify(directApplyDenied, null, 2)}`,
863
+ );
864
+
865
+ const collaboratorArchiveDenied = runCli([
866
+ "feedback",
867
+ "move",
868
+ "--feedback-id",
869
+ staleGuardFeedbackId,
870
+ "--from-state",
871
+ "todo",
872
+ "--to-state",
873
+ "archived",
874
+ "--reason",
875
+ "Harness expects regular collaborator archive to be denied.",
876
+ "--no-sync",
877
+ "--json",
878
+ ...baseArgs,
879
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
880
+ roleMatrix.collaborator_archive_denied =
881
+ collaboratorArchiveDenied.ok === false && collaboratorArchiveDenied.status === 403;
882
+ assert(
883
+ roleMatrix.collaborator_archive_denied,
884
+ `Regular collaborator archive was not denied: ${JSON.stringify(collaboratorArchiveDenied, null, 2)}`,
885
+ );
886
+ }
538
887
 
539
888
  const directMove = runCli([
540
889
  "feedback",
@@ -819,11 +1168,38 @@ function main() {
819
1168
  assert(blocked.status === 409, `Expected 409 pending review block, got ${blocked.status}.`);
820
1169
  assert(blocked.data?.code === "pending_feedback_review", `Expected pending_feedback_review, got ${JSON.stringify(blocked, null, 2)}.`);
821
1170
 
1171
+ const ownerApproval = runCli([
1172
+ "feedback",
1173
+ "review",
1174
+ "--request-id",
1175
+ reviewRequestIds[0],
1176
+ "--action",
1177
+ "approve",
1178
+ "--reason",
1179
+ "Harness owner approval.",
1180
+ "--verification-evidence",
1181
+ "Stale-state, validation, role denial, request-changes, and revision checks passed.",
1182
+ "--deployment-evidence",
1183
+ "Dev post-deploy certification run; artifacts will be archived.",
1184
+ "--no-sync",
1185
+ "--json",
1186
+ ...baseArgs,
1187
+ ], workspace, { apiKey: ownerApiKey }).stdout;
1188
+ roleMatrix.owner_review_used = true;
1189
+ assert(
1190
+ String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
1191
+ `Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
1192
+ );
1193
+
1194
+ const batchApprovalRequestIds = [
1195
+ reviewRequestIds[3],
1196
+ reviewRequestIds[4],
1197
+ ];
822
1198
  const review = runCli([
823
1199
  "feedback",
824
1200
  "review",
825
1201
  "--request-ids",
826
- pendingApprovalRequestIds.join(","),
1202
+ batchApprovalRequestIds.join(","),
827
1203
  "--action",
828
1204
  "approve",
829
1205
  "--reason",
@@ -836,7 +1212,7 @@ function main() {
836
1212
  ...baseArgs,
837
1213
  ], workspace, { apiKey: operationalReviewKey }).stdout;
838
1214
  roleMatrix.delegate_review_used = Boolean(delegateApiKey);
839
- assert(review.ok !== false && review.updated_count === 3, `batch review failed: ${JSON.stringify(review, null, 2)}`);
1215
+ assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
840
1216
 
841
1217
  runCli(["feedback-sync", "--json", ...baseArgs], workspace);
842
1218
  assertFeedbackPresent(
@@ -1086,14 +1462,35 @@ function main() {
1086
1462
 
1087
1463
  if (roleMatrix.required) {
1088
1464
  assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
1465
+ assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
1089
1466
  assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
1467
+ assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
1468
+ assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
1469
+ assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
1090
1470
  assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
1091
1471
  }
1472
+ if (args["require-comment-attachment-sync"]) {
1473
+ assert(
1474
+ commentAttachmentSyncVerified,
1475
+ "Web-created Feedback comment attachment did not survive Project Assistant sync.",
1476
+ );
1477
+ }
1092
1478
  manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
1093
1479
  id: scenario.id,
1094
- status:
1095
- scenario.id === "owner-delegate-collaborator-matrix" && !roleMatrix.required
1096
- ? "optional_not_requested"
1480
+ status: scenario.id === "owner-delegate-collaborator-matrix"
1481
+ ? (
1482
+ roleMatrix.required
1483
+ ? "passed"
1484
+ : delegateApiKey && collaboratorApiKey
1485
+ ? "passed_optional"
1486
+ : "optional_not_requested"
1487
+ )
1488
+ : scenario.id === "feedback-comment-attachment-sync"
1489
+ ? (
1490
+ commentAttachmentSyncVerified
1491
+ ? "passed"
1492
+ : "optional_not_requested"
1493
+ )
1097
1494
  : "passed",
1098
1495
  checked_at: new Date().toISOString(),
1099
1496
  }));
@@ -1114,6 +1511,10 @@ function main() {
1114
1511
  review_request_ids: reviewRequestIds,
1115
1512
  role_matrix: roleMatrix,
1116
1513
  comment_verified: true,
1514
+ comment_sync_roundtrip_verified: true,
1515
+ comment_attachment_sync_verified: commentAttachmentSyncVerified,
1516
+ stale_edit_rejection_and_resync_verified: true,
1517
+ forced_stale_override_verified: true,
1117
1518
  move_undo_history_verified: true,
1118
1519
  validation_verified: true,
1119
1520
  request_changes_revision_verified: true,
@@ -1180,4 +1581,10 @@ function main() {
1180
1581
  }
1181
1582
  }
1182
1583
 
1183
- main();
1584
+ main().catch((err) => {
1585
+ console.error(JSON.stringify({
1586
+ ok: false,
1587
+ message: err?.message || String(err),
1588
+ }, null, 2));
1589
+ process.exitCode = 1;
1590
+ });