@mytegroupinc/myte-core 0.0.53 → 0.0.55

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,52 +2,52 @@
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");
7
- const { spawnSync } = require("node:child_process");
8
- const {
9
- createCertificationManifest,
10
- recordCertificationArtifact,
11
- validateCertificationManifest,
12
- } = require("../lib/certification-manifest");
13
- const { readYamlFile } = require("../cli");
14
-
15
- const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
16
- const LIVE_SCENARIO_MATRIX = [
17
- { id: "doctor", kind: "transport", mutation: "none", required: true },
18
- { id: "config", kind: "scope", mutation: "none", required: true },
19
- { id: "create-independent-prds", kind: "write", mutation: "test_feedback", required: true },
20
- { id: "create-three-document-prd", kind: "write", mutation: "test_feedback", required: true },
21
- { id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
22
- { id: "feedback-get-history", kind: "read", mutation: "none", required: true },
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 },
33
- { id: "feedback-move-undo", kind: "governed-write", mutation: "test_feedback", required: true },
34
- { id: "feedback-validate-submit", kind: "review", mutation: "test_review", required: true },
35
- { id: "feedback-review-list-detail", kind: "read", mutation: "none", required: true },
36
- { id: "feedback-request-changes-revise", kind: "review", mutation: "test_review", required: true },
37
- { id: "feedback-reject-cancel", kind: "review", mutation: "test_review", required: true },
38
- { id: "feedback-batch-review", kind: "governed-write", mutation: "test_review", required: true },
39
- { id: "feedback-document-refinement", kind: "review", mutation: "test_feedback", required: true },
40
- { id: "feedback-prd-versions-diff", kind: "read", mutation: "none", required: true },
41
- { id: "feedback-batch-move-archive", kind: "governed-write", mutation: "test_feedback", required: true },
42
- { id: "project-key-unarchive-denial", kind: "negative-permission", mutation: "none", required: true },
43
- {
44
- id: "owner-delegate-collaborator-matrix",
45
- kind: "role-permission",
46
- mutation: "test_feedback",
47
- required: "when --require-role-matrix is set",
48
- },
49
- { id: "exact-id-cleanup", kind: "cleanup", mutation: "archive_test_feedback", required: true },
50
- ];
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+ const { spawnSync } = require("node:child_process");
8
+ const {
9
+ createCertificationManifest,
10
+ recordCertificationArtifact,
11
+ validateCertificationManifest,
12
+ } = require("../lib/certification-manifest");
13
+ const { readYamlFile } = require("../cli");
14
+
15
+ const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
16
+ const LIVE_SCENARIO_MATRIX = [
17
+ { id: "doctor", kind: "transport", mutation: "none", required: true },
18
+ { id: "config", kind: "scope", mutation: "none", required: true },
19
+ { id: "create-independent-prds", kind: "write", mutation: "test_feedback", required: true },
20
+ { id: "create-three-document-prd", kind: "write", mutation: "test_feedback", required: true },
21
+ { id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
22
+ { id: "feedback-get-history", kind: "read", mutation: "none", required: true },
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 },
33
+ { id: "feedback-move-undo", kind: "governed-write", mutation: "test_feedback", required: true },
34
+ { id: "feedback-validate-submit", kind: "review", mutation: "test_review", required: true },
35
+ { id: "feedback-review-list-detail", kind: "read", mutation: "none", required: true },
36
+ { id: "feedback-request-changes-revise", kind: "review", mutation: "test_review", required: true },
37
+ { id: "feedback-reject-cancel", kind: "review", mutation: "test_review", required: true },
38
+ { id: "feedback-batch-review", kind: "governed-write", mutation: "test_review", required: true },
39
+ { id: "feedback-document-refinement", kind: "review", mutation: "test_feedback", required: true },
40
+ { id: "feedback-prd-versions-diff", kind: "read", mutation: "none", required: true },
41
+ { id: "feedback-batch-move-archive", kind: "governed-write", mutation: "test_feedback", required: true },
42
+ { id: "project-key-unarchive-denial", kind: "negative-permission", mutation: "none", required: true },
43
+ {
44
+ id: "owner-delegate-collaborator-matrix",
45
+ kind: "role-permission",
46
+ mutation: "test_feedback",
47
+ required: "when --require-role-matrix is set",
48
+ },
49
+ { id: "exact-id-cleanup", kind: "cleanup", mutation: "archive_test_feedback", required: true },
50
+ ];
51
51
 
52
52
  function parseArgs(argv) {
53
53
  const args = { _: [] };
@@ -69,32 +69,32 @@ function parseArgs(argv) {
69
69
  return args;
70
70
  }
71
71
 
72
- function requireConfirm(args) {
73
- if (!args["confirm-live"]) {
74
- throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
72
+ function requireConfirm(args) {
73
+ if (!args["confirm-live"]) {
74
+ throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
75
+ }
76
+ if (
77
+ !process.env.MYTE_OWNER_API_KEY
78
+ && !process.env.MYTE_API_KEY
79
+ && !process.env.MYTE_PROJECT_API_KEY
80
+ ) {
81
+ throw new Error("Missing MYTE_OWNER_API_KEY, MYTE_API_KEY, or MYTE_PROJECT_API_KEY.");
82
+ }
83
+ }
84
+
85
+ function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
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;
91
+ if (apiKey) {
92
+ env.MYTE_API_KEY = apiKey;
93
+ env.MYTE_PROJECT_API_KEY = apiKey;
75
94
  }
76
- if (
77
- !process.env.MYTE_OWNER_API_KEY
78
- && !process.env.MYTE_API_KEY
79
- && !process.env.MYTE_PROJECT_API_KEY
80
- ) {
81
- throw new Error("Missing MYTE_OWNER_API_KEY, MYTE_API_KEY, or MYTE_PROJECT_API_KEY.");
82
- }
83
- }
84
-
85
- function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
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;
91
- if (apiKey) {
92
- env.MYTE_API_KEY = apiKey;
93
- env.MYTE_PROJECT_API_KEY = apiKey;
94
- }
95
- const result = spawnSync(process.execPath, [CLI_PATH, ...cliArgs], {
96
- cwd,
97
- env,
95
+ const result = spawnSync(process.execPath, [CLI_PATH, ...cliArgs], {
96
+ cwd,
97
+ env,
98
98
  encoding: "utf8",
99
99
  stdio: ["ignore", "pipe", "pipe"],
100
100
  });
@@ -113,75 +113,75 @@ function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
113
113
  result.stderr || stdout || "(no output)",
114
114
  ].join("\n"));
115
115
  }
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
- }
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
+ }
185
185
 
186
186
  function assert(condition, message) {
187
187
  if (!condition) {
@@ -189,7 +189,7 @@ function assert(condition, message) {
189
189
  }
190
190
  }
191
191
 
192
- function writePrd(workspace, title, index) {
192
+ function writePrd(workspace, title, index) {
193
193
  const filePath = path.join(workspace, `feedback-live-harness-${index}.md`);
194
194
  fs.writeFileSync(
195
195
  filePath,
@@ -208,117 +208,117 @@ function writePrd(workspace, title, index) {
208
208
  ].join("\n"),
209
209
  "utf8",
210
210
  );
211
- return filePath;
212
- }
213
-
214
- function writeCreateApproval(workspace, runId, prdFiles) {
215
- const approvalPath = path.join(workspace, `feedback-live-harness-${runId}-approval.md`);
216
- fs.writeFileSync(
217
- approvalPath,
218
- [
219
- "# Myte Feedback Certification Approval",
220
- "",
221
- `run_id: ${runId}`,
222
- "command: myte create-prd",
223
- `batch_count: ${prdFiles.length}`,
224
- "targets:",
225
- ...prdFiles.map((filePath) => `- ${path.basename(filePath)}`),
226
- "proposed_change: Create disposable, namespaced TEST Feedback records for post-deploy certification.",
227
- "reason: Verify the published CLI and deployed Feedback workflow before exact-ID archival.",
228
- "",
229
- ].join("\n"),
230
- "utf8",
231
- );
232
- return approvalPath;
233
- }
234
-
235
- function writeCommentApproval(workspace, runId, feedbackId) {
236
- const filePath = path.join(workspace, `feedback-live-harness-${runId}-comment.md`);
237
- fs.writeFileSync(
238
- filePath,
239
- [
240
- `# ${runId} Feedback certification comment`,
241
- "",
242
- `feedback_id: ${feedbackId}`,
243
- `run_id: ${runId}`,
244
- "proposed_change: Record a disposable, namespaced certification comment.",
245
- "reason: Verify the project-scoped Feedback comment route and idempotency controls.",
246
- "",
247
- ].join("\n"),
248
- "utf8",
249
- );
250
- return filePath;
251
- }
252
-
253
- function responseRequestId(payload) {
254
- return String(
255
- payload?.request?.request_id
256
- || payload?.request?._id
257
- || payload?.request_id
258
- || payload?._id
259
- || "",
260
- ).trim();
261
- }
262
-
263
- function responseEventId(payload) {
264
- return String(
265
- payload?.event?.event_id
266
- || payload?.event?._id
267
- || payload?.event_id
268
- || "",
269
- ).trim();
270
- }
271
-
272
- function responseVersionId(version) {
273
- return String(version?.version_id || version?._id || "").trim();
274
- }
275
-
276
- function assertDocumentSetMaterialized(workspace, feedbackId, expectedTitles) {
277
- const root = path.join(
278
- workspace,
279
- "MyteCommandCenter",
280
- "PRD",
281
- "feedback-sync",
282
- feedbackId,
283
- );
284
- assert(fs.existsSync(root), `Missing synchronized PRD document directory for ${feedbackId}.`);
285
- const files = fs.readdirSync(root)
286
- .filter((name) => name.toLowerCase().endsWith(".md"))
287
- .sort();
288
- assert(files.length === expectedTitles.length, `Expected ${expectedTitles.length} synchronized PRD files, got ${files.length}.`);
289
- files.forEach((name, index) => {
290
- const content = fs.readFileSync(path.join(root, name), "utf8");
291
- assert(
292
- content.includes(`# ${expectedTitles[index]}`),
293
- `Synchronized document ${name} did not preserve its expected title.`,
294
- );
295
- });
296
- return files;
297
- }
298
-
299
- function writeCertificationManifest(manifestPath, manifest) {
300
- validateCertificationManifest(manifest);
301
- fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
302
- fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
303
- }
304
-
305
- function loadFeedbackManifest(workspace) {
211
+ return filePath;
212
+ }
213
+
214
+ function writeCreateApproval(workspace, runId, prdFiles) {
215
+ const approvalPath = path.join(workspace, `feedback-live-harness-${runId}-approval.md`);
216
+ fs.writeFileSync(
217
+ approvalPath,
218
+ [
219
+ "# Myte Feedback Certification Approval",
220
+ "",
221
+ `run_id: ${runId}`,
222
+ "command: myte create-prd",
223
+ `batch_count: ${prdFiles.length}`,
224
+ "targets:",
225
+ ...prdFiles.map((filePath) => `- ${path.basename(filePath)}`),
226
+ "proposed_change: Create disposable, namespaced TEST Feedback records for post-deploy certification.",
227
+ "reason: Verify the published CLI and deployed Feedback workflow before exact-ID archival.",
228
+ "",
229
+ ].join("\n"),
230
+ "utf8",
231
+ );
232
+ return approvalPath;
233
+ }
234
+
235
+ function writeCommentApproval(workspace, runId, feedbackId) {
236
+ const filePath = path.join(workspace, `feedback-live-harness-${runId}-comment.md`);
237
+ fs.writeFileSync(
238
+ filePath,
239
+ [
240
+ `# ${runId} Feedback certification comment`,
241
+ "",
242
+ `feedback_id: ${feedbackId}`,
243
+ `run_id: ${runId}`,
244
+ "proposed_change: Record a disposable, namespaced certification comment.",
245
+ "reason: Verify the project-scoped Feedback comment route and idempotency controls.",
246
+ "",
247
+ ].join("\n"),
248
+ "utf8",
249
+ );
250
+ return filePath;
251
+ }
252
+
253
+ function responseRequestId(payload) {
254
+ return String(
255
+ payload?.request?.request_id
256
+ || payload?.request?._id
257
+ || payload?.request_id
258
+ || payload?._id
259
+ || "",
260
+ ).trim();
261
+ }
262
+
263
+ function responseEventId(payload) {
264
+ return String(
265
+ payload?.event?.event_id
266
+ || payload?.event?._id
267
+ || payload?.event_id
268
+ || "",
269
+ ).trim();
270
+ }
271
+
272
+ function responseVersionId(version) {
273
+ return String(version?.version_id || version?._id || "").trim();
274
+ }
275
+
276
+ function assertDocumentSetMaterialized(workspace, feedbackId, expectedTitles) {
277
+ const root = path.join(
278
+ workspace,
279
+ "MyteCommandCenter",
280
+ "PRD",
281
+ "feedback-sync",
282
+ feedbackId,
283
+ );
284
+ assert(fs.existsSync(root), `Missing synchronized PRD document directory for ${feedbackId}.`);
285
+ const files = fs.readdirSync(root)
286
+ .filter((name) => name.toLowerCase().endsWith(".md"))
287
+ .sort();
288
+ assert(files.length === expectedTitles.length, `Expected ${expectedTitles.length} synchronized PRD files, got ${files.length}.`);
289
+ files.forEach((name, index) => {
290
+ const content = fs.readFileSync(path.join(root, name), "utf8");
291
+ assert(
292
+ content.includes(`# ${expectedTitles[index]}`),
293
+ `Synchronized document ${name} did not preserve its expected title.`,
294
+ );
295
+ });
296
+ return files;
297
+ }
298
+
299
+ function writeCertificationManifest(manifestPath, manifest) {
300
+ validateCertificationManifest(manifest);
301
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
302
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
303
+ }
304
+
305
+ function loadFeedbackManifest(workspace) {
306
306
  const manifestPath = path.join(workspace, "MyteCommandCenter", "data", "feedback.yml");
307
307
  if (!fs.existsSync(manifestPath)) {
308
308
  return { items: [] };
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");
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");
322
322
  const items = [];
323
323
  let current = null;
324
324
  for (const line of text.split(/\r?\n/)) {
@@ -340,62 +340,62 @@ function loadFeedbackManifest(workspace) {
340
340
  }
341
341
  }
342
342
  if (current) items.push(current);
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
- }
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
+ }
399
399
 
400
400
  function findFeedbackState(workspace, feedbackId) {
401
401
  const manifest = loadFeedbackManifest(workspace);
@@ -430,52 +430,52 @@ function assertFeedbackAbsent(workspace, feedbackIds) {
430
430
  }
431
431
  }
432
432
 
433
- async function main() {
434
- const args = parseArgs(process.argv.slice(2));
435
- if (!args["confirm-live"]) {
436
- console.log(JSON.stringify({
437
- ok: true,
438
- dry_run: true,
439
- message: "No network, project mutation, or local certification artifact was created.",
440
- required_live_flag: "--confirm-live",
441
- optional_strict_role_flag: "--require-role-matrix",
442
- optional_comment_attachment_flag: "--require-comment-attachment-sync",
443
- required_role_environment: [
444
- "MYTE_OWNER_API_KEY",
445
- "MYTE_DELEGATE_API_KEY",
446
- "MYTE_COLLABORATOR_API_KEY",
447
- ],
448
- comment_attachment_environment: "MYTE_WEB_ACCESS_TOKEN",
449
- scenario_matrix: LIVE_SCENARIO_MATRIX,
450
- }, null, 2));
451
- return;
452
- }
453
- requireConfirm(args);
454
-
455
- const ownerApiKey = String(
456
- process.env.MYTE_OWNER_API_KEY
457
- || process.env.MYTE_API_KEY
458
- || process.env.MYTE_PROJECT_API_KEY
459
- || "",
460
- ).trim();
461
- const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
462
- const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
463
- const webAccessToken = String(process.env.MYTE_WEB_ACCESS_TOKEN || "").trim();
464
- process.env.MYTE_API_KEY = ownerApiKey;
465
- process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
466
- if (
467
- args["require-role-matrix"]
468
- && (!ownerApiKey || !delegateApiKey || !collaboratorApiKey)
469
- ) {
470
- throw new Error(
471
- "--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
472
- );
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
- }
433
+ async function main() {
434
+ const args = parseArgs(process.argv.slice(2));
435
+ if (!args["confirm-live"]) {
436
+ console.log(JSON.stringify({
437
+ ok: true,
438
+ dry_run: true,
439
+ message: "No network, project mutation, or local certification artifact was created.",
440
+ required_live_flag: "--confirm-live",
441
+ optional_strict_role_flag: "--require-role-matrix",
442
+ optional_comment_attachment_flag: "--require-comment-attachment-sync",
443
+ required_role_environment: [
444
+ "MYTE_OWNER_API_KEY",
445
+ "MYTE_DELEGATE_API_KEY",
446
+ "MYTE_COLLABORATOR_API_KEY",
447
+ ],
448
+ comment_attachment_environment: "MYTE_WEB_ACCESS_TOKEN",
449
+ scenario_matrix: LIVE_SCENARIO_MATRIX,
450
+ }, null, 2));
451
+ return;
452
+ }
453
+ requireConfirm(args);
454
+
455
+ const ownerApiKey = String(
456
+ process.env.MYTE_OWNER_API_KEY
457
+ || process.env.MYTE_API_KEY
458
+ || process.env.MYTE_PROJECT_API_KEY
459
+ || "",
460
+ ).trim();
461
+ const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
462
+ const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
463
+ const webAccessToken = String(process.env.MYTE_WEB_ACCESS_TOKEN || "").trim();
464
+ process.env.MYTE_API_KEY = ownerApiKey;
465
+ process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
466
+ if (
467
+ args["require-role-matrix"]
468
+ && (!ownerApiKey || !delegateApiKey || !collaboratorApiKey)
469
+ ) {
470
+ throw new Error(
471
+ "--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
472
+ );
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
+ }
479
479
 
480
480
  const workspace = path.resolve(
481
481
  args.workspace || fs.mkdtempSync(path.join(os.tmpdir(), "myte-feedback-full-harness-")),
@@ -487,494 +487,494 @@ async function main() {
487
487
  baseArgs.push("--base-url", String(args["base-url"]));
488
488
  }
489
489
 
490
- const runId = String(args["run-id"] || new Date().toISOString().replace(/[:.]/g, "-"));
491
- const namespace = `MYTE_TEST_${runId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
492
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
493
- const reason = String(args.reason || "Disposable live Feedback harness verification");
494
- const createdFeedbackIds = [];
495
- const reviewRequestIds = [];
496
- const reviewArtifactByRequestId = new Map();
497
- let documentSetFeedbackId = null;
498
- let documentSetRequestId = null;
499
- const roleMatrix = {
500
- required: Boolean(args["require-role-matrix"]),
501
- owner: true,
502
- delegate: Boolean(delegateApiKey),
503
- collaborator: Boolean(collaboratorApiKey),
504
- same_project: true,
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,
510
- delegate_review_used: false,
511
- };
512
- let commentAttachmentSyncVerified = false;
513
- const manifestPath = path.resolve(
514
- args.manifest
515
- || path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
516
- );
517
- const manifest = createCertificationManifest({
518
- runId,
519
- namespace,
520
- environment: String(args.environment || "dev"),
521
- });
522
- manifest.state = "running";
523
- writeCertificationManifest(manifestPath, manifest);
524
-
525
- try {
526
- const doctor = runCli(
527
- ["doctor", "--json", "--timeout-ms", String(args["timeout-ms"] || 60000), ...baseArgs],
528
- workspace,
529
- { apiKey: ownerApiKey },
530
- ).stdout;
531
- assert(doctor.ok === true, `Myte doctor failed at ${doctor.failure_layer || "unknown"} layer.`);
532
-
533
- const config = runCli(
534
- ["config", "--json", ...baseArgs],
535
- workspace,
536
- { apiKey: ownerApiKey },
537
- ).stdout;
538
- manifest.project_id = String(
539
- config.project_id || config.project?.id || config.project?._id || "",
540
- ) || null;
541
- for (const [role, apiKey] of [
542
- ["delegate", delegateApiKey],
543
- ["collaborator", collaboratorApiKey],
544
- ]) {
545
- if (!apiKey) continue;
546
- const roleConfig = runCli(
547
- ["config", "--json", ...baseArgs],
548
- workspace,
549
- { apiKey },
550
- ).stdout;
551
- const roleProjectId = String(
552
- roleConfig.project_id || roleConfig.project?.id || roleConfig.project?._id || "",
553
- ).trim();
554
- if (!manifest.project_id || roleProjectId !== manifest.project_id) {
555
- roleMatrix.same_project = false;
556
- throw new Error(`${role} certification key is not scoped to the owner key's project.`);
557
- }
558
- }
559
- writeCertificationManifest(manifestPath, manifest);
560
-
561
- const prdFiles = [];
562
- for (let index = 1; index <= 5; index += 1) {
563
- const title = `${namespace} Feedback ${stamp} ${index}`;
564
- prdFiles.push(writePrd(workspace, title, index));
565
- }
566
- const createApprovalPath = writeCreateApproval(workspace, runId, prdFiles);
567
-
568
- const create = runCli([
569
- "create-prd",
490
+ const runId = String(args["run-id"] || new Date().toISOString().replace(/[:.]/g, "-"));
491
+ const namespace = `MYTE_TEST_${runId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
492
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
493
+ const reason = String(args.reason || "Disposable live Feedback harness verification");
494
+ const createdFeedbackIds = [];
495
+ const reviewRequestIds = [];
496
+ const reviewArtifactByRequestId = new Map();
497
+ let documentSetFeedbackId = null;
498
+ let documentSetRequestId = null;
499
+ const roleMatrix = {
500
+ required: Boolean(args["require-role-matrix"]),
501
+ owner: true,
502
+ delegate: Boolean(delegateApiKey),
503
+ collaborator: Boolean(collaboratorApiKey),
504
+ same_project: true,
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,
510
+ delegate_review_used: false,
511
+ };
512
+ let commentAttachmentSyncVerified = false;
513
+ const manifestPath = path.resolve(
514
+ args.manifest
515
+ || path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
516
+ );
517
+ const manifest = createCertificationManifest({
518
+ runId,
519
+ namespace,
520
+ environment: String(args.environment || "dev"),
521
+ });
522
+ manifest.state = "running";
523
+ writeCertificationManifest(manifestPath, manifest);
524
+
525
+ try {
526
+ const doctor = runCli(
527
+ ["doctor", "--json", "--timeout-ms", String(args["timeout-ms"] || 60000), ...baseArgs],
528
+ workspace,
529
+ { apiKey: ownerApiKey },
530
+ ).stdout;
531
+ assert(doctor.ok === true, `Myte doctor failed at ${doctor.failure_layer || "unknown"} layer.`);
532
+
533
+ const config = runCli(
534
+ ["config", "--json", ...baseArgs],
535
+ workspace,
536
+ { apiKey: ownerApiKey },
537
+ ).stdout;
538
+ manifest.project_id = String(
539
+ config.project_id || config.project?.id || config.project?._id || "",
540
+ ) || null;
541
+ for (const [role, apiKey] of [
542
+ ["delegate", delegateApiKey],
543
+ ["collaborator", collaboratorApiKey],
544
+ ]) {
545
+ if (!apiKey) continue;
546
+ const roleConfig = runCli(
547
+ ["config", "--json", ...baseArgs],
548
+ workspace,
549
+ { apiKey },
550
+ ).stdout;
551
+ const roleProjectId = String(
552
+ roleConfig.project_id || roleConfig.project?.id || roleConfig.project?._id || "",
553
+ ).trim();
554
+ if (!manifest.project_id || roleProjectId !== manifest.project_id) {
555
+ roleMatrix.same_project = false;
556
+ throw new Error(`${role} certification key is not scoped to the owner key's project.`);
557
+ }
558
+ }
559
+ writeCertificationManifest(manifestPath, manifest);
560
+
561
+ const prdFiles = [];
562
+ for (let index = 1; index <= 5; index += 1) {
563
+ const title = `${namespace} Feedback ${stamp} ${index}`;
564
+ prdFiles.push(writePrd(workspace, title, index));
565
+ }
566
+ const createApprovalPath = writeCreateApproval(workspace, runId, prdFiles);
567
+
568
+ const create = runCli([
569
+ "create-prd",
570
570
  ...prdFiles,
571
- "--description",
572
- `Disposable ${namespace} Feedback created by the live certification harness.`,
573
- "--confirm-write",
574
- "--approval-artifact",
575
- createApprovalPath,
576
- "--json",
571
+ "--description",
572
+ `Disposable ${namespace} Feedback created by the live certification harness.`,
573
+ "--confirm-write",
574
+ "--approval-artifact",
575
+ createApprovalPath,
576
+ "--json",
577
577
  ...baseArgs,
578
578
  ], workspace).stdout;
579
579
 
580
580
  const createdItems = Array.isArray(create.items) ? create.items : [];
581
581
  for (const item of createdItems) {
582
- if (item.status === "created" && item.feedback_id) {
583
- createdFeedbackIds.push(String(item.feedback_id));
584
- recordCertificationArtifact(manifest, {
585
- kind: "feedback",
586
- id: String(item.feedback_id),
587
- metadata: { source: "create-prd", cleanup: "archive" },
588
- });
589
- }
590
- }
591
- writeCertificationManifest(manifestPath, manifest);
592
- assert(createdFeedbackIds.length === 5, `Expected 5 created TEST feedback items, got ${createdFeedbackIds.length}: ${JSON.stringify(create, null, 2)}`);
593
-
594
- const documentSetTitles = [
595
- `${namespace} Document Set Overview`,
596
- `${namespace} Document Set Harness`,
597
- `${namespace} Document Set Security`,
598
- ];
599
- const documentSetFiles = documentSetTitles.map((title, index) =>
600
- writePrd(workspace, title, 101 + index),
601
- );
602
- const documentSetApprovalPath = writeCreateApproval(
603
- workspace,
604
- `${runId}-document-set`,
605
- documentSetFiles,
606
- );
607
- const documentSetCreate = runCli([
608
- "create-prd",
609
- ...documentSetFiles,
610
- "--document-set",
611
- "--title",
612
- `${namespace} Three Document PRD`,
613
- "--description",
614
- `Disposable ${namespace} three-document PRD certification record.`,
615
- "--confirm-write",
616
- "--approval-artifact",
617
- documentSetApprovalPath,
618
- "--json",
619
- ...baseArgs,
620
- ], workspace).stdout;
621
- documentSetFeedbackId = String(documentSetCreate.feedback_id || "");
622
- assert(documentSetFeedbackId, `Document-set creation did not return feedback_id: ${JSON.stringify(documentSetCreate, null, 2)}`);
623
- assert(documentSetCreate.document_count === 3, `Expected three PRD documents: ${JSON.stringify(documentSetCreate, null, 2)}`);
624
- assert(Array.isArray(documentSetCreate.documents) && documentSetCreate.documents.length === 3, "Document-set metadata did not contain three documents.");
625
- recordCertificationArtifact(manifest, {
626
- kind: "feedback",
627
- id: documentSetFeedbackId,
628
- metadata: {
629
- source: "create-prd-document-set",
630
- document_count: 3,
631
- cleanup: "archive",
632
- },
633
- });
634
- for (const document of documentSetCreate.documents) {
635
- if (!document.document_id) continue;
636
- recordCertificationArtifact(manifest, {
637
- kind: "object",
638
- id: String(document.document_id),
639
- metadata: {
640
- object_type: "prd_document",
641
- feedback_id: documentSetFeedbackId,
642
- cleanup: "retained_with_archived_parent",
643
- },
644
- });
645
- }
646
- writeCertificationManifest(manifestPath, manifest);
647
-
648
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
649
- assertFeedbackPresent(workspace, createdFeedbackIds, "todo");
650
- assertFeedbackPresent(workspace, [documentSetFeedbackId], "todo");
651
- assertDocumentSetMaterialized(
652
- workspace,
653
- documentSetFeedbackId,
654
- documentSetTitles,
655
- );
656
-
657
- const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
658
- const operationalReviewKey = delegateApiKey || ownerApiKey;
659
- roleMatrix.collaborator_writes_used = Boolean(collaboratorApiKey);
660
- const commentApprovalPath = writeCommentApproval(
661
- workspace,
662
- runId,
663
- createdFeedbackIds[0],
664
- );
665
- const comment = runCli([
666
- "feedback",
667
- "comment",
668
- "--feedback-id",
669
- createdFeedbackIds[0],
670
- "--body-file",
671
- commentApprovalPath,
672
- "--confirm-write",
673
- "--approval-artifact",
674
- commentApprovalPath,
675
- "--no-sync",
676
- "--json",
677
- ...baseArgs,
678
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
679
- assert(comment.comment_id, `Feedback comment did not return comment_id: ${JSON.stringify(comment, null, 2)}`);
680
- recordCertificationArtifact(manifest, {
681
- kind: "comment",
682
- id: String(comment.comment_id),
683
- metadata: {
684
- feedback_id: createdFeedbackIds[0],
685
- cleanup: "retained_with_archived_parent",
686
- },
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
- }
887
-
888
- const directMove = runCli([
889
- "feedback",
890
- "move",
891
- "--feedback-id",
892
- createdFeedbackIds[0],
893
- "--from-state",
894
- "todo",
895
- "--to-state",
896
- "in_progress",
897
- "--reason",
898
- "Harness verifies a collaborator-safe active-state move.",
899
- "--no-sync",
900
- "--json",
901
- ...baseArgs,
902
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
903
- const directMoveEventId = responseEventId(directMove);
904
- assert(directMoveEventId, `Feedback move did not return an event id: ${JSON.stringify(directMove, null, 2)}`);
905
- recordCertificationArtifact(manifest, {
906
- kind: "feedback_event",
907
- id: directMoveEventId,
908
- metadata: {
909
- feedback_id: createdFeedbackIds[0],
910
- action: "move",
911
- cleanup: "retain_audit",
912
- },
913
- });
914
-
915
- const undo = runCli([
916
- "feedback",
917
- "undo",
918
- "--feedback-id",
919
- createdFeedbackIds[0],
920
- "--event-id",
921
- directMoveEventId,
922
- "--reason",
923
- "Harness verifies exact event undo.",
924
- "--json",
925
- ...baseArgs,
926
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
927
- assert(
928
- String(undo.feedback_state || "") === "todo",
929
- `Feedback undo did not restore todo: ${JSON.stringify(undo, null, 2)}`,
930
- );
931
- const undoEventId = responseEventId(undo);
932
- if (undoEventId) {
933
- recordCertificationArtifact(manifest, {
934
- kind: "feedback_event",
935
- id: undoEventId,
936
- metadata: {
937
- feedback_id: createdFeedbackIds[0],
938
- action: "undo",
939
- cleanup: "retain_audit",
940
- },
941
- });
942
- }
943
-
944
- const getAfterUndo = runCli([
945
- "feedback",
946
- "get",
947
- "--feedback-id",
948
- createdFeedbackIds[0],
949
- "--json",
950
- ...baseArgs,
951
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
952
- assert(
953
- String(getAfterUndo.feedback?.feedback_state || "") === "todo",
954
- `Feedback get did not return the restored state: ${JSON.stringify(getAfterUndo, null, 2)}`,
955
- );
956
- const historyAfterUndo = runCli([
957
- "feedback",
958
- "history",
959
- "--feedback-id",
960
- createdFeedbackIds[0],
961
- "--json",
962
- ...baseArgs,
963
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
964
- assert(
965
- Array.isArray(historyAfterUndo.events) && historyAfterUndo.events.length >= 2,
966
- "Feedback history did not include move and undo evidence.",
967
- );
968
- writeCertificationManifest(manifestPath, manifest);
969
-
970
- runCli(
971
- ["feedback-sync", "--json", ...baseArgs],
972
- workspace,
973
- { apiKey: collaboratorWriteKey },
974
- );
975
- for (const feedbackId of createdFeedbackIds) {
976
- const draft = runCli([
977
- "feedback",
582
+ if (item.status === "created" && item.feedback_id) {
583
+ createdFeedbackIds.push(String(item.feedback_id));
584
+ recordCertificationArtifact(manifest, {
585
+ kind: "feedback",
586
+ id: String(item.feedback_id),
587
+ metadata: { source: "create-prd", cleanup: "archive" },
588
+ });
589
+ }
590
+ }
591
+ writeCertificationManifest(manifestPath, manifest);
592
+ assert(createdFeedbackIds.length === 5, `Expected 5 created TEST feedback items, got ${createdFeedbackIds.length}: ${JSON.stringify(create, null, 2)}`);
593
+
594
+ const documentSetTitles = [
595
+ `${namespace} Document Set Overview`,
596
+ `${namespace} Document Set Harness`,
597
+ `${namespace} Document Set Security`,
598
+ ];
599
+ const documentSetFiles = documentSetTitles.map((title, index) =>
600
+ writePrd(workspace, title, 101 + index),
601
+ );
602
+ const documentSetApprovalPath = writeCreateApproval(
603
+ workspace,
604
+ `${runId}-document-set`,
605
+ documentSetFiles,
606
+ );
607
+ const documentSetCreate = runCli([
608
+ "create-prd",
609
+ ...documentSetFiles,
610
+ "--document-set",
611
+ "--title",
612
+ `${namespace} Three Document PRD`,
613
+ "--description",
614
+ `Disposable ${namespace} three-document PRD certification record.`,
615
+ "--confirm-write",
616
+ "--approval-artifact",
617
+ documentSetApprovalPath,
618
+ "--json",
619
+ ...baseArgs,
620
+ ], workspace).stdout;
621
+ documentSetFeedbackId = String(documentSetCreate.feedback_id || "");
622
+ assert(documentSetFeedbackId, `Document-set creation did not return feedback_id: ${JSON.stringify(documentSetCreate, null, 2)}`);
623
+ assert(documentSetCreate.document_count === 3, `Expected three PRD documents: ${JSON.stringify(documentSetCreate, null, 2)}`);
624
+ assert(Array.isArray(documentSetCreate.documents) && documentSetCreate.documents.length === 3, "Document-set metadata did not contain three documents.");
625
+ recordCertificationArtifact(manifest, {
626
+ kind: "feedback",
627
+ id: documentSetFeedbackId,
628
+ metadata: {
629
+ source: "create-prd-document-set",
630
+ document_count: 3,
631
+ cleanup: "archive",
632
+ },
633
+ });
634
+ for (const document of documentSetCreate.documents) {
635
+ if (!document.document_id) continue;
636
+ recordCertificationArtifact(manifest, {
637
+ kind: "object",
638
+ id: String(document.document_id),
639
+ metadata: {
640
+ object_type: "prd_document",
641
+ feedback_id: documentSetFeedbackId,
642
+ cleanup: "retained_with_archived_parent",
643
+ },
644
+ });
645
+ }
646
+ writeCertificationManifest(manifestPath, manifest);
647
+
648
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
649
+ assertFeedbackPresent(workspace, createdFeedbackIds, "todo");
650
+ assertFeedbackPresent(workspace, [documentSetFeedbackId], "todo");
651
+ assertDocumentSetMaterialized(
652
+ workspace,
653
+ documentSetFeedbackId,
654
+ documentSetTitles,
655
+ );
656
+
657
+ const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
658
+ const operationalReviewKey = delegateApiKey || ownerApiKey;
659
+ roleMatrix.collaborator_writes_used = Boolean(collaboratorApiKey);
660
+ const commentApprovalPath = writeCommentApproval(
661
+ workspace,
662
+ runId,
663
+ createdFeedbackIds[0],
664
+ );
665
+ const comment = runCli([
666
+ "feedback",
667
+ "comment",
668
+ "--feedback-id",
669
+ createdFeedbackIds[0],
670
+ "--body-file",
671
+ commentApprovalPath,
672
+ "--confirm-write",
673
+ "--approval-artifact",
674
+ commentApprovalPath,
675
+ "--no-sync",
676
+ "--json",
677
+ ...baseArgs,
678
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
679
+ assert(comment.comment_id, `Feedback comment did not return comment_id: ${JSON.stringify(comment, null, 2)}`);
680
+ recordCertificationArtifact(manifest, {
681
+ kind: "comment",
682
+ id: String(comment.comment_id),
683
+ metadata: {
684
+ feedback_id: createdFeedbackIds[0],
685
+ cleanup: "retained_with_archived_parent",
686
+ },
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
+ }
887
+
888
+ const directMove = runCli([
889
+ "feedback",
890
+ "move",
891
+ "--feedback-id",
892
+ createdFeedbackIds[0],
893
+ "--from-state",
894
+ "todo",
895
+ "--to-state",
896
+ "in_progress",
897
+ "--reason",
898
+ "Harness verifies a collaborator-safe active-state move.",
899
+ "--no-sync",
900
+ "--json",
901
+ ...baseArgs,
902
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
903
+ const directMoveEventId = responseEventId(directMove);
904
+ assert(directMoveEventId, `Feedback move did not return an event id: ${JSON.stringify(directMove, null, 2)}`);
905
+ recordCertificationArtifact(manifest, {
906
+ kind: "feedback_event",
907
+ id: directMoveEventId,
908
+ metadata: {
909
+ feedback_id: createdFeedbackIds[0],
910
+ action: "move",
911
+ cleanup: "retain_audit",
912
+ },
913
+ });
914
+
915
+ const undo = runCli([
916
+ "feedback",
917
+ "undo",
918
+ "--feedback-id",
919
+ createdFeedbackIds[0],
920
+ "--event-id",
921
+ directMoveEventId,
922
+ "--reason",
923
+ "Harness verifies exact event undo.",
924
+ "--json",
925
+ ...baseArgs,
926
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
927
+ assert(
928
+ String(undo.feedback_state || "") === "todo",
929
+ `Feedback undo did not restore todo: ${JSON.stringify(undo, null, 2)}`,
930
+ );
931
+ const undoEventId = responseEventId(undo);
932
+ if (undoEventId) {
933
+ recordCertificationArtifact(manifest, {
934
+ kind: "feedback_event",
935
+ id: undoEventId,
936
+ metadata: {
937
+ feedback_id: createdFeedbackIds[0],
938
+ action: "undo",
939
+ cleanup: "retain_audit",
940
+ },
941
+ });
942
+ }
943
+
944
+ const getAfterUndo = runCli([
945
+ "feedback",
946
+ "get",
947
+ "--feedback-id",
948
+ createdFeedbackIds[0],
949
+ "--json",
950
+ ...baseArgs,
951
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
952
+ assert(
953
+ String(getAfterUndo.feedback?.feedback_state || "") === "todo",
954
+ `Feedback get did not return the restored state: ${JSON.stringify(getAfterUndo, null, 2)}`,
955
+ );
956
+ const historyAfterUndo = runCli([
957
+ "feedback",
958
+ "history",
959
+ "--feedback-id",
960
+ createdFeedbackIds[0],
961
+ "--json",
962
+ ...baseArgs,
963
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
964
+ assert(
965
+ Array.isArray(historyAfterUndo.events) && historyAfterUndo.events.length >= 2,
966
+ "Feedback history did not include move and undo evidence.",
967
+ );
968
+ writeCertificationManifest(manifestPath, manifest);
969
+
970
+ runCli(
971
+ ["feedback-sync", "--json", ...baseArgs],
972
+ workspace,
973
+ { apiKey: collaboratorWriteKey },
974
+ );
975
+ for (const feedbackId of createdFeedbackIds) {
976
+ const draft = runCli([
977
+ "feedback",
978
978
  "status",
979
979
  "--feedback-id",
980
980
  feedbackId,
@@ -984,177 +984,177 @@ async function main() {
984
984
  reason,
985
985
  "--json",
986
986
  ...baseArgs,
987
- ], workspace).stdout;
988
- assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
989
-
990
- const validation = runCli([
991
- "feedback",
992
- "validate",
993
- "--file",
994
- draft.artifact_path,
995
- "--json",
996
- ...baseArgs,
997
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
998
- assert(
999
- validation.ok === true && validation.valid !== false,
1000
- `feedback validate failed for ${feedbackId}: ${JSON.stringify(validation, null, 2)}`,
1001
- );
1002
-
1003
- const submit = runCli([
987
+ ], workspace).stdout;
988
+ assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
989
+
990
+ const validation = runCli([
991
+ "feedback",
992
+ "validate",
993
+ "--file",
994
+ draft.artifact_path,
995
+ "--json",
996
+ ...baseArgs,
997
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
998
+ assert(
999
+ validation.ok === true && validation.valid !== false,
1000
+ `feedback validate failed for ${feedbackId}: ${JSON.stringify(validation, null, 2)}`,
1001
+ );
1002
+
1003
+ const submit = runCli([
1004
1004
  "feedback",
1005
1005
  "submit",
1006
- "--file",
1007
- draft.artifact_path,
1008
- "--confirm-write",
1009
- "--approval-artifact",
1010
- draft.artifact_path,
1011
- "--json",
1012
- ...baseArgs,
1013
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1014
- const requestId = responseRequestId(submit);
1006
+ "--file",
1007
+ draft.artifact_path,
1008
+ "--confirm-write",
1009
+ "--approval-artifact",
1010
+ draft.artifact_path,
1011
+ "--json",
1012
+ ...baseArgs,
1013
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1014
+ const requestId = responseRequestId(submit);
1015
1015
  assert(requestId, `feedback submit did not return request id for ${feedbackId}: ${JSON.stringify(submit, null, 2)}`);
1016
- reviewRequestIds.push(requestId);
1017
- reviewArtifactByRequestId.set(requestId, draft.artifact_path);
1018
- recordCertificationArtifact(manifest, {
1019
- kind: "review_request",
1020
- id: requestId,
1021
- metadata: { feedback_id: feedbackId },
1022
- });
1023
- writeCertificationManifest(manifestPath, manifest);
1024
- }
1025
-
1026
- const openReviews = runCli([
1027
- "feedback",
1028
- "reviews",
1029
- "--status",
1030
- "open",
1031
- "--json",
1032
- ...baseArgs,
1033
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1034
- const openRequestIds = new Set(
1035
- (openReviews.requests || []).map((request) => responseRequestId(request)).filter(Boolean),
1036
- );
1037
- assert(
1038
- reviewRequestIds.every((requestId) => openRequestIds.has(requestId)),
1039
- "Feedback review list did not include every submitted certification request.",
1040
- );
1041
- const reviewDetail = runCli([
1042
- "feedback",
1043
- "reviews",
1044
- "--request-id",
1045
- reviewRequestIds[0],
1046
- "--json",
1047
- ...baseArgs,
1048
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1049
- assert(
1050
- responseRequestId(reviewDetail) === reviewRequestIds[0],
1051
- "Feedback review detail returned the wrong request.",
1052
- );
1053
-
1054
- if (collaboratorApiKey) {
1055
- const deniedReview = runCli([
1056
- "feedback",
1057
- "review",
1058
- "--request-id",
1059
- reviewRequestIds[0],
1060
- "--action",
1061
- "approve",
1062
- "--reason",
1063
- "Harness expects regular collaborator approval to be denied.",
1064
- "--no-sync",
1065
- "--json",
1066
- ...baseArgs,
1067
- ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
1068
- roleMatrix.collaborator_review_denied =
1069
- deniedReview.ok === false && deniedReview.status === 403;
1070
- assert(
1071
- roleMatrix.collaborator_review_denied,
1072
- `Regular collaborator review was not denied as expected: ${JSON.stringify(deniedReview, null, 2)}`,
1073
- );
1074
- }
1075
-
1076
- const requestChanges = runCli([
1077
- "feedback",
1078
- "review",
1079
- "--request-id",
1080
- reviewRequestIds[0],
1081
- "--action",
1082
- "request_changes",
1083
- "--reason",
1084
- "Harness requests a submitter revision.",
1085
- "--verification-evidence",
1086
- "Initial validation completed; revision path remains to be certified.",
1087
- "--no-sync",
1088
- "--json",
1089
- ...baseArgs,
1090
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1091
- assert(
1092
- String(requestChanges.request?.status || requestChanges.status || "") === "needs_changes",
1093
- `Request-changes did not enter needs_changes: ${JSON.stringify(requestChanges, null, 2)}`,
1094
- );
1095
-
1096
- const revised = runCli([
1097
- "feedback",
1098
- "revise",
1099
- "--request-id",
1100
- reviewRequestIds[0],
1101
- "--file",
1102
- reviewArtifactByRequestId.get(reviewRequestIds[0]),
1103
- "--confirm-write",
1104
- "--approval-artifact",
1105
- reviewArtifactByRequestId.get(reviewRequestIds[0]),
1106
- "--json",
1107
- ...baseArgs,
1108
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1109
- assert(
1110
- responseRequestId(revised) === reviewRequestIds[0],
1111
- `Feedback revise returned the wrong request: ${JSON.stringify(revised, null, 2)}`,
1112
- );
1113
-
1114
- const rejected = runCli([
1115
- "feedback",
1116
- "review",
1117
- "--request-id",
1118
- reviewRequestIds[1],
1119
- "--action",
1120
- "reject",
1121
- "--reason",
1122
- "Harness verifies terminal rejection without applying changes.",
1123
- "--verification-evidence",
1124
- "Negative review path certified.",
1125
- "--no-sync",
1126
- "--json",
1127
- ...baseArgs,
1128
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1129
- assert(
1130
- String(rejected.request?.status || rejected.status || "") === "rejected",
1131
- `Feedback rejection did not become terminal: ${JSON.stringify(rejected, null, 2)}`,
1132
- );
1133
-
1134
- const cancelled = runCli([
1135
- "feedback",
1136
- "review",
1137
- "--request-id",
1138
- reviewRequestIds[2],
1139
- "--action",
1140
- "cancel",
1141
- "--reason",
1142
- "Harness submitter cancels a disposable review.",
1143
- "--no-sync",
1144
- "--json",
1145
- ...baseArgs,
1146
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1147
- assert(
1148
- String(cancelled.request?.status || cancelled.status || "") === "cancelled",
1149
- `Feedback cancellation did not become terminal: ${JSON.stringify(cancelled, null, 2)}`,
1150
- );
1151
-
1152
- const pendingApprovalRequestIds = [
1153
- reviewRequestIds[0],
1154
- reviewRequestIds[3],
1155
- reviewRequestIds[4],
1156
- ];
1157
- const blocked = runCli([
1016
+ reviewRequestIds.push(requestId);
1017
+ reviewArtifactByRequestId.set(requestId, draft.artifact_path);
1018
+ recordCertificationArtifact(manifest, {
1019
+ kind: "review_request",
1020
+ id: requestId,
1021
+ metadata: { feedback_id: feedbackId },
1022
+ });
1023
+ writeCertificationManifest(manifestPath, manifest);
1024
+ }
1025
+
1026
+ const openReviews = runCli([
1027
+ "feedback",
1028
+ "reviews",
1029
+ "--status",
1030
+ "open",
1031
+ "--json",
1032
+ ...baseArgs,
1033
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1034
+ const openRequestIds = new Set(
1035
+ (openReviews.requests || []).map((request) => responseRequestId(request)).filter(Boolean),
1036
+ );
1037
+ assert(
1038
+ reviewRequestIds.every((requestId) => openRequestIds.has(requestId)),
1039
+ "Feedback review list did not include every submitted certification request.",
1040
+ );
1041
+ const reviewDetail = runCli([
1042
+ "feedback",
1043
+ "reviews",
1044
+ "--request-id",
1045
+ reviewRequestIds[0],
1046
+ "--json",
1047
+ ...baseArgs,
1048
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1049
+ assert(
1050
+ responseRequestId(reviewDetail) === reviewRequestIds[0],
1051
+ "Feedback review detail returned the wrong request.",
1052
+ );
1053
+
1054
+ if (collaboratorApiKey) {
1055
+ const deniedReview = runCli([
1056
+ "feedback",
1057
+ "review",
1058
+ "--request-id",
1059
+ reviewRequestIds[0],
1060
+ "--action",
1061
+ "approve",
1062
+ "--reason",
1063
+ "Harness expects regular collaborator approval to be denied.",
1064
+ "--no-sync",
1065
+ "--json",
1066
+ ...baseArgs,
1067
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
1068
+ roleMatrix.collaborator_review_denied =
1069
+ deniedReview.ok === false && deniedReview.status === 403;
1070
+ assert(
1071
+ roleMatrix.collaborator_review_denied,
1072
+ `Regular collaborator review was not denied as expected: ${JSON.stringify(deniedReview, null, 2)}`,
1073
+ );
1074
+ }
1075
+
1076
+ const requestChanges = runCli([
1077
+ "feedback",
1078
+ "review",
1079
+ "--request-id",
1080
+ reviewRequestIds[0],
1081
+ "--action",
1082
+ "request_changes",
1083
+ "--reason",
1084
+ "Harness requests a submitter revision.",
1085
+ "--verification-evidence",
1086
+ "Initial validation completed; revision path remains to be certified.",
1087
+ "--no-sync",
1088
+ "--json",
1089
+ ...baseArgs,
1090
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1091
+ assert(
1092
+ String(requestChanges.request?.status || requestChanges.status || "") === "needs_changes",
1093
+ `Request-changes did not enter needs_changes: ${JSON.stringify(requestChanges, null, 2)}`,
1094
+ );
1095
+
1096
+ const revised = runCli([
1097
+ "feedback",
1098
+ "revise",
1099
+ "--request-id",
1100
+ reviewRequestIds[0],
1101
+ "--file",
1102
+ reviewArtifactByRequestId.get(reviewRequestIds[0]),
1103
+ "--confirm-write",
1104
+ "--approval-artifact",
1105
+ reviewArtifactByRequestId.get(reviewRequestIds[0]),
1106
+ "--json",
1107
+ ...baseArgs,
1108
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1109
+ assert(
1110
+ responseRequestId(revised) === reviewRequestIds[0],
1111
+ `Feedback revise returned the wrong request: ${JSON.stringify(revised, null, 2)}`,
1112
+ );
1113
+
1114
+ const rejected = runCli([
1115
+ "feedback",
1116
+ "review",
1117
+ "--request-id",
1118
+ reviewRequestIds[1],
1119
+ "--action",
1120
+ "reject",
1121
+ "--reason",
1122
+ "Harness verifies terminal rejection without applying changes.",
1123
+ "--verification-evidence",
1124
+ "Negative review path certified.",
1125
+ "--no-sync",
1126
+ "--json",
1127
+ ...baseArgs,
1128
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1129
+ assert(
1130
+ String(rejected.request?.status || rejected.status || "") === "rejected",
1131
+ `Feedback rejection did not become terminal: ${JSON.stringify(rejected, null, 2)}`,
1132
+ );
1133
+
1134
+ const cancelled = runCli([
1135
+ "feedback",
1136
+ "review",
1137
+ "--request-id",
1138
+ reviewRequestIds[2],
1139
+ "--action",
1140
+ "cancel",
1141
+ "--reason",
1142
+ "Harness submitter cancels a disposable review.",
1143
+ "--no-sync",
1144
+ "--json",
1145
+ ...baseArgs,
1146
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1147
+ assert(
1148
+ String(cancelled.request?.status || cancelled.status || "") === "cancelled",
1149
+ `Feedback cancellation did not become terminal: ${JSON.stringify(cancelled, null, 2)}`,
1150
+ );
1151
+
1152
+ const pendingApprovalRequestIds = [
1153
+ reviewRequestIds[0],
1154
+ reviewRequestIds[3],
1155
+ reviewRequestIds[4],
1156
+ ];
1157
+ const blocked = runCli([
1158
1158
  "feedback",
1159
1159
  "move",
1160
1160
  "--feedback-ids",
@@ -1173,69 +1173,69 @@ async function main() {
1173
1173
  assert(blocked.status === 409, `Expected 409 pending review block, got ${blocked.status}.`);
1174
1174
  assert(blocked.data?.code === "pending_feedback_review", `Expected pending_feedback_review, got ${JSON.stringify(blocked, null, 2)}.`);
1175
1175
 
1176
- const ownerApproval = runCli([
1177
- "feedback",
1178
- "review",
1179
- "--request-id",
1180
- reviewRequestIds[0],
1181
- "--action",
1182
- "approve",
1183
- "--reason",
1184
- "Harness owner approval.",
1185
- "--verification-evidence",
1186
- "Stale-state, validation, role denial, request-changes, and revision checks passed.",
1187
- "--deployment-evidence",
1188
- "Dev post-deploy certification run; artifacts will be archived.",
1189
- "--no-sync",
1190
- "--json",
1191
- ...baseArgs,
1192
- ], workspace, { apiKey: ownerApiKey }).stdout;
1193
- roleMatrix.owner_review_used = true;
1194
- assert(
1195
- String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
1196
- `Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
1197
- );
1198
-
1199
- const batchApprovalRequestIds = [
1200
- reviewRequestIds[3],
1201
- reviewRequestIds[4],
1202
- ];
1203
- const review = runCli([
1204
- "feedback",
1205
- "review",
1206
- "--request-ids",
1207
- batchApprovalRequestIds.join(","),
1208
- "--action",
1209
- "approve",
1210
- "--reason",
1211
- "Harness batch approval.",
1212
- "--verification-evidence",
1213
- "Validation, role checks, request-changes, and direct board guards passed.",
1214
- "--deployment-evidence",
1215
- "Dev post-deploy certification run; artifacts will be archived.",
1216
- "--json",
1217
- ...baseArgs,
1218
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1219
- roleMatrix.delegate_review_used = Boolean(delegateApiKey);
1220
- assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
1221
-
1222
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1223
- assertFeedbackPresent(
1224
- workspace,
1225
- [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1226
- "completed",
1227
- );
1228
- assertFeedbackPresent(
1229
- workspace,
1230
- [createdFeedbackIds[1], createdFeedbackIds[2]],
1231
- "todo",
1232
- );
1233
-
1234
- const reopen = runCli([
1176
+ const ownerApproval = runCli([
1177
+ "feedback",
1178
+ "review",
1179
+ "--request-id",
1180
+ reviewRequestIds[0],
1181
+ "--action",
1182
+ "approve",
1183
+ "--reason",
1184
+ "Harness owner approval.",
1185
+ "--verification-evidence",
1186
+ "Stale-state, validation, role denial, request-changes, and revision checks passed.",
1187
+ "--deployment-evidence",
1188
+ "Dev post-deploy certification run; artifacts will be archived.",
1189
+ "--no-sync",
1190
+ "--json",
1191
+ ...baseArgs,
1192
+ ], workspace, { apiKey: ownerApiKey }).stdout;
1193
+ roleMatrix.owner_review_used = true;
1194
+ assert(
1195
+ String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
1196
+ `Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
1197
+ );
1198
+
1199
+ const batchApprovalRequestIds = [
1200
+ reviewRequestIds[3],
1201
+ reviewRequestIds[4],
1202
+ ];
1203
+ const review = runCli([
1204
+ "feedback",
1205
+ "review",
1206
+ "--request-ids",
1207
+ batchApprovalRequestIds.join(","),
1208
+ "--action",
1209
+ "approve",
1210
+ "--reason",
1211
+ "Harness batch approval.",
1212
+ "--verification-evidence",
1213
+ "Validation, role checks, request-changes, and direct board guards passed.",
1214
+ "--deployment-evidence",
1215
+ "Dev post-deploy certification run; artifacts will be archived.",
1216
+ "--json",
1217
+ ...baseArgs,
1218
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1219
+ roleMatrix.delegate_review_used = Boolean(delegateApiKey);
1220
+ assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
1221
+
1222
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1223
+ assertFeedbackPresent(
1224
+ workspace,
1225
+ [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1226
+ "completed",
1227
+ );
1228
+ assertFeedbackPresent(
1229
+ workspace,
1230
+ [createdFeedbackIds[1], createdFeedbackIds[2]],
1231
+ "todo",
1232
+ );
1233
+
1234
+ const reopen = runCli([
1235
1235
  "feedback",
1236
1236
  "move",
1237
1237
  "--feedback-ids",
1238
- [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]].join(","),
1238
+ [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]].join(","),
1239
1239
  "--from-state",
1240
1240
  "completed",
1241
1241
  "--to-state",
@@ -1245,206 +1245,206 @@ async function main() {
1245
1245
  "--json",
1246
1246
  ...baseArgs,
1247
1247
  ], workspace).stdout;
1248
- assert(reopen.ok !== false && reopen.updated_count === 3, `batch active move failed: ${JSON.stringify(reopen, null, 2)}`);
1249
-
1250
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1251
- assertFeedbackPresent(
1252
- workspace,
1253
- [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1254
- "in_progress",
1255
- );
1248
+ assert(reopen.ok !== false && reopen.updated_count === 3, `batch active move failed: ${JSON.stringify(reopen, null, 2)}`);
1249
+
1250
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1251
+ assertFeedbackPresent(
1252
+ workspace,
1253
+ [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1254
+ "in_progress",
1255
+ );
1256
1256
 
1257
1257
  const archive = runCli([
1258
1258
  "feedback",
1259
- "move",
1260
- "--feedback-ids",
1261
- createdFeedbackIds.join(","),
1262
- "--to-state",
1263
- "archived",
1259
+ "move",
1260
+ "--feedback-ids",
1261
+ createdFeedbackIds.join(","),
1262
+ "--to-state",
1263
+ "archived",
1264
1264
  "--reason",
1265
1265
  "Harness cleanup archive.",
1266
1266
  "--json",
1267
1267
  ...baseArgs,
1268
- ], workspace).stdout;
1269
- assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
1270
-
1271
- const selectedDocument = documentSetCreate.documents[1];
1272
- const selectedDocumentId = String(selectedDocument?.document_id || "").trim();
1273
- assert(selectedDocumentId, "Three-document PRD did not return a second document id.");
1274
- const revisedDocumentTitle = `${namespace} Document Set Harness Revised`;
1275
- const revisedDocumentPath = writePrd(
1276
- workspace,
1277
- revisedDocumentTitle,
1278
- 202,
1279
- );
1280
- const documentDraft = runCli([
1281
- "feedback",
1282
- "edit",
1283
- "--feedback-id",
1284
- documentSetFeedbackId,
1285
- "--prd-file",
1286
- revisedDocumentPath,
1287
- "--document-id",
1288
- selectedDocumentId,
1289
- "--reason",
1290
- "Harness verifies document-scoped refinement.",
1291
- "--json",
1292
- ...baseArgs,
1293
- ], workspace).stdout;
1294
- assert(documentDraft.artifact_path, "Document-scoped edit did not create a review artifact.");
1295
- const documentValidation = runCli([
1296
- "feedback",
1297
- "validate",
1298
- "--file",
1299
- documentDraft.artifact_path,
1300
- "--json",
1301
- ...baseArgs,
1302
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1303
- assert(
1304
- documentValidation.ok === true && documentValidation.valid !== false,
1305
- `Document-scoped refinement did not validate: ${JSON.stringify(documentValidation, null, 2)}`,
1306
- );
1307
- const documentSubmit = runCli([
1308
- "feedback",
1309
- "submit",
1310
- "--file",
1311
- documentDraft.artifact_path,
1312
- "--confirm-write",
1313
- "--approval-artifact",
1314
- documentDraft.artifact_path,
1315
- "--json",
1316
- ...baseArgs,
1317
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1318
- documentSetRequestId = responseRequestId(documentSubmit);
1319
- assert(documentSetRequestId, "Document-scoped refinement did not return a review request id.");
1320
- recordCertificationArtifact(manifest, {
1321
- kind: "review_request",
1322
- id: documentSetRequestId,
1323
- metadata: {
1324
- feedback_id: documentSetFeedbackId,
1325
- document_id: selectedDocumentId,
1326
- },
1327
- });
1328
- const documentApproval = runCli([
1329
- "feedback",
1330
- "review",
1331
- "--request-id",
1332
- documentSetRequestId,
1333
- "--action",
1334
- "approve",
1335
- "--reason",
1336
- "Harness approves one document without replacing its siblings.",
1337
- "--verification-evidence",
1338
- "Document-set validation and sibling-preservation checks passed.",
1339
- "--deployment-evidence",
1340
- "Dev post-deploy certification only.",
1341
- "--no-sync",
1342
- "--json",
1343
- ...baseArgs,
1344
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1345
- assert(
1346
- String(documentApproval.request?.status || documentApproval.status || "") === "applied",
1347
- `Document-scoped approval did not apply: ${JSON.stringify(documentApproval, null, 2)}`,
1348
- );
1349
-
1350
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1351
- const synchronizedDocumentRoot = path.join(
1352
- workspace,
1353
- "MyteCommandCenter",
1354
- "PRD",
1355
- "feedback-sync",
1356
- documentSetFeedbackId,
1357
- );
1358
- const synchronizedDocuments = fs.readdirSync(synchronizedDocumentRoot)
1359
- .filter((name) => name.toLowerCase().endsWith(".md"))
1360
- .sort();
1361
- assert(synchronizedDocuments.length === 3, "Document refinement changed the document count.");
1362
- assert(
1363
- fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[0]), "utf8")
1364
- .includes(`# ${documentSetTitles[0]}`),
1365
- "Document refinement replaced the first sibling document.",
1366
- );
1367
- assert(
1368
- fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[1]), "utf8")
1369
- .includes(`# ${revisedDocumentTitle}`),
1370
- "Document refinement did not update the selected second document.",
1371
- );
1372
- assert(
1373
- fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[2]), "utf8")
1374
- .includes(`# ${documentSetTitles[2]}`),
1375
- "Document refinement replaced the third sibling document.",
1376
- );
1377
-
1378
- const versions = runCli([
1379
- "feedback",
1380
- "prd-versions",
1381
- "--feedback-id",
1382
- documentSetFeedbackId,
1383
- "--json",
1384
- ...baseArgs,
1385
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1386
- assert(
1387
- Array.isArray(versions.versions) && versions.versions.length >= 2,
1388
- `Document refinement did not retain PRD versions: ${JSON.stringify(versions, null, 2)}`,
1389
- );
1390
- const activeVersionId = String(versions.active_prd_version_id || "").trim();
1391
- const targetVersion =
1392
- versions.versions.find((version) => responseVersionId(version) !== activeVersionId)
1393
- || versions.versions[0];
1394
- const targetVersionId = responseVersionId(targetVersion);
1395
- assert(targetVersionId, "PRD version list did not return a target version id.");
1396
- const versionDiff = runCli([
1397
- "feedback",
1398
- "prd-diff",
1399
- "--feedback-id",
1400
- documentSetFeedbackId,
1401
- "--version-id",
1402
- targetVersionId,
1403
- "--document-id",
1404
- selectedDocumentId,
1405
- "--json",
1406
- ...baseArgs,
1407
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1408
- assert(
1409
- String(versionDiff.document_id || "") === selectedDocumentId,
1410
- `PRD diff was not scoped to the selected document: ${JSON.stringify(versionDiff, null, 2)}`,
1411
- );
1412
- for (const version of versions.versions) {
1413
- const versionId = responseVersionId(version);
1414
- if (!versionId) continue;
1415
- recordCertificationArtifact(manifest, {
1416
- kind: "object",
1417
- id: versionId,
1418
- metadata: {
1419
- object_type: "prd_version",
1420
- feedback_id: documentSetFeedbackId,
1421
- cleanup: "retain_audit",
1422
- },
1423
- });
1424
- }
1425
- writeCertificationManifest(manifestPath, manifest);
1426
-
1427
- const archiveDocumentSet = runCli([
1428
- "feedback",
1429
- "move",
1430
- "--feedback-id",
1431
- documentSetFeedbackId,
1432
- "--from-state",
1433
- "todo",
1434
- "--to-state",
1435
- "archived",
1436
- "--reason",
1437
- "Harness cleanup archive for three-document PRD.",
1438
- "--json",
1439
- ...baseArgs,
1440
- ], workspace).stdout;
1441
- assert(
1442
- archiveDocumentSet.ok !== false,
1443
- `document-set archive failed: ${JSON.stringify(archiveDocumentSet, null, 2)}`,
1444
- );
1445
-
1446
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1447
- assertFeedbackAbsent(workspace, [...createdFeedbackIds, documentSetFeedbackId]);
1268
+ ], workspace).stdout;
1269
+ assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
1270
+
1271
+ const selectedDocument = documentSetCreate.documents[1];
1272
+ const selectedDocumentId = String(selectedDocument?.document_id || "").trim();
1273
+ assert(selectedDocumentId, "Three-document PRD did not return a second document id.");
1274
+ const revisedDocumentTitle = `${namespace} Document Set Harness Revised`;
1275
+ const revisedDocumentPath = writePrd(
1276
+ workspace,
1277
+ revisedDocumentTitle,
1278
+ 202,
1279
+ );
1280
+ const documentDraft = runCli([
1281
+ "feedback",
1282
+ "edit",
1283
+ "--feedback-id",
1284
+ documentSetFeedbackId,
1285
+ "--prd-file",
1286
+ revisedDocumentPath,
1287
+ "--document-id",
1288
+ selectedDocumentId,
1289
+ "--reason",
1290
+ "Harness verifies document-scoped refinement.",
1291
+ "--json",
1292
+ ...baseArgs,
1293
+ ], workspace).stdout;
1294
+ assert(documentDraft.artifact_path, "Document-scoped edit did not create a review artifact.");
1295
+ const documentValidation = runCli([
1296
+ "feedback",
1297
+ "validate",
1298
+ "--file",
1299
+ documentDraft.artifact_path,
1300
+ "--json",
1301
+ ...baseArgs,
1302
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1303
+ assert(
1304
+ documentValidation.ok === true && documentValidation.valid !== false,
1305
+ `Document-scoped refinement did not validate: ${JSON.stringify(documentValidation, null, 2)}`,
1306
+ );
1307
+ const documentSubmit = runCli([
1308
+ "feedback",
1309
+ "submit",
1310
+ "--file",
1311
+ documentDraft.artifact_path,
1312
+ "--confirm-write",
1313
+ "--approval-artifact",
1314
+ documentDraft.artifact_path,
1315
+ "--json",
1316
+ ...baseArgs,
1317
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1318
+ documentSetRequestId = responseRequestId(documentSubmit);
1319
+ assert(documentSetRequestId, "Document-scoped refinement did not return a review request id.");
1320
+ recordCertificationArtifact(manifest, {
1321
+ kind: "review_request",
1322
+ id: documentSetRequestId,
1323
+ metadata: {
1324
+ feedback_id: documentSetFeedbackId,
1325
+ document_id: selectedDocumentId,
1326
+ },
1327
+ });
1328
+ const documentApproval = runCli([
1329
+ "feedback",
1330
+ "review",
1331
+ "--request-id",
1332
+ documentSetRequestId,
1333
+ "--action",
1334
+ "approve",
1335
+ "--reason",
1336
+ "Harness approves one document without replacing its siblings.",
1337
+ "--verification-evidence",
1338
+ "Document-set validation and sibling-preservation checks passed.",
1339
+ "--deployment-evidence",
1340
+ "Dev post-deploy certification only.",
1341
+ "--no-sync",
1342
+ "--json",
1343
+ ...baseArgs,
1344
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1345
+ assert(
1346
+ String(documentApproval.request?.status || documentApproval.status || "") === "applied",
1347
+ `Document-scoped approval did not apply: ${JSON.stringify(documentApproval, null, 2)}`,
1348
+ );
1349
+
1350
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1351
+ const synchronizedDocumentRoot = path.join(
1352
+ workspace,
1353
+ "MyteCommandCenter",
1354
+ "PRD",
1355
+ "feedback-sync",
1356
+ documentSetFeedbackId,
1357
+ );
1358
+ const synchronizedDocuments = fs.readdirSync(synchronizedDocumentRoot)
1359
+ .filter((name) => name.toLowerCase().endsWith(".md"))
1360
+ .sort();
1361
+ assert(synchronizedDocuments.length === 3, "Document refinement changed the document count.");
1362
+ assert(
1363
+ fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[0]), "utf8")
1364
+ .includes(`# ${documentSetTitles[0]}`),
1365
+ "Document refinement replaced the first sibling document.",
1366
+ );
1367
+ assert(
1368
+ fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[1]), "utf8")
1369
+ .includes(`# ${revisedDocumentTitle}`),
1370
+ "Document refinement did not update the selected second document.",
1371
+ );
1372
+ assert(
1373
+ fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[2]), "utf8")
1374
+ .includes(`# ${documentSetTitles[2]}`),
1375
+ "Document refinement replaced the third sibling document.",
1376
+ );
1377
+
1378
+ const versions = runCli([
1379
+ "feedback",
1380
+ "prd-versions",
1381
+ "--feedback-id",
1382
+ documentSetFeedbackId,
1383
+ "--json",
1384
+ ...baseArgs,
1385
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1386
+ assert(
1387
+ Array.isArray(versions.versions) && versions.versions.length >= 2,
1388
+ `Document refinement did not retain PRD versions: ${JSON.stringify(versions, null, 2)}`,
1389
+ );
1390
+ const activeVersionId = String(versions.active_prd_version_id || "").trim();
1391
+ const targetVersion =
1392
+ versions.versions.find((version) => responseVersionId(version) !== activeVersionId)
1393
+ || versions.versions[0];
1394
+ const targetVersionId = responseVersionId(targetVersion);
1395
+ assert(targetVersionId, "PRD version list did not return a target version id.");
1396
+ const versionDiff = runCli([
1397
+ "feedback",
1398
+ "prd-diff",
1399
+ "--feedback-id",
1400
+ documentSetFeedbackId,
1401
+ "--version-id",
1402
+ targetVersionId,
1403
+ "--document-id",
1404
+ selectedDocumentId,
1405
+ "--json",
1406
+ ...baseArgs,
1407
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1408
+ assert(
1409
+ String(versionDiff.document_id || "") === selectedDocumentId,
1410
+ `PRD diff was not scoped to the selected document: ${JSON.stringify(versionDiff, null, 2)}`,
1411
+ );
1412
+ for (const version of versions.versions) {
1413
+ const versionId = responseVersionId(version);
1414
+ if (!versionId) continue;
1415
+ recordCertificationArtifact(manifest, {
1416
+ kind: "object",
1417
+ id: versionId,
1418
+ metadata: {
1419
+ object_type: "prd_version",
1420
+ feedback_id: documentSetFeedbackId,
1421
+ cleanup: "retain_audit",
1422
+ },
1423
+ });
1424
+ }
1425
+ writeCertificationManifest(manifestPath, manifest);
1426
+
1427
+ const archiveDocumentSet = runCli([
1428
+ "feedback",
1429
+ "move",
1430
+ "--feedback-id",
1431
+ documentSetFeedbackId,
1432
+ "--from-state",
1433
+ "todo",
1434
+ "--to-state",
1435
+ "archived",
1436
+ "--reason",
1437
+ "Harness cleanup archive for three-document PRD.",
1438
+ "--json",
1439
+ ...baseArgs,
1440
+ ], workspace).stdout;
1441
+ assert(
1442
+ archiveDocumentSet.ok !== false,
1443
+ `document-set archive failed: ${JSON.stringify(archiveDocumentSet, null, 2)}`,
1444
+ );
1445
+
1446
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1447
+ assertFeedbackAbsent(workspace, [...createdFeedbackIds, documentSetFeedbackId]);
1448
1448
 
1449
1449
  const unarchive = runCli([
1450
1450
  "feedback",
@@ -1462,90 +1462,90 @@ async function main() {
1462
1462
  ...baseArgs,
1463
1463
  ], workspace, { allowFailure: true }).stdout;
1464
1464
  assert(unarchive.ok === false, "Project-key Feedback unarchive unexpectedly succeeded.");
1465
- assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
1466
- assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
1467
-
1468
- if (roleMatrix.required) {
1469
- assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
1470
- assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
1471
- assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
1472
- assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
1473
- assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
1474
- assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
1475
- assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
1476
- }
1477
- if (args["require-comment-attachment-sync"]) {
1478
- assert(
1479
- commentAttachmentSyncVerified,
1480
- "Web-created Feedback comment attachment did not survive Project Assistant sync.",
1481
- );
1482
- }
1483
- manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
1484
- id: scenario.id,
1485
- status: scenario.id === "owner-delegate-collaborator-matrix"
1486
- ? (
1487
- roleMatrix.required
1488
- ? "passed"
1489
- : delegateApiKey && collaboratorApiKey
1490
- ? "passed_optional"
1491
- : "optional_not_requested"
1492
- )
1493
- : scenario.id === "feedback-comment-attachment-sync"
1494
- ? (
1495
- commentAttachmentSyncVerified
1496
- ? "passed"
1497
- : "optional_not_requested"
1498
- )
1499
- : "passed",
1500
- checked_at: new Date().toISOString(),
1501
- }));
1502
- manifest.state = "cleaned";
1503
- manifest.completed_at = new Date().toISOString();
1504
- manifest.updated_at = manifest.completed_at;
1505
- writeCertificationManifest(manifestPath, manifest);
1506
-
1507
- console.log(JSON.stringify({
1508
- ok: true,
1509
- run_id: runId,
1510
- namespace,
1511
- workspace,
1512
- manifest_path: manifestPath,
1513
- created_feedback_ids: createdFeedbackIds,
1514
- document_set_feedback_id: documentSetFeedbackId,
1515
- document_set_review_request_id: documentSetRequestId,
1516
- review_request_ids: reviewRequestIds,
1517
- role_matrix: roleMatrix,
1518
- comment_verified: true,
1519
- comment_sync_roundtrip_verified: true,
1520
- comment_attachment_sync_verified: commentAttachmentSyncVerified,
1521
- stale_edit_rejection_and_resync_verified: true,
1522
- forced_stale_override_verified: true,
1523
- move_undo_history_verified: true,
1524
- validation_verified: true,
1525
- request_changes_revision_verified: true,
1526
- rejection_and_cancellation_verified: true,
1527
- pending_review_batch_block_verified: true,
1528
- batch_review_verified: true,
1529
- batch_active_move_verified: true,
1530
- batch_archive_verified: true,
1531
- three_document_prd_verified: true,
1532
- document_scoped_refinement_verified: true,
1533
- prd_version_diff_verified: true,
1534
- archived_sync_exclusion_verified: true,
1465
+ assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
1466
+ assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
1467
+
1468
+ if (roleMatrix.required) {
1469
+ assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
1470
+ assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
1471
+ assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
1472
+ assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
1473
+ assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
1474
+ assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
1475
+ assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
1476
+ }
1477
+ if (args["require-comment-attachment-sync"]) {
1478
+ assert(
1479
+ commentAttachmentSyncVerified,
1480
+ "Web-created Feedback comment attachment did not survive Project Assistant sync.",
1481
+ );
1482
+ }
1483
+ manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
1484
+ id: scenario.id,
1485
+ status: scenario.id === "owner-delegate-collaborator-matrix"
1486
+ ? (
1487
+ roleMatrix.required
1488
+ ? "passed"
1489
+ : delegateApiKey && collaboratorApiKey
1490
+ ? "passed_optional"
1491
+ : "optional_not_requested"
1492
+ )
1493
+ : scenario.id === "feedback-comment-attachment-sync"
1494
+ ? (
1495
+ commentAttachmentSyncVerified
1496
+ ? "passed"
1497
+ : "optional_not_requested"
1498
+ )
1499
+ : "passed",
1500
+ checked_at: new Date().toISOString(),
1501
+ }));
1502
+ manifest.state = "cleaned";
1503
+ manifest.completed_at = new Date().toISOString();
1504
+ manifest.updated_at = manifest.completed_at;
1505
+ writeCertificationManifest(manifestPath, manifest);
1506
+
1507
+ console.log(JSON.stringify({
1508
+ ok: true,
1509
+ run_id: runId,
1510
+ namespace,
1511
+ workspace,
1512
+ manifest_path: manifestPath,
1513
+ created_feedback_ids: createdFeedbackIds,
1514
+ document_set_feedback_id: documentSetFeedbackId,
1515
+ document_set_review_request_id: documentSetRequestId,
1516
+ review_request_ids: reviewRequestIds,
1517
+ role_matrix: roleMatrix,
1518
+ comment_verified: true,
1519
+ comment_sync_roundtrip_verified: true,
1520
+ comment_attachment_sync_verified: commentAttachmentSyncVerified,
1521
+ stale_edit_rejection_and_resync_verified: true,
1522
+ forced_stale_override_verified: true,
1523
+ move_undo_history_verified: true,
1524
+ validation_verified: true,
1525
+ request_changes_revision_verified: true,
1526
+ rejection_and_cancellation_verified: true,
1527
+ pending_review_batch_block_verified: true,
1528
+ batch_review_verified: true,
1529
+ batch_active_move_verified: true,
1530
+ batch_archive_verified: true,
1531
+ three_document_prd_verified: true,
1532
+ document_scoped_refinement_verified: true,
1533
+ prd_version_diff_verified: true,
1534
+ archived_sync_exclusion_verified: true,
1535
1535
  project_key_unarchive_block_verified: true,
1536
1536
  }, null, 2));
1537
- } catch (err) {
1538
- const cleanupFeedbackIds = [
1539
- ...createdFeedbackIds,
1540
- ...(documentSetFeedbackId ? [documentSetFeedbackId] : []),
1541
- ];
1542
- if (cleanupFeedbackIds.length) {
1543
- try {
1537
+ } catch (err) {
1538
+ const cleanupFeedbackIds = [
1539
+ ...createdFeedbackIds,
1540
+ ...(documentSetFeedbackId ? [documentSetFeedbackId] : []),
1541
+ ];
1542
+ if (cleanupFeedbackIds.length) {
1543
+ try {
1544
1544
  runCli([
1545
1545
  "feedback",
1546
- "move",
1547
- "--feedback-ids",
1548
- cleanupFeedbackIds.join(","),
1546
+ "move",
1547
+ "--feedback-ids",
1548
+ cleanupFeedbackIds.join(","),
1549
1549
  "--to-state",
1550
1550
  "archived",
1551
1551
  "--reason",
@@ -1556,29 +1556,29 @@ async function main() {
1556
1556
  } catch (_cleanupErr) {
1557
1557
  // Keep the original error. Cleanup is best effort and must not hide failure evidence.
1558
1558
  }
1559
- }
1560
- manifest.state = "cleanup_required";
1561
- manifest.failure = {
1562
- message: String(err?.message || err).slice(0, 2000),
1563
- recorded_at: new Date().toISOString(),
1564
- };
1565
- manifest.updated_at = new Date().toISOString();
1566
- try {
1567
- writeCertificationManifest(manifestPath, manifest);
1568
- } catch (_manifestErr) {
1569
- // Preserve the primary harness failure.
1570
- }
1571
- const failure = {
1572
- ok: false,
1573
- run_id: runId,
1574
- namespace,
1575
- workspace,
1576
- manifest_path: manifestPath,
1577
- created_feedback_ids: createdFeedbackIds,
1578
- document_set_feedback_id: documentSetFeedbackId,
1579
- document_set_review_request_id: documentSetRequestId,
1580
- review_request_ids: reviewRequestIds,
1581
- role_matrix: roleMatrix,
1559
+ }
1560
+ manifest.state = "cleanup_required";
1561
+ manifest.failure = {
1562
+ message: String(err?.message || err).slice(0, 2000),
1563
+ recorded_at: new Date().toISOString(),
1564
+ };
1565
+ manifest.updated_at = new Date().toISOString();
1566
+ try {
1567
+ writeCertificationManifest(manifestPath, manifest);
1568
+ } catch (_manifestErr) {
1569
+ // Preserve the primary harness failure.
1570
+ }
1571
+ const failure = {
1572
+ ok: false,
1573
+ run_id: runId,
1574
+ namespace,
1575
+ workspace,
1576
+ manifest_path: manifestPath,
1577
+ created_feedback_ids: createdFeedbackIds,
1578
+ document_set_feedback_id: documentSetFeedbackId,
1579
+ document_set_review_request_id: documentSetRequestId,
1580
+ review_request_ids: reviewRequestIds,
1581
+ role_matrix: roleMatrix,
1582
1582
  message: err?.message || String(err),
1583
1583
  };
1584
1584
  console.error(JSON.stringify(failure, null, 2));
@@ -1586,10 +1586,10 @@ async function main() {
1586
1586
  }
1587
1587
  }
1588
1588
 
1589
- main().catch((err) => {
1590
- console.error(JSON.stringify({
1591
- ok: false,
1592
- message: err?.message || String(err),
1593
- }, null, 2));
1594
- process.exitCode = 1;
1595
- });
1589
+ main().catch((err) => {
1590
+ console.error(JSON.stringify({
1591
+ ok: false,
1592
+ message: err?.message || String(err),
1593
+ }, null, 2));
1594
+ process.exitCode = 1;
1595
+ });