@mytegroupinc/myte-core 0.0.46 → 0.0.49
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.
- package/README.md +104 -43
- package/cli.js +1896 -430
- package/lib/certification-manifest.js +178 -0
- package/package.json +7 -4
- package/scripts/feedback-certification-cleanup.js +53 -0
- package/scripts/feedback-live-full-harness.js +929 -122
- package/scripts/project-assistant-read-certification.js +258 -0
|
@@ -4,9 +4,40 @@
|
|
|
4
4
|
const fs = require("node:fs");
|
|
5
5
|
const os = require("node:os");
|
|
6
6
|
const path = require("node:path");
|
|
7
|
-
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
8
|
+
const {
|
|
9
|
+
createCertificationManifest,
|
|
10
|
+
recordCertificationArtifact,
|
|
11
|
+
validateCertificationManifest,
|
|
12
|
+
} = require("../lib/certification-manifest");
|
|
8
13
|
|
|
9
|
-
const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
|
|
14
|
+
const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
|
|
15
|
+
const LIVE_SCENARIO_MATRIX = [
|
|
16
|
+
{ id: "doctor", kind: "transport", mutation: "none", required: true },
|
|
17
|
+
{ id: "config", kind: "scope", mutation: "none", required: true },
|
|
18
|
+
{ id: "create-independent-prds", kind: "write", mutation: "test_feedback", required: true },
|
|
19
|
+
{ id: "create-three-document-prd", kind: "write", mutation: "test_feedback", required: true },
|
|
20
|
+
{ id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
|
|
21
|
+
{ id: "feedback-get-history", kind: "read", mutation: "none", required: true },
|
|
22
|
+
{ id: "feedback-comment", kind: "write", mutation: "test_comment", required: true },
|
|
23
|
+
{ id: "feedback-move-undo", kind: "governed-write", mutation: "test_feedback", required: true },
|
|
24
|
+
{ id: "feedback-validate-submit", kind: "review", mutation: "test_review", required: true },
|
|
25
|
+
{ id: "feedback-review-list-detail", kind: "read", mutation: "none", required: true },
|
|
26
|
+
{ id: "feedback-request-changes-revise", kind: "review", mutation: "test_review", required: true },
|
|
27
|
+
{ id: "feedback-reject-cancel", kind: "review", mutation: "test_review", required: true },
|
|
28
|
+
{ id: "feedback-batch-review", kind: "governed-write", mutation: "test_review", required: true },
|
|
29
|
+
{ id: "feedback-document-refinement", kind: "review", mutation: "test_feedback", required: true },
|
|
30
|
+
{ id: "feedback-prd-versions-diff", kind: "read", mutation: "none", required: true },
|
|
31
|
+
{ id: "feedback-batch-move-archive", kind: "governed-write", mutation: "test_feedback", required: true },
|
|
32
|
+
{ id: "project-key-unarchive-denial", kind: "negative-permission", mutation: "none", required: true },
|
|
33
|
+
{
|
|
34
|
+
id: "owner-delegate-collaborator-matrix",
|
|
35
|
+
kind: "role-permission",
|
|
36
|
+
mutation: "test_feedback",
|
|
37
|
+
required: "when --require-role-matrix is set",
|
|
38
|
+
},
|
|
39
|
+
{ id: "exact-id-cleanup", kind: "cleanup", mutation: "archive_test_feedback", required: true },
|
|
40
|
+
];
|
|
10
41
|
|
|
11
42
|
function parseArgs(argv) {
|
|
12
43
|
const args = { _: [] };
|
|
@@ -28,19 +59,28 @@ function parseArgs(argv) {
|
|
|
28
59
|
return args;
|
|
29
60
|
}
|
|
30
61
|
|
|
31
|
-
function requireConfirm(args) {
|
|
32
|
-
if (!args["confirm-live"]) {
|
|
33
|
-
throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
|
|
62
|
+
function requireConfirm(args) {
|
|
63
|
+
if (!args["confirm-live"]) {
|
|
64
|
+
throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
|
|
34
65
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
if (
|
|
67
|
+
!process.env.MYTE_OWNER_API_KEY
|
|
68
|
+
&& !process.env.MYTE_API_KEY
|
|
69
|
+
&& !process.env.MYTE_PROJECT_API_KEY
|
|
70
|
+
) {
|
|
71
|
+
throw new Error("Missing MYTE_OWNER_API_KEY, MYTE_API_KEY, or MYTE_PROJECT_API_KEY.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
|
|
76
|
+
const env = { ...process.env };
|
|
77
|
+
if (apiKey) {
|
|
78
|
+
env.MYTE_API_KEY = apiKey;
|
|
79
|
+
env.MYTE_PROJECT_API_KEY = apiKey;
|
|
80
|
+
}
|
|
81
|
+
const result = spawnSync(process.execPath, [CLI_PATH, ...cliArgs], {
|
|
82
|
+
cwd,
|
|
83
|
+
env,
|
|
44
84
|
encoding: "utf8",
|
|
45
85
|
stdio: ["ignore", "pipe", "pipe"],
|
|
46
86
|
});
|
|
@@ -68,7 +108,7 @@ function assert(condition, message) {
|
|
|
68
108
|
}
|
|
69
109
|
}
|
|
70
110
|
|
|
71
|
-
function writePrd(workspace, title, index) {
|
|
111
|
+
function writePrd(workspace, title, index) {
|
|
72
112
|
const filePath = path.join(workspace, `feedback-live-harness-${index}.md`);
|
|
73
113
|
fs.writeFileSync(
|
|
74
114
|
filePath,
|
|
@@ -87,8 +127,99 @@ function writePrd(workspace, title, index) {
|
|
|
87
127
|
].join("\n"),
|
|
88
128
|
"utf8",
|
|
89
129
|
);
|
|
90
|
-
return filePath;
|
|
91
|
-
}
|
|
130
|
+
return filePath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function writeCreateApproval(workspace, runId, prdFiles) {
|
|
134
|
+
const approvalPath = path.join(workspace, `feedback-live-harness-${runId}-approval.md`);
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
approvalPath,
|
|
137
|
+
[
|
|
138
|
+
"# Myte Feedback Certification Approval",
|
|
139
|
+
"",
|
|
140
|
+
`run_id: ${runId}`,
|
|
141
|
+
"command: myte create-prd",
|
|
142
|
+
`batch_count: ${prdFiles.length}`,
|
|
143
|
+
"targets:",
|
|
144
|
+
...prdFiles.map((filePath) => `- ${path.basename(filePath)}`),
|
|
145
|
+
"proposed_change: Create disposable, namespaced TEST Feedback records for post-deploy certification.",
|
|
146
|
+
"reason: Verify the published CLI and deployed Feedback workflow before exact-ID archival.",
|
|
147
|
+
"",
|
|
148
|
+
].join("\n"),
|
|
149
|
+
"utf8",
|
|
150
|
+
);
|
|
151
|
+
return approvalPath;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function writeCommentApproval(workspace, runId, feedbackId) {
|
|
155
|
+
const filePath = path.join(workspace, `feedback-live-harness-${runId}-comment.md`);
|
|
156
|
+
fs.writeFileSync(
|
|
157
|
+
filePath,
|
|
158
|
+
[
|
|
159
|
+
`# ${runId} Feedback certification comment`,
|
|
160
|
+
"",
|
|
161
|
+
`feedback_id: ${feedbackId}`,
|
|
162
|
+
`run_id: ${runId}`,
|
|
163
|
+
"proposed_change: Record a disposable, namespaced certification comment.",
|
|
164
|
+
"reason: Verify the project-scoped Feedback comment route and idempotency controls.",
|
|
165
|
+
"",
|
|
166
|
+
].join("\n"),
|
|
167
|
+
"utf8",
|
|
168
|
+
);
|
|
169
|
+
return filePath;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function responseRequestId(payload) {
|
|
173
|
+
return String(
|
|
174
|
+
payload?.request?.request_id
|
|
175
|
+
|| payload?.request?._id
|
|
176
|
+
|| payload?.request_id
|
|
177
|
+
|| payload?._id
|
|
178
|
+
|| "",
|
|
179
|
+
).trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function responseEventId(payload) {
|
|
183
|
+
return String(
|
|
184
|
+
payload?.event?.event_id
|
|
185
|
+
|| payload?.event?._id
|
|
186
|
+
|| payload?.event_id
|
|
187
|
+
|| "",
|
|
188
|
+
).trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function responseVersionId(version) {
|
|
192
|
+
return String(version?.version_id || version?._id || "").trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function assertDocumentSetMaterialized(workspace, feedbackId, expectedTitles) {
|
|
196
|
+
const root = path.join(
|
|
197
|
+
workspace,
|
|
198
|
+
"MyteCommandCenter",
|
|
199
|
+
"PRD",
|
|
200
|
+
"feedback-sync",
|
|
201
|
+
feedbackId,
|
|
202
|
+
);
|
|
203
|
+
assert(fs.existsSync(root), `Missing synchronized PRD document directory for ${feedbackId}.`);
|
|
204
|
+
const files = fs.readdirSync(root)
|
|
205
|
+
.filter((name) => name.toLowerCase().endsWith(".md"))
|
|
206
|
+
.sort();
|
|
207
|
+
assert(files.length === expectedTitles.length, `Expected ${expectedTitles.length} synchronized PRD files, got ${files.length}.`);
|
|
208
|
+
files.forEach((name, index) => {
|
|
209
|
+
const content = fs.readFileSync(path.join(root, name), "utf8");
|
|
210
|
+
assert(
|
|
211
|
+
content.includes(`# ${expectedTitles[index]}`),
|
|
212
|
+
`Synchronized document ${name} did not preserve its expected title.`,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
return files;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeCertificationManifest(manifestPath, manifest) {
|
|
219
|
+
validateCertificationManifest(manifest);
|
|
220
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
221
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
222
|
+
}
|
|
92
223
|
|
|
93
224
|
function loadFeedbackManifest(workspace) {
|
|
94
225
|
const manifestPath = path.join(workspace, "MyteCommandCenter", "data", "feedback.yml");
|
|
@@ -163,9 +294,44 @@ function assertFeedbackAbsent(workspace, feedbackIds) {
|
|
|
163
294
|
}
|
|
164
295
|
}
|
|
165
296
|
|
|
166
|
-
function main() {
|
|
167
|
-
const args = parseArgs(process.argv.slice(2));
|
|
168
|
-
|
|
297
|
+
function main() {
|
|
298
|
+
const args = parseArgs(process.argv.slice(2));
|
|
299
|
+
if (!args["confirm-live"]) {
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
ok: true,
|
|
302
|
+
dry_run: true,
|
|
303
|
+
message: "No network, project mutation, or local certification artifact was created.",
|
|
304
|
+
required_live_flag: "--confirm-live",
|
|
305
|
+
optional_strict_role_flag: "--require-role-matrix",
|
|
306
|
+
required_role_environment: [
|
|
307
|
+
"MYTE_OWNER_API_KEY",
|
|
308
|
+
"MYTE_DELEGATE_API_KEY",
|
|
309
|
+
"MYTE_COLLABORATOR_API_KEY",
|
|
310
|
+
],
|
|
311
|
+
scenario_matrix: LIVE_SCENARIO_MATRIX,
|
|
312
|
+
}, null, 2));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
requireConfirm(args);
|
|
316
|
+
|
|
317
|
+
const ownerApiKey = String(
|
|
318
|
+
process.env.MYTE_OWNER_API_KEY
|
|
319
|
+
|| process.env.MYTE_API_KEY
|
|
320
|
+
|| process.env.MYTE_PROJECT_API_KEY
|
|
321
|
+
|| "",
|
|
322
|
+
).trim();
|
|
323
|
+
const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
|
|
324
|
+
const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
|
|
325
|
+
process.env.MYTE_API_KEY = ownerApiKey;
|
|
326
|
+
process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
|
|
327
|
+
if (
|
|
328
|
+
args["require-role-matrix"]
|
|
329
|
+
&& (!ownerApiKey || !delegateApiKey || !collaboratorApiKey)
|
|
330
|
+
) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
"--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
169
335
|
|
|
170
336
|
const workspace = path.resolve(
|
|
171
337
|
args.workspace || fs.mkdtempSync(path.join(os.tmpdir(), "myte-feedback-full-harness-")),
|
|
@@ -177,40 +343,283 @@ function main() {
|
|
|
177
343
|
baseArgs.push("--base-url", String(args["base-url"]));
|
|
178
344
|
}
|
|
179
345
|
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
346
|
+
const runId = String(args["run-id"] || new Date().toISOString().replace(/[:.]/g, "-"));
|
|
347
|
+
const namespace = `MYTE_TEST_${runId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
348
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
349
|
+
const reason = String(args.reason || "Disposable live Feedback harness verification");
|
|
350
|
+
const createdFeedbackIds = [];
|
|
351
|
+
const reviewRequestIds = [];
|
|
352
|
+
const reviewArtifactByRequestId = new Map();
|
|
353
|
+
let documentSetFeedbackId = null;
|
|
354
|
+
let documentSetRequestId = null;
|
|
355
|
+
const roleMatrix = {
|
|
356
|
+
required: Boolean(args["require-role-matrix"]),
|
|
357
|
+
owner: true,
|
|
358
|
+
delegate: Boolean(delegateApiKey),
|
|
359
|
+
collaborator: Boolean(collaboratorApiKey),
|
|
360
|
+
same_project: true,
|
|
361
|
+
collaborator_review_denied: null,
|
|
362
|
+
delegate_review_used: false,
|
|
363
|
+
};
|
|
364
|
+
const manifestPath = path.resolve(
|
|
365
|
+
args.manifest
|
|
366
|
+
|| path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
|
|
367
|
+
);
|
|
368
|
+
const manifest = createCertificationManifest({
|
|
369
|
+
runId,
|
|
370
|
+
namespace,
|
|
371
|
+
environment: String(args.environment || "dev"),
|
|
372
|
+
});
|
|
373
|
+
manifest.state = "running";
|
|
374
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const doctor = runCli(
|
|
378
|
+
["doctor", "--json", "--timeout-ms", String(args["timeout-ms"] || 60000), ...baseArgs],
|
|
379
|
+
workspace,
|
|
380
|
+
{ apiKey: ownerApiKey },
|
|
381
|
+
).stdout;
|
|
382
|
+
assert(doctor.ok === true, `Myte doctor failed at ${doctor.failure_layer || "unknown"} layer.`);
|
|
383
|
+
|
|
384
|
+
const config = runCli(
|
|
385
|
+
["config", "--json", ...baseArgs],
|
|
386
|
+
workspace,
|
|
387
|
+
{ apiKey: ownerApiKey },
|
|
388
|
+
).stdout;
|
|
389
|
+
manifest.project_id = String(
|
|
390
|
+
config.project_id || config.project?.id || config.project?._id || "",
|
|
391
|
+
) || null;
|
|
392
|
+
for (const [role, apiKey] of [
|
|
393
|
+
["delegate", delegateApiKey],
|
|
394
|
+
["collaborator", collaboratorApiKey],
|
|
395
|
+
]) {
|
|
396
|
+
if (!apiKey) continue;
|
|
397
|
+
const roleConfig = runCli(
|
|
398
|
+
["config", "--json", ...baseArgs],
|
|
399
|
+
workspace,
|
|
400
|
+
{ apiKey },
|
|
401
|
+
).stdout;
|
|
402
|
+
const roleProjectId = String(
|
|
403
|
+
roleConfig.project_id || roleConfig.project?.id || roleConfig.project?._id || "",
|
|
404
|
+
).trim();
|
|
405
|
+
if (!manifest.project_id || roleProjectId !== manifest.project_id) {
|
|
406
|
+
roleMatrix.same_project = false;
|
|
407
|
+
throw new Error(`${role} certification key is not scoped to the owner key's project.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
411
|
+
|
|
412
|
+
const prdFiles = [];
|
|
413
|
+
for (let index = 1; index <= 5; index += 1) {
|
|
414
|
+
const title = `${namespace} Feedback ${stamp} ${index}`;
|
|
415
|
+
prdFiles.push(writePrd(workspace, title, index));
|
|
416
|
+
}
|
|
417
|
+
const createApprovalPath = writeCreateApproval(workspace, runId, prdFiles);
|
|
418
|
+
|
|
419
|
+
const create = runCli([
|
|
420
|
+
"create-prd",
|
|
194
421
|
...prdFiles,
|
|
195
|
-
"--description",
|
|
196
|
-
|
|
197
|
-
"--
|
|
422
|
+
"--description",
|
|
423
|
+
`Disposable ${namespace} Feedback created by the live certification harness.`,
|
|
424
|
+
"--confirm-write",
|
|
425
|
+
"--approval-artifact",
|
|
426
|
+
createApprovalPath,
|
|
427
|
+
"--json",
|
|
198
428
|
...baseArgs,
|
|
199
429
|
], workspace).stdout;
|
|
200
430
|
|
|
201
431
|
const createdItems = Array.isArray(create.items) ? create.items : [];
|
|
202
432
|
for (const item of createdItems) {
|
|
203
|
-
if (item.status === "created" && item.feedback_id) {
|
|
204
|
-
createdFeedbackIds.push(String(item.feedback_id));
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
433
|
+
if (item.status === "created" && item.feedback_id) {
|
|
434
|
+
createdFeedbackIds.push(String(item.feedback_id));
|
|
435
|
+
recordCertificationArtifact(manifest, {
|
|
436
|
+
kind: "feedback",
|
|
437
|
+
id: String(item.feedback_id),
|
|
438
|
+
metadata: { source: "create-prd", cleanup: "archive" },
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
443
|
+
assert(createdFeedbackIds.length === 5, `Expected 5 created TEST feedback items, got ${createdFeedbackIds.length}: ${JSON.stringify(create, null, 2)}`);
|
|
444
|
+
|
|
445
|
+
const documentSetTitles = [
|
|
446
|
+
`${namespace} Document Set Overview`,
|
|
447
|
+
`${namespace} Document Set Harness`,
|
|
448
|
+
`${namespace} Document Set Security`,
|
|
449
|
+
];
|
|
450
|
+
const documentSetFiles = documentSetTitles.map((title, index) =>
|
|
451
|
+
writePrd(workspace, title, 101 + index),
|
|
452
|
+
);
|
|
453
|
+
const documentSetApprovalPath = writeCreateApproval(
|
|
454
|
+
workspace,
|
|
455
|
+
`${runId}-document-set`,
|
|
456
|
+
documentSetFiles,
|
|
457
|
+
);
|
|
458
|
+
const documentSetCreate = runCli([
|
|
459
|
+
"create-prd",
|
|
460
|
+
...documentSetFiles,
|
|
461
|
+
"--document-set",
|
|
462
|
+
"--title",
|
|
463
|
+
`${namespace} Three Document PRD`,
|
|
464
|
+
"--description",
|
|
465
|
+
`Disposable ${namespace} three-document PRD certification record.`,
|
|
466
|
+
"--confirm-write",
|
|
467
|
+
"--approval-artifact",
|
|
468
|
+
documentSetApprovalPath,
|
|
469
|
+
"--json",
|
|
470
|
+
...baseArgs,
|
|
471
|
+
], workspace).stdout;
|
|
472
|
+
documentSetFeedbackId = String(documentSetCreate.feedback_id || "");
|
|
473
|
+
assert(documentSetFeedbackId, `Document-set creation did not return feedback_id: ${JSON.stringify(documentSetCreate, null, 2)}`);
|
|
474
|
+
assert(documentSetCreate.document_count === 3, `Expected three PRD documents: ${JSON.stringify(documentSetCreate, null, 2)}`);
|
|
475
|
+
assert(Array.isArray(documentSetCreate.documents) && documentSetCreate.documents.length === 3, "Document-set metadata did not contain three documents.");
|
|
476
|
+
recordCertificationArtifact(manifest, {
|
|
477
|
+
kind: "feedback",
|
|
478
|
+
id: documentSetFeedbackId,
|
|
479
|
+
metadata: {
|
|
480
|
+
source: "create-prd-document-set",
|
|
481
|
+
document_count: 3,
|
|
482
|
+
cleanup: "archive",
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
for (const document of documentSetCreate.documents) {
|
|
486
|
+
if (!document.document_id) continue;
|
|
487
|
+
recordCertificationArtifact(manifest, {
|
|
488
|
+
kind: "object",
|
|
489
|
+
id: String(document.document_id),
|
|
490
|
+
metadata: {
|
|
491
|
+
object_type: "prd_document",
|
|
492
|
+
feedback_id: documentSetFeedbackId,
|
|
493
|
+
cleanup: "retained_with_archived_parent",
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
498
|
+
|
|
499
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
500
|
+
assertFeedbackPresent(workspace, createdFeedbackIds, "todo");
|
|
501
|
+
assertFeedbackPresent(workspace, [documentSetFeedbackId], "todo");
|
|
502
|
+
assertDocumentSetMaterialized(
|
|
503
|
+
workspace,
|
|
504
|
+
documentSetFeedbackId,
|
|
505
|
+
documentSetTitles,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
|
|
509
|
+
const operationalReviewKey = delegateApiKey || ownerApiKey;
|
|
510
|
+
const commentApprovalPath = writeCommentApproval(
|
|
511
|
+
workspace,
|
|
512
|
+
runId,
|
|
513
|
+
createdFeedbackIds[0],
|
|
514
|
+
);
|
|
515
|
+
const comment = runCli([
|
|
516
|
+
"feedback",
|
|
517
|
+
"comment",
|
|
518
|
+
"--feedback-id",
|
|
519
|
+
createdFeedbackIds[0],
|
|
520
|
+
"--body-file",
|
|
521
|
+
commentApprovalPath,
|
|
522
|
+
"--confirm-write",
|
|
523
|
+
"--approval-artifact",
|
|
524
|
+
commentApprovalPath,
|
|
525
|
+
"--no-sync",
|
|
526
|
+
"--json",
|
|
527
|
+
...baseArgs,
|
|
528
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
529
|
+
assert(comment.comment_id, `Feedback comment did not return comment_id: ${JSON.stringify(comment, null, 2)}`);
|
|
530
|
+
recordCertificationArtifact(manifest, {
|
|
531
|
+
kind: "comment",
|
|
532
|
+
id: String(comment.comment_id),
|
|
533
|
+
metadata: {
|
|
534
|
+
feedback_id: createdFeedbackIds[0],
|
|
535
|
+
cleanup: "retained_with_archived_parent",
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const directMove = runCli([
|
|
540
|
+
"feedback",
|
|
541
|
+
"move",
|
|
542
|
+
"--feedback-id",
|
|
543
|
+
createdFeedbackIds[0],
|
|
544
|
+
"--from-state",
|
|
545
|
+
"todo",
|
|
546
|
+
"--to-state",
|
|
547
|
+
"in_progress",
|
|
548
|
+
"--reason",
|
|
549
|
+
"Harness verifies a collaborator-safe active-state move.",
|
|
550
|
+
"--no-sync",
|
|
551
|
+
"--json",
|
|
552
|
+
...baseArgs,
|
|
553
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
554
|
+
const directMoveEventId = responseEventId(directMove);
|
|
555
|
+
assert(directMoveEventId, `Feedback move did not return an event id: ${JSON.stringify(directMove, null, 2)}`);
|
|
556
|
+
recordCertificationArtifact(manifest, {
|
|
557
|
+
kind: "feedback_event",
|
|
558
|
+
id: directMoveEventId,
|
|
559
|
+
metadata: {
|
|
560
|
+
feedback_id: createdFeedbackIds[0],
|
|
561
|
+
action: "move",
|
|
562
|
+
cleanup: "retain_audit",
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const undo = runCli([
|
|
567
|
+
"feedback",
|
|
568
|
+
"undo",
|
|
569
|
+
"--feedback-id",
|
|
570
|
+
createdFeedbackIds[0],
|
|
571
|
+
"--event-id",
|
|
572
|
+
directMoveEventId,
|
|
573
|
+
"--reason",
|
|
574
|
+
"Harness verifies exact event undo.",
|
|
575
|
+
"--json",
|
|
576
|
+
...baseArgs,
|
|
577
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
578
|
+
assert(
|
|
579
|
+
String(undo.feedback_state || "") === "todo",
|
|
580
|
+
`Feedback undo did not restore todo: ${JSON.stringify(undo, null, 2)}`,
|
|
581
|
+
);
|
|
582
|
+
const undoEventId = responseEventId(undo);
|
|
583
|
+
if (undoEventId) {
|
|
584
|
+
recordCertificationArtifact(manifest, {
|
|
585
|
+
kind: "feedback_event",
|
|
586
|
+
id: undoEventId,
|
|
587
|
+
metadata: {
|
|
588
|
+
feedback_id: createdFeedbackIds[0],
|
|
589
|
+
action: "undo",
|
|
590
|
+
cleanup: "retain_audit",
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const getAfterUndo = runCli([
|
|
596
|
+
"feedback",
|
|
597
|
+
"get",
|
|
598
|
+
"--feedback-id",
|
|
599
|
+
createdFeedbackIds[0],
|
|
600
|
+
"--json",
|
|
601
|
+
...baseArgs,
|
|
602
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
603
|
+
assert(
|
|
604
|
+
String(getAfterUndo.feedback?.feedback_state || "") === "todo",
|
|
605
|
+
`Feedback get did not return the restored state: ${JSON.stringify(getAfterUndo, null, 2)}`,
|
|
606
|
+
);
|
|
607
|
+
const historyAfterUndo = runCli([
|
|
608
|
+
"feedback",
|
|
609
|
+
"history",
|
|
610
|
+
"--feedback-id",
|
|
611
|
+
createdFeedbackIds[0],
|
|
612
|
+
"--json",
|
|
613
|
+
...baseArgs,
|
|
614
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
615
|
+
assert(
|
|
616
|
+
Array.isArray(historyAfterUndo.events) && historyAfterUndo.events.length >= 2,
|
|
617
|
+
"Feedback history did not include move and undo evidence.",
|
|
618
|
+
);
|
|
619
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
620
|
+
|
|
621
|
+
for (const feedbackId of createdFeedbackIds) {
|
|
622
|
+
const draft = runCli([
|
|
214
623
|
"feedback",
|
|
215
624
|
"status",
|
|
216
625
|
"--feedback-id",
|
|
@@ -221,23 +630,177 @@ function main() {
|
|
|
221
630
|
reason,
|
|
222
631
|
"--json",
|
|
223
632
|
...baseArgs,
|
|
224
|
-
], workspace).stdout;
|
|
225
|
-
assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
|
|
226
|
-
|
|
227
|
-
const
|
|
633
|
+
], workspace).stdout;
|
|
634
|
+
assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
|
|
635
|
+
|
|
636
|
+
const validation = runCli([
|
|
637
|
+
"feedback",
|
|
638
|
+
"validate",
|
|
639
|
+
"--file",
|
|
640
|
+
draft.artifact_path,
|
|
641
|
+
"--json",
|
|
642
|
+
...baseArgs,
|
|
643
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
644
|
+
assert(
|
|
645
|
+
validation.ok === true && validation.valid !== false,
|
|
646
|
+
`feedback validate failed for ${feedbackId}: ${JSON.stringify(validation, null, 2)}`,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const submit = runCli([
|
|
228
650
|
"feedback",
|
|
229
651
|
"submit",
|
|
230
|
-
"--file",
|
|
231
|
-
draft.artifact_path,
|
|
232
|
-
"--
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
652
|
+
"--file",
|
|
653
|
+
draft.artifact_path,
|
|
654
|
+
"--confirm-write",
|
|
655
|
+
"--approval-artifact",
|
|
656
|
+
draft.artifact_path,
|
|
657
|
+
"--json",
|
|
658
|
+
...baseArgs,
|
|
659
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
660
|
+
const requestId = responseRequestId(submit);
|
|
236
661
|
assert(requestId, `feedback submit did not return request id for ${feedbackId}: ${JSON.stringify(submit, null, 2)}`);
|
|
237
|
-
reviewRequestIds.push(requestId);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
662
|
+
reviewRequestIds.push(requestId);
|
|
663
|
+
reviewArtifactByRequestId.set(requestId, draft.artifact_path);
|
|
664
|
+
recordCertificationArtifact(manifest, {
|
|
665
|
+
kind: "review_request",
|
|
666
|
+
id: requestId,
|
|
667
|
+
metadata: { feedback_id: feedbackId },
|
|
668
|
+
});
|
|
669
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const openReviews = runCli([
|
|
673
|
+
"feedback",
|
|
674
|
+
"reviews",
|
|
675
|
+
"--status",
|
|
676
|
+
"open",
|
|
677
|
+
"--json",
|
|
678
|
+
...baseArgs,
|
|
679
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
680
|
+
const openRequestIds = new Set(
|
|
681
|
+
(openReviews.requests || []).map((request) => responseRequestId(request)).filter(Boolean),
|
|
682
|
+
);
|
|
683
|
+
assert(
|
|
684
|
+
reviewRequestIds.every((requestId) => openRequestIds.has(requestId)),
|
|
685
|
+
"Feedback review list did not include every submitted certification request.",
|
|
686
|
+
);
|
|
687
|
+
const reviewDetail = runCli([
|
|
688
|
+
"feedback",
|
|
689
|
+
"reviews",
|
|
690
|
+
"--request-id",
|
|
691
|
+
reviewRequestIds[0],
|
|
692
|
+
"--json",
|
|
693
|
+
...baseArgs,
|
|
694
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
695
|
+
assert(
|
|
696
|
+
responseRequestId(reviewDetail) === reviewRequestIds[0],
|
|
697
|
+
"Feedback review detail returned the wrong request.",
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
if (collaboratorApiKey) {
|
|
701
|
+
const deniedReview = runCli([
|
|
702
|
+
"feedback",
|
|
703
|
+
"review",
|
|
704
|
+
"--request-id",
|
|
705
|
+
reviewRequestIds[0],
|
|
706
|
+
"--action",
|
|
707
|
+
"approve",
|
|
708
|
+
"--reason",
|
|
709
|
+
"Harness expects regular collaborator approval to be denied.",
|
|
710
|
+
"--no-sync",
|
|
711
|
+
"--json",
|
|
712
|
+
...baseArgs,
|
|
713
|
+
], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
|
|
714
|
+
roleMatrix.collaborator_review_denied =
|
|
715
|
+
deniedReview.ok === false && deniedReview.status === 403;
|
|
716
|
+
assert(
|
|
717
|
+
roleMatrix.collaborator_review_denied,
|
|
718
|
+
`Regular collaborator review was not denied as expected: ${JSON.stringify(deniedReview, null, 2)}`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const requestChanges = runCli([
|
|
723
|
+
"feedback",
|
|
724
|
+
"review",
|
|
725
|
+
"--request-id",
|
|
726
|
+
reviewRequestIds[0],
|
|
727
|
+
"--action",
|
|
728
|
+
"request_changes",
|
|
729
|
+
"--reason",
|
|
730
|
+
"Harness requests a submitter revision.",
|
|
731
|
+
"--verification-evidence",
|
|
732
|
+
"Initial validation completed; revision path remains to be certified.",
|
|
733
|
+
"--no-sync",
|
|
734
|
+
"--json",
|
|
735
|
+
...baseArgs,
|
|
736
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
737
|
+
assert(
|
|
738
|
+
String(requestChanges.request?.status || requestChanges.status || "") === "needs_changes",
|
|
739
|
+
`Request-changes did not enter needs_changes: ${JSON.stringify(requestChanges, null, 2)}`,
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
const revised = runCli([
|
|
743
|
+
"feedback",
|
|
744
|
+
"revise",
|
|
745
|
+
"--request-id",
|
|
746
|
+
reviewRequestIds[0],
|
|
747
|
+
"--file",
|
|
748
|
+
reviewArtifactByRequestId.get(reviewRequestIds[0]),
|
|
749
|
+
"--confirm-write",
|
|
750
|
+
"--approval-artifact",
|
|
751
|
+
reviewArtifactByRequestId.get(reviewRequestIds[0]),
|
|
752
|
+
"--json",
|
|
753
|
+
...baseArgs,
|
|
754
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
755
|
+
assert(
|
|
756
|
+
responseRequestId(revised) === reviewRequestIds[0],
|
|
757
|
+
`Feedback revise returned the wrong request: ${JSON.stringify(revised, null, 2)}`,
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const rejected = runCli([
|
|
761
|
+
"feedback",
|
|
762
|
+
"review",
|
|
763
|
+
"--request-id",
|
|
764
|
+
reviewRequestIds[1],
|
|
765
|
+
"--action",
|
|
766
|
+
"reject",
|
|
767
|
+
"--reason",
|
|
768
|
+
"Harness verifies terminal rejection without applying changes.",
|
|
769
|
+
"--verification-evidence",
|
|
770
|
+
"Negative review path certified.",
|
|
771
|
+
"--no-sync",
|
|
772
|
+
"--json",
|
|
773
|
+
...baseArgs,
|
|
774
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
775
|
+
assert(
|
|
776
|
+
String(rejected.request?.status || rejected.status || "") === "rejected",
|
|
777
|
+
`Feedback rejection did not become terminal: ${JSON.stringify(rejected, null, 2)}`,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const cancelled = runCli([
|
|
781
|
+
"feedback",
|
|
782
|
+
"review",
|
|
783
|
+
"--request-id",
|
|
784
|
+
reviewRequestIds[2],
|
|
785
|
+
"--action",
|
|
786
|
+
"cancel",
|
|
787
|
+
"--reason",
|
|
788
|
+
"Harness submitter cancels a disposable review.",
|
|
789
|
+
"--no-sync",
|
|
790
|
+
"--json",
|
|
791
|
+
...baseArgs,
|
|
792
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
793
|
+
assert(
|
|
794
|
+
String(cancelled.request?.status || cancelled.status || "") === "cancelled",
|
|
795
|
+
`Feedback cancellation did not become terminal: ${JSON.stringify(cancelled, null, 2)}`,
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
const pendingApprovalRequestIds = [
|
|
799
|
+
reviewRequestIds[0],
|
|
800
|
+
reviewRequestIds[3],
|
|
801
|
+
reviewRequestIds[4],
|
|
802
|
+
];
|
|
803
|
+
const blocked = runCli([
|
|
241
804
|
"feedback",
|
|
242
805
|
"move",
|
|
243
806
|
"--feedback-ids",
|
|
@@ -256,28 +819,42 @@ function main() {
|
|
|
256
819
|
assert(blocked.status === 409, `Expected 409 pending review block, got ${blocked.status}.`);
|
|
257
820
|
assert(blocked.data?.code === "pending_feedback_review", `Expected pending_feedback_review, got ${JSON.stringify(blocked, null, 2)}.`);
|
|
258
821
|
|
|
259
|
-
const review = runCli([
|
|
260
|
-
"feedback",
|
|
261
|
-
"review",
|
|
262
|
-
"--request-ids",
|
|
263
|
-
|
|
264
|
-
"--action",
|
|
265
|
-
"approve",
|
|
266
|
-
"--reason",
|
|
267
|
-
"Harness batch approval.",
|
|
268
|
-
"--
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
822
|
+
const review = runCli([
|
|
823
|
+
"feedback",
|
|
824
|
+
"review",
|
|
825
|
+
"--request-ids",
|
|
826
|
+
pendingApprovalRequestIds.join(","),
|
|
827
|
+
"--action",
|
|
828
|
+
"approve",
|
|
829
|
+
"--reason",
|
|
830
|
+
"Harness batch approval.",
|
|
831
|
+
"--verification-evidence",
|
|
832
|
+
"Validation, role checks, request-changes, and direct board guards passed.",
|
|
833
|
+
"--deployment-evidence",
|
|
834
|
+
"Dev post-deploy certification run; artifacts will be archived.",
|
|
835
|
+
"--json",
|
|
836
|
+
...baseArgs,
|
|
837
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
838
|
+
roleMatrix.delegate_review_used = Boolean(delegateApiKey);
|
|
839
|
+
assert(review.ok !== false && review.updated_count === 3, `batch review failed: ${JSON.stringify(review, null, 2)}`);
|
|
840
|
+
|
|
841
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
842
|
+
assertFeedbackPresent(
|
|
843
|
+
workspace,
|
|
844
|
+
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
|
|
845
|
+
"completed",
|
|
846
|
+
);
|
|
847
|
+
assertFeedbackPresent(
|
|
848
|
+
workspace,
|
|
849
|
+
[createdFeedbackIds[1], createdFeedbackIds[2]],
|
|
850
|
+
"todo",
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
const reopen = runCli([
|
|
277
854
|
"feedback",
|
|
278
855
|
"move",
|
|
279
856
|
"--feedback-ids",
|
|
280
|
-
createdFeedbackIds.join(","),
|
|
857
|
+
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]].join(","),
|
|
281
858
|
"--from-state",
|
|
282
859
|
"completed",
|
|
283
860
|
"--to-state",
|
|
@@ -287,29 +864,206 @@ function main() {
|
|
|
287
864
|
"--json",
|
|
288
865
|
...baseArgs,
|
|
289
866
|
], workspace).stdout;
|
|
290
|
-
assert(reopen.ok !== false && reopen.updated_count ===
|
|
291
|
-
|
|
292
|
-
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
293
|
-
assertFeedbackPresent(
|
|
867
|
+
assert(reopen.ok !== false && reopen.updated_count === 3, `batch active move failed: ${JSON.stringify(reopen, null, 2)}`);
|
|
868
|
+
|
|
869
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
870
|
+
assertFeedbackPresent(
|
|
871
|
+
workspace,
|
|
872
|
+
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
|
|
873
|
+
"in_progress",
|
|
874
|
+
);
|
|
294
875
|
|
|
295
876
|
const archive = runCli([
|
|
296
877
|
"feedback",
|
|
297
|
-
"move",
|
|
298
|
-
"--feedback-ids",
|
|
299
|
-
createdFeedbackIds.join(","),
|
|
300
|
-
"--
|
|
301
|
-
"
|
|
302
|
-
"--to-state",
|
|
303
|
-
"archived",
|
|
878
|
+
"move",
|
|
879
|
+
"--feedback-ids",
|
|
880
|
+
createdFeedbackIds.join(","),
|
|
881
|
+
"--to-state",
|
|
882
|
+
"archived",
|
|
304
883
|
"--reason",
|
|
305
884
|
"Harness cleanup archive.",
|
|
306
885
|
"--json",
|
|
307
886
|
...baseArgs,
|
|
308
|
-
], workspace).stdout;
|
|
309
|
-
assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
887
|
+
], workspace).stdout;
|
|
888
|
+
assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
|
|
889
|
+
|
|
890
|
+
const selectedDocument = documentSetCreate.documents[1];
|
|
891
|
+
const selectedDocumentId = String(selectedDocument?.document_id || "").trim();
|
|
892
|
+
assert(selectedDocumentId, "Three-document PRD did not return a second document id.");
|
|
893
|
+
const revisedDocumentTitle = `${namespace} Document Set Harness Revised`;
|
|
894
|
+
const revisedDocumentPath = writePrd(
|
|
895
|
+
workspace,
|
|
896
|
+
revisedDocumentTitle,
|
|
897
|
+
202,
|
|
898
|
+
);
|
|
899
|
+
const documentDraft = runCli([
|
|
900
|
+
"feedback",
|
|
901
|
+
"edit",
|
|
902
|
+
"--feedback-id",
|
|
903
|
+
documentSetFeedbackId,
|
|
904
|
+
"--prd-file",
|
|
905
|
+
revisedDocumentPath,
|
|
906
|
+
"--document-id",
|
|
907
|
+
selectedDocumentId,
|
|
908
|
+
"--reason",
|
|
909
|
+
"Harness verifies document-scoped refinement.",
|
|
910
|
+
"--json",
|
|
911
|
+
...baseArgs,
|
|
912
|
+
], workspace).stdout;
|
|
913
|
+
assert(documentDraft.artifact_path, "Document-scoped edit did not create a review artifact.");
|
|
914
|
+
const documentValidation = runCli([
|
|
915
|
+
"feedback",
|
|
916
|
+
"validate",
|
|
917
|
+
"--file",
|
|
918
|
+
documentDraft.artifact_path,
|
|
919
|
+
"--json",
|
|
920
|
+
...baseArgs,
|
|
921
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
922
|
+
assert(
|
|
923
|
+
documentValidation.ok === true && documentValidation.valid !== false,
|
|
924
|
+
`Document-scoped refinement did not validate: ${JSON.stringify(documentValidation, null, 2)}`,
|
|
925
|
+
);
|
|
926
|
+
const documentSubmit = runCli([
|
|
927
|
+
"feedback",
|
|
928
|
+
"submit",
|
|
929
|
+
"--file",
|
|
930
|
+
documentDraft.artifact_path,
|
|
931
|
+
"--confirm-write",
|
|
932
|
+
"--approval-artifact",
|
|
933
|
+
documentDraft.artifact_path,
|
|
934
|
+
"--json",
|
|
935
|
+
...baseArgs,
|
|
936
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
937
|
+
documentSetRequestId = responseRequestId(documentSubmit);
|
|
938
|
+
assert(documentSetRequestId, "Document-scoped refinement did not return a review request id.");
|
|
939
|
+
recordCertificationArtifact(manifest, {
|
|
940
|
+
kind: "review_request",
|
|
941
|
+
id: documentSetRequestId,
|
|
942
|
+
metadata: {
|
|
943
|
+
feedback_id: documentSetFeedbackId,
|
|
944
|
+
document_id: selectedDocumentId,
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
const documentApproval = runCli([
|
|
948
|
+
"feedback",
|
|
949
|
+
"review",
|
|
950
|
+
"--request-id",
|
|
951
|
+
documentSetRequestId,
|
|
952
|
+
"--action",
|
|
953
|
+
"approve",
|
|
954
|
+
"--reason",
|
|
955
|
+
"Harness approves one document without replacing its siblings.",
|
|
956
|
+
"--verification-evidence",
|
|
957
|
+
"Document-set validation and sibling-preservation checks passed.",
|
|
958
|
+
"--deployment-evidence",
|
|
959
|
+
"Dev post-deploy certification only.",
|
|
960
|
+
"--no-sync",
|
|
961
|
+
"--json",
|
|
962
|
+
...baseArgs,
|
|
963
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
964
|
+
assert(
|
|
965
|
+
String(documentApproval.request?.status || documentApproval.status || "") === "applied",
|
|
966
|
+
`Document-scoped approval did not apply: ${JSON.stringify(documentApproval, null, 2)}`,
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
970
|
+
const synchronizedDocumentRoot = path.join(
|
|
971
|
+
workspace,
|
|
972
|
+
"MyteCommandCenter",
|
|
973
|
+
"PRD",
|
|
974
|
+
"feedback-sync",
|
|
975
|
+
documentSetFeedbackId,
|
|
976
|
+
);
|
|
977
|
+
const synchronizedDocuments = fs.readdirSync(synchronizedDocumentRoot)
|
|
978
|
+
.filter((name) => name.toLowerCase().endsWith(".md"))
|
|
979
|
+
.sort();
|
|
980
|
+
assert(synchronizedDocuments.length === 3, "Document refinement changed the document count.");
|
|
981
|
+
assert(
|
|
982
|
+
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[0]), "utf8")
|
|
983
|
+
.includes(`# ${documentSetTitles[0]}`),
|
|
984
|
+
"Document refinement replaced the first sibling document.",
|
|
985
|
+
);
|
|
986
|
+
assert(
|
|
987
|
+
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[1]), "utf8")
|
|
988
|
+
.includes(`# ${revisedDocumentTitle}`),
|
|
989
|
+
"Document refinement did not update the selected second document.",
|
|
990
|
+
);
|
|
991
|
+
assert(
|
|
992
|
+
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[2]), "utf8")
|
|
993
|
+
.includes(`# ${documentSetTitles[2]}`),
|
|
994
|
+
"Document refinement replaced the third sibling document.",
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
const versions = runCli([
|
|
998
|
+
"feedback",
|
|
999
|
+
"prd-versions",
|
|
1000
|
+
"--feedback-id",
|
|
1001
|
+
documentSetFeedbackId,
|
|
1002
|
+
"--json",
|
|
1003
|
+
...baseArgs,
|
|
1004
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1005
|
+
assert(
|
|
1006
|
+
Array.isArray(versions.versions) && versions.versions.length >= 2,
|
|
1007
|
+
`Document refinement did not retain PRD versions: ${JSON.stringify(versions, null, 2)}`,
|
|
1008
|
+
);
|
|
1009
|
+
const activeVersionId = String(versions.active_prd_version_id || "").trim();
|
|
1010
|
+
const targetVersion =
|
|
1011
|
+
versions.versions.find((version) => responseVersionId(version) !== activeVersionId)
|
|
1012
|
+
|| versions.versions[0];
|
|
1013
|
+
const targetVersionId = responseVersionId(targetVersion);
|
|
1014
|
+
assert(targetVersionId, "PRD version list did not return a target version id.");
|
|
1015
|
+
const versionDiff = runCli([
|
|
1016
|
+
"feedback",
|
|
1017
|
+
"prd-diff",
|
|
1018
|
+
"--feedback-id",
|
|
1019
|
+
documentSetFeedbackId,
|
|
1020
|
+
"--version-id",
|
|
1021
|
+
targetVersionId,
|
|
1022
|
+
"--document-id",
|
|
1023
|
+
selectedDocumentId,
|
|
1024
|
+
"--json",
|
|
1025
|
+
...baseArgs,
|
|
1026
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1027
|
+
assert(
|
|
1028
|
+
String(versionDiff.document_id || "") === selectedDocumentId,
|
|
1029
|
+
`PRD diff was not scoped to the selected document: ${JSON.stringify(versionDiff, null, 2)}`,
|
|
1030
|
+
);
|
|
1031
|
+
for (const version of versions.versions) {
|
|
1032
|
+
const versionId = responseVersionId(version);
|
|
1033
|
+
if (!versionId) continue;
|
|
1034
|
+
recordCertificationArtifact(manifest, {
|
|
1035
|
+
kind: "object",
|
|
1036
|
+
id: versionId,
|
|
1037
|
+
metadata: {
|
|
1038
|
+
object_type: "prd_version",
|
|
1039
|
+
feedback_id: documentSetFeedbackId,
|
|
1040
|
+
cleanup: "retain_audit",
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1045
|
+
|
|
1046
|
+
const archiveDocumentSet = runCli([
|
|
1047
|
+
"feedback",
|
|
1048
|
+
"move",
|
|
1049
|
+
"--feedback-id",
|
|
1050
|
+
documentSetFeedbackId,
|
|
1051
|
+
"--from-state",
|
|
1052
|
+
"todo",
|
|
1053
|
+
"--to-state",
|
|
1054
|
+
"archived",
|
|
1055
|
+
"--reason",
|
|
1056
|
+
"Harness cleanup archive for three-document PRD.",
|
|
1057
|
+
"--json",
|
|
1058
|
+
...baseArgs,
|
|
1059
|
+
], workspace).stdout;
|
|
1060
|
+
assert(
|
|
1061
|
+
archiveDocumentSet.ok !== false,
|
|
1062
|
+
`document-set archive failed: ${JSON.stringify(archiveDocumentSet, null, 2)}`,
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1066
|
+
assertFeedbackAbsent(workspace, [...createdFeedbackIds, documentSetFeedbackId]);
|
|
313
1067
|
|
|
314
1068
|
const unarchive = runCli([
|
|
315
1069
|
"feedback",
|
|
@@ -327,29 +1081,65 @@ function main() {
|
|
|
327
1081
|
...baseArgs,
|
|
328
1082
|
], workspace, { allowFailure: true }).stdout;
|
|
329
1083
|
assert(unarchive.ok === false, "Project-key Feedback unarchive unexpectedly succeeded.");
|
|
330
|
-
assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
|
|
331
|
-
assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1084
|
+
assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
|
|
1085
|
+
assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
|
|
1086
|
+
|
|
1087
|
+
if (roleMatrix.required) {
|
|
1088
|
+
assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
|
|
1089
|
+
assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
|
|
1090
|
+
assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
|
|
1091
|
+
}
|
|
1092
|
+
manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
|
|
1093
|
+
id: scenario.id,
|
|
1094
|
+
status:
|
|
1095
|
+
scenario.id === "owner-delegate-collaborator-matrix" && !roleMatrix.required
|
|
1096
|
+
? "optional_not_requested"
|
|
1097
|
+
: "passed",
|
|
1098
|
+
checked_at: new Date().toISOString(),
|
|
1099
|
+
}));
|
|
1100
|
+
manifest.state = "cleaned";
|
|
1101
|
+
manifest.completed_at = new Date().toISOString();
|
|
1102
|
+
manifest.updated_at = manifest.completed_at;
|
|
1103
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1104
|
+
|
|
1105
|
+
console.log(JSON.stringify({
|
|
1106
|
+
ok: true,
|
|
1107
|
+
run_id: runId,
|
|
1108
|
+
namespace,
|
|
1109
|
+
workspace,
|
|
1110
|
+
manifest_path: manifestPath,
|
|
1111
|
+
created_feedback_ids: createdFeedbackIds,
|
|
1112
|
+
document_set_feedback_id: documentSetFeedbackId,
|
|
1113
|
+
document_set_review_request_id: documentSetRequestId,
|
|
1114
|
+
review_request_ids: reviewRequestIds,
|
|
1115
|
+
role_matrix: roleMatrix,
|
|
1116
|
+
comment_verified: true,
|
|
1117
|
+
move_undo_history_verified: true,
|
|
1118
|
+
validation_verified: true,
|
|
1119
|
+
request_changes_revision_verified: true,
|
|
1120
|
+
rejection_and_cancellation_verified: true,
|
|
1121
|
+
pending_review_batch_block_verified: true,
|
|
1122
|
+
batch_review_verified: true,
|
|
1123
|
+
batch_active_move_verified: true,
|
|
1124
|
+
batch_archive_verified: true,
|
|
1125
|
+
three_document_prd_verified: true,
|
|
1126
|
+
document_scoped_refinement_verified: true,
|
|
1127
|
+
prd_version_diff_verified: true,
|
|
1128
|
+
archived_sync_exclusion_verified: true,
|
|
343
1129
|
project_key_unarchive_block_verified: true,
|
|
344
1130
|
}, null, 2));
|
|
345
|
-
} catch (err) {
|
|
346
|
-
|
|
347
|
-
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
const cleanupFeedbackIds = [
|
|
1133
|
+
...createdFeedbackIds,
|
|
1134
|
+
...(documentSetFeedbackId ? [documentSetFeedbackId] : []),
|
|
1135
|
+
];
|
|
1136
|
+
if (cleanupFeedbackIds.length) {
|
|
1137
|
+
try {
|
|
348
1138
|
runCli([
|
|
349
1139
|
"feedback",
|
|
350
|
-
"move",
|
|
351
|
-
"--feedback-ids",
|
|
352
|
-
|
|
1140
|
+
"move",
|
|
1141
|
+
"--feedback-ids",
|
|
1142
|
+
cleanupFeedbackIds.join(","),
|
|
353
1143
|
"--to-state",
|
|
354
1144
|
"archived",
|
|
355
1145
|
"--reason",
|
|
@@ -360,12 +1150,29 @@ function main() {
|
|
|
360
1150
|
} catch (_cleanupErr) {
|
|
361
1151
|
// Keep the original error. Cleanup is best effort and must not hide failure evidence.
|
|
362
1152
|
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
1153
|
+
}
|
|
1154
|
+
manifest.state = "cleanup_required";
|
|
1155
|
+
manifest.failure = {
|
|
1156
|
+
message: String(err?.message || err).slice(0, 2000),
|
|
1157
|
+
recorded_at: new Date().toISOString(),
|
|
1158
|
+
};
|
|
1159
|
+
manifest.updated_at = new Date().toISOString();
|
|
1160
|
+
try {
|
|
1161
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1162
|
+
} catch (_manifestErr) {
|
|
1163
|
+
// Preserve the primary harness failure.
|
|
1164
|
+
}
|
|
1165
|
+
const failure = {
|
|
1166
|
+
ok: false,
|
|
1167
|
+
run_id: runId,
|
|
1168
|
+
namespace,
|
|
1169
|
+
workspace,
|
|
1170
|
+
manifest_path: manifestPath,
|
|
1171
|
+
created_feedback_ids: createdFeedbackIds,
|
|
1172
|
+
document_set_feedback_id: documentSetFeedbackId,
|
|
1173
|
+
document_set_review_request_id: documentSetRequestId,
|
|
1174
|
+
review_request_ids: reviewRequestIds,
|
|
1175
|
+
role_matrix: roleMatrix,
|
|
369
1176
|
message: err?.message || String(err),
|
|
370
1177
|
};
|
|
371
1178
|
console.error(JSON.stringify(failure, null, 2));
|