repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
pi/agent/skillz/ims-process-review/scripts/ims.mjs
Raw#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const SKILL_DIR = path.dirname(SCRIPT_DIR);
const BASELINE_FILE = path.join(SKILL_DIR, "references", "policy-baseline.md");
const REQUEST_HELPER = path.join(SCRIPT_DIR, "atlassian-request.mjs");
const OFFICIAL_AREAS = ["RD", "isp", "GA", "CS", "SM"];
const DEFAULT_PERSON = "Oliver Krylow";
const DEFAULT_ACCOUNT_ID = "619fbba4744c4d0069c8a49c";
const HEADER_FIELDS = new Map([
["document owner", "Document Owner"],
["risk owner", "Risk Owner"],
["asset owner", "Asset Owner"],
["klassifizierung", "Klassifizierung"],
["status", "Status"],
["geprüft von", "Geprüft von"],
["geprüft am", "Geprüft am"],
["freigegeben von", "Freigegeben von"],
["freigegeben am", "Freigegeben am"],
]);
export function parseCli(argv) {
const args = [...argv];
const command = args.shift();
const options = {
command,
positionals: [],
profile: undefined,
mode: undefined,
person: "Oliver Krylow",
accountId: undefined,
area: "all",
today: undefined,
expectedVersion: undefined,
planSha256: undefined,
allowMutation: false,
help: command === "--help" || command === "-h",
};
while (args.length) {
const arg = args.shift();
switch (arg) {
case "--profile":
options.profile = requiredValue(arg, args.shift());
break;
case "--mode":
options.mode = requiredValue(arg, args.shift());
break;
case "--person":
options.person = requiredValue(arg, args.shift());
break;
case "--account-id":
options.accountId = requiredValue(arg, args.shift());
break;
case "--area":
options.area = normalizeArea(requiredValue(arg, args.shift()));
break;
case "--today":
options.today = requiredValue(arg, args.shift());
break;
case "--expected-version": {
const value = Number(requiredValue(arg, args.shift()));
if (!Number.isSafeInteger(value) || value < 1) throw new Error("--expected-version must be a positive integer");
options.expectedVersion = value;
break;
}
case "--plan-sha256":
options.planSha256 = requiredValue(arg, args.shift()).toLowerCase();
break;
case "--allow-mutation":
options.allowMutation = true;
break;
case "--help":
case "-h":
options.help = true;
break;
default:
if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
options.positionals.push(arg);
}
}
if (options.help) return options;
if (!command) throw new Error("Command is required");
if (!new Set(["policy", "worklist", "snapshot", "check-plan", "apply"]).has(command)) {
throw new Error(`Unknown command: ${command}`);
}
if (command === "worklist" && !new Set(["owner", "reviewer"]).has(options.mode)) {
throw new Error("worklist requires --mode owner|reviewer");
}
if (command === "snapshot" && options.positionals.length !== 1) throw new Error("snapshot requires PAGE_ID");
if (new Set(["check-plan", "apply"]).has(command) && options.positionals.length !== 1) {
throw new Error(`${command} requires PLAN.json`);
}
if (command === "apply") {
if (!options.allowMutation) throw new Error("apply requires --allow-mutation");
if (!options.expectedVersion) throw new Error("apply requires --expected-version");
if (!/^[0-9a-f]{64}$/.test(options.planSha256 ?? "")) throw new Error("apply requires --plan-sha256");
} else if (options.allowMutation) {
throw new Error("--allow-mutation is valid only with apply");
}
return options;
}
function requiredValue(option, value) {
if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`);
return value;
}
export function normalizeArea(value) {
if (value.toLowerCase() === "all") return "all";
if (value.toLowerCase() === "isp") return "isp";
if (value.toUpperCase() === "R&D") return "RD";
const upper = value.toUpperCase();
if (!OFFICIAL_AREAS.includes(upper)) throw new Error(`Unknown IMS area: ${value}`);
return upper;
}
export function parseBaseline(markdown) {
const source = (label, kind) => {
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = markdown.match(new RegExp(`- ${escaped}: ` + "`(\\d+)`(?:, version `(\\d+)`)?[^\\n]*\\((https://[^)]+)\\)"));
if (!match) throw new Error(`Policy baseline lacks ${label}`);
return { kind, id: match[1], version: match[2] ? Number(match[2]) : undefined, url: match[3] };
};
const instruction = source("Instruction page", "page");
const announcement = source("Announcement", "blogpost");
const report = source("Worklist report", "page");
return {
instruction,
announcement,
report,
baseUrl: new URL(instruction.url).origin,
};
}
export async function checkPolicy(client, baseline) {
const checked = [];
for (const [name, source] of [["instruction", baseline.instruction], ["announcement", baseline.announcement]]) {
const current = await client.get(`wiki/api/v2/${source.kind === "blogpost" ? "blogposts" : "pages"}/${source.id}`);
checked.push({
name,
id: source.id,
url: source.url,
baselineVersion: source.version,
currentVersion: current.version?.number,
changed: current.version?.number !== source.version,
});
}
return { changed: checked.some((source) => source.changed), sources: checked };
}
const ENTITY_MAP = new Map([
["amp", "&"],
["lt", "<"],
["gt", ">"],
["quot", '"'],
["apos", "'"],
["nbsp", " "],
["auml", "ä"],
["ouml", "ö"],
["uuml", "ü"],
["Auml", "Ä"],
["Ouml", "Ö"],
["Uuml", "Ü"],
["szlig", "ß"],
]);
export function decodeEntities(value) {
return value.replace(/&(#x[0-9a-f]+|#\d+|[a-zA-Z]+);/g, (entity, key) => {
if (key.startsWith("#x")) return String.fromCodePoint(Number.parseInt(key.slice(2), 16));
if (key.startsWith("#")) return String.fromCodePoint(Number(key.slice(1)));
return ENTITY_MAP.get(key) ?? entity;
});
}
export function plainTextFromHtml(html) {
return normalizeWhitespace(decodeEntities(String(html).replace(/<br\s*\/?\s*>/gi, " ").replace(/<[^>]*>/g, " ")));
}
function normalizeWhitespace(value) {
return String(value).replace(/\s+/g, " ").trim();
}
function cellsFromRow(rowHtml) {
return [...rowHtml.matchAll(/<t[dh]\b[^>]*>([\s\S]*?)<\/t[dh]>/gi)].map((match) => match[1]);
}
function attrValues(html, name) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return [...String(html).matchAll(new RegExp(`${escaped}\\s*=\\s*(["'])(.*?)\\1`, "gi"))].map((match) => decodeEntities(match[2]));
}
function accountIds(html) {
return sortedUnique([...attrValues(html, "data-account-id"), ...attrValues(html, "ri:account-id")]);
}
function firstHref(html) {
return attrValues(html, "href")[0];
}
function normalizeReportArea(href) {
const value = href?.match(/\/spaces\/([^/?#]+)/i)?.[1];
if (!value) return undefined;
try {
return normalizeArea(decodeURIComponent(value));
} catch {
return value;
}
}
export function parseReportHtml(html) {
const rows = [];
for (const match of String(html).matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)) {
const cells = cellsFromRow(match[1]);
if (cells.length < 10) continue;
const href = firstHref(cells[0]);
const id = attrValues(cells[0], "data-content-id")[0]
?? href?.match(/\/pages\/(\d+)/)?.[1]
?? href?.match(/[?&]pageId=(\d+)/)?.[1];
if (!id) continue;
rows.push({
id,
title: plainTextFromHtml(cells[0]),
href,
area: normalizeReportArea(href),
documentOwner: plainTextFromHtml(cells[1]),
documentOwnerIds: accountIds(cells[1]),
riskOwner: plainTextFromHtml(cells[2]),
riskOwnerIds: accountIds(cells[2]),
assetOwner: plainTextFromHtml(cells[3]),
assetOwnerIds: accountIds(cells[3]),
classification: plainTextFromHtml(cells[4]),
status: plainTextFromHtml(cells[5]),
reviewer: plainTextFromHtml(cells[6]),
reviewerIds: accountIds(cells[6]),
reviewedAt: plainTextFromHtml(cells[7]),
releasedBy: plainTextFromHtml(cells[8]),
releasedAt: plainTextFromHtml(cells[9]),
});
}
return rows;
}
function matchesPerson(text, ids, person, accountId) {
return accountId ? ids.includes(accountId) : normalizeWhitespace(text) === normalizeWhitespace(person);
}
function malformedFields(row) {
return [
["documentOwner", row.documentOwner],
["riskOwner", row.riskOwner],
["assetOwner", row.assetOwner],
["classification", row.classification],
["status", row.status],
].filter(([, value]) => !value).map(([field]) => field);
}
export function parseDate(value) {
if (!value) return undefined;
const german = value.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
const iso = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
const parts = german
? { day: Number(german[1]), month: Number(german[2]) - 1, year: Number(german[3]) }
: iso
? { day: Number(iso[3]), month: Number(iso[2]) - 1, year: Number(iso[1]) }
: undefined;
if (parts) {
const date = new Date(Date.UTC(parts.year, parts.month, parts.day));
return date.getUTCFullYear() === parts.year && date.getUTCMonth() === parts.month && date.getUTCDate() === parts.day ? date : undefined;
}
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? undefined : new Date(timestamp);
}
function annualReviewDue(row, today) {
const date = parseDate(row.releasedAt || row.reviewedAt);
if (!date) return false;
const threshold = new Date(today);
threshold.setUTCFullYear(threshold.getUTCFullYear() - 1);
return date <= threshold;
}
function areaRank(area) {
const index = OFFICIAL_AREAS.indexOf(area);
return index < 0 ? OFFICIAL_AREAS.length : index;
}
export function selectWorklist(rows, options) {
const mode = options.mode;
const person = options.person ?? DEFAULT_PERSON;
const accountId = options.accountId ?? (person === DEFAULT_PERSON ? DEFAULT_ACCOUNT_ID : undefined);
const area = options.area ?? "all";
const today = options.today ? new Date(`${options.today}T00:00:00Z`) : new Date();
if (Number.isNaN(today.valueOf())) throw new Error(`Invalid --today: ${options.today}`);
const candidates = [];
let roleMatchCount = 0;
for (const row of rows) {
if (area !== "all" && row.area !== area) continue;
const malformed = malformedFields(row);
const reasons = [];
if (mode === "reviewer") {
if (!matchesPerson(row.reviewer, row.reviewerIds, person, accountId)) continue;
roleMatchCount += 1;
if (row.status !== "Prüfung") continue;
reasons.push("awaiting-review");
} else {
if (!matchesPerson(row.documentOwner, row.documentOwnerIds, person, accountId)) continue;
roleMatchCount += 1;
if (row.status === "Entwurf") reasons.push("draft");
if (!row.reviewer) reasons.push("missing-reviewer");
if (matchesPerson(row.reviewer, row.reviewerIds, person, accountId)) reasons.push("self-reviewer");
if (annualReviewDue(row, today)) reasons.push("annual-review-due");
if (row.status === "Freigegeben" && !parseDate(row.releasedAt || row.reviewedAt)) reasons.push("missing-review-date");
if (malformed.length) reasons.push("malformed-header");
if (!reasons.length) continue;
}
candidates.push({ ...row, reasons, malformedFields: malformed });
}
candidates.sort((left, right) => areaRank(left.area) - areaRank(right.area) || left.title.localeCompare(right.title, "de"));
return {
mode,
person,
area,
roleMatchCount,
candidates,
malformed: candidates.filter((row) => row.malformedFields.length),
};
}
export function parseHeaderHtml(html) {
const header = {};
const keyFor = (cell) => HEADER_FIELDS.get(plainTextFromHtml(cell).toLowerCase().replace(/:$/, ""));
const tables = [...String(html).matchAll(/<table\b[^>]*>([\s\S]*?)<\/table>/gi)].map((match) => match[1]);
for (const table of tables) {
const rows = [...table.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)].map((match) => cellsFromRow(match[1]));
for (let index = 0; index + 1 < rows.length; index++) {
const keys = rows[index].map(keyFor);
const values = rows[index + 1];
if (keys.length >= 2 && keys.every(Boolean) && values.length >= keys.length) {
keys.forEach((key, cell) => {
if (header[key] === undefined) header[key] = plainTextFromHtml(values[cell]);
});
index += 1;
}
}
for (const row of rows) {
if (row.length !== 2) continue;
const key = keyFor(row[0]);
if (key && !keyFor(row[1]) && header[key] === undefined) header[key] = plainTextFromHtml(row[1]);
}
}
return header;
}
function pageBody(page, representation) {
const value = page.body?.[representation]?.value;
if (typeof value !== "string") throw new Error(`Page ${page.id ?? "?"} lacks ${representation} body`);
return value;
}
function flattenResults(response) {
const pages = Array.isArray(response) ? response : [response];
return pages.flatMap((page) => page?.results ?? []);
}
function pageUrl(page, baseUrl) {
const webui = page._links?.webui;
if (webui && /^https?:/i.test(webui)) return webui;
const base = page._links?.base ?? baseUrl;
if (webui && base) return new URL(webui, `${base.replace(/\/$/, "")}/`).href;
return `${baseUrl.replace(/\/$/, "")}/wiki/pages/viewpage.action?pageId=${page.id}`;
}
export async function createSnapshot(client, pageId, baseUrl) {
const storagePage = await client.get(`wiki/api/v2/pages/${pageId}?body-format=storage`);
const capturedVersion = storagePage.version?.number;
const viewPage = await client.get(`wiki/api/v2/pages/${pageId}?body-format=view`);
if (viewPage.version?.number !== capturedVersion) throw new Error(`Page version changed during snapshot: ${capturedVersion} -> ${viewPage.version?.number}`);
const labels = flattenResults(await client.get(`wiki/api/v2/pages/${pageId}/labels?limit=100`, { paginate: true }));
const inlineComments = flattenResults(await client.get(`wiki/api/v2/pages/${pageId}/inline-comments?resolution-status=open&body-format=storage&limit=100`, { paginate: true }));
const footerComments = flattenResults(await client.get(`wiki/api/v2/pages/${pageId}/footer-comments?body-format=storage&limit=100`, { paginate: true }));
const attachments = flattenResults(await client.get(`wiki/api/v2/pages/${pageId}/attachments?limit=100`, { paginate: true }));
const storage = pageBody(storagePage, "storage");
const view = pageBody(viewPage, "view");
const confirmedPage = await client.get(`wiki/api/v2/pages/${pageId}?body-format=storage`);
const confirmedStorage = pageBody(confirmedPage, "storage");
if (confirmedPage.version?.number !== capturedVersion || hashText(confirmedStorage) !== hashText(storage)) {
throw new Error(`Page changed during snapshot after version ${capturedVersion}`);
}
return {
pageId: String(storagePage.id),
title: storagePage.title,
url: pageUrl(storagePage, baseUrl),
version: storagePage.version,
header: parseHeaderHtml(view),
labels,
inlineComments,
footerComments,
attachments,
body: {
storage,
view,
storageSha256: hashText(storage),
viewSha256: hashText(view),
},
};
}
export function extractMarkerRefs(storage) {
return sortedUnique([...String(storage).matchAll(/<ac:inline-comment-marker\b[^>]*\bac:ref\s*=\s*(["'])(.*?)\1/gi)].map((match) => match[2]));
}
const SERVER_STORAGE_ATTRIBUTES = new Set(["local-id", "data-local-id", "ac:local-id", "data-macro-id", "ac:macro-id"]);
function normalizeStorage(storage) {
const tokens = [];
for (const match of String(storage).matchAll(/<[^>]*>|[^<]+/g)) {
const token = match[0];
if (!token.startsWith("<")) {
const text = decodeEntities(token).replaceAll("\r\n", "\n");
if (text.trim()) tokens.push(`#${text}`);
continue;
}
const closing = token.match(/^<\s*\/\s*([^\s>]+)/);
if (closing) {
tokens.push(`</${closing[1].toLowerCase()}>`);
continue;
}
const opening = token.match(/^<\s*([^\s/>]+)([\s\S]*?)(\/?)\s*>$/);
if (!opening) {
tokens.push(token);
continue;
}
const attributes = [...opening[2].matchAll(/([^\s=/>]+)\s*=\s*(["'])(.*?)\2/g)]
.map((attribute) => [attribute[1].toLowerCase(), decodeEntities(attribute[3])])
.filter(([name]) => !SERVER_STORAGE_ATTRIBUTES.has(name))
.sort(([leftName, leftValue], [rightName, rightValue]) => leftName.localeCompare(rightName) || leftValue.localeCompare(rightValue));
tokens.push(`<${opening[1].toLowerCase()} ${JSON.stringify(attributes)}${opening[3] ? "/" : ""}>`);
}
return tokens;
}
export function assertEquivalentStorage(expected, actual) {
const expectedNormalized = normalizeStorage(expected);
const actualNormalized = normalizeStorage(actual);
if (JSON.stringify(expectedNormalized) !== JSON.stringify(actualNormalized)) {
const error = new Error("Stored page differs from the planned semantic structure");
error.expectedNormalized = expectedNormalized;
error.actualNormalized = actualNormalized;
throw error;
}
}
export function applyReplacements(storage, replacements) {
let result = storage;
const markersBefore = extractMarkerRefs(storage);
for (const replacement of replacements) {
const expectedCount = replacement.count ?? 1;
if (!replacement.from) throw new Error("Replacement from must be non-empty");
if (!Number.isSafeInteger(expectedCount) || expectedCount < 1) throw new Error("Replacement count must be a positive integer");
const actualCount = result.split(replacement.from).length - 1;
if (actualCount !== expectedCount) {
throw new Error(`Replacement expected ${expectedCount} occurrence(s), found ${actualCount}: ${replacement.from}`);
}
result = result.split(replacement.from).join(replacement.to);
}
const markersAfter = extractMarkerRefs(result);
if (JSON.stringify(markersBefore) !== JSON.stringify(markersAfter)) throw new Error("Page replacement would remove or change inline-comment markers");
return result;
}
export function validatePlan(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Plan must be an object");
const pageId = String(value.pageId ?? "");
if (!/^\d+$/.test(pageId)) throw new Error("Plan pageId must be numeric");
if (!Number.isSafeInteger(value.expectedVersion) || value.expectedVersion < 1) throw new Error("Plan expectedVersion must be a positive integer");
if (!Array.isArray(value.operations) || !value.operations.length) throw new Error("Plan operations must be non-empty");
let pageOperationSeen = false;
const operations = value.operations.map((operation, index) => {
if (operation?.kind === "page") {
pageOperationSeen = true;
if (!operation.versionComment) throw new Error(`Page operation ${index} lacks versionComment`);
if (!Array.isArray(operation.replacements) || !operation.replacements.length) throw new Error(`Page operation ${index} lacks replacements`);
for (const replacement of operation.replacements) {
if (typeof replacement.from !== "string" || typeof replacement.to !== "string") throw new Error(`Page operation ${index} has an invalid replacement`);
if (replacement.count !== undefined && (!Number.isSafeInteger(replacement.count) || replacement.count < 1)) throw new Error(`Page operation ${index} has an invalid replacement count`);
}
const contains = operation.expect?.contains ?? [];
const notContains = operation.expect?.notContains ?? [];
if (!Array.isArray(contains) || contains.some((entry) => typeof entry !== "string")) throw new Error(`Page operation ${index} has invalid expect.contains`);
if (!Array.isArray(notContains) || notContains.some((entry) => typeof entry !== "string")) throw new Error(`Page operation ${index} has invalid expect.notContains`);
if (!contains.length && !notContains.length) throw new Error(`Page operation ${index} requires at least one expectation`);
return {
kind: "page",
versionComment: operation.versionComment,
replacements: operation.replacements.map((replacement) => ({ ...replacement })),
expect: { contains, notContains },
};
}
if (operation?.kind === "inline-comment") {
if (pageOperationSeen) throw new Error("Inline-comment operations must precede every page operation");
if (typeof operation.target !== "string" || !operation.target || typeof operation.text !== "string" || !operation.text) throw new Error(`Inline-comment operation ${index} requires target and text`);
const matchCount = operation.matchCount ?? 1;
const matchIndex = operation.matchIndex ?? 0;
if (!Number.isSafeInteger(matchCount) || matchCount < 1) throw new Error(`Inline-comment operation ${index} has invalid matchCount`);
if (!Number.isSafeInteger(matchIndex) || matchIndex < 0 || matchIndex >= matchCount) throw new Error(`Inline-comment operation ${index} has invalid matchIndex`);
return { kind: "inline-comment", target: operation.target, text: operation.text, matchCount, matchIndex };
}
throw new Error(`Operation ${index} has unknown kind`);
});
return { pageId, expectedVersion: value.expectedVersion, operations };
}
export function readPlan(planFile) {
const source = readFileSync(planFile, "utf8");
return { source, sha256: hashText(source), plan: validatePlan(JSON.parse(source)) };
}
function collectAdfTexts(value, texts = []) {
if (Array.isArray(value)) {
for (const child of value) collectAdfTexts(child, texts);
} else if (value && typeof value === "object") {
if (value.type === "text" && typeof value.text === "string") texts.push(value.text);
for (const child of Object.values(value)) collectAdfTexts(child, texts);
}
return texts;
}
const ADF_TEXT_BLOCKS = new Set(["paragraph", "heading", "codeBlock"]);
function collectAdfBlocks(value, blocks = []) {
if (Array.isArray(value)) {
for (const child of value) collectAdfBlocks(child, blocks);
} else if (value && typeof value === "object") {
if (ADF_TEXT_BLOCKS.has(value.type)) blocks.push(collectAdfTexts(value).join(""));
else for (const child of value.content ?? []) collectAdfBlocks(child, blocks);
}
return blocks;
}
function textSelectionCount(adf, target) {
const blocks = collectAdfBlocks(adf);
const renderedBlocks = blocks.length ? blocks : [collectAdfTexts(adf).join("")];
return renderedBlocks.reduce((count, text) => count + text.split(target).length - 1, 0);
}
function escapeHtml(value) {
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
}
function assertExpectation(storage, expect) {
const text = plainTextFromHtml(storage);
for (const expected of expect.contains) {
if (!text.includes(normalizeWhitespace(expected))) throw new Error(`Validated page lacks expected text: ${expected}`);
}
for (const forbidden of expect.notContains) {
if (text.includes(normalizeWhitespace(forbidden))) throw new Error(`Validated page still contains forbidden text: ${forbidden}`);
}
}
async function requireVersion(client, pageId, expectedVersion, representation = "storage") {
const page = await client.get(`wiki/api/v2/pages/${pageId}?body-format=${representation}`);
if (page.version?.number !== expectedVersion) {
throw new Error(`Page version drift: expected ${expectedVersion}, found ${page.version?.number}`);
}
return page;
}
async function createInlineComment(client, pageId, expectedVersion, operation) {
const adfPage = await requireVersion(client, pageId, expectedVersion, "atlas_doc_format");
const adf = JSON.parse(pageBody(adfPage, "atlas_doc_format"));
const count = textSelectionCount(adf, operation.target);
if (count !== operation.matchCount) {
throw new Error(`Inline-comment target expected ${operation.matchCount} occurrence(s), found ${count}: ${operation.target}`);
}
const created = await client.mutate("POST", "wiki/api/v2/inline-comments", {
pageId,
body: { representation: "storage", value: `<p>${escapeHtml(operation.text)}</p>` },
inlineCommentProperties: {
textSelection: operation.target,
textSelectionMatchCount: operation.matchCount,
textSelectionMatchIndex: operation.matchIndex,
},
});
const verified = await client.get(`wiki/api/v2/inline-comments/${created.id}?body-format=storage&include-properties=true&include-version=true`);
const selection = verified.properties?.inlineOriginalSelection ?? verified.properties?.["inline-original-selection"];
const markerRef = verified.properties?.inlineMarkerRef ?? verified.properties?.["inline-marker-ref"];
if (String(verified.pageId) !== pageId || verified.resolutionStatus !== "open") throw new Error(`Inline comment ${created.id} has unexpected page or status`);
if (plainTextFromHtml(verified.body?.storage?.value ?? "") !== normalizeWhitespace(operation.text)) throw new Error(`Inline comment ${created.id} body validation failed`);
if (selection !== operation.target || !markerRef) throw new Error(`Inline comment ${created.id} selection validation failed`);
const storagePage = await requireVersion(client, pageId, expectedVersion, "storage");
if (!extractMarkerRefs(pageBody(storagePage, "storage")).includes(markerRef)) throw new Error(`Inline comment ${created.id} marker is not visible in page storage`);
const open = flattenResults(await client.get(`wiki/api/v2/pages/${pageId}/inline-comments?resolution-status=open&body-format=storage&limit=100`, { paginate: true }));
if (!open.some((comment) => String(comment.id) === String(created.id))) throw new Error(`Inline comment ${created.id} is absent from the open-comment list`);
return { id: String(created.id), target: operation.target, markerRef, validation: "passed" };
}
export async function applyPlan(plan, client, baseUrl) {
let version = plan.expectedVersion;
const receipt = {
pageId: plan.pageId,
initialVersion: version,
operations: [],
inlineComments: [],
validation: "pending",
};
try {
let current = await requireVersion(client, plan.pageId, version, "storage");
receipt.url = pageUrl(current, baseUrl);
for (const operation of plan.operations) {
current = await requireVersion(client, plan.pageId, version, "storage");
if (operation.kind === "inline-comment") {
const comment = await createInlineComment(client, plan.pageId, version, operation);
receipt.inlineComments.push(comment);
receipt.operations.push({ kind: operation.kind, id: comment.id, validation: "passed" });
continue;
}
if (!current.spaceId) throw new Error(`Page ${plan.pageId} lacks spaceId required for update`);
const before = pageBody(current, "storage");
const intended = applyReplacements(before, operation.replacements);
await client.mutate("PUT", `wiki/api/v2/pages/${plan.pageId}`, {
id: plan.pageId,
status: current.status ?? "current",
title: current.title,
spaceId: current.spaceId,
body: { representation: "storage", value: intended },
version: { number: version + 1, message: operation.versionComment },
});
const verified = await requireVersion(client, plan.pageId, version + 1, "storage");
const actual = pageBody(verified, "storage");
if (verified.version?.message !== operation.versionComment) throw new Error(`Version ${version + 1} has unexpected version comment`);
assertEquivalentStorage(intended, actual);
assertExpectation(actual, operation.expect);
version += 1;
receipt.operations.push({ kind: operation.kind, version, versionComment: operation.versionComment, validation: "passed" });
}
const finalPage = await requireVersion(client, plan.pageId, version, "storage");
const finalStorage = pageBody(finalPage, "storage");
receipt.finalVersion = version;
receipt.versionComment = finalPage.version?.message ?? "";
receipt.header = parseHeaderHtml(finalStorage);
receipt.validation = "passed";
return receipt;
} catch (error) {
receipt.finalVersion = version;
receipt.validation = "failed";
error.receipt = receipt;
throw error;
}
}
export function createAtlassianClient({ profile, helper = REQUEST_HELPER, spawn = spawnSync } = {}) {
const run = (requestArgs, body) => {
let dir;
try {
const args = [helper, ...requestArgs];
if (body !== undefined) {
dir = mkdtempSync(path.join(tmpdir(), "ims-request-"));
const bodyFile = path.join(dir, "body.json");
writeFileSync(bodyFile, JSON.stringify(body));
args.push("--body", bodyFile, "--allow-mutation");
}
if (profile) args.push("--profile", profile);
const result = spawn(process.execPath, args, { encoding: "utf8", windowsHide: true, maxBuffer: 50 * 1024 * 1024 });
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(result.stderr.trim() || `Atlassian request exited ${result.status}`);
return result.stdout.trim() ? JSON.parse(result.stdout) : null;
} finally {
if (dir) rmSync(dir, { recursive: true, force: true });
}
};
return {
get(requestPath, options = {}) {
return run([requestPath, ...(options.paginate ? ["--paginate"] : [])]);
},
mutate(method, requestPath, body) {
return run([method, requestPath], body);
},
};
}
function hashText(value) {
return createHash("sha256").update(value).digest("hex");
}
function sortedUnique(values) {
return [...new Set(values.filter((value) => value !== undefined && value !== ""))].sort();
}
function usage() {
return `Usage: node scripts/ims.mjs COMMAND [options]
Commands:
policy [--profile NAME]
worklist --mode owner|reviewer [--person NAME] [--account-id ID] [--area all|RD|isp|GA|CS|SM] [--today YYYY-MM-DD] [--profile NAME]
snapshot PAGE_ID [--profile NAME]
check-plan PLAN.json
apply PLAN.json --expected-version N --plan-sha256 HASH --allow-mutation [--profile NAME]
check-plan and every command except apply are read-only. apply accepts this plan shape:
{
"pageId": "123",
"expectedVersion": 7,
"operations": [
{
"kind": "inline-comment",
"target": "exact rendered selection",
"text": "Comment text",
"matchCount": 1,
"matchIndex": 0
},
{
"kind": "page",
"versionComment": "zurück in Entwurf, da Korrektur notwendig",
"replacements": [{ "from": "exact storage fragment", "to": "replacement", "count": 1 }],
"expect": { "contains": ["expected rendered text"], "notContains": ["removed rendered text"] }
}
]
}
Run check-plan after plan approval. Run apply only after a separate explicit go and pass the unchanged hash.`;
}
async function main() {
const options = parseCli(process.argv.slice(2));
if (options.help) {
console.log(usage());
return;
}
const baseline = parseBaseline(readFileSync(BASELINE_FILE, "utf8"));
if (options.command === "check-plan") {
const loaded = readPlan(options.positionals[0]);
console.log(JSON.stringify({
pageId: loaded.plan.pageId,
expectedVersion: loaded.plan.expectedVersion,
operations: loaded.plan.operations.length,
planSha256: loaded.sha256,
mutation: false,
}, null, 2));
return;
}
if (options.command === "apply") {
const loaded = readPlan(options.positionals[0]);
if (loaded.plan.expectedVersion !== options.expectedVersion) throw new Error(`Plan expectedVersion ${loaded.plan.expectedVersion} differs from --expected-version ${options.expectedVersion}`);
if (loaded.sha256 !== options.planSha256) throw new Error(`Plan hash mismatch: expected ${options.planSha256}, found ${loaded.sha256}`);
const receipt = await applyPlan(loaded.plan, createAtlassianClient(options), baseline.baseUrl);
console.log(JSON.stringify(receipt, null, 2));
return;
}
const client = createAtlassianClient(options);
if (options.command === "policy") {
const result = await checkPolicy(client, baseline);
console.log(JSON.stringify(result, null, 2));
if (result.changed) process.exitCode = 2;
return;
}
if (options.command === "worklist") {
const report = await client.get(`wiki/rest/api/content/${baseline.report.id}?expand=body.export_view%2Cversion`);
const rows = parseReportHtml(report.body?.export_view?.value ?? "");
const result = selectWorklist(rows, options);
console.log(JSON.stringify({ reportId: baseline.report.id, reportVersion: report.version?.number, rowCount: rows.length, ...result }, null, 2));
return;
}
if (options.command === "snapshot") {
if (!/^\d+$/.test(options.positionals[0])) throw new Error("PAGE_ID must be numeric");
console.log(JSON.stringify(await createSnapshot(client, options.positionals[0], baseline.baseUrl), null, 2));
}
}
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
main().catch((error) => {
if (error.receipt) console.error(JSON.stringify({ error: error.message, receipt: error.receipt }, null, 2));
else console.error(error.message);
process.exit(1);
});
}