@mytegroupinc/myte-core 0.0.54 → 0.0.56

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.
@@ -8,6 +8,7 @@ const { spawnSync } = require("node:child_process");
8
8
 
9
9
  const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
10
10
  const DEFAULT_BASE_URL = "https://api.myte.dev/api";
11
+ const PACKAGE_VERSION = require("../package.json").version;
11
12
 
12
13
  function parseArgs(argv) {
13
14
  const args = { _: [] };
@@ -285,7 +286,7 @@ class Harness {
285
286
  this.key = getProjectKey();
286
287
  this.actorBase = String(args["actor-scope"] || `mission-live-full-${nowId()}`);
287
288
  this.cliMode = String(args.cli || "local").toLowerCase();
288
- this.packageVersion = String(args["package-version"] || "0.0.28");
289
+ this.packageVersion = String(args["package-version"] || PACKAGE_VERSION);
289
290
  this.batchSize = Math.max(1, Math.min(10, Number(args["batch-size"] || 10)));
290
291
  this.includeQaqcRun = args["include-qaqc-run"] === true;
291
292
  this.workspace = path.resolve(
@@ -433,7 +434,7 @@ class Harness {
433
434
  change_set: { description: "Harness proposed description. This should be rejected during cleanup." },
434
435
  }],
435
436
  });
436
- const first = this.cli(["suggestions", "create", "--file", createOne, "--no-sync", "--json"], { actorScope: actor });
437
+ const first = this.cli(["suggestions", "create", "--file", createOne, "--confirm-write", "--approval-artifact", createOne, "--no-sync", "--json"], { actorScope: actor });
437
438
  const suggestionId = pickSuggestionId(first);
438
439
  assertCondition(suggestionId, "Update suggestion create did not return suggestion_id.");
439
440
  this.openSuggestions.add(suggestionId);
@@ -446,7 +447,7 @@ class Harness {
446
447
  change_set: { acceptance_criteria: ["Harness proposed acceptance criterion. This should be rejected."] },
447
448
  }],
448
449
  });
449
- const second = this.cli(["suggestions", "create", "--file", createTwo, "--no-sync", "--json"], { actorScope: actor });
450
+ const second = this.cli(["suggestions", "create", "--file", createTwo, "--confirm-write", "--approval-artifact", createTwo, "--no-sync", "--json"], { actorScope: actor });
450
451
  const appendedId = pickSuggestionId(second);
451
452
  assertCondition(appendedId === suggestionId, `Expected append to same suggestion ${suggestionId}, got ${appendedId || "(none)"}.`);
452
453
 
@@ -457,7 +458,7 @@ class Harness {
457
458
  change_set: { technical_requirements: ["Harness proposed requirement. This should be rejected."] },
458
459
  }],
459
460
  });
460
- const revised = this.cli(["suggestions", "revise", "--file", revise, "--no-sync", "--json"], { actorScope: actor });
461
+ const revised = this.cli(["suggestions", "revise", "--file", revise, "--confirm-write", "--approval-artifact", revise, "--no-sync", "--json"], { actorScope: actor });
461
462
  assertCondition((revised.revised_count || 0) >= 1, "Update suggestion revise did not revise the thread.");
462
463
 
463
464
  const requestChanges = writeJson(this.payloadDir, "update-loop-request-changes.json", {
@@ -473,7 +474,7 @@ class Harness {
473
474
  change_set: { labels: ["harness-proposed-update"] },
474
475
  }],
475
476
  });
476
- const revisedAgain = this.cli(["suggestions", "revise", "--file", reviseAfterRequest, "--no-sync", "--json"], { actorScope: actor });
477
+ const revisedAgain = this.cli(["suggestions", "revise", "--file", reviseAfterRequest, "--confirm-write", "--approval-artifact", reviseAfterRequest, "--no-sync", "--json"], { actorScope: actor });
477
478
  assertCondition((revisedAgain.revised_count || 0) >= 1, "Update suggestion revise after request_changes did not process.");
478
479
 
479
480
  const reject = writeJson(this.payloadDir, "update-loop-reject.json", {
@@ -504,7 +505,7 @@ class Harness {
504
505
  },
505
506
  }],
506
507
  });
507
- const created = this.cli(["suggestions", "create", "--file", createFile, "--no-sync", "--json"], { actorScope: actor });
508
+ const created = this.cli(["suggestions", "create", "--file", createFile, "--confirm-write", "--approval-artifact", createFile, "--no-sync", "--json"], { actorScope: actor });
508
509
  const suggestionId = pickSuggestionId(created);
509
510
  assertCondition(suggestionId, "Create suggestion did not return suggestion_id.");
510
511
  this.openSuggestions.add(suggestionId);
@@ -522,7 +523,7 @@ class Harness {
522
523
  change_set: { description: "Harness revised proposal. This should still be rejected." },
523
524
  }],
524
525
  });
525
- const revised = this.cli(["suggestions", "revise", "--file", reviseFile, "--no-sync", "--json"], { actorScope: actor });
526
+ const revised = this.cli(["suggestions", "revise", "--file", reviseFile, "--confirm-write", "--approval-artifact", reviseFile, "--no-sync", "--json"], { actorScope: actor });
526
527
  assertCondition((revised.revised_count || 0) >= 1, "Create suggestion revise did not process.");
527
528
 
528
529
  const requestChanges = writeJson(this.payloadDir, "create-reject-request-changes.json", {
@@ -579,7 +580,7 @@ class Harness {
579
580
  },
580
581
  }],
581
582
  });
582
- const created = this.cli(["suggestions", "create", "--file", createFile, "--no-sync", "--json"], { actorScope: actor, timeoutMs: 180000 });
583
+ const created = this.cli(["suggestions", "create", "--file", createFile, "--confirm-write", "--approval-artifact", createFile, "--no-sync", "--json"], { actorScope: actor, timeoutMs: 180000 });
583
584
  const suggestionId = pickSuggestionId(created);
584
585
  assertCondition(suggestionId, "Approved mission create did not return suggestion_id.");
585
586
  this.openSuggestions.add(suggestionId);
@@ -633,7 +634,7 @@ class Harness {
633
634
  change_set: { description: "Harness pending suggestion should be hidden from actionable queue while archived." },
634
635
  }],
635
636
  });
636
- const pending = this.cli(["suggestions", "create", "--file", pendingFile, "--no-sync", "--json"], { actorScope: actor, timeoutMs: 180000 });
637
+ const pending = this.cli(["suggestions", "create", "--file", pendingFile, "--confirm-write", "--approval-artifact", pendingFile, "--no-sync", "--json"], { actorScope: actor, timeoutMs: 180000 });
637
638
  pendingSuggestionId = pickSuggestionId(pending);
638
639
  assertCondition(pendingSuggestionId, "Pending update suggestion for approved mission was not created.");
639
640
  this.openSuggestions.add(pendingSuggestionId);
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+ const { randomUUID } = require("node:crypto");
8
+ const { spawn, spawnSync } = require("node:child_process");
9
+
10
+ const ROOT = path.resolve(__dirname, "..");
11
+ const CLI = path.join(ROOT, "cli.js");
12
+ const DEFAULT_BASE_URL = "https://api.myte.dev/api";
13
+ const GAAHFIELD_PROJECT_ALIASES = new Set(["gaahfield", "gaahfield marketplace"]);
14
+ const ROUTES = [
15
+ "POST /query", "GET /query/<job_id>", "GET /config", "GET /bootstrap",
16
+ "GET /qaqc-sync", "POST /run-qaqc", "GET /run-qaqc/<batch_id>",
17
+ "POST /mission-status-update", "POST /mission-archive", "GET /feedback-sync",
18
+ "GET /feedback/<feedback_id>", "POST /feedback/<feedback_id>/refinement/validate",
19
+ "POST /feedback/<feedback_id>/refinement/requests",
20
+ "POST /feedback-review-requests/<request_id>/revise",
21
+ "POST /feedback/<feedback_id>/refinement/apply",
22
+ "GET /feedback/<feedback_id>/refinement/history", "GET /feedback-review-requests",
23
+ "GET /feedback-review-requests/<request_id>",
24
+ "POST /feedback-review-requests/<request_id>/review",
25
+ "POST /feedback-review-requests/batch-review",
26
+ "POST /feedback-review-requests/<request_id>/request-changes",
27
+ "POST /feedback-review-requests/<request_id>/cancel",
28
+ "POST /feedback/<feedback_id>/board-move", "POST /feedback/batch-board-move",
29
+ "POST /feedback/<feedback_id>/comments", "GET /feedback/<feedback_id>/events",
30
+ "GET /feedback/<feedback_id>/tickets", "POST /feedback/<feedback_id>/tickets",
31
+ "POST /feedback/<feedback_id>/tickets/batch", "POST /feedback/<feedback_id>/tickets/batch/validate",
32
+ "PATCH /feedback/<feedback_id>/tickets/<ticket_id>", "PATCH /feedback/<feedback_id>/tickets/batch",
33
+ "POST /feedback/<feedback_id>/tickets/batch-update/validate",
34
+ "PATCH /feedback/<feedback_id>/tickets/<ticket_id>/prd-coverage",
35
+ "POST /feedback/<feedback_id>/events/<event_id>/undo",
36
+ "GET /feedback/<feedback_id>/prd/versions",
37
+ "GET /feedback/<feedback_id>/prd/versions/<version_id>/diff",
38
+ "POST /project-comment", "POST /update-owner", "POST /client-update-drafts",
39
+ "POST /create-prd", "POST /create-prds", "GET /suggestions", "POST /suggestions",
40
+ "POST /suggestions/revise", "POST /suggestions/review",
41
+ ];
42
+
43
+ const COVERAGE = {
44
+ read: ROUTES.filter((route) => route.startsWith("GET ") || route.startsWith("POST /query")),
45
+ mission: ROUTES.filter((route) => /mission|qaqc|suggestion/.test(route)),
46
+ feedback: ROUTES.filter((route) => /feedback|create-prd/.test(route)),
47
+ communications: ROUTES.filter((route) => /project-comment|update-owner|client-update/.test(route)),
48
+ };
49
+
50
+ function parseArgs(argv) {
51
+ const args = {};
52
+ for (let index = 0; index < argv.length; index += 1) {
53
+ const token = argv[index];
54
+ if (!token.startsWith("--")) continue;
55
+ const next = argv[index + 1];
56
+ if (!next || next.startsWith("--")) args[token.slice(2)] = true;
57
+ else { args[token.slice(2)] = next; index += 1; }
58
+ }
59
+ return args;
60
+ }
61
+
62
+ function normalizeProjectTitle(value) {
63
+ return String(value || "").trim().toLowerCase().replace(/\s+/g, " ");
64
+ }
65
+
66
+ function isGaahFieldProject(value) {
67
+ return GAAHFIELD_PROJECT_ALIASES.has(normalizeProjectTitle(value));
68
+ }
69
+
70
+ function routeCoverage() {
71
+ const accounted = new Set(Object.values(COVERAGE).flat());
72
+ const missing = ROUTES.filter((route) => !accounted.has(route));
73
+ if (ROUTES.length !== 46 || new Set(ROUTES).size !== 46 || missing.length) {
74
+ throw new Error(`Route manifest is incomplete: ${missing.join(", ") || "count/duplicate mismatch"}`);
75
+ }
76
+ return { implemented_project_key_routes: 46, accounted_routes: accounted.size, missing };
77
+ }
78
+
79
+ function runNode(script, args, cwd) {
80
+ const result = spawnSync(process.execPath, [script, ...args], {
81
+ cwd, env: process.env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 1_800_000,
82
+ });
83
+ let output = null;
84
+ try { output = JSON.parse(String(result.stdout || "").trim() || "null"); } catch {}
85
+ return {
86
+ command: path.basename(script), ok: result.status === 0,
87
+ exit_code: result.status, output,
88
+ error: result.status === 0 ? null : String(result.stderr || result.stdout || "failed").trim().slice(0, 2000),
89
+ };
90
+ }
91
+
92
+ function runCli(args, cwd) {
93
+ return runNode(CLI, args, cwd);
94
+ }
95
+
96
+ function runNodeAsync(script, args, cwd) {
97
+ return new Promise((resolve) => {
98
+ const child = spawn(process.execPath, [script, ...args], { cwd, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
99
+ let stdout = "";
100
+ let stderr = "";
101
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
102
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
103
+ child.on("close", (code) => {
104
+ let output = null;
105
+ try { output = JSON.parse(stdout.trim() || "null"); } catch {}
106
+ resolve({ ok: code === 0, exit_code: code, output, error: code === 0 ? null : (stderr || stdout).trim().slice(0, 2000) });
107
+ });
108
+ });
109
+ }
110
+
111
+ function isIdempotencyInProgress(result) {
112
+ return !result?.ok && /idempotency key is still in progress/i.test(String(result?.error || ""));
113
+ }
114
+
115
+ async function settleIdempotentReplay(script, args, cwd, initialResult) {
116
+ let result = initialResult;
117
+ for (let attempt = 0; attempt < 6 && isIdempotencyInProgress(result); attempt += 1) {
118
+ await new Promise((resolve) => setTimeout(resolve, Math.min(250 * (2 ** attempt), 2000)));
119
+ result = await runNodeAsync(script, args, cwd);
120
+ }
121
+ return result;
122
+ }
123
+
124
+ async function assertExpectedProject(baseUrl, expectedProject) {
125
+ const key = process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY;
126
+ if (!key) throw new Error("Missing MYTE_API_KEY or MYTE_PROJECT_API_KEY.");
127
+ const response = await fetch(`${String(baseUrl).replace(/\/+$/, "")}/project-assistant/bootstrap`, {
128
+ headers: { Authorization: `Bearer ${key}` },
129
+ });
130
+ const body = await response.json();
131
+ const title = String(body?.data?.project?.title || "").trim();
132
+ if (!response.ok || body?.status !== "success") throw new Error("Project bootstrap preflight failed.");
133
+ if (!isGaahFieldProject(expectedProject) || !isGaahFieldProject(title)) {
134
+ throw new Error(`Live target mismatch: expected ${expectedProject}, received ${title || "unknown"}.`);
135
+ }
136
+ return { project_id: body.data.project.id, title };
137
+ }
138
+
139
+ async function main() {
140
+ const args = parseArgs(process.argv.slice(2));
141
+ const coverage = routeCoverage();
142
+ if (!args["confirm-live"]) {
143
+ console.log(JSON.stringify({
144
+ ok: true, dry_run: true, network_requests: 0, live_mutations: 0, coverage,
145
+ stages: ["read", "mission", "feedback", "communications", "concurrent-idempotency", "exact-id-cleanup"],
146
+ required_live_flags: ["--confirm-live", "--expected-project GaahField", "--include-communications"],
147
+ owner_email_requires: "--include-owner-email",
148
+ }, null, 2));
149
+ return;
150
+ }
151
+ const expectedProject = String(args["expected-project"] || "").trim();
152
+ if (!isGaahFieldProject(expectedProject)) throw new Error("Live certification is pinned to --expected-project GaahField.");
153
+ if (!args["include-communications"]) throw new Error("Full live certification requires --include-communications.");
154
+ const baseUrl = String(args["base-url"] || process.env.MYTE_API_BASE || DEFAULT_BASE_URL).replace(/\/+$/, "");
155
+ const project = await assertExpectedProject(baseUrl, expectedProject);
156
+ const runId = String(args["run-id"] || randomUUID());
157
+ const workspace = path.resolve(args.workspace || fs.mkdtempSync(path.join(os.tmpdir(), `myte-full-${runId}-`)));
158
+ fs.mkdirSync(workspace, { recursive: true });
159
+ const common = ["--base-url", baseUrl, "--run-id", runId];
160
+ const results = [];
161
+ results.push(runNode(path.join(__dirname, "project-assistant-read-certification.js"), ["--confirm-read", "--include-query", ...common], workspace));
162
+ results.push(runNode(path.join(__dirname, "mission-live-full-harness.js"), ["--confirm-live", ...common], workspace));
163
+ results.push(runNode(path.join(__dirname, "feedback-live-full-harness.js"), ["--confirm-live", ...common], workspace));
164
+
165
+ const team = path.join(workspace, `team-${runId}.md`);
166
+ const client = path.join(workspace, `client-${runId}.md`);
167
+ fs.writeFileSync(team, `Project Assistant certification ${runId}: team update route verified.\n`, "utf8");
168
+ fs.writeFileSync(client, `Project Assistant certification ${runId}: client draft route verified; do not send.\n`, "utf8");
169
+ const teamArgs = ["update-team", `Certification ${runId}`, "--confirm-write", "--approval-artifact", team, "--idempotency-key", `cert-team-${runId}`, "--json", "--base-url", baseUrl];
170
+ const concurrentTeamInitial = await Promise.all(Array.from({ length: 10 }, () => runNodeAsync(CLI, teamArgs, workspace)));
171
+ const transientInProgress = concurrentTeamInitial.filter(isIdempotencyInProgress).length;
172
+ const concurrentTeam = [];
173
+ for (const result of concurrentTeamInitial) {
174
+ concurrentTeam.push(await settleIdempotentReplay(CLI, teamArgs, workspace, result));
175
+ }
176
+ const teamFingerprints = new Set(
177
+ concurrentTeam.filter((item) => item.ok).map((item) => JSON.stringify(item.output?.data || item.output || null)),
178
+ );
179
+ results.push({
180
+ command: "concurrent-idempotency:update-team",
181
+ ok: concurrentTeam.every((item) => item.ok) && teamFingerprints.size === 1,
182
+ attempts: concurrentTeam.length,
183
+ transient_in_progress: transientInProgress,
184
+ unique_outcomes: teamFingerprints.size,
185
+ error: concurrentTeam.find((item) => !item.ok)?.error || (teamFingerprints.size === 1 ? null : "Concurrent replays returned different mutation outcomes."),
186
+ });
187
+ results.push(runCli(["update-client", "--subject", `Certification ${runId}`, "--body-file", client, "--confirm-write", "--approval-artifact", client, "--idempotency-key", `cert-client-${runId}`, "--json", "--base-url", baseUrl], workspace));
188
+ if (args["include-owner-email"]) {
189
+ const owner = path.join(workspace, `owner-${runId}.md`);
190
+ fs.writeFileSync(owner, `Project Assistant certification ${runId}: owner delivery route verified.\n`, "utf8");
191
+ results.push(runCli(["update-owner", "--subject", `Certification ${runId}`, "--body-file", owner, "--confirm-write", "--approval-artifact", owner, "--idempotency-key", `cert-owner-${runId}`, "--json", "--base-url", baseUrl], workspace));
192
+ }
193
+ const failed = results.filter((result) => !result.ok);
194
+ const report = { ok: failed.length === 0, run_id: runId, project, coverage, workspace, results, failed_count: failed.length };
195
+ const reportPath = path.join(workspace, `project-assistant-certification-${runId}.json`);
196
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
197
+ console.log(JSON.stringify({ ...report, report_path: reportPath }, null, 2));
198
+ if (failed.length) process.exitCode = 1;
199
+ }
200
+
201
+ if (require.main === module) {
202
+ main().catch((error) => {
203
+ console.error(JSON.stringify({ ok: false, message: error?.message || String(error) }, null, 2));
204
+ process.exit(1);
205
+ });
206
+ } else {
207
+ module.exports = { COVERAGE, DEFAULT_BASE_URL, ROUTES, isGaahFieldProject, isIdempotencyInProgress, routeCoverage };
208
+ }