@mutmutco/cli 4.3.41 → 4.3.43
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/dist/main.cjs +82 -33
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -13692,8 +13692,10 @@ function walk(dir, root, out) {
|
|
|
13692
13692
|
return out;
|
|
13693
13693
|
}
|
|
13694
13694
|
function defaultListDocs(root) {
|
|
13695
|
-
const
|
|
13696
|
-
|
|
13695
|
+
const docs = ["docs", "skills/org-guide", "skills/mmi-help"].flatMap((path2) => {
|
|
13696
|
+
const directory = (0, import_node_path12.join)(root, path2);
|
|
13697
|
+
return (0, import_node_fs14.existsSync)(directory) ? walk(directory, root, []) : [];
|
|
13698
|
+
}).filter(
|
|
13697
13699
|
(rel) => !SKIP_WALK.some((skip) => rel.startsWith(skip))
|
|
13698
13700
|
);
|
|
13699
13701
|
return [...ROOT_DOCS.filter((rel) => (0, import_node_fs14.existsSync)((0, import_node_path12.join)(root, rel))), ...docs];
|
|
@@ -15708,10 +15710,10 @@ var rollout_plan_default = {
|
|
|
15708
15710
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
15709
15711
|
},
|
|
15710
15712
|
baseline: {
|
|
15711
|
-
version: "4.3.
|
|
15712
|
-
tag: "v4.3.
|
|
15713
|
-
commit: "
|
|
15714
|
-
npm: "@mutmutco/cli@4.3.
|
|
15713
|
+
version: "4.3.43",
|
|
15714
|
+
tag: "v4.3.43",
|
|
15715
|
+
commit: "120c58230cfc",
|
|
15716
|
+
npm: "@mutmutco/cli@4.3.43"
|
|
15715
15717
|
},
|
|
15716
15718
|
exitCriterion: "fleet-n-of-n",
|
|
15717
15719
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15728,14 +15730,14 @@ var rollout_plan_default = {
|
|
|
15728
15730
|
repo: "mutmutco/mmi-hub",
|
|
15729
15731
|
role: "canary",
|
|
15730
15732
|
schedule: "train",
|
|
15731
|
-
v3Target: "v4.3.
|
|
15733
|
+
v3Target: "v4.3.43"
|
|
15732
15734
|
}
|
|
15733
15735
|
],
|
|
15734
15736
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
15735
15737
|
rollback: {
|
|
15736
15738
|
independent: true,
|
|
15737
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15738
|
-
v3Target: "v4.3.
|
|
15739
|
+
mechanism: "npm dist-tag latest -> 4.3.43 and redeploy the Hub Lambda from tag v4.3.43 (120c58230cfc); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15740
|
+
v3Target: "v4.3.43 (@mutmutco/cli@4.3.43, tag commit 120c58230cfc \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15739
15741
|
}
|
|
15740
15742
|
},
|
|
15741
15743
|
{
|
|
@@ -31054,7 +31056,16 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
31054
31056
|
readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
|
|
31055
31057
|
readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
|
|
31056
31058
|
});
|
|
31057
|
-
return {
|
|
31059
|
+
return {
|
|
31060
|
+
guide: "Read the MMI org-guide skill before project work; load only the relevant topic. Use mmi-help for a short introduction.",
|
|
31061
|
+
track,
|
|
31062
|
+
board,
|
|
31063
|
+
registry: registry2,
|
|
31064
|
+
secrets,
|
|
31065
|
+
plugin,
|
|
31066
|
+
doors: opts.doors ?? [],
|
|
31067
|
+
nextCommand
|
|
31068
|
+
};
|
|
31058
31069
|
}
|
|
31059
31070
|
function registerDiscoveryCommands(program3) {
|
|
31060
31071
|
program3.command("status").description("unified repo snapshot: branch, worktrees, open PRs (yours), claimed board items, stage state").option("--json", "machine-readable output").action(async (o) => {
|
|
@@ -31104,6 +31115,7 @@ function registerDiscoveryCommands(program3) {
|
|
|
31104
31115
|
if (o.json) {
|
|
31105
31116
|
console.log(JSON.stringify(report, null, 2));
|
|
31106
31117
|
} else {
|
|
31118
|
+
console.log(`Guide: ${report.guide}`);
|
|
31107
31119
|
console.log(`Track: ${report.track}`);
|
|
31108
31120
|
console.log(`Board: ${report.board.ok ? "\u2713" : "\u2717"} ${report.board.detail}`);
|
|
31109
31121
|
console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
|
|
@@ -42221,7 +42233,7 @@ function registerDeveloperCommands(program3) {
|
|
|
42221
42233
|
await failGraceful(e.message);
|
|
42222
42234
|
}
|
|
42223
42235
|
});
|
|
42224
|
-
docs.command("refs").description("deterministic doc reference gate:
|
|
42236
|
+
docs.command("refs").description("deterministic doc reference gate: repo paths, markdown links, mmi-cli command refs, and pins across docs, org-guide, mmi-help, README and architecture must resolve, or exit 1").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs refs reads the current checkout").hideHelp()).action(async (o) => {
|
|
42225
42237
|
try {
|
|
42226
42238
|
if (o.repo) {
|
|
42227
42239
|
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs refs${o.json ? " --json" : ""}`, "docs refs");
|
|
@@ -42345,7 +42357,7 @@ var SSM_REGION = "eu-central-1";
|
|
|
42345
42357
|
var SSM_TOKEN_PARAM = "/mmi-future/_org/slack/SLACK_BOT_TOKEN";
|
|
42346
42358
|
var SSM_CHANNEL_PARAM = "/mmi-future/_org/slack/SLACK_ALERTS_CHANNEL";
|
|
42347
42359
|
var SLACK_TIMEOUT_MS = 1e4;
|
|
42348
|
-
async function
|
|
42360
|
+
async function readSsmParameter(deps, name, decrypt) {
|
|
42349
42361
|
const args = [
|
|
42350
42362
|
"ssm",
|
|
42351
42363
|
"get-parameter",
|
|
@@ -42363,23 +42375,30 @@ async function ssmParameter(deps, name, decrypt) {
|
|
|
42363
42375
|
if (!value || value === "None") throw new Error(`SSM parameter ${name} is empty`);
|
|
42364
42376
|
return value;
|
|
42365
42377
|
}
|
|
42366
|
-
async function
|
|
42367
|
-
const
|
|
42368
|
-
|
|
42378
|
+
async function postToChannel(deps, channel, text) {
|
|
42379
|
+
const resolved = channel.trim();
|
|
42380
|
+
if (!resolved || resolved === "None") throw new Error("no Slack channel resolved");
|
|
42381
|
+
const token = await readSsmParameter(deps, SSM_TOKEN_PARAM, true);
|
|
42369
42382
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
42370
42383
|
const res = await fetchImpl("https://slack.com/api/chat.postMessage", {
|
|
42371
42384
|
method: "POST",
|
|
42372
42385
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json; charset=utf-8" },
|
|
42373
|
-
body: JSON.stringify({ channel, text, unfurl_links: false }),
|
|
42386
|
+
body: JSON.stringify({ channel: resolved, text, unfurl_links: false }),
|
|
42374
42387
|
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
42375
42388
|
});
|
|
42376
42389
|
const json = await res.json().catch(() => ({}));
|
|
42377
42390
|
if (!json.ok) throw new Error(`slack postMessage failed: ${json.error ?? `http ${res.status}`}`);
|
|
42391
|
+
return { status: "posted", note: "posted" };
|
|
42392
|
+
}
|
|
42393
|
+
async function postToAlertsChannel(deps, text) {
|
|
42394
|
+
const channel = await readSsmParameter(deps, SSM_CHANNEL_PARAM, false);
|
|
42395
|
+
await postToChannel(deps, channel, text);
|
|
42378
42396
|
return { status: "posted", note: "posted to the alerts channel" };
|
|
42379
42397
|
}
|
|
42380
42398
|
|
|
42381
42399
|
// src/release-announce.ts
|
|
42382
42400
|
var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
|
|
42401
|
+
var RELEASE_CHANNEL_KEY = "SLACK_RELEASE_CHANNEL";
|
|
42383
42402
|
var MAX_BULLETS = 6;
|
|
42384
42403
|
var MAX_SUMMARY_LINES = 8;
|
|
42385
42404
|
function neutralizeHubProductBrands(text) {
|
|
@@ -42387,10 +42406,17 @@ function neutralizeHubProductBrands(text) {
|
|
|
42387
42406
|
s = s.replace(/\bdesign-system\s+design-system\b/gi, "design-system");
|
|
42388
42407
|
return s.replace(/\s{2,}/g, " ").trim();
|
|
42389
42408
|
}
|
|
42409
|
+
function releaseChannelParam(repo) {
|
|
42410
|
+
return `/mmi-future/${slugOf(repo)}/${RELEASE_CHANNEL_KEY}`;
|
|
42411
|
+
}
|
|
42412
|
+
function isAbsentParameter(e) {
|
|
42413
|
+
const message2 = e instanceof Error ? e.message : String(e);
|
|
42414
|
+
return /ParameterNotFound/.test(message2) || /is empty$/.test(message2);
|
|
42415
|
+
}
|
|
42390
42416
|
function escapeMrkdwn(text) {
|
|
42391
42417
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
42392
42418
|
}
|
|
42393
|
-
function bulletToLine(raw) {
|
|
42419
|
+
function bulletToLine(raw, neutralize) {
|
|
42394
42420
|
const m = /^[*-]\s+(.*)$/.exec(raw.trim());
|
|
42395
42421
|
if (!m) return null;
|
|
42396
42422
|
let text = m[1].trim();
|
|
@@ -42405,9 +42431,9 @@ function bulletToLine(raw) {
|
|
|
42405
42431
|
text = text.slice(0, tail.index).trim();
|
|
42406
42432
|
}
|
|
42407
42433
|
if (!text) return null;
|
|
42408
|
-
return `\u2022 ${escapeMrkdwn(neutralizeHubProductBrands(text))}${link}`;
|
|
42434
|
+
return `\u2022 ${escapeMrkdwn(neutralize ? neutralizeHubProductBrands(text) : text)}${link}`;
|
|
42409
42435
|
}
|
|
42410
|
-
function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
|
|
42436
|
+
function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS, neutralize = true) {
|
|
42411
42437
|
const lines2 = [];
|
|
42412
42438
|
let bullets = 0;
|
|
42413
42439
|
let dropped = 0;
|
|
@@ -42425,7 +42451,7 @@ function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
|
|
|
42425
42451
|
continue;
|
|
42426
42452
|
}
|
|
42427
42453
|
if (/new contributors/i.test(section)) continue;
|
|
42428
|
-
const line = bulletToLine(raw);
|
|
42454
|
+
const line = bulletToLine(raw, neutralize);
|
|
42429
42455
|
if (!line) continue;
|
|
42430
42456
|
if (bullets >= maxBullets) {
|
|
42431
42457
|
dropped += 1;
|
|
@@ -42451,21 +42477,39 @@ function formatAnnouncement(args) {
|
|
|
42451
42477
|
`<${args.releaseUrl}|Release notes>`
|
|
42452
42478
|
].join("\n");
|
|
42453
42479
|
}
|
|
42454
|
-
function summaryFileLines(content) {
|
|
42455
|
-
return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) =>
|
|
42480
|
+
function summaryFileLines(content, neutralize = true) {
|
|
42481
|
+
return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) => {
|
|
42482
|
+
const bare = l.replace(/^[•*-]\s*/, "");
|
|
42483
|
+
return escapeMrkdwn(neutralize ? neutralizeHubProductBrands(bare) : bare);
|
|
42484
|
+
}).filter(Boolean).map((l) => `\u2022 ${l}`);
|
|
42456
42485
|
}
|
|
42457
|
-
async function
|
|
42458
|
-
if (
|
|
42459
|
-
|
|
42486
|
+
async function resolveAnnounceTarget(deps, repo) {
|
|
42487
|
+
if (repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) return { kind: "alerts" };
|
|
42488
|
+
const param = releaseChannelParam(repo);
|
|
42489
|
+
try {
|
|
42490
|
+
const channel = await readSsmParameter(deps, param, false);
|
|
42491
|
+
return { kind: "channel", channel };
|
|
42492
|
+
} catch (e) {
|
|
42493
|
+
if (isAbsentParameter(e)) return { kind: "none", param };
|
|
42494
|
+
throw e;
|
|
42460
42495
|
}
|
|
42496
|
+
}
|
|
42497
|
+
function missingChannelNote(repo) {
|
|
42498
|
+
const slug = slugOf(repo);
|
|
42499
|
+
return `no ${RELEASE_CHANNEL_KEY} declared for ${slug} \u2014 declare it with \`mmi-cli vault secrets declare ${RELEASE_CHANNEL_KEY} --group slack --consumers agent --purpose \u2026\` then \`mmi-cli vault secrets set ${RELEASE_CHANNEL_KEY}\``;
|
|
42500
|
+
}
|
|
42501
|
+
async function announceRelease(deps, args) {
|
|
42502
|
+
const neutralize = args.repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase();
|
|
42461
42503
|
try {
|
|
42504
|
+
const target = await resolveAnnounceTarget(deps, args.repo);
|
|
42505
|
+
if (target.kind === "none") return { status: "skipped", note: missingChannelNote(args.repo) };
|
|
42462
42506
|
const viewRaw = await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body,url"]);
|
|
42463
42507
|
const view = JSON.parse(viewRaw);
|
|
42464
42508
|
const releaseUrl = view.url ?? `https://github.com/${args.repo}/releases/tag/${args.tag}`;
|
|
42465
42509
|
let lines2 = [];
|
|
42466
42510
|
if (args.summaryFile) {
|
|
42467
42511
|
if (!deps.readFile) throw new Error("summary file given but deps.readFile is missing");
|
|
42468
|
-
lines2 = summaryFileLines(await deps.readFile(args.summaryFile));
|
|
42512
|
+
lines2 = summaryFileLines(await deps.readFile(args.summaryFile), neutralize);
|
|
42469
42513
|
if (deps.removeFile) {
|
|
42470
42514
|
try {
|
|
42471
42515
|
await deps.removeFile(args.summaryFile);
|
|
@@ -42473,11 +42517,15 @@ async function announceRelease(deps, args) {
|
|
|
42473
42517
|
}
|
|
42474
42518
|
}
|
|
42475
42519
|
}
|
|
42476
|
-
if (lines2.length === 0) lines2 = curateReleaseNotes(view.body ?? "");
|
|
42520
|
+
if (lines2.length === 0) lines2 = curateReleaseNotes(view.body ?? "", MAX_BULLETS, neutralize);
|
|
42477
42521
|
if (lines2.length === 0) lines2 = ["\u2022 maintenance release \u2014 see the notes for details"];
|
|
42478
42522
|
const text = formatAnnouncement({ repo: args.repo, tag: args.tag, lines: lines2, releaseUrl });
|
|
42479
|
-
|
|
42480
|
-
|
|
42523
|
+
if (target.kind === "alerts") {
|
|
42524
|
+
await postToAlertsChannel(deps, text);
|
|
42525
|
+
return { status: "announced", note: `announced ${args.tag} to the alerts channel` };
|
|
42526
|
+
}
|
|
42527
|
+
await postToChannel(deps, target.channel, text);
|
|
42528
|
+
return { status: "announced", note: `announced ${args.tag} to the project release channel` };
|
|
42481
42529
|
} catch (e) {
|
|
42482
42530
|
return { status: "failed", note: `announce failed (release unaffected): ${e.message}` };
|
|
42483
42531
|
}
|
|
@@ -43892,7 +43940,8 @@ function trainApplyDeps() {
|
|
|
43892
43940
|
// train lands the back-merge itself instead of leaving a manual step. Same enqueue path `pr land` uses,
|
|
43893
43941
|
// but method `--merge` (NOT squash): the misalignment guard needs the merge parentage to survive.
|
|
43894
43942
|
mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--merge"),
|
|
43895
|
-
// Slack release announcement (#883):
|
|
43943
|
+
// Slack release announcement (#883/#6517): the org alerts channel for the Hub, the project's own
|
|
43944
|
+
// SLACK_RELEASE_CHANNEL for a product repo — best-effort inside announceRelease itself.
|
|
43896
43945
|
announce: (args) => announceRelease({
|
|
43897
43946
|
run: async (file, cmdArgs) => (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
43898
43947
|
readFile: (path2) => (0, import_promises10.readFile)(path2, "utf8"),
|
|
@@ -44156,7 +44205,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44156
44205
|
for (const commandName of ["rcand", "release"]) {
|
|
44157
44206
|
const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
|
|
44158
44207
|
const RELEASE_ONLY_FLAGS = [
|
|
44159
|
-
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line
|
|
44208
|
+
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901), optional for a product repo announcing to its project release channel (#6517)" },
|
|
44160
44209
|
{ flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
|
|
44161
44210
|
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
|
|
44162
44211
|
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
|
|
@@ -44182,7 +44231,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44182
44231
|
return fail(`${commandName}: --dev applies only to release \u2014 it ships development -> main skipping rc, which rcand cannot do. Run: mmi-cli devops release --dev`);
|
|
44183
44232
|
}
|
|
44184
44233
|
if (o.announceSummaryFile && commandName !== "release") {
|
|
44185
|
-
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no
|
|
44234
|
+
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no release announcement. Run: mmi-cli devops release --announce-summary-file <path>`);
|
|
44186
44235
|
}
|
|
44187
44236
|
if (o.abort && commandName !== "release") {
|
|
44188
44237
|
return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished release tag. Run: mmi-cli devops release --abort --apply`);
|
|
@@ -44434,7 +44483,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44434
44483
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
|
|
44435
44484
|
});
|
|
44436
44485
|
hotfixCmd.command("start").description("cherry-pick one or more merged development PRs (or SHAs) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR number(s) or commit SHA(s) to cherry-pick, in pick order \u2014 one hotfix cycle carries as many fixes as you name (#4411)").option("--json", "machine-readable output").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o, renderHotfixStart));
|
|
44437
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line
|
|
44486
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => runHotfixSub("release", async () => {
|
|
44438
44487
|
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
|
|
44439
44488
|
const postReleaseJervDoctor = await runPostReleaseJervDoctor(result.repo);
|
|
44440
44489
|
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
package/package.json
CHANGED