From bc6dbf3e27106efbd6b637697fa271846e444757 Mon Sep 17 00:00:00 2001 From: Todd Wolfe Date: Thu, 8 Oct 2026 22:04:47 -0400 Subject: [PATCH] Version browser dependency paths to prevent stale module export failures --- README.md | 2 + app-1.0.0.js | 2 +- dist/browser-1.0.0.js | 1586 +++++++++++++++++ index.html | 8 +- siblings-1.0.0.js | 8 +- test/value.test.js | 10 +- value-1.0.0.js | 10 +- vendor/v1/doggo-log/LICENSE | 21 + vendor/v1/doggo-log/commentary.js | 77 + vendor/v1/doggo-log/index.js | 165 ++ vendor/v1/doggo-log/levels.js | 8 + vendor/v1/doggo-log/redaction.js | 73 + vendor/v1/error-translator/LICENSE | 21 + vendor/v1/error-translator/definitions.js | 85 + vendor/v1/error-translator/diagnostics.js | 79 + .../v1/error-translator/extra-definitions.js | 494 +++++ vendor/v1/error-translator/index.js | 171 ++ vendor/v1/excuse-js/LICENSE | 21 + vendor/v1/excuse-js/excuses.js | 157 ++ vendor/v1/excuse-js/incident.js | 83 + vendor/v1/excuse-js/index.js | 105 ++ vendor/v1/ship-it-meter/LICENSE | 21 + vendor/v1/ship-it-meter/index.js | 304 ++++ vendor/v1/ship-it-meter/reports.js | 262 +++ vendor/v1/ship-it-meter/scenarios.js | 133 ++ 25 files changed, 3887 insertions(+), 19 deletions(-) create mode 100644 dist/browser-1.0.0.js create mode 100644 vendor/v1/doggo-log/LICENSE create mode 100644 vendor/v1/doggo-log/commentary.js create mode 100644 vendor/v1/doggo-log/index.js create mode 100644 vendor/v1/doggo-log/levels.js create mode 100644 vendor/v1/doggo-log/redaction.js create mode 100644 vendor/v1/error-translator/LICENSE create mode 100644 vendor/v1/error-translator/definitions.js create mode 100644 vendor/v1/error-translator/diagnostics.js create mode 100644 vendor/v1/error-translator/extra-definitions.js create mode 100644 vendor/v1/error-translator/index.js create mode 100644 vendor/v1/excuse-js/LICENSE create mode 100644 vendor/v1/excuse-js/excuses.js create mode 100644 vendor/v1/excuse-js/incident.js create mode 100644 vendor/v1/excuse-js/index.js create mode 100644 vendor/v1/ship-it-meter/LICENSE create mode 100644 vendor/v1/ship-it-meter/index.js create mode 100644 vendor/v1/ship-it-meter/reports.js create mode 100644 vendor/v1/ship-it-meter/scenarios.js diff --git a/README.md b/README.md index 0d6f69d..a1535dd 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,5 @@ MIT. Vendored package licenses are included in their directories. ## Practical v1 integrations Each tool keeps its playful library and adds an equally visible developer-value panel: measured build summaries, Error cause diagnostics, accountable incident drafts, context redaction plus a runnable Node async-scope example, and report-backed release policies. The browser runs actual pure package APIs. AsyncLocalStorage, file reading, CI report collection and process exit preservation run in Node; examples identify those boundaries. Demo receipt data is labeled sample data, never claimed as real CI evidence. + +Browser entry scripts and their v1 bundle/vendor directories use release-specific paths. Changing only entry-script queries is insufficient when cached dependent modules have gained exports. Refresh the versioned dependency paths together with entry scripts. diff --git a/app-1.0.0.js b/app-1.0.0.js index c7e6571..36f347d 100644 --- a/app-1.0.0.js +++ b/app-1.0.0.js @@ -6,7 +6,7 @@ import { broMessage, messageCategories, messagePresets, -} from "./dist/browser.js"; +} from "./dist/browser-1.0.0.js"; const element = (id) => document.getElementById(id); for (const [id, names, selected] of [ diff --git a/dist/browser-1.0.0.js b/dist/browser-1.0.0.js new file mode 100644 index 0000000..d1e2796 --- /dev/null +++ b/dist/browser-1.0.0.js @@ -0,0 +1,1586 @@ +// src/characters/bro.json +var bro_default = { + name: "bro", + art: [ + " .----.", + " / __ \\", + " | o o |", + " \\ __ /", + " /|==|\\", + " /_|__|_\\", + " | |", + " _| |_" + ] +}; + +// src/characters/bug.json +var bug_default = { + name: "bug", + art: [ + " \\ | /", + " .---.", + " --( o o )--", + " --( # )--", + " \\___/", + " / | \\", + " [ FEATURE? ]" + ] +}; + +// src/characters/coffee.json +var coffee_default = { + name: "coffee", + art: [ + " ~ ~", + " .----.", + " | CI |--.", + " | | |", + " |____|--'", + " [REFILL]" + ] +}; + +// src/characters/developer.json +var developer_default = { + name: "developer", + art: [ + " .-------.", + " | [] [] |", + " | ___ |", + " \\_______/", + " /|_#_|\\", + " /_ _\\", + " [__DEV___]" + ] +}; + +// src/characters/duck.json +var duck_default = { + name: "duck", + art: [" __", " (o )___", " / __/", " /___/", " [QUACK]"] +}; + +// src/characters/dumpster-fire.json +var dumpster_fire_default = { + name: "dumpster-fire", + art: [ + " ^ ^ ^", + " / \\/ \\/ \\", + " | HOT |", + " .----------.", + " | RELEASE |", + " |__________|", + " O O" + ] +}; + +// src/characters/husky.json +var husky_default = { + name: "husky", + art: [ + " /\\___/\\", + " / o o \\", + " | v |", + " \\_===_/", + " /| <> |\\", + " (_|____|_)", + " / \\", + " [_] [_]", + " [ LGTM ]" + ] +}; + +// src/characters/intern.json +var intern_default = { + name: "intern", + art: [ + " .----.", + " |o o|", + " | ?? |", + " \\____/", + " /| |\\", + " [ NOTES ]", + " | |", + " [_][_]" + ] +}; + +// src/characters/laptop.json +var laptop_default = { + name: "laptop", + art: [ + " .----------.", + " | > ship_ |", + " | |", + " +----------+", + " /____________\\", + " [ ESCAPE? ]" + ] +}; + +// src/characters/robot.json +var robot_default = { + name: "robot", + art: [ + " [ READY ]", + " .-------.", + " | o o |", + " | === |", + " +-------+", + " /| [] |\\", + " |_______|", + " [] []" + ] +}; + +// src/characters/rocket.json +var rocket_default = { + name: "rocket", + art: [ + " /\\", + " / \\", + " | [] |", + " | DA |", + " /|____|\\", + " /_|_||_|_\\", + " /\\/\\", + " /_||_\\" + ] +}; + +// src/characters/server.json +var server_default = { + name: "server", + art: [ + " .-----------.", + " | [==] [o] |", + " | [==] [o] |", + " | [==] [!] |", + " +-----------+", + " [ STILL UP ]" + ] +}; + +// src/characters/wizard.json +var wizard_default = { + name: "wizard", + art: [ + " /\\", + " / .\\", + " /__*_\\", + " (o o)", + " / = \\", + " /|___|\\", + " | |", + " [ SPELL:CI ]" + ] +}; + +// src/characters/index.js +var characters = Object.fromEntries( + [bro_default, bug_default, coffee_default, developer_default, duck_default, dumpster_fire_default, husky_default, intern_default, laptop_default, robot_default, rocket_default, server_default, wizard_default].map((c) => [ + c.name, + c + ]) +); + +// src/personas.js +var personas = { + dallas: { + intro: "DALLAS HAS ARRIVED AT FULL ZOOMIE SPEED.", + label: "ENTHUSIASM WITH EVIDENCE", + outro: "Take the victory lap after the checks pass." + }, + benji: { + intro: "BENJI HAS AN IDEA AND WOULD LIKE YOUR ATTENTION.", + label: "NEXT ADVENTURE", + outro: "Investigate the interesting thing. Keep a return route." + }, + "rubber-duck": { + intro: "Tell me what you expected. Then tell me what happened.", + label: "QUACK-SIZED REPRODUCTION", + outro: "One clear example beats a pond full of guesses." + }, + "on-call": { + intro: "The alert is loud. We can stay methodical.", + label: "INCIDENT NOTE", + outro: "Check impact, capture evidence, and confirm rollback." + }, + "code-review": { + intro: "A friendly question before the merge.", + label: "KINDNESS IS A REVIEW TOOL", + outro: "Make the reasoning easy for the next developer to follow." + }, + minimalist: { + intro: "Less magic. More clarity.", + label: "SMALLEST USEFUL STEP", + outro: "Keep the part that solves the problem." + }, + optimist: { + intro: "This can get better. Let us prove it.", + label: "HOPE WITH A TEST CASE", + outro: "A small verified improvement is still an improvement." + }, + skeptic: { + intro: "Interesting claim. Where are the receipts?", + label: "SHOW THE EVIDENCE", + outro: "Confidence is welcome. Verification gets the merge." + }, + classic: { intro: "BRO...", label: "", outro: "" }, + hype: { + intro: "BRO! THE TERMINAL IS APPLAUDING!", + label: "[ CONFETTI PENDING ]", + outro: "Victory lap after the smoke test." + }, + chill: { + intro: "Bro. One thing at a time.", + label: "", + outro: "Small step. Deep breath. Next log line." + }, + panic: { + intro: "BRO! DEEP BREATH. CHECK THE LOGS.", + label: "[ INCIDENT, NOT A PERSONALITY ]", + outro: "1. Read the evidence. 2. Find rollback. 3. Breathe again." + }, + corporate: { + intro: "Bro, please find the following update for alignment.", + label: "ACTION ITEM", + outro: "NEXT STEP: assign an owner before scheduling another meeting." + }, + coach: { + intro: "Bro, you have got this. Take the next step.", + label: "TODAY'S REP", + outro: "Progress beats heroic guesswork." + }, + "senior-dev": { + intro: "I have seen this release before.", + label: "REVIEW NOTE", + outro: "Reproduce it. Make it boring. Then ship it." + }, + intern: { + intro: "I brought notes and exactly one sensible question.", + label: "QUESTION FOR THE TEAM", + outro: "Could we add a test so future-me understands this too?" + }, + dramatic: { + intro: "ACT III: THE DEPLOYMENT ENTERS.", + label: "[ spotlight finds the terminal ]", + outro: "Curtain call postponed until health checks pass." + }, + sarcastic: { + intro: "An ambitious interpretation of 'probably fine'.", + label: "EVIDENCE RECEIVED", + outro: "Excellent. Now let us ask the tests for a second opinion." + }, + motivational: { + intro: "One small fix. One less future incident.", + label: "YOU CAN BUILD ON THIS", + outro: "Make the next developer's afternoon easier." + }, + friday: { + intro: "The weekend has entered the review thread.", + label: "FRIDAY PREFLIGHT", + outro: "Rollback ready? On-call awake? Coffee legally available?" + } +}; + +// src/themes.js +var themes = { + classic: { + top: ["+", "-", "+"], + side: "|", + bottom: ["+", "-", "+"], + color: 36 + }, + minimal: { + top: [".", "-", "."], + side: ":", + bottom: ["'", "-", "'"], + color: 37 + }, + neon: { top: ["\u256D", "\u2500", "\u256E"], side: "\u2502", bottom: ["\u2570", "\u2500", "\u256F"], color: 95 }, + retro: { + top: ["/", "=", "\\"], + side: "|", + bottom: ["\\", "=", "/"], + color: 33 + }, + hacker: { + top: ["[", "=", "]"], + side: ":", + bottom: ["[", "=", "]"], + color: 32 + }, + corporate: { + top: ["+", "=", "+"], + side: "|", + bottom: ["+", "=", "+"], + color: 34 + } +}; + +// node_modules/ansi-regex/index.js +function ansiRegex({ onlyFirst = false } = {}) { + const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"; + const osc = `(?:(?:\\u001B\\]|\\u009D)[^\\u0007\\u001B\\u009C\\u009D]*${ST})`; + const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]"; + const pattern = `${osc}|${csi}`; + return new RegExp(pattern, onlyFirst ? void 0 : "g"); +} + +// node_modules/strip-ansi/index.js +var regex = ansiRegex(); +function stripAnsi(string) { + if (typeof string !== "string") { + throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``); + } + if (!string.includes("\x1B") && !string.includes("\x9B")) { + return string; + } + return string.replace(regex, ""); +} + +// node_modules/get-east-asian-width/lookup-data.js +var ambiguousMinimalCodePoint = 161; +var ambiguousMaximumCodePoint = 1114109; +var ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109]; +var fullwidthMinimalCodePoint = 12288; +var fullwidthMaximumCodePoint = 65510; +var fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510]; +var wideMinimalCodePoint = 4352; +var wideMaximumCodePoint = 262141; +var wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101594, 101631, 101664, 101760, 101874, 101888, 102801, 102816, 102866, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110888, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110952, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127406, 127406, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128729, 128732, 128735, 128747, 128748, 128756, 128764, 128986, 128986, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129734, 129736, 129736, 129740, 129757, 129759, 129771, 129775, 129786, 131072, 196605, 196608, 262141]; + +// node_modules/get-east-asian-width/utilities.js +var isInRange = (ranges, codePoint) => { + let low = 0; + let high = Math.floor(ranges.length / 2) - 1; + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const i = mid * 2; + if (codePoint < ranges[i]) { + high = mid - 1; + } else if (codePoint > ranges[i + 1]) { + low = mid + 1; + } else { + return true; + } + } + return false; +}; + +// node_modules/get-east-asian-width/lookup.js +var commonCjkCodePoint = 19968; +var [wideFastPathStart, wideFastPathEnd] = /* @__PURE__ */ findWideFastPathRange(wideRanges); +function findWideFastPathRange(ranges) { + let fastPathStart = ranges[0]; + let fastPathEnd = ranges[1]; + for (let index = 0; index < ranges.length; index += 2) { + const start = ranges[index]; + const end = ranges[index + 1]; + if (commonCjkCodePoint >= start && commonCjkCodePoint <= end) { + return [start, end]; + } + if (end - start > fastPathEnd - fastPathStart) { + fastPathStart = start; + fastPathEnd = end; + } + } + return [fastPathStart, fastPathEnd]; +} +var isAmbiguous = (codePoint) => { + if (codePoint < ambiguousMinimalCodePoint || codePoint > ambiguousMaximumCodePoint) { + return false; + } + return isInRange(ambiguousRanges, codePoint); +}; +var isFullwidth = (codePoint) => { + if (codePoint < fullwidthMinimalCodePoint || codePoint > fullwidthMaximumCodePoint) { + return false; + } + return isInRange(fullwidthRanges, codePoint); +}; +var isWide = (codePoint) => { + if (codePoint >= wideFastPathStart && codePoint <= wideFastPathEnd) { + return true; + } + if (codePoint < wideMinimalCodePoint || codePoint > wideMaximumCodePoint) { + return false; + } + return isInRange(wideRanges, codePoint); +}; + +// node_modules/get-east-asian-width/index.js +function validate(codePoint) { + if (!Number.isSafeInteger(codePoint)) { + throw new TypeError(`Expected a code point, got \`${typeof codePoint}\`.`); + } +} +function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) { + validate(codePoint); + if (isFullwidth(codePoint) || isWide(codePoint) || ambiguousAsWide && isAmbiguous(codePoint)) { + return 2; + } + return 1; +} + +// node_modules/string-width/index.js +var segmenter = new Intl.Segmenter(); +var visibleCharacterRegex = /[^\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Nonspacing_Mark}\p{Enclosing_Mark}\p{Surrogate}]/v; +var spacingMarkRegex = /\p{Spacing_Mark}/v; +var rgiEmojiRegex = /^\p{RGI_Emoji}$/v; +var unqualifiedKeycapRegex = /^[\d#*]\u20E3$/; +var extendedPictographicRegex = /\p{Extended_Pictographic}/gu; +function isDoubleWidthNonRgiEmojiSequence(segment) { + if (segment.length > 50) { + return false; + } + if (unqualifiedKeycapRegex.test(segment)) { + return true; + } + if (segment.includes("\u200D")) { + const pictographics = segment.match(extendedPictographicRegex); + return pictographics !== null && pictographics.length >= 2; + } + return false; +} +function baseVisible(segment) { + const index = segment.search(visibleCharacterRegex); + return index === -1 ? void 0 : segment.slice(index); +} +function isHangulLeadingJamo(codePoint) { + return codePoint >= 4352 && codePoint <= 4447 || codePoint >= 43360 && codePoint <= 43388; +} +function isHangulVowelJamo(codePoint) { + return codePoint >= 4448 && codePoint <= 4519 || codePoint >= 55216 && codePoint <= 55238; +} +function isHangulTrailingJamo(codePoint) { + return codePoint >= 4520 && codePoint <= 4607 || codePoint >= 55243 && codePoint <= 55291; +} +function isHangulJamo(codePoint) { + return isHangulLeadingJamo(codePoint) || isHangulVowelJamo(codePoint) || isHangulTrailingJamo(codePoint); +} +function hangulClusterWidth(visibleSegment, eastAsianWidthOptions) { + const codePoints = []; + for (const character of visibleSegment) { + if (!visibleCharacterRegex.test(character)) { + continue; + } + codePoints.push(character.codePointAt(0)); + } + if (codePoints.length === 0) { + return void 0; + } + let width = 0; + for (let index = 0; index < codePoints.length; index++) { + const codePoint = codePoints[index]; + if (!isHangulJamo(codePoint)) { + if (width === 0) { + return void 0; + } + for (let remaining = index; remaining < codePoints.length; remaining++) { + width += eastAsianWidth(codePoints[remaining], eastAsianWidthOptions); + } + return width; + } + if (isHangulLeadingJamo(codePoint) && isHangulVowelJamo(codePoints[index + 1])) { + width += 2; + index += isHangulTrailingJamo(codePoints[index + 2]) ? 2 : 1; + continue; + } + width += eastAsianWidth(codePoint, eastAsianWidthOptions); + } + return width; +} +function trailingWidth(visibleSegment, eastAsianWidthOptions) { + let extra = 0; + let first = true; + for (const character of visibleSegment) { + if (first) { + first = false; + continue; + } + if (spacingMarkRegex.test(character) || character >= "\uFF00" && character <= "\uFFEF") { + extra += eastAsianWidth(character.codePointAt(0), eastAsianWidthOptions); + } + } + return extra; +} +function stringWidth(input, options = {}) { + if (typeof input !== "string" || input.length === 0) { + return 0; + } + const { + ambiguousIsNarrow = true, + countAnsiEscapeCodes = false + } = options; + let string = input; + if (!countAnsiEscapeCodes && (string.includes("\x1B") || string.includes("\x9B"))) { + string = stripAnsi(string); + } + if (string.length === 0) { + return 0; + } + if (/^[\u0020-\u007E]*$/.test(string)) { + return string.length; + } + let width = 0; + const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow }; + for (const { segment } of segmenter.segment(string)) { + const visibleSegment = baseVisible(segment); + if (visibleSegment === void 0) { + continue; + } + if (rgiEmojiRegex.test(segment) || isDoubleWidthNonRgiEmojiSequence(segment)) { + width += 2; + continue; + } + const hangulWidth = hangulClusterWidth(visibleSegment, eastAsianWidthOptions); + if (hangulWidth !== void 0) { + width += hangulWidth; + continue; + } + const codePoint = visibleSegment.codePointAt(0); + width += eastAsianWidth(codePoint, eastAsianWidthOptions); + width += trailingWidth(visibleSegment, eastAsianWidthOptions); + } + return width; +} + +// node_modules/ansi-styles/index.js +var ANSI_BACKGROUND_OFFSET = 10; +var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`; +var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`; +var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`; +var styles = { + modifier: { + reset: [0, 0], + // 21 isn't widely supported and 22 does the same thing + bold: [1, 22], + dim: [2, 22], + italic: [3, 23], + underline: [4, 24], + overline: [53, 55], + inverse: [7, 27], + hidden: [8, 28], + strikethrough: [9, 29] + }, + color: { + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + // Bright color + blackBright: [90, 39], + gray: [90, 39], + // Alias of `blackBright` + grey: [90, 39], + // Alias of `blackBright` + redBright: [91, 39], + greenBright: [92, 39], + yellowBright: [93, 39], + blueBright: [94, 39], + magentaBright: [95, 39], + cyanBright: [96, 39], + whiteBright: [97, 39] + }, + bgColor: { + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49], + // Bright color + bgBlackBright: [100, 49], + bgGray: [100, 49], + // Alias of `bgBlackBright` + bgGrey: [100, 49], + // Alias of `bgBlackBright` + bgRedBright: [101, 49], + bgGreenBright: [102, 49], + bgYellowBright: [103, 49], + bgBlueBright: [104, 49], + bgMagentaBright: [105, 49], + bgCyanBright: [106, 49], + bgWhiteBright: [107, 49] + } +}; +var modifierNames = Object.keys(styles.modifier); +var foregroundColorNames = Object.keys(styles.color); +var backgroundColorNames = Object.keys(styles.bgColor); +var colorNames = [...foregroundColorNames, ...backgroundColorNames]; +function assembleStyles() { + const codes = /* @__PURE__ */ new Map(); + for (const [groupName, group] of Object.entries(styles)) { + for (const [styleName, style] of Object.entries(group)) { + styles[styleName] = { + open: `\x1B[${style[0]}m`, + close: `\x1B[${style[1]}m` + }; + group[styleName] = styles[styleName]; + codes.set(style[0], style[1]); + } + Object.defineProperty(styles, groupName, { + value: group, + enumerable: false + }); + } + Object.defineProperty(styles, "codes", { + value: codes, + enumerable: false + }); + styles.color.close = "\x1B[39m"; + styles.bgColor.close = "\x1B[49m"; + styles.color.ansi = wrapAnsi16(); + styles.color.ansi256 = wrapAnsi256(); + styles.color.ansi16m = wrapAnsi16m(); + styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET); + Object.defineProperties(styles, { + rgbToAnsi256: { + value(red, green, blue) { + if (red === green && green === blue) { + if (red < 8) { + return 16; + } + if (red > 248) { + return 231; + } + return Math.round((red - 8) / 247 * 24) + 232; + } + return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5); + }, + enumerable: false + }, + hexToRgb: { + value(hex) { + const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16)); + if (!matches) { + return [0, 0, 0]; + } + let [colorString] = matches; + if (colorString.length === 3) { + colorString = [...colorString].map((character) => character + character).join(""); + } + const integer = Number.parseInt(colorString, 16); + return [ + /* eslint-disable no-bitwise */ + integer >> 16 & 255, + integer >> 8 & 255, + integer & 255 + /* eslint-enable no-bitwise */ + ]; + }, + enumerable: false + }, + hexToAnsi256: { + value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)), + enumerable: false + }, + ansi256ToAnsi: { + value(code) { + if (code < 8) { + return 30 + code; + } + if (code < 16) { + return 90 + (code - 8); + } + let red; + let green; + let blue; + if (code >= 232) { + red = ((code - 232) * 10 + 8) / 255; + green = red; + blue = red; + } else { + code -= 16; + const remainder = code % 36; + red = Math.floor(code / 36) / 5; + green = Math.floor(remainder / 6) / 5; + blue = remainder % 6 / 5; + } + const value = Math.max(red, green, blue) * 2; + if (value === 0) { + return 30; + } + let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red)); + if (value === 2) { + result += 60; + } + return result; + }, + enumerable: false + }, + rgbToAnsi: { + value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)), + enumerable: false + }, + hexToAnsi: { + value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)), + enumerable: false + } + }); + return styles; +} +var ansiStyles = assembleStyles(); +var ansi_styles_default = ansiStyles; + +// node_modules/wrap-ansi/index.js +var ANSI_ESCAPE = "\x1B"; +var ANSI_ESCAPE_BELL = "\x07"; +var C1_CSI = "\x9B"; +var ANSI_CSI = "["; +var ANSI_OSC = "]"; +var ANSI_SGR_TERMINATOR = "m"; +var ANSI_SGR_RESET = 0; +var ANSI_SGR_RESET_FOREGROUND = 39; +var ANSI_SGR_RESET_BACKGROUND = 49; +var ANSI_SGR_RESET_UNDERLINE_COLOR = 59; +var ANSI_SGR_FOREGROUND_EXTENDED = 38; +var ANSI_SGR_BACKGROUND_EXTENDED = 48; +var ANSI_SGR_UNDERLINE_COLOR_EXTENDED = 58; +var ANSI_SGR_COLOR_MODE_RGB = 2; +var ANSI_SGR_COLOR_MODE_256 = 5; +var ANSI_ESCAPE_LINK = `${ANSI_OSC}8;`; +var ESCAPES = /* @__PURE__ */ new Set([ + ANSI_ESCAPE, + C1_CSI +]); +var ESCAPE_CHARACTERS = [...ESCAPES].join(""); +var CSI_INTRODUCER = `(?:${ANSI_ESCAPE}\\${ANSI_CSI}|${C1_CSI})`; +var CSI_PARAMETERS = "[0-?]*[ -/]*[@-~]"; +var SGR_PARAMETERS = `(?[0-9;:]*)${ANSI_SGR_TERMINATOR}`; +var OSC_STRING_TERMINATOR = `(?:${ANSI_ESCAPE_BELL}|${ANSI_ESCAPE}\\\\)`; +var OSC_STRING_PAYLOAD = String.raw`[^\u0000-\u001F\u007F-\u009F]*`; +var LINK_PARAMETERS = String.raw`8;(?[^;\u0000-\u001F\u007F-\u009F]*);(?${OSC_STRING_PAYLOAD})${OSC_STRING_TERMINATOR}`; +var OSC_STRING = `${OSC_STRING_PAYLOAD}${OSC_STRING_TERMINATOR}`; +var ANSI_ESCAPE_REGEX = new RegExp( + `${CSI_INTRODUCER}(?:${SGR_PARAMETERS}|${CSI_PARAMETERS})|${ANSI_ESCAPE}\\${ANSI_OSC}(?:${LINK_PARAMETERS}|${OSC_STRING})`, + "y" +); +var ANSI_SGR_MODIFIER_CLOSE_CODES = new Set(ansi_styles_default.codes.values()); +ANSI_SGR_MODIFIER_CLOSE_CODES.delete(ANSI_SGR_RESET); +var segmenter2 = new Intl.Segmenter(); +var getStringWidth = (string) => stringWidth(string, { countAnsiEscapeCodes: true }); +var TAB_SIZE = 8; +var ESCAPE_INTRODUCER_REGEX = new RegExp(`[${ESCAPE_CHARACTERS}]`, "g"); +var ROW_BOUNDARY_REGEX = new RegExp(`[\\n${ESCAPE_CHARACTERS}]`, "g"); +var ASCII_PRINTABLE_REGEX = /^[ -~]*$/; +var wrapAnsiCode = (code) => `${ANSI_ESCAPE}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`; +var wrapAnsiHyperlink = (url, parameters = "") => `${ANSI_ESCAPE}${ANSI_ESCAPE_LINK}${parameters};${url}${ANSI_ESCAPE_BELL}`; +var matchAnsiEscape = (string, index) => { + if (!ESCAPES.has(string[index])) { + return; + } + ANSI_ESCAPE_REGEX.lastIndex = index; + return ANSI_ESCAPE_REGEX.exec(string) ?? void 0; +}; +var forEachSegment = (string, onPlainText, onEscape = () => { +}) => { + let plainStart = 0; + let index = 0; + while (index < string.length) { + ESCAPE_INTRODUCER_REGEX.lastIndex = index; + const introducer = ESCAPE_INTRODUCER_REGEX.exec(string); + if (!introducer) { + break; + } + const escape = matchAnsiEscape(string, introducer.index); + if (!escape) { + index = introducer.index + 1; + continue; + } + if (introducer.index > plainStart) { + onPlainText(string.slice(plainStart, introducer.index)); + } + onEscape(escape[0]); + index = introducer.index + escape[0].length; + plainStart = index; + } + if (plainStart < string.length) { + onPlainText(string.slice(plainStart)); + } +}; +var getWidth = (string) => { + let plainText = ""; + forEachSegment(string, (part) => { + plainText += part; + }); + return getStringWidth(plainText); +}; +var getTokens = (string) => { + const tokens = []; + forEachSegment(string, (plainText) => { + if (ASCII_PRINTABLE_REGEX.test(plainText)) { + for (const character of plainText) { + tokens.push({ value: character, width: 1 }); + } + return; + } + for (const { segment } of segmenter2.segment(plainText)) { + tokens.push({ value: segment, width: getStringWidth(segment) }); + } + }, (escape) => { + tokens.push({ value: escape, width: 0 }); + }); + return tokens; +}; +var splitWords = (string) => { + let currentWord = { value: "", plainText: "" }; + const words = [currentWord]; + forEachSegment(string, (plainText) => { + const parts = plainText.split(" "); + currentWord.value += parts[0]; + currentWord.plainText += parts[0]; + for (let index = 1; index < parts.length; index++) { + currentWord = { value: parts[index], plainText: parts[index] }; + words.push(currentWord); + } + }, (escape) => { + currentWord.value += escape; + }); + for (const word of words) { + word.width = getStringWidth(word.plainText); + } + return words; +}; +var getColonColorToken = (parameter) => { + const parts = parameter.split(":"); + const code = Number.parseInt(parts[0], 10); + const mode = Number.parseInt(parts[1], 10); + if (![ANSI_SGR_FOREGROUND_EXTENDED, ANSI_SGR_BACKGROUND_EXTENDED, ANSI_SGR_UNDERLINE_COLOR_EXTENDED].includes(code)) { + return; + } + if (mode === ANSI_SGR_COLOR_MODE_256 && parts.length === 3 && /^\d+$/.test(parts[2])) { + return { code, open: parameter, hasArguments: true }; + } + if (mode !== ANSI_SGR_COLOR_MODE_RGB) { + return; + } + const components = parts.length === 6 ? parts.slice(3) : parts.slice(2); + const colorSpace = parts.length === 6 ? parts[2] : void 0; + if (components.length === 3 && components.every((component) => /^\d+$/.test(component)) && (colorSpace === void 0 || /^\d*$/.test(colorSpace))) { + return { code, open: parameter, hasArguments: true }; + } +}; +var getSgrTokens = (sgrParameters) => { + const parameters = sgrParameters.split(";"); + const sgrTokens = []; + for (let index = 0; index < parameters.length; index++) { + const parameter = parameters[index]; + if (parameter.includes(":")) { + const colonColorToken = getColonColorToken(parameter); + if (colonColorToken) { + sgrTokens.push(colonColorToken); + } + continue; + } + const code = parameter === "" ? ANSI_SGR_RESET : Number.parseInt(parameter, 10); + if (!Number.isFinite(code)) { + continue; + } + if (code === ANSI_SGR_FOREGROUND_EXTENDED || code === ANSI_SGR_BACKGROUND_EXTENDED || code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED) { + if (index + 1 >= parameters.length) { + break; + } + const mode = Number.parseInt(parameters[index + 1], 10); + const colorIndex = Number.parseInt(parameters[index + 2], 10); + if (mode === ANSI_SGR_COLOR_MODE_256 && Number.isFinite(colorIndex)) { + sgrTokens.push({ code, open: [code, mode, colorIndex].join(";"), hasArguments: true }); + index += 2; + continue; + } + const red = Number.parseInt(parameters[index + 2], 10); + const green = Number.parseInt(parameters[index + 3], 10); + const blue = Number.parseInt(parameters[index + 4], 10); + if (mode === ANSI_SGR_COLOR_MODE_RGB && Number.isFinite(red) && Number.isFinite(green) && Number.isFinite(blue)) { + sgrTokens.push({ code, open: [code, mode, red, green, blue].join(";"), hasArguments: true }); + index += 4; + continue; + } + break; + } + sgrTokens.push({ code, open: String(code), hasArguments: false }); + } + return sgrTokens; +}; +var removeActiveStyle = (activeStyles, family) => { + const activeStyleIndex = activeStyles.findIndex((activeStyle) => activeStyle.family === family); + if (activeStyleIndex !== -1) { + activeStyles.splice(activeStyleIndex, 1); + } +}; +var upsertActiveStyle = (activeStyles, nextActiveStyle) => { + removeActiveStyle(activeStyles, nextActiveStyle.family); + activeStyles.push(nextActiveStyle); +}; +var removeModifierStylesByClose = (activeStyles, closeCode) => { + for (let index = activeStyles.length - 1; index >= 0; index--) { + const activeStyle = activeStyles[index]; + if (activeStyle.family.startsWith("modifier-") && activeStyle.close === closeCode) { + activeStyles.splice(index, 1); + } + } +}; +var getColorStyle = (sgrToken) => { + const { code, open, hasArguments } = sgrToken; + if (code >= 30 && code <= 37 || code >= 90 && code <= 97 || code === ANSI_SGR_FOREGROUND_EXTENDED && hasArguments) { + return { + family: "foreground", + open, + close: ANSI_SGR_RESET_FOREGROUND + }; + } + if (code >= 40 && code <= 47 || code >= 100 && code <= 107 || code === ANSI_SGR_BACKGROUND_EXTENDED && hasArguments) { + return { + family: "background", + open, + close: ANSI_SGR_RESET_BACKGROUND + }; + } + if (code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED && hasArguments) { + return { + family: "underlineColor", + open, + close: ANSI_SGR_RESET_UNDERLINE_COLOR + }; + } +}; +var applySgrResetCode = (code, activeStyles) => { + if (code === ANSI_SGR_RESET) { + activeStyles.length = 0; + return true; + } + if (code === ANSI_SGR_RESET_FOREGROUND) { + removeActiveStyle(activeStyles, "foreground"); + return true; + } + if (code === ANSI_SGR_RESET_BACKGROUND) { + removeActiveStyle(activeStyles, "background"); + return true; + } + if (code === ANSI_SGR_RESET_UNDERLINE_COLOR) { + removeActiveStyle(activeStyles, "underlineColor"); + return true; + } + if (ANSI_SGR_MODIFIER_CLOSE_CODES.has(code)) { + removeModifierStylesByClose(activeStyles, code); + return true; + } + return false; +}; +var applySgrToken = (sgrToken, activeStyles) => { + const { code } = sgrToken; + if (applySgrResetCode(code, activeStyles)) { + return; + } + const colorStyle = getColorStyle(sgrToken); + if (colorStyle) { + upsertActiveStyle(activeStyles, colorStyle); + return; + } + const close = ansi_styles_default.codes.get(code); + if (close !== void 0 && close !== ANSI_SGR_RESET) { + upsertActiveStyle(activeStyles, { + family: `modifier-${code}`, + open: sgrToken.open, + close + }); + } +}; +var applySgrParameters = (sgrParameters, activeStyles) => { + for (const sgrToken of getSgrTokens(sgrParameters)) { + applySgrToken(sgrToken, activeStyles); + } +}; +var applySgrResets = (sgrParameters, activeStyles) => { + for (const { code } of getSgrTokens(sgrParameters)) { + applySgrResetCode(code, activeStyles); + } +}; +var applyLeadingSgrResets = (string, startIndex, activeStyles) => { + let index = startIndex; + while (index < string.length) { + const match = matchAnsiEscape(string, index); + if (!match) { + break; + } + if (match.groups.sgr !== void 0) { + applySgrResets(match.groups.sgr, activeStyles); + } + index += match[0].length; + } +}; +var getClosingSgrSequence = (activeStyles) => [...activeStyles].reverse().map((activeStyle) => wrapAnsiCode(activeStyle.close)).join(""); +var getOpeningSgrSequence = (activeStyles) => activeStyles.map((activeStyle) => wrapAnsiCode(activeStyle.open)).join(""); +var wrapWord = (rows, word, columns, rowWidth) => { + const tokens = getTokens(word); + let visible = rowWidth; + for (let index = 0; index < tokens.length; index++) { + const token = tokens[index]; + if (token.width > 0 && visible > 0 && visible + token.width > columns) { + rows.push(""); + visible = 0; + } + rows[rows.length - 1] += token.value; + visible += token.width; + if (visible === columns && index < tokens.length - 1) { + rows.push(""); + visible = 0; + } + } + if (!visible && rows.at(-1).length > 0 && rows.length > 1) { + rows[rows.length - 2] += rows.pop(); + } + return getWidth(rows.at(-1)); +}; +var stringVisibleTrimSpacesRight = (string) => { + if (!string.includes(" ")) { + return string; + } + const segments = []; + forEachSegment(string, (plainText) => { + segments.push({ value: plainText, isEscape: false }); + }, (escape) => { + segments.push({ value: escape, isEscape: true }); + }); + for (let index = segments.length - 1; index >= 0; index--) { + const segment = segments[index]; + if (segment.isEscape) { + continue; + } + let end = segment.value.length; + while (end > 0 && segment.value[end - 1] === " ") { + end--; + } + segment.value = segment.value.slice(0, end); + if (getStringWidth(segment.value) > 0) { + break; + } + } + return segments.map((segment) => segment.value).join(""); +}; +var expandTabs = (line) => { + if (!line.includes(" ")) { + return line; + } + let visible = 0; + let expandedLine = ""; + let plainTextSinceTab = ""; + const expandPlainText = (plainText) => { + const segments = plainText.split(" "); + for (const [index, segment] of segments.entries()) { + expandedLine += segment; + plainTextSinceTab += segment; + if (index < segments.length - 1) { + visible += getStringWidth(plainTextSinceTab); + plainTextSinceTab = ""; + const spaces = TAB_SIZE - visible % TAB_SIZE; + expandedLine += " ".repeat(spaces); + visible += spaces; + } + } + }; + forEachSegment(line, expandPlainText, (escape) => { + expandedLine += escape; + }); + return expandedLine; +}; +var restoreStylesAcrossRows = (preString) => { + let returnValue = ""; + let activeHyperlink; + const activeStyles = []; + let index = 0; + let copiedIndex = 0; + while (index < preString.length) { + ROW_BOUNDARY_REGEX.lastIndex = index; + const boundary = ROW_BOUNDARY_REGEX.exec(preString); + if (!boundary) { + break; + } + index = boundary.index; + if (boundary[0] !== "\n") { + const escape = matchAnsiEscape(preString, index); + if (!escape) { + index++; + continue; + } + const { groups } = escape; + if (groups.sgr !== void 0) { + applySgrParameters(groups.sgr, activeStyles); + } else if (groups.uri !== void 0) { + activeHyperlink = groups.uri.length === 0 ? void 0 : { parameters: groups.parameters, uri: groups.uri }; + } + index += escape[0].length; + continue; + } + returnValue += preString.slice(copiedIndex, index); + if (index > copiedIndex) { + if (activeHyperlink) { + returnValue += wrapAnsiHyperlink(""); + } + returnValue += getClosingSgrSequence(activeStyles); + } + returnValue += "\n"; + index++; + copiedIndex = index; + if (index < preString.length && preString[index] !== "\n") { + const openingStyles = [...activeStyles]; + applyLeadingSgrResets(preString, index, openingStyles); + returnValue += getOpeningSgrSequence(openingStyles); + if (activeHyperlink) { + returnValue += wrapAnsiHyperlink(activeHyperlink.uri, activeHyperlink.parameters); + } + } + } + return returnValue + preString.slice(copiedIndex); +}; +var exec = (string, columns, options = {}) => { + if (options.trim !== false && string.trim() === "") { + return ""; + } + const words = splitWords(string); + let rows = [""]; + let rowLength = 0; + let trimmedRowIndex = -1; + let isFirstWord = true; + for (const word of words) { + const rowIndex = rows.length - 1; + if (options.trim !== false && trimmedRowIndex !== rowIndex) { + const row = rows[rowIndex]; + const trimmedRow = row.trimStart(); + if (trimmedRow.length !== row.length) { + rows[rowIndex] = trimmedRow; + rowLength = getWidth(trimmedRow); + } + if (trimmedRow.length > 0) { + trimmedRowIndex = rowIndex; + } + } + if (isFirstWord) { + isFirstWord = false; + } else { + if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) { + rows.push(""); + rowLength = 0; + } + if (rowLength > 0 || options.trim === false) { + rows[rows.length - 1] += " "; + rowLength++; + } + } + if (options.hard && options.wordWrap !== false && word.width > columns) { + const remainingColumns = columns - rowLength; + const breaksStartingThisLine = 1 + Math.floor((word.width - remainingColumns - 1) / columns); + const breaksStartingNextLine = Math.floor((word.width - 1) / columns); + if (breaksStartingNextLine < breaksStartingThisLine) { + rows.push(""); + rowLength = 0; + } + rowLength = wrapWord(rows, word.value, columns, rowLength); + continue; + } + if (rowLength + word.width > columns && rowLength > 0 && word.width > 0) { + if (options.wordWrap === false && rowLength < columns) { + rowLength = wrapWord(rows, word.value, columns, rowLength); + continue; + } + rows.push(""); + rowLength = 0; + } + if (rowLength + word.width > columns && options.wordWrap === false) { + rowLength = wrapWord(rows, word.value, columns, rowLength); + continue; + } + rows[rows.length - 1] += word.value; + rowLength += word.width; + } + if (options.trim !== false) { + rows = rows.map((row) => stringVisibleTrimSpacesRight(row)); + } + return restoreStylesAcrossRows(rows.join("\n")); +}; +var normalizeText = (string) => { + let normalizedString = ""; + forEachSegment(string, (plainText) => { + normalizedString += plainText.normalize(); + }, (escape) => { + normalizedString += escape; + }); + return normalizedString; +}; +function wrapAnsi(string, columns, options) { + return normalizeText(String(string)).replaceAll("\r\n", "\n").split("\n").map((line) => exec(expandTabs(line), columns, options)).join("\n"); +} + +// src/text.js +var sgr = /(\x1b\[[0-9;:]*m)/g; +function safeText(text, color = false) { + return text.split(sgr).map((value, index) => { + if (index % 2) return color ? value : ""; + return value.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1b[^\n]?/g, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, ""); + }).join(""); +} +function displayWidth(text) { + return stringWidth(text); +} +function wrapText(text, width, wrap = true) { + if (!wrap) return text.split("\n"); + return wrapAnsi(text, width, { hard: true, trim: false }).split("\n"); +} + +// src/random.js +function choose(names, seed, salt) { + if (seed === void 0) + return names[Math.floor(Math.random() * names.length)]; + const value = `${salt}:${typeof seed}:${seed}`; + let hash = 2166136261; + for (const char of value) { + hash ^= char.codePointAt(0); + hash = Math.imul(hash, 16777619) >>> 0; + } + return names[hash % names.length]; +} + +// src/moods.js +var intros = { + classic: "BRO...", + hype: "BRO! THE TERMINAL IS APPLAUDING!", + chill: "Bro. One thing at a time.", + panic: "BRO! DEEP BREATH. CHECK THE LOGS.", + corporate: "Bro, please find the following update for alignment.", + coach: "Bro, you have got this. Take the next step." +}; + +// src/legacy.js +function brosay(message, { mood = "classic" } = {}) { + return `${intros[mood]} + +${message.trim()}`; +} + +// src/render.js +var listCharacters = () => Object.keys(characters); +var moods = () => Object.keys(personas); +var listThemes = () => Object.keys(themes); +function name(value, catalog, fallback, kind) { + if (value === void 0 || value === null) return fallback; + if (typeof value !== "string") + throw new TypeError(`${kind} must be a string.`); + const key = value.trim().toLowerCase(); + if (!Object.hasOwn(catalog, key)) + throw new RangeError( + `Unknown ${kind}: ${value}. Choose: ${Object.keys(catalog).join(", ")}.` + ); + return key; +} +function characterData(value) { + if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.name !== "string" || !/^[a-z0-9-]{1,40}$/.test(value.name) || !Array.isArray(value.art) || value.art.length < 1 || value.art.length > 20 || !value.art.every( + (line) => typeof line === "string" && line.length <= 64 && /^[\x20-\x7e]*$/.test(line) + )) + throw new TypeError( + "Custom character needs a slug name and 1\u201320 printable ASCII lines of at most 64 characters." + ); + return { name: value.name, art: [...value.art] }; +} +function renderBro(options) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Provide a render options object."); + const { text } = options; + if (typeof text !== "string" || !text.trim()) + throw new TypeError("Message must be a nonempty string."); + if (text.length > 262144) + throw new RangeError("Message exceeds 262144 UTF-16 code units."); + for (const key of ["random", "color", "wrap", "think", "box"]) + if (options[key] !== void 0 && typeof options[key] !== "boolean") + throw new TypeError(`${key} must be a boolean.`); + if (options.seed !== void 0 && typeof options.seed !== "string" && !(typeof options.seed === "number" && Number.isFinite(options.seed))) + throw new TypeError("seed must be a string or finite number."); + if (options.seed !== void 0 && !options.random) + throw new TypeError("seed requires random: true."); + const width = options.width ?? 48; + if (!Number.isSafeInteger(width) || width < 8 || width > 200) + throw new RangeError( + "width must be an integer from 8 to 200 (content columns)." + ); + const mode = name( + options.mode, + { say: 1, think: 1 }, + options.think ? "think" : "say", + "mode" + ); + const theme = name( + options.theme, + themes, + options.random ? choose(listThemes(), options.seed, "theme") : "classic", + "theme" + ); + const mood = name( + options.mood, + personas, + options.random ? choose(moods(), options.seed, "mood") : "classic", + "mood" + ); + const custom = typeof options.character === "object" && options.character !== null; + const character = custom ? characterData(options.character) : characters[name( + options.character, + characters, + options.random ? choose(listCharacters(), options.seed, "character") : "husky", + "character" + )]; + const layout = name( + options.layout, + { bubble: 1, plain: 1 }, + "bubble", + "layout" + ); + const message = safeText(text, options.color ?? false).trim(); + if (!message) throw new TypeError("Message contains no printable content."); + const persona = personas[mood]; + let rendered; + if (layout === "plain" || options.box) { + const legacy = brosay(message, { + mood: Object.hasOwn(legacyIntros, mood) ? mood : "classic", + box: false + }); + const lines = legacy.split("\n"); + if (options.box && (lines.length > 100 || Math.max(...lines.map(displayWidth)) > 200)) + throw new RangeError("Legacy box exceeds 200 columns or 100 lines."); + rendered = options.box ? bubble(lines, themes.classic, false) : legacy; + } else { + const contents = [ + persona.intro, + ...persona.label ? ["", persona.label] : [], + "", + message, + ...persona.outro ? ["", persona.outro] : [] + ].join("\n"); + const lines = wrapText(contents, width, options.wrap ?? true); + if (lines.length > 4096) + throw new RangeError( + "Rendered output exceeds 4096 lines. Increase width or shorten input." + ); + const connector = mode === "think" ? " o\n o" : " \\\n \\"; + rendered = bubble(lines, themes[theme], options.color ?? false) + "\n" + connector + "\n" + character.art.map((line) => " " + line).join("\n"); + } + return { + text: message, + character: character.name, + mood, + theme, + mode, + width, + rendered + }; +} +var legacyIntros = { + classic: 1, + hype: 1, + chill: 1, + panic: 1, + corporate: 1, + coach: 1 +}; +function bubble(lines, theme, color) { + const columns = Math.max(...lines.map(displayWidth)); + const border = (parts) => parts[0] + parts[1].repeat(columns + 2) + parts[2]; + const body = lines.map( + (line) => `${theme.side} ${line}${color ? "\x1B[0m" : ""}${" ".repeat(columns - displayWidth(line))} ${theme.side}` + ); + const result = [border(theme.top), ...body, border(theme.bottom)].join("\n"); + return color ? `\x1B[${theme.color}m${result}\x1B[0m` : result; +} +function brosay2(text, options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + return renderBro({ ...options, text }).rendered; +} +function brothink(text, options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + return renderBro({ ...options, text, mode: "think" }).rendered; +} + +// src/messages.js +var presets = { + deployment: [ + "The release has landed. Please keep your monitoring inside the vehicle.", + "Shipped with confidence. Verified with health checks.", + "Today we deploy code and a reasonable rollback plan.", + "The canary is singing. Let us keep listening.", + "A small release for the app. A large relief for the team.", + "The deployment passed. The dashboard gets the final word." + ], + testing: [ + "Red, green, refactor. Snacks are an optional fourth step.", + "The test failed usefully. That is a good beginning.", + "A passing test is a receipt, not a prophecy.", + "Make the edge case boring enough to test.", + "One regression test today saves a mystery tomorrow.", + "The suite is green. Check what it actually covered." + ], + debugging: [ + "Read the first error before collecting the entire stack trace.", + "The rubber duck requests a smaller reproduction.", + "Follow the value. It knows where it has been.", + "A breakpoint is cheaper than a theory committee.", + "The bug cannot hide from a clear input and a patient developer.", + "One hypothesis. One experiment. One less guess." + ], + review: [ + "Review the change, not the person who wrote it.", + "A kind question can save a very loud incident.", + "The clever line would appreciate a simple explanation.", + "If future-you needs a map, leave a comment today.", + "LGTM comes after understanding the diff.", + "Small commits make excellent conversation starters." + ], + coffee: [ + "The code is brewing. So is the coffee.", + "A refill is not a retry policy, but it can improve the meeting.", + "This mug contains the unofficial build coordinator.", + "The coffee is hot. The take can wait.", + "Hydrate between compilations. Your compiler does not need another cup.", + "A short break is a legitimate debugging technique." + ], + celebration: [ + "The fix worked. Give the test its share of the applause.", + "Celebrate the boring release. It took good work to make it boring.", + "One fewer alert. One more reason to take a walk.", + "The team shipped a useful thing. That deserves a victory lap.", + "Success has receipts and a very happy tail.", + "Treat budget approved after verification." + ], + husky: [ + "Dallas brings the zoomies. Benji brings the next idea.", + "Two huskies, one keyboard, absolutely no quiet roadmap.", + "Commit small. Run fast. Leave the socks out of production.", + "Dallas heard deploy. Benji heard play. Both approve a smoke test.", + "High energy is a feature. Good boundaries keep it useful.", + "Our huskies believe every adventure deserves enthusiasm and a return route." + ], + focus: [ + "Pick the next useful step. Finish that one.", + "Reduce the scope until the problem fits in your afternoon.", + "Your terminal can have personality. Your interfaces should have clarity.", + "Small progress counts even without a dramatic soundtrack.", + "A helpful tool should make the next action easier to see.", + "Close one loop before opening six more tabs." + ] +}; +var messageCategories = () => Object.keys(presets); +function messagePresets(category) { + if (typeof category !== "string" || !Object.hasOwn(presets, category.trim().toLowerCase())) + throw new RangeError( + "Choose a supported message category: " + messageCategories().join(", ") + ); + return [...presets[category.trim().toLowerCase()]]; +} +function broMessage(category = "deployment", options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + if (options.seed !== void 0 && typeof options.seed !== "string" && !(typeof options.seed === "number" && Number.isFinite(options.seed))) + throw new TypeError("seed must be a string or finite number."); + return choose( + messagePresets(category), + options.seed, + "message:" + category.trim().toLowerCase() + ); +} + +// src/build-summary.js +var clean = (value) => Array.from(value).filter( + (char) => char.charCodeAt(0) >= 32 || char === "\n" || char === " " +).join(""); +var count = (value, label) => { + if (!Number.isSafeInteger(value) || value < 0) + throw new TypeError(`${label} must be a nonnegative safe integer.`); + return value; +}; +function summarizeBuild(input = {}) { + if (!input || typeof input !== "object" || Array.isArray(input)) + throw new TypeError("Build input must be an object."); + const result = { label: "Build", status: "unknown" }; + for (const key of ["label", "commit"]) + if (input[key] !== void 0) { + if (typeof input[key] !== "string" || !input[key].trim() || input[key].length > 200) + throw new TypeError( + `${key} must be a nonempty string up to 200 characters.` + ); + result[key] = clean(input[key].trim()); + } + if (input.exitCode !== void 0) { + result.exitCode = input.exitCode === null ? null : count(input.exitCode, "exitCode"); + if (input.exitCode !== null) + result.status = input.exitCode === 0 ? "passed" : "failed"; + } + if (input.durationMs !== void 0) { + if (typeof input.durationMs !== "number" || !Number.isFinite(input.durationMs) || input.durationMs < 0) + throw new TypeError("durationMs must be a nonnegative finite number."); + result.durationMs = input.durationMs; + } + if (input.tests !== void 0) { + if (!input.tests || typeof input.tests !== "object" || Array.isArray(input.tests)) + throw new TypeError("tests must contain measured counts."); + result.tests = { + passed: count(input.tests.passed, "tests.passed"), + failed: count(input.tests.failed, "tests.failed"), + skipped: count(input.tests.skipped ?? 0, "tests.skipped") + }; + if (result.tests.failed > 0) result.status = "failed"; + } + const failures = input.failures ?? []; + if (!Array.isArray(failures) || failures.length > 20 || !failures.every( + (item) => typeof item === "string" && item.trim() && item.length <= 2e3 + )) + throw new TypeError( + "failures must contain up to 20 nonempty strings of at most 2000 characters." + ); + result.failures = failures.map(clean); + if (failures.length) result.status = "failed"; + result.text = [ + `${result.label}: ${result.status.toUpperCase()}`, + `Build exit: ${result.exitCode === void 0 || result.exitCode === null ? "not provided" : result.exitCode}`, + result.tests ? `Tests: ${result.tests.passed} passed, ${result.tests.failed} failed, ${result.tests.skipped} skipped` : "Tests: not provided", + result.durationMs !== void 0 ? `Duration: ${result.durationMs} ms` : "Duration: not provided", + result.commit ? `Commit: ${result.commit}` : "Commit: not provided", + ...result.failures.length ? ["Failures:", ...result.failures.map((item) => "- " + item)] : [] + ].join("\n"); + return result; +} +function renderBuildSummary(input = {}, options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + const { format = "character", ...broOptions } = options; + if (!["character", "plain"].includes(format)) + throw new RangeError("Summary format must be character or plain."); + const summary = summarizeBuild(input); + const rendered = format === "plain" ? summary.text : renderBro({ + ...broOptions, + text: summary.text, + mood: broOptions.mood ?? (summary.status === "passed" ? "benji" : summary.status === "failed" ? "on-call" : "skeptic") + }).rendered; + return { status: summary.status, summary, rendered }; +} +export { + broMessage, + brosay2 as brosay, + brothink, + displayWidth, + listCharacters, + listThemes, + messageCategories, + messagePresets, + moods, + renderBro, + renderBuildSummary, + summarizeBuild +}; diff --git a/index.html b/index.html index 876ae47..e0d0bb8 100644 --- a/index.html +++ b/index.html @@ -12,13 +12,13 @@ - + - - - + + +
diff --git a/siblings-1.0.0.js b/siblings-1.0.0.js index 04d3b3f..a576d98 100644 --- a/siblings-1.0.0.js +++ b/siblings-1.0.0.js @@ -4,15 +4,15 @@ import { translateError, translateErrors, renderTranslation, -} from "./vendor/error-translator/index.js"; +} from "./vendor/v1/error-translator/index.js"; import { listExcuses, categories, excuse, excuseBatch, excuseReport, -} from "./vendor/excuse-js/index.js"; -import { barkLines, createDogLogger } from "./vendor/doggo-log/index.js"; +} from "./vendor/v1/excuse-js/index.js"; +import { barkLines, createDogLogger } from "./vendor/v1/doggo-log/index.js"; import { releasePlan, releaseScenarios, @@ -20,7 +20,7 @@ import { releaseGate, preflight, shipIt, -} from "./vendor/ship-it-meter/index.js"; +} from "./vendor/v1/ship-it-meter/index.js"; const el = (id) => document.getElementById(id); const q = (value) => "'" + String(value).replaceAll("'", "'\\''") + "'"; const select = (id, label, values, selected) => { diff --git a/test/value.test.js b/test/value.test.js index 515e69d..752e893 100644 --- a/test/value.test.js +++ b/test/value.test.js @@ -1,11 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; -import { renderBuildSummary } from "../dist/browser.js"; -import { diagnoseError } from "../vendor/error-translator/index.js"; -import { incidentUpdate } from "../vendor/excuse-js/index.js"; -import { createDogLogger } from "../vendor/doggo-log/index.js"; -import { evaluateReports } from "../vendor/ship-it-meter/index.js"; +import { renderBuildSummary } from "../dist/browser-1.0.0.js"; +import { diagnoseError } from "../vendor/v1/error-translator/index.js"; +import { incidentUpdate } from "../vendor/v1/excuse-js/index.js"; +import { createDogLogger } from "../vendor/v1/doggo-log/index.js"; +import { evaluateReports } from "../vendor/v1/ship-it-meter/index.js"; test("v1 demo assets preserve failure, causes, missing facts, redaction and report blockers", () => { assert.equal(renderBuildSummary({ exitCode: 1 }).status, "failed"); assert.equal(renderBuildSummary({}).status, "unknown"); diff --git a/value-1.0.0.js b/value-1.0.0.js index afd182f..297e85e 100644 --- a/value-1.0.0.js +++ b/value-1.0.0.js @@ -3,14 +3,14 @@ import { listCharacters, moods, listThemes, -} from "./dist/browser.js"; +} from "./dist/browser-1.0.0.js"; import { diagnoseError, renderDiagnosis, -} from "./vendor/error-translator/index.js"; -import { incidentUpdate } from "./vendor/excuse-js/index.js"; -import { createDogLogger } from "./vendor/doggo-log/index.js"; -import { evaluateReports } from "./vendor/ship-it-meter/index.js"; +} from "./vendor/v1/error-translator/index.js"; +import { incidentUpdate } from "./vendor/v1/excuse-js/index.js"; +import { createDogLogger } from "./vendor/v1/doggo-log/index.js"; +import { evaluateReports } from "./vendor/v1/ship-it-meter/index.js"; const el = (id) => document.getElementById(id), quote = (s) => "'" + String(s).replaceAll("'", "'\\''") + "'"; const control = (id, label, value, choices) => { diff --git a/vendor/v1/doggo-log/LICENSE b/vendor/v1/doggo-log/LICENSE new file mode 100644 index 0000000..9fe7218 --- /dev/null +++ b/vendor/v1/doggo-log/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Deploy Anyway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/v1/doggo-log/commentary.js b/vendor/v1/doggo-log/commentary.js new file mode 100644 index 0000000..14e8c1b --- /dev/null +++ b/vendor/v1/doggo-log/commentary.js @@ -0,0 +1,77 @@ +// Original commentary; never replaces the caller's message. +const lines = { + debug: [ + "Sniffing for clues.", + "Follow the trace, not the squirrel.", + "One breakpoint. Two ears. Maximum attention.", + "The missing value has left a scent trail.", + "Logging the evidence before chasing the theory.", + "The stack trace is our walking route.", + "Small reproduction, big detective energy.", + "Dallas found a clue. Benji would like to inspect the keyboard.", + ], + log: [ + "Filed under things I sniffed.", + "Fetch complete. Filing the interesting bit.", + "Another breadcrumb for future-us.", + "This line has been approved by the tail department.", + "A small update with excellent ears.", + "Evidence delivered without chewing it.", + "Keeping a trail through the code forest.", + "Benji brought the log. Dallas brought enthusiasm.", + ], + info: [ + "Good to know. Good dog to tell you.", + "A useful update, delivered at husky speed.", + "The facts have arrived wearing sensible paws.", + "Worth knowing before the next zoomie.", + "No alarm. Just a well-timed nose boop.", + "Status fetched. Tail at a responsible speed.", + "A little context saves a lot of barking.", + "Dallas and Benji have entered the observability business.", + ], + success: [ + "Treat budget approved.", + "Good result. Better evidence. Best dog.", + "The check passed. Save some applause for monitoring.", + "One fewer problem between us and the walk.", + "Tail deployment successful.", + "The happy path has receipts today.", + "Achievement unlocked: boring, repeatable success.", + "Dallas celebrates. Benji is already planning the victory lap.", + ], + warn: [ + "Suspicious squirrel detected.", + "Ears up. This deserves a closer look.", + "Something smells odd; inspect before retrying.", + "A warning is a breadcrumb, not a dare.", + "The tail slowed down for a reason.", + "Check the evidence before this becomes an incident.", + "Potential trouble has arrived with muddy paws.", + "Benji heard something. Dallas recommends checking the logs.", + ], + error: [ + "The dog has fetched the incident report.", + "Read the first failure before chasing the pack.", + "The red light is evidence, not a personality review.", + "Pause the zoomies. Find the cause and the rollback.", + "This needs a fix, not louder barking.", + "Capture the reproduction while the scent is fresh.", + "The operation failed. The team still gets kindness.", + "Dallas and Benji are standing by with emotional support.", + ], +}; +export function barkLines(level) { + if (typeof level !== "string" || !Object.hasOwn(lines, level)) + throw new RangeError("Choose debug, log, info, success, warn or error."); + return [...lines[level]]; +} +export function commentaryIndex(seed, salt, length) { + if (seed === undefined) return 0; + let hash = 2166136261; + for (const char of salt + ":" + typeof seed + ":" + seed) { + hash ^= char.codePointAt(0); + hash = Math.imul(hash, 16777619) >>> 0; + } + return hash % length; +} diff --git a/vendor/v1/doggo-log/index.js b/vendor/v1/doggo-log/index.js new file mode 100644 index 0000000..ea76407 --- /dev/null +++ b/vendor/v1/doggo-log/index.js @@ -0,0 +1,165 @@ +import { format } from "node:util"; +import { levels } from "./levels.js"; +import { barkLines, commentaryIndex } from "./commentary.js"; +import { copyContext, createRedactor } from "./redaction.js"; +export { barkLines }; + +/** + * Create a small logger. Methods return the emitted line, or undefined if filtered. + * @param {{emoji?: boolean, color?: boolean, timestamp?: boolean, json?: boolean, quiet?: boolean, prefix?: string, level?: string, write?: (line: string, level: string) => void, clock?: () => Date}} [options] + */ +export function createDogLogger(options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + const config = { + emoji: true, + bark: false, + barkMode: "classic", + seed: undefined, + color: false, + timestamp: false, + json: false, + quiet: false, + prefix: "", + level: "info", + context: {}, + contextProvider: () => ({}), + write: (line, level) => + level === "warn" || level === "error" + ? console.error(line) + : console.log(line), + clock: () => new Date(), + ...options, + }; + for (const key of ["emoji", "color", "timestamp", "json", "quiet", "bark"]) { + if (typeof config[key] !== "boolean") + throw new TypeError(`${key} must be a boolean.`); + } + if (typeof config.prefix !== "string") + throw new TypeError("prefix must be a string."); + if ( + !config.context || + typeof config.context !== "object" || + Array.isArray(config.context) + ) + throw new TypeError("context must be an object of scalar fields."); + for (const value of Object.values(config.context)) + if (!( + value === null || + typeof value === "string" || + typeof value === "boolean" || + (typeof value === "number" && Number.isFinite(value)) + )) + throw new TypeError( + "Context values must be strings, finite numbers, booleans or null.", + ); + config.context = copyContext(config.context); + if (typeof config.contextProvider !== "function") + throw new TypeError("contextProvider must be a function."); + const redactor = createRedactor(config.redact); + if (config.redact !== false) + config.redact = { + ...config.redact, + ...(config.redact?.keys ? { keys: [...config.redact.keys] } : {}), + ...(config.redact?.values ? { values: [...config.redact.values] } : {}), + }; + if (typeof config.level !== "string" || !Object.hasOwn(levels, config.level)) + throw new RangeError( + `level must be one of: ${Object.keys(levels).join(", ")}.`, + ); + if (typeof config.write !== "function" || typeof config.clock !== "function") + throw new TypeError("write and clock must be functions."); + if (!["classic", "rotate"].includes(config.barkMode)) + throw new RangeError("barkMode must be classic or rotate."); + if ( + config.seed !== undefined && + typeof config.seed !== "string" && + !(typeof config.seed === "number" && Number.isFinite(config.seed)) + ) + throw new TypeError("seed must be a string or finite number."); + const counters = {}; + const logger = Object.fromEntries( + Object.entries(levels).map(([level, style]) => [ + level, + (...args) => { + if (config.quiet || style.rank < levels[config.level].rank) + return undefined; + const message = redactor.text(format(...args)); + const context = redactor.context({ + ...config.context, + ...copyContext(config.contextProvider()), + }); + const pool = config.bark ? barkLines(level) : undefined; + const index = + config.barkMode === "rotate" + ? (commentaryIndex( + config.seed, + config.prefix + ":" + level, + pool?.length ?? 1, + ) + + (counters[level] ?? 0)) % + (pool?.length ?? 1) + : 0; + const commentary = pool?.[index]; + let timestamp; + if (config.timestamp) { + const date = config.clock(); + if (!(date instanceof Date) || !Number.isFinite(date.getTime())) + throw new TypeError("clock must return a valid Date."); + timestamp = date.toISOString(); + } + let line; + if (config.json) { + line = JSON.stringify({ + level, + message, + ...(Object.keys(context).length ? { context } : {}), + ...(commentary ? { commentary } : {}), + ...(config.prefix ? { prefix: redactor.text(config.prefix) } : {}), + ...(timestamp ? { timestamp } : {}), + }); + } else { + line = [ + timestamp, + redactor.text(config.prefix), + config.emoji ? style.emoji : undefined, + level.toUpperCase().padEnd(7), + message, + Object.keys(context).length ? JSON.stringify(context) : undefined, + commentary, + ] + .filter((part) => part !== undefined && part !== "") + .join(" "); + if (config.color) line = `\u001b[${style.color}m${line}\u001b[0m`; + } + config.write(line, level); + if (config.bark) + counters[level] = ((counters[level] ?? 0) + 1) % pool.length; + return line; + }, + ]), + ); + logger.child = (prefix, context = {}) => { + if (!context || typeof context !== "object" || Array.isArray(context)) + throw new TypeError("context must be an object."); + if (typeof prefix !== "string" || !prefix.trim()) + throw new TypeError("Child prefix must be a nonempty string."); + return createDogLogger({ + ...config, + context: { ...config.context, ...context }, + prefix: [config.prefix, prefix.trim()].filter(Boolean).join(":"), + }); + }; + logger.withContext = (context) => { + if (!context || typeof context !== "object" || Array.isArray(context)) + throw new TypeError("context must be an object."); + return createDogLogger({ + ...config, + context: { ...config.context, ...context }, + }); + }; + return logger; +} + +/** Default logger: emoji enabled, no timestamp/color, info threshold. */ +export const doglog = createDogLogger(); diff --git a/vendor/v1/doggo-log/levels.js b/vendor/v1/doggo-log/levels.js new file mode 100644 index 0000000..3419425 --- /dev/null +++ b/vendor/v1/doggo-log/levels.js @@ -0,0 +1,8 @@ +export const levels = { + debug: { rank: 10, emoji: "🔍", color: 90 }, + log: { rank: 20, emoji: "🐶", color: 37 }, + info: { rank: 20, emoji: "🐶", color: 36 }, + success: { rank: 20, emoji: "🐾", color: 32 }, + warn: { rank: 30, emoji: "🦴", color: 33 }, + error: { rank: 40, emoji: "🚨", color: 31 }, +}; diff --git a/vendor/v1/doggo-log/redaction.js b/vendor/v1/doggo-log/redaction.js new file mode 100644 index 0000000..104d801 --- /dev/null +++ b/vendor/v1/doggo-log/redaction.js @@ -0,0 +1,73 @@ +const defaultKeys = [ + "password", + "passwd", + "token", + "accessToken", + "refreshToken", + "authorization", + "cookie", + "secret", + "apiKey", +]; +const normalize = (key) => key.toLowerCase().replaceAll(/[-_]/g, ""); +export function copyContext(context) { + if (!context || typeof context !== "object" || Array.isArray(context)) + throw new TypeError("context must be an object of scalar fields."); + for (const value of Object.values(context)) + if (!( + value === null || + typeof value === "string" || + typeof value === "boolean" || + (typeof value === "number" && Number.isFinite(value)) + )) + throw new TypeError( + "Context values must be strings, finite numbers, booleans or null.", + ); + return { ...context }; +} +export function createRedactor(options) { + if (options === false) + return { text: (value) => value, context: copyContext }; + if (options === undefined) options = {}; + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("redact must be false or an options object."); + const keys = options.keys ?? defaultKeys, + values = options.values ?? [], + replacement = options.replacement ?? "[REDACTED]"; + for (const [name, list] of [ + ["keys", keys], + ["values", values], + ]) + if ( + !Array.isArray(list) || + list.length > 100 || + !list.every((value) => typeof value === "string" && value.length > 0) + ) + throw new TypeError( + `Redaction ${name} must contain at most 100 nonempty strings.`, + ); + if (typeof replacement !== "string") + throw new TypeError("Redaction replacement must be a string."); + const protectedKeys = new Set(keys.map(normalize)); + // Longest first prevents a short secret from exposing a suffix of a longer one. + const literals = [...new Set(values)].sort((a, b) => b.length - a.length); + const text = (value) => + literals.reduce( + (output, secret) => output.split(secret).join(replacement), + value, + ); + return { + text, + context: (context) => + Object.fromEntries( + Object.entries(copyContext(context)).map(([key, value]) => [ + key, + protectedKeys.has(normalize(key)) + ? replacement + : typeof value === "string" + ? text(value) + : value, + ]), + ), + }; +} diff --git a/vendor/v1/error-translator/LICENSE b/vendor/v1/error-translator/LICENSE new file mode 100644 index 0000000..9fe7218 --- /dev/null +++ b/vendor/v1/error-translator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Deploy Anyway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/v1/error-translator/definitions.js b/vendor/v1/error-translator/definitions.js new file mode 100644 index 0000000..ebad2b3 --- /dev/null +++ b/vendor/v1/error-translator/definitions.js @@ -0,0 +1,85 @@ +import { extraDefinitions } from "./extra-definitions.js"; +// Keep technical guidance here so contributors can add errors without changing logic. +export const definitions = { + ...extraDefinitions, + ECONNREFUSED: { + title: "Connection refused", + explanation: + "The application tried to connect to a service, but nothing accepted the connection.", + likelyCauses: [ + "The service is not running", + "The host or port is incorrect", + "A firewall actively rejected the connection", + ], + suggestions: [ + "Confirm that the service is running and listening on the configured host and port.", + "Check connection settings and firewall rules.", + ], + }, + ENOENT: { + title: "File or directory not found", + explanation: + "The application tried to use a filesystem path that does not exist.", + likelyCauses: [ + "The path is misspelled", + "The file was moved or deleted", + "A relative path is being resolved from an unexpected working directory", + ], + suggestions: [ + "Check the full path and the current working directory.", + "Create the required file or directory if it should exist.", + ], + }, + EADDRINUSE: { + title: "Address already in use", + explanation: + "The application tried to listen on an address and port that another listener already uses.", + likelyCauses: [ + "Another instance of the application is running", + "Another service uses the same port", + ], + suggestions: [ + "Identify the process using the port before stopping it.", + "Choose a different available port or stop the duplicate instance.", + ], + }, + MODULE_NOT_FOUND: { + title: "Module not found", + explanation: + "Node.js could not resolve a module requested by the application.", + likelyCauses: [ + "A dependency is missing", + "The module path is incorrect", + "A dependency references a missing module", + ], + suggestions: [ + "Check the missing module name and the require stack.", + "Install declared dependencies with npm install and verify local paths.", + ], + }, + ERR_MODULE_NOT_FOUND: { + title: "ES module not found", + explanation: "Node.js could not resolve an imported ES module.", + likelyCauses: [ + "A dependency is missing", + "The import path or file extension is incorrect", + ], + suggestions: [ + "Install declared dependencies and verify the import path.", + "Include the file extension for relative Node.js ES module imports.", + ], + }, + TypeError: { + title: "Unexpected value type", + explanation: + "An operation received a value it cannot use, such as calling a non-function or reading a property of undefined.", + likelyCauses: [ + "A value is null or undefined", + "A value has a different type than expected", + ], + suggestions: [ + "Inspect the value at the first relevant line in the stack trace.", + "Check inputs and return values before accessing properties or calling functions.", + ], + }, +}; diff --git a/vendor/v1/error-translator/diagnostics.js b/vendor/v1/error-translator/diagnostics.js new file mode 100644 index 0000000..2e38dac --- /dev/null +++ b/vendor/v1/error-translator/diagnostics.js @@ -0,0 +1,79 @@ +import { translateError, renderTranslation } from "./index.js"; + +/** Snapshot a bounded cause chain; preserve the original without serializing it. */ +export function diagnoseError(error, options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + const maxDepth = options.maxDepth ?? 8, + includeStack = options.includeStack ?? false; + if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 32) + throw new RangeError("maxDepth must be an integer from 1 to 32."); + if (typeof includeStack !== "boolean") + throw new TypeError("includeStack must be a boolean."); + // Validate the root using the established translation contract. + translateError(error, options); + const seen = new Set(), + chain = []; + let current = error, + stopped = "complete"; + while (current !== undefined) { + if (chain.length === maxDepth) { + stopped = "depth-limit"; + break; + } + if (current !== null && typeof current === "object") { + if (seen.has(current)) { + stopped = "cycle"; + break; + } + seen.add(current); + } + const object = + current !== null && typeof current === "object" && !Array.isArray(current) + ? current + : undefined; + const raw = typeof current === "string" ? current : object?.message; + const message = typeof raw === "string" ? raw : String(current); + const name = typeof object?.name === "string" ? object.name : "Error"; + const code = typeof object?.code === "string" ? object.code : undefined; + const translation = translateError( + { name, message, ...(code ? { code } : {}) }, + options, + ); + chain.push({ + depth: chain.length, + name, + message, + ...(code ? { code } : {}), + translation, + ...(includeStack && typeof object?.stack === "string" + ? { stack: object.stack } + : {}), + }); + current = object?.cause; + } + const result = { chain, stopped, truncated: stopped !== "complete" }; + Object.defineProperty(result, "original", { + value: error, + enumerable: false, + }); + return result; +} +export function renderDiagnosis(result) { + if ( + !result || + !Array.isArray(result.chain) || + !result.chain.length || + !["complete", "cycle", "depth-limit"].includes(result.stopped) + ) + throw new TypeError("Provide a diagnosis result."); + return ( + result.chain + .map( + (item, index) => + `${index === 0 ? "Failure" : "Caused by"}: ${item.name}${item.code ? " [" + item.code + "]" : ""}: ${item.message}\n${renderTranslation(item.translation)}${item.stack ? "\n\nOriginal stack:\n" + item.stack : ""}`, + ) + .join("\n\n---\n\n") + + (result.truncated ? `\n\nCause traversal stopped: ${result.stopped}.` : "") + ); +} diff --git a/vendor/v1/error-translator/extra-definitions.js b/vendor/v1/error-translator/extra-definitions.js new file mode 100644 index 0000000..92decce --- /dev/null +++ b/vendor/v1/error-translator/extra-definitions.js @@ -0,0 +1,494 @@ +// Original explanations and practical checks; no automatic remediation. +export const extraDefinitions = { + EACCES: { + title: "Permission denied", + explanation: "The process lacks permission for the requested operation.", + likelyCauses: [ + "A directory or file belongs to another account", + "The application runs under a restricted service identity", + ], + suggestions: [ + "Check the service identity and permissions on each parent directory.", + "Grant only the access the application needs; avoid running everything as administrator.", + ], + }, + EPERM: { + title: "Operation not permitted", + explanation: + "The operating system rejected an operation even though the target may exist.", + likelyCauses: [ + "A protected resource or locked file", + "An operation forbidden by the current platform or identity", + ], + suggestions: [ + "Inspect the operation, ownership and platform restrictions.", + "Close the process holding the resource and retry only if the operation is intended.", + ], + }, + EEXIST: { + title: "Target already exists", + explanation: + "A creation operation requires a new target, but that target already exists.", + likelyCauses: ["A repeated initialization step", "A filename collision"], + suggestions: [ + "Check the target before creating it and make initialization idempotent.", + "Use a distinct name rather than deleting an existing target blindly.", + ], + }, + ENOTDIR: { + title: "Expected a directory", + explanation: + "A path component used as a directory is actually another kind of file.", + likelyCauses: [ + "A file appears in the middle of the path", + "A configuration value points to the wrong target", + ], + suggestions: [ + "Inspect each path component and its type.", + "Correct the directory configuration before retrying.", + ], + }, + EISDIR: { + title: "Expected a file", + explanation: "The operation received a directory where it expected a file.", + likelyCauses: [ + "The filename is missing from a configured path", + "A directory was passed to a file operation", + ], + suggestions: [ + "Check the complete path and inspect the target type.", + "Use the appropriate directory API when working with directories.", + ], + }, + ENOTEMPTY: { + title: "Directory is not empty", + explanation: + "A directory operation requires an empty target, but entries remain.", + likelyCauses: [ + "Generated files remain in the directory", + "Another process is still writing files", + ], + suggestions: [ + "List the remaining entries and identify their owner.", + "Remove only confirmed disposable content or choose a new directory.", + ], + }, + EMFILE: { + title: "Too many open files", + explanation: "The process has exhausted its available file descriptors.", + likelyCauses: [ + "Handles are not closed", + "Too many files or sockets are opened concurrently", + ], + suggestions: [ + "Close handles in cleanup paths and bound concurrent work.", + "Inspect descriptor usage before considering a higher process limit.", + ], + }, + ENFILE: { + title: "System file table exhausted", + explanation: "The system cannot allocate another file handle.", + likelyCauses: [ + "Many processes hold open files", + "The host has reached a system resource limit", + ], + suggestions: [ + "Inspect host-wide handle usage and identify runaway processes.", + "Reduce concurrency and resolve leaks before changing system limits.", + ], + }, + ENOSPC: { + title: "No space left", + explanation: + "The filesystem cannot allocate the space or resources required by the operation.", + likelyCauses: [ + "The volume is full", + "Filesystem quotas or resource limits are exhausted", + ], + suggestions: [ + "Check free space, quotas and the volume receiving the write.", + "Clear verified disposable data and investigate rapid growth.", + ], + }, + EROFS: { + title: "Read-only filesystem", + explanation: "The target filesystem does not permit writes.", + likelyCauses: [ + "A read-only container mount", + "A protected or recovery-mounted volume", + ], + suggestions: [ + "Inspect mount settings and choose an intended writable data location.", + "Change mount configuration only when the write is part of the deployment design.", + ], + }, + EBUSY: { + title: "Resource busy", + explanation: + "A resource cannot complete the requested operation while another operation holds it.", + likelyCauses: [ + "A mounted or open resource", + "A concurrent operation has not finished", + ], + suggestions: [ + "Identify the process or operation holding the resource.", + "Wait for its completion or coordinate ownership instead of looping without a limit.", + ], + }, + EXDEV: { + title: "Cross-device operation", + explanation: + "An operation such as a rename crosses filesystem boundaries that it cannot cross directly.", + likelyCauses: [ + "Temporary and destination files live on different volumes", + "A container mount creates a filesystem boundary", + ], + suggestions: [ + "Create temporary files on the destination filesystem when atomic rename is required.", + "Use an intentional copy-and-cleanup workflow when atomic movement is unnecessary.", + ], + }, + EPIPE: { + title: "Broken pipe", + explanation: + "The application wrote to a stream whose receiving end has closed.", + likelyCauses: [ + "A downstream command exited", + "A peer disconnected before the write completed", + ], + suggestions: [ + "Handle stream errors and stop producing data when the consumer closes.", + "Distinguish an expected pipeline close from an unexpected connection failure.", + ], + }, + ECONNRESET: { + title: "Connection reset", + explanation: + "A connection was forcibly closed while the application was using it.", + likelyCauses: [ + "The remote peer closed the connection", + "An intermediary interrupted the session", + ], + suggestions: [ + "Inspect peer logs, timeout settings and recent connection changes.", + "Retry only safe operations with a bounded backoff and avoid duplicate side effects.", + ], + }, + ETIMEDOUT: { + title: "Operation timed out", + explanation: + "A connection or operation did not finish within its allowed time.", + likelyCauses: [ + "An unreachable or slow peer", + "A timeout shorter than the expected operation", + ], + suggestions: [ + "Check network reachability, service latency and the configured timeout.", + "Use bounded retries only for operations that are safe to repeat.", + ], + }, + ENOTFOUND: { + title: "Name lookup failed", + explanation: + "The requested hostname could not be resolved by the lookup operation.", + likelyCauses: [ + "A misspelled hostname", + "Resolver configuration or lookup resources failed", + ], + suggestions: [ + "Check the exact hostname and DNS behavior from the same runtime environment.", + "Do not assume every lookup failure proves the hostname does not exist.", + ], + }, + EAI_AGAIN: { + title: "Temporary name lookup failure", + explanation: "Name resolution reported a temporary failure.", + likelyCauses: [ + "A resolver is temporarily unavailable", + "A transient network or DNS problem", + ], + suggestions: [ + "Check the resolver and network from the affected host.", + "Apply bounded retry with backoff and report persistent failures.", + ], + }, + EADDRNOTAVAIL: { + title: "Local address unavailable", + explanation: + "The application tried to use an address that is not available on the local system.", + likelyCauses: [ + "Binding to an address on another host", + "An interface address changed", + ], + suggestions: [ + "Inspect local interfaces and the configured bind address.", + "Use the intended local interface rather than a remote destination address.", + ], + }, + ECONNABORTED: { + title: "Connection aborted", + explanation: "A connection was aborted before the operation completed.", + likelyCauses: [ + "A connection failure during setup or transfer", + "A local or intermediary cancellation", + ], + suggestions: [ + "Inspect the failure timing and both ends of the connection.", + "Handle cancellation separately from application errors when appropriate.", + ], + }, + ENETUNREACH: { + title: "Network unreachable", + explanation: "The host has no usable route to the requested network.", + likelyCauses: [ + "Missing route or disconnected interface", + "Network isolation in a container or host", + ], + suggestions: [ + "Inspect connectivity and routing in the application environment.", + "Confirm the destination network is intended to be reachable.", + ], + }, + EHOSTUNREACH: { + title: "Host unreachable", + explanation: + "The destination host could not be reached through the available network path.", + likelyCauses: [ + "The destination is unavailable", + "Routing or filtering blocks the path", + ], + suggestions: [ + "Check the destination address and routing from the affected host.", + "Inspect network policy rather than disabling protection blindly.", + ], + }, + EINVAL: { + title: "Invalid argument", + explanation: "The operating system rejected an argument to the operation.", + likelyCauses: [ + "A value outside the operation contract", + "A platform-specific unsupported value", + ], + suggestions: [ + "Inspect the syscall and the exact values passed to it.", + "Validate values against the API and platform requirements.", + ], + }, + ABORT_ERR: { + title: "Operation aborted", + explanation: "An operation was cancelled through an abort mechanism.", + likelyCauses: [ + "An AbortSignal was triggered", + "A caller timeout cancelled the operation", + ], + suggestions: [ + "Inspect who owns the abort controller and when cancellation occurred.", + "Treat expected cancellation separately and clean up resources.", + ], + }, + ERR_INVALID_ARG_TYPE: { + title: "Wrong argument type", + explanation: "A Node API received an argument of the wrong type.", + likelyCauses: [ + "A missing or incorrect value", + "An unexpected return type passed to another API", + ], + suggestions: [ + "Read the parameter name in the error and inspect its actual value.", + "Validate the argument at the boundary where it enters your application.", + ], + }, + ERR_INVALID_ARG_VALUE: { + title: "Invalid argument value", + explanation: "An argument has a value the API does not accept.", + likelyCauses: [ + "A valid type with an unsupported value", + "An invalid option combination", + ], + suggestions: [ + "Inspect the named parameter and documented accepted values.", + "Validate options before calling the API.", + ], + }, + ERR_OUT_OF_RANGE: { + title: "Value outside allowed range", + explanation: "A numeric or size argument falls outside the API range.", + likelyCauses: [ + "An invalid offset or length", + "A computed limit exceeds the supported bounds", + ], + suggestions: [ + "Check the reported minimum and maximum and the calculation producing the value.", + "Add boundary tests for zero, limits and oversized input.", + ], + }, + ERR_HTTP_HEADERS_SENT: { + title: "Headers already sent", + explanation: + "The server tried to change headers after the response headers had been sent.", + likelyCauses: [ + "Two code paths respond to the same request", + "An asynchronous callback runs after the response is complete", + ], + suggestions: [ + "Trace every response path and return after sending the response.", + "Coordinate asynchronous work so only one path owns the final response.", + ], + }, + ERR_HTTP_INVALID_STATUS_CODE: { + title: "Invalid HTTP status", + explanation: "The response status code is outside the accepted API range.", + likelyCauses: [ + "An undefined value becomes a status", + "A business code is mistaken for an HTTP status", + ], + suggestions: [ + "Check the status value before assigning or sending it.", + "Keep internal result codes separate from HTTP status codes.", + ], + }, + ERR_INVALID_URL: { + title: "Invalid URL", + explanation: "The URL parser cannot interpret the supplied URL.", + likelyCauses: [ + "A malformed absolute URL", + "A relative URL without a valid base", + ], + suggestions: [ + "Inspect the original input and supply an explicit base for relative paths.", + "Validate user-controlled URL inputs before making requests.", + ], + }, + ERR_PACKAGE_PATH_NOT_EXPORTED: { + title: "Package path not exported", + explanation: "A package exports map does not expose the requested subpath.", + likelyCauses: [ + "Importing an internal package file", + "A dependency changed its public entry points", + ], + suggestions: [ + "Inspect the package exports and use a documented public entry.", + "Upgrade or pin compatible dependencies instead of relying on private paths.", + ], + }, + ERR_PACKAGE_IMPORT_NOT_DEFINED: { + title: "Package import not defined", + explanation: "A package imports map has no matching internal specifier.", + likelyCauses: [ + "A missing imports mapping", + "A specifier is used outside its intended package scope", + ], + suggestions: [ + "Check package.json imports and the exact specifier.", + "Keep internal import aliases inside the package that defines them.", + ], + }, + ERR_INVALID_PACKAGE_CONFIG: { + title: "Invalid package configuration", + explanation: "Node could not use the package configuration.", + likelyCauses: ["Malformed package.json", "An invalid configuration field"], + suggestions: [ + "Read the named package.json and validate its JSON and relevant fields.", + "Check the first configuration error before reinstalling dependencies.", + ], + }, + ERR_UNKNOWN_FILE_EXTENSION: { + title: "Unknown module file extension", + explanation: + "The module loader does not recognize the file extension in this context.", + likelyCauses: [ + "An unsupported file type is imported", + "The required build or loader is missing", + ], + suggestions: [ + "Check the imported extension and the runtime module configuration.", + "Compile to a supported format or use a documented loader setup.", + ], + }, + ERR_IMPORT_ATTRIBUTE_MISSING: { + title: "Required import attribute missing", + explanation: "A module import requires an attribute that was not provided.", + likelyCauses: [ + "A JSON import lacks the required type attribute", + "An import was copied from a different runtime version", + ], + suggestions: [ + "Check the module type and import syntax for your supported Node version.", + "Use the required import attributes or an appropriate file-reading API.", + ], + }, + ERR_STREAM_WRITE_AFTER_END: { + title: "Write after stream end", + explanation: "Data was written after the writable stream had ended.", + likelyCauses: [ + "A late callback writes to a closed stream", + "Multiple owners end and write the same stream", + ], + suggestions: [ + "Trace the stream lifecycle and coordinate the writer with completion.", + "Test delayed callbacks and ensure end is called once work is complete.", + ], + }, + ERR_STREAM_DESTROYED: { + title: "Stream destroyed", + explanation: "An operation used a stream after it had been destroyed.", + likelyCauses: [ + "A prior error destroyed the stream", + "Cleanup ran while work was still pending", + ], + suggestions: [ + "Inspect the original stream error and cancellation timing.", + "Stop dependent work when the stream closes or is destroyed.", + ], + }, + ERR_SOCKET_BAD_PORT: { + title: "Invalid socket port", + explanation: "A socket port value is not accepted by the API.", + likelyCauses: [ + "A malformed environment variable", + "A number outside the port range", + ], + suggestions: [ + "Validate and parse the port configuration before creating the connection.", + "Do not substitute a silent default for a visibly invalid production setting.", + ], + }, + SyntaxError: { + title: "Invalid JavaScript syntax", + explanation: "The parser cannot interpret the source or input syntax.", + likelyCauses: [ + "Malformed JavaScript or JSON", + "Syntax unsupported by the current runtime", + ], + suggestions: [ + "Read the first reported location and inspect nearby delimiters and tokens.", + "Confirm the input format and runtime version before changing unrelated code.", + ], + }, + ReferenceError: { + title: "Name is not available", + explanation: + "An expression refers to a binding that is not available in its current scope or initialization state.", + likelyCauses: [ + "A misspelled or out-of-scope variable", + "Access before initialization", + ], + suggestions: [ + "Inspect the referenced name and its scope at the reported line.", + "Check initialization order and add a focused regression test.", + ], + }, + RangeError: { + title: "Value exceeds a permitted range", + explanation: + "An operation received a value outside its allowed range or exhausted a range-related resource.", + likelyCauses: [ + "An invalid numeric size or index", + "Excessive recursion or allocation", + ], + suggestions: [ + "Read the exact message to distinguish numeric bounds from stack exhaustion.", + "Inspect boundary values and recursion termination rather than treating every RangeError alike.", + ], + }, +}; diff --git a/vendor/v1/error-translator/index.js b/vendor/v1/error-translator/index.js new file mode 100644 index 0000000..b22be53 --- /dev/null +++ b/vendor/v1/error-translator/index.js @@ -0,0 +1,171 @@ +import { definitions } from "./definitions.js"; +export { diagnoseError, renderDiagnosis } from "./diagnostics.js"; +export const listErrors = () => Object.keys(definitions); + +/** Translate an ordered, bounded batch without mutating input. */ +export function translateErrors(errors, options = {}) { + if (!Array.isArray(errors) || errors.length < 1 || errors.length > 100) + throw new RangeError("Provide 1–100 errors."); + return Array.from(errors, (error) => translateError(error, options)); +} +const duckLines = { + EACCES: "The file has a guest list. Your process is not on it.", + EPERM: "The operating system has declined your ambitious proposal.", + EEXIST: "The target is already here and has not agreed to be replaced.", + ENOTDIR: "That hallway is actually a cupboard.", + EISDIR: "You asked for one file and received the whole neighborhood.", + ENOTEMPTY: "The directory still has roommates.", + EMFILE: "Your process opened every door and forgot to close them.", + ENFILE: "The host has run out of chairs for file handles.", + ENOSPC: "The disk has eaten the final snack.", + EROFS: "This filesystem is a museum: look, but do not rearrange.", + EBUSY: "The resource is in another meeting.", + EXDEV: "The rename has reached the border and needs a different passport.", + EPIPE: "The audience left before your stream finished the speech.", + ECONNRESET: "The other end hung up without the usual pleasantries.", + ETIMEDOUT: "The operation is late and has not brought a believable excuse.", + ENOTFOUND: "The hostname is not answering roll call.", + EAI_AGAIN: "DNS requests a brief intermission, not eternal optimism.", + EADDRNOTAVAIL: "That address does not live on this machine.", + ECONNABORTED: "The connection left before the conversation was finished.", + ENETUNREACH: "The packet has no road to the destination.", + EHOSTUNREACH: + "The route exists; the destination is still playing hard to reach.", + EINVAL: "The argument has failed the entrance interview.", + ABORT_ERR: "The caller pulled the emergency stop. Ask why before restarting.", + ERR_INVALID_ARG_TYPE: + "The parameter ordered a string and received a surprise guest.", + ERR_INVALID_ARG_VALUE: "The type is correct. Its life choices are not.", + ERR_OUT_OF_RANGE: "The number wandered beyond the approved fence.", + ERR_HTTP_HEADERS_SENT: + "The response left the station. You cannot redecorate its headers now.", + ERR_HTTP_INVALID_STATUS_CODE: + "That number is not a useful mood for an HTTP response.", + ERR_INVALID_URL: + "The address has syntax ambitions but no navigable destination.", + ERR_PACKAGE_PATH_NOT_EXPORTED: "You are trying the package staff entrance.", + ERR_PACKAGE_IMPORT_NOT_DEFINED: + "The alias has not been introduced to this package.", + ERR_INVALID_PACKAGE_CONFIG: + "The package instructions have become abstract art.", + ERR_UNKNOWN_FILE_EXTENSION: "The loader does not speak this file dialect.", + ERR_IMPORT_ATTRIBUTE_MISSING: + "The import arrived without its required name badge.", + ERR_STREAM_WRITE_AFTER_END: + "The stream said goodbye. Your callback is still talking.", + ERR_STREAM_DESTROYED: "The stream has retired. Please respect its notice.", + ERR_SOCKET_BAD_PORT: "The port number did not pass the harbor inspection.", + SyntaxError: "The parser has found punctuation with questionable intentions.", + ReferenceError: "That name is not available for comment in this scope.", + RangeError: "The value or call stack has climbed over the fence.", + ECONNREFUSED: "Your app knocked. The service has apparently gone for coffee.", + ENOENT: "The file is playing hide-and-seek. It is currently winning.", + EADDRINUSE: "Two servers reserved the same chair. Only one gets to sit.", + MODULE_NOT_FOUND: + "The dependency missed roll call. Check its invitation to node_modules.", + ERR_MODULE_NOT_FOUND: + "The module took a wrong turn. Extensions are street signs, not decorations.", + TypeError: "JavaScript received a surprise guest and forgot how to behave.", +}; + +/** + * Translate a nonempty string, Error, or error-like object with a code/name/message. + * Explicit codes take precedence over names and message matching. + * @param {string | Error | {code?: string, name?: string, message?: string}} error + * @param {{mode?: 'plain' | 'rubber-duck'}} [options] + * @returns {{code: string, title: string, explanation: string, likelyCauses: string[], suggestions: string[], mode: string}} + */ +export function translateError(error, options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) { + throw new TypeError("Options must be an object."); + } + const mode = options.mode ?? "plain"; + if (!["plain", "rubber-duck"].includes(mode)) + throw new RangeError("Supported modes: plain, rubber-duck."); + let code; + let name; + let message; + if (typeof error === "string") { + message = error.trim(); + } else if (error && typeof error === "object" && !Array.isArray(error)) { + for (const field of ["code", "name", "message"]) { + if (error[field] !== undefined && typeof error[field] !== "string") { + throw new TypeError(`Error ${field} must be a string.`); + } + } + code = error.code?.trim(); + name = error.name?.trim(); + message = error.message?.trim(); + } else { + throw new TypeError( + "Provide a nonempty error string or error-like object.", + ); + } + if (!code && !name && !message) + throw new TypeError( + "Provide a nonempty error string or error-like object.", + ); + const matched = + code || + (Object.hasOwn(definitions, name ?? "") ? name : undefined) || + Object.keys(definitions).find((key) => + new RegExp(`\\b${key}\\b`).test(message ?? ""), + ); + const definition = Object.hasOwn(definitions, matched ?? "") + ? definitions[matched] + : undefined; + const fallback = { + title: "Unrecognized error", + explanation: "No built-in translation matches this error yet.", + likelyCauses: [], + suggestions: [ + "Read the original error and inspect the first relevant line in its stack trace.", + "Check the documentation for the component that reported the error.", + ], + }; + const result = definition ?? fallback; + return { + code: matched ?? "UNKNOWN", + ...result, + explanation: + mode === "rubber-duck" + ? `${result.explanation} ${Object.hasOwn(duckLines, matched ?? "") ? duckLines[matched] : "The duck recommends investigating before blaming the compiler."}` + : result.explanation, + likelyCauses: [...result.likelyCauses], + suggestions: [...result.suggestions], + mode, + }; +} + +/** Render a structured translation as readable text without logging or exiting. */ +export function renderTranslation(result) { + if ( + !result || + typeof result.title !== "string" || + typeof result.explanation !== "string" || + !Array.isArray(result.likelyCauses) || + !Array.isArray(result.suggestions) || + ![...result.likelyCauses, ...result.suggestions].every( + (item) => typeof item === "string", + ) + ) { + throw new TypeError("Provide a structured translation result."); + } + return [ + result.title, + `What happened:\n${result.explanation}`, + ...(result.likelyCauses.length + ? [ + `Likely causes:\n${result.likelyCauses.map((item) => `- ${item}`).join("\n")}`, + ] + : []), + ...(result.suggestions.length + ? [`Try:\n${result.suggestions.map((item) => `- ${item}`).join("\n")}`] + : []), + ].join("\n\n"); +} + +/** Read the built-in guidance as independent structured records. */ +export function errorCatalog(options = {}) { + return listErrors().map((code) => translateError({ code }, options)); +} diff --git a/vendor/v1/excuse-js/LICENSE b/vendor/v1/excuse-js/LICENSE new file mode 100644 index 0000000..9fe7218 --- /dev/null +++ b/vendor/v1/excuse-js/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Deploy Anyway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/v1/excuse-js/excuses.js b/vendor/v1/excuse-js/excuses.js new file mode 100644 index 0000000..50f58c9 --- /dev/null +++ b/vendor/v1/excuse-js/excuses.js @@ -0,0 +1,157 @@ +// Original, workplace-safe phrases. Each category has twelve distinct excuses. +export const phrases = { + deployment: [ + "The deployment passed rehearsal and requested a stunt double for production.", + "The release arrived on time. Its dependencies are still finding parking.", + "The deployment is waiting for the server to finish its dramatic entrance.", + "The release notes say smooth landing. The landing gear has requested clarification.", + "The deploy button has confidence. The health check has follow-up questions.", + "The release is technically live and emotionally still in staging.", + "Our rollback plan has achieved the rare status of being read before the incident.", + "The canary found a problem and would like its consulting fee in seeds.", + "The release was ready until production introduced itself.", + "We shipped the fix. The monitoring dashboard is deciding whether to clap.", + "The deployment arrived wearing a parachute. This is considered progress.", + "The server accepted the release but declined to endorse its lifestyle.", + ], + build: [ + "The compiler found a typo and promoted it to a company-wide announcement.", + "The build is assembling a committee to discuss the missing semicolon.", + "The artifacts are ready; the build is negotiating custody.", + "The compiler is proofreading a novel we thought was a configuration file.", + "The build cache remembers a different version of reality.", + "The bundle got smaller after we removed the feature that made it work.", + "The build system asked for a dependency and received a family reunion.", + "One warning invited its friends and now the terminal needs chairs.", + "The clean build is clean because it contains no artifacts yet.", + "The compiler rejected the argument that it worked yesterday.", + "Our build pipeline is a conveyor belt with trust issues.", + "The source maps know where the problem is. They are being dramatic about it.", + ], + production: [ + "Production interpreted the happy path as an optional scenic route.", + "The server is performing an unscheduled demonstration of our recovery plan.", + "Production has requested that our assumptions submit identification.", + "Production has discovered an edge case shaped exactly like our largest customer.", + "The incident is stable. Our coffee consumption is scaling horizontally.", + "The dashboards are green because the broken metric stopped reporting.", + "Production read the happy-path test and chose creative freedom.", + "The service is healthy in the same way a husky is quiet before stealing a sock.", + "We have narrowed the cause to the part of the system currently existing.", + "The fallback works. It has been promoted to the main plan without a ceremony.", + "The alert arrived before the customer email. We are counting that as a win.", + "Our recovery estimate is being reviewed by the concept of time.", + ], + bug: [ + "The edge case moved to the center and brought furniture.", + "That variable has been exploring career opportunities outside its scope.", + "The bug declined to reproduce until the screen-sharing session began.", + "The bug is intermittent, which is engineer for witness protection.", + "The reproduction is minimal. The emotional damage is not.", + "The issue disappeared while screen sharing and returned to clock in afterward.", + "The edge case has built a small apartment in the main path.", + "We found the bug. It was wearing a feature flag as a disguise.", + "The fix is one line. The explanation requires a whiteboard and snacks.", + "The bug survived three refactors and now qualifies for seniority.", + "The stack trace has provided directions to a place we never wanted to visit.", + "The defect is resolved in the branch where optimism is enabled.", + ], + api: [ + "The API sent a response, but it used the envelope for the payload.", + "The endpoint is observing a moment of silence for our timeout setting.", + "The contract was clear. The response preferred interpretive dance.", + "The endpoint returned success with the confidence of an empty response body.", + "The API contract and implementation are currently seeing other people.", + "The request timed out while the server composed a thoughtful silence.", + "The response schema evolved during the meeting about freezing the schema.", + "The retry succeeded in creating the same problem twice.", + "The status code is correct. The payload is expressing itself artistically.", + "The endpoint is idempotent on days when nobody asks it twice.", + "The client sent a request. The server returned a character development arc.", + "The integration test has discovered that both sides mean something different by optional.", + ], + database: [ + "The database is taking a full-table stroll to consider our question.", + "The query asked for one row and accidentally invited the whole neighborhood.", + "The migration packed the columns but forgot the forwarding address.", + "The query planner chose the scenic route through every row.", + "The migration passed locally on a database containing exactly our confidence.", + "The index exists. The query would prefer to meet all the data personally.", + "The connection pool is full of connections discussing their retirement.", + "The transaction is waiting on a lock that is waiting on a conversation.", + "The backup completed. We are now attempting the bold sequel: restore.", + "The schema is flexible because we forgot where it was documented.", + "The slow query has enough history to apply for heritage status.", + "The data is consistent about disagreeing with the application.", + ], + deadline: [ + "The estimate included coding time, but the calendar added plot twists.", + "The deadline arrived before the requirements finished introducing themselves.", + "The last ten percent has applied for its own project manager.", + "The estimate included development, testing and a completely fictional Tuesday.", + "The smallest deliverable grew after someone used the word just.", + "The timeline assumed dependencies would arrive with their shoes on.", + "We can deliver quickly if the definition of delivered stops moving.", + "The calendar is immutable. The scope is doing parkour.", + "The critical path has stopped to attend a planning meeting.", + "The deadline is realistic in the timezone where requirements never change.", + "We reduced the scope. It is now only moderately larger than the week.", + "The schedule has a buffer. Unfortunately it is written in pencil.", + ], + testing: [ + "The test fixture has developed stronger opinions than the specification.", + "The flaky test only accepts results delivered under a full cache.", + "The assertion expected certainty; the asynchronous task offered suspense.", + "The test passed because the assertion was exceptionally supportive.", + "The fixture describes a world with fewer users and better manners.", + "The flaky test is conducting a long-term study of team patience.", + "Coverage went up after we tested the part that says hello.", + "The integration test has introduced the units to each other. They are arguing.", + "The mock is perfect at simulating the service we wish we had.", + "The regression test passed and immediately asked why it was not here earlier.", + "The test suite is green. The skipped tests are enjoying a quiet holiday.", + "The failing test has a point and an unfortunate way of making it.", + ], + network: [ + "The packets took a detour to admire the routing table.", + "DNS is still deciding which address feels most like home.", + "The connection is buffering its explanation for the delay.", + "DNS is resolving everything except the team's uncertainty.", + "The packet took a detour through a router with strong opinions.", + "The connection is stable between the moments when it is not.", + "The firewall followed the rules. The configuration forgot to read them.", + "The timeout was selected by someone who believed all servers live next door.", + "The service is reachable from everywhere except the place that needs it.", + "The proxy is helping by changing the question before it arrives.", + "The request is in transit and has started a travel blog.", + "The network is technically up. The route is considering its options.", + ], + "merge-conflict": [ + "Both branches brought a solution and neither brought a seating chart.", + "Git discovered that two correct answers wanted the same line.", + "The branches agree on the goal and disagree on every comma.", + "Both branches improved the same line in incompatible directions.", + "The conflict is resolved. The naming debate is still open.", + "Git found two versions of the truth and asked us to facilitate.", + "The rebase succeeded after we explained time travel to the tests.", + "The diff is small if you exclude the file everybody reformatted.", + "The merge was automatic until the humans got involved.", + "The branch is current with main and several unresolved feelings.", + "We chose both changes. The compiler would like us to choose again.", + "The conflict marker survived review by pretending to be punctuation.", + ], + demo: [ + "The demo saw an audience and switched from software to performance art.", + "The prototype is saving its best behavior for the recording we forgot to start.", + "The live demo has added a surprise question to the presentation.", + "The demo works best when the audience is a test fixture.", + "The projector has detected confidence and activated its troubleshooting mode.", + "The live demo is a distributed system consisting of nerves and Wi-Fi.", + "The happy path worked until someone clicked the interesting button.", + "The offline fallback is now the star of the presentation.", + "The demo data is realistic enough to have missing fields.", + "The browser refreshed and introduced us to the unsaved version.", + "The feature worked backstage. It is negotiating public appearances.", + "The rehearsal was flawless. The room has different opinions about networking.", + ], +}; diff --git a/vendor/v1/excuse-js/incident.js b/vendor/v1/excuse-js/incident.js new file mode 100644 index 0000000..95fe42d --- /dev/null +++ b/vendor/v1/excuse-js/incident.js @@ -0,0 +1,83 @@ +import { excuse } from "./index.js"; +const iso = (value, label) => { + if ( + typeof value !== "string" || + !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(value) || + !Number.isFinite(Date.parse(value)) || + new Date(value).toISOString().slice(0, 19) !== value.slice(0, 19) + ) + throw new TypeError(`${label} must be a UTC ISO timestamp.`); + return value; +}; +const clean = (value) => + Array.from(value) + .filter( + (char) => char.charCodeAt(0) >= 32 || char === "\n" || char === "\t", + ) + .join(""); +/** Draft factual incident communication; public output never adds a joke. */ +export function incidentUpdate(facts = {}, options = {}) { + if (!facts || typeof facts !== "object" || Array.isArray(facts)) + throw new TypeError("Incident facts must be an object."); + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + const status = facts.status ?? "investigating", + audience = options.audience ?? "public", + humor = options.humor ?? false; + if ( + !["investigating", "identified", "monitoring", "resolved"].includes(status) + ) + throw new RangeError("Unsupported incident status."); + if (!["public", "internal"].includes(audience) || typeof humor !== "boolean") + throw new TypeError("Choose public/internal audience and boolean humor."); + if (humor && audience !== "internal") + throw new TypeError("Comic relief requires an internal audience."); + const result = { status, audience, missing: [], overdue: false }; + for (const key of ["service", "impact", "action", "owner"]) { + if ( + facts[key] !== undefined && + facts[key] !== null && + (typeof facts[key] !== "string" || + !facts[key].trim() || + facts[key].length > 2000) + ) + throw new TypeError( + `${key} must be a nonempty string up to 2000 characters.`, + ); + result[key] = + facts[key] === undefined || facts[key] === null + ? null + : clean(facts[key].trim()); + if (result[key] === null) result.missing.push(key); + } + const now = iso(options.now ?? new Date().toISOString(), "now"); + result.nextUpdate = + facts.nextUpdate === undefined || facts.nextUpdate === null + ? null + : iso(facts.nextUpdate, "nextUpdate"); + if (result.nextUpdate === null && status !== "resolved") + result.missing.push("nextUpdate"); + result.overdue = + status !== "resolved" && + result.nextUpdate !== null && + Date.parse(result.nextUpdate) < Date.parse(now); + const lines = [ + `Status: ${status}`, + `Service: ${result.service ?? "Not provided"}`, + `Impact: ${result.impact ?? "Not provided — verify before claiming an impact"}`, + `Current action: ${result.action ?? "Not provided"}`, + `Owner: ${result.owner ?? "Not assigned"}`, + `Next update: ${result.nextUpdate ?? (status === "resolved" ? "Not scheduled (status marked resolved)" : "Not scheduled")}${result.overdue ? " — OVERDUE" : ""}`, + ]; + if (result.missing.length) + lines.push("Missing facts: " + result.missing.join(", ")); + result.complete = result.missing.length === 0; + if (humor) { + result.comicRelief = excuse(options.category ?? "production", { + seed: options.seed, + }); + lines.push("\nComic relief (internal only): " + result.comicRelief); + } + result.message = lines.join("\n"); + return result; +} diff --git a/vendor/v1/excuse-js/index.js b/vendor/v1/excuse-js/index.js new file mode 100644 index 0000000..76a44eb --- /dev/null +++ b/vendor/v1/excuse-js/index.js @@ -0,0 +1,105 @@ +import { phrases } from "./excuses.js"; +const nextSteps = { + "merge-conflict": + "Understand both changes, resolve the conflict deliberately, then run tests for the combined behavior. Git does not accept a coin toss.", + deployment: + "Check the release diff and rollback plan before delivering this line.", + testing: + "Reproduce the failure and add a regression test. The punchline is optional.", + build: + "Read the first build error, verify dependencies, then rebuild. The compiler does not accept applause.", + production: + "Check health metrics and the latest release diff; confirm rollback before improving the punchline.", + bug: "Save a minimal reproduction and add a regression test. The bug would prefer neither.", + api: "Inspect the status, payload and timeout contract. The endpoint is not communicating through vibes.", + database: + "Check connectivity and migration state before retrying. The data deserves a backup, not a speech.", + deadline: + "Name the smallest deliverable and communicate the revised estimate. Calendars cannot compile optimism.", + network: + "Check DNS, connectivity and timeout settings. Blame needs a packet capture.", + demo: "Rehearse the happy path and prepare an offline fallback. The projector can smell confidence.", +}; + +/** A comic excuse paired with an honest, actionable follow-up. */ +export function excuseReport(category = "deployment", options = {}) { + const text = excuse(category, options); + const key = category.trim().toLowerCase(); + return { + category: key, + excuse: text, + nextStep: + nextSteps[key] ?? + "Read the evidence, name an owner, and document the next small fix.", + }; +} + +/** Return the available lowercase category names as a fresh array. */ +export function categories() { + return Object.keys(phrases); +} + +/** + * Choose an excuse. Seeds are stateless and deterministic within this version. + * @param {string} [category='deployment'] Category (case insensitive). + * @param {{seed?: string | number}} [options] Optional string or finite number seed. + * @returns {string} An original workplace-safe excuse. + */ +export function excuse(category = "deployment", options = {}) { + if (typeof category !== "string" || !category.trim()) + throw new TypeError("Category must be a nonempty string."); + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + const key = category.trim().toLowerCase(); + if (!Object.hasOwn(phrases, key)) + throw new RangeError( + `Unknown category: ${category}. Choose: ${categories().join(", ")}.`, + ); + const { seed } = options; + let index; + if (seed === undefined) { + index = Math.floor(Math.random() * phrases[key].length); + } else { + if ( + typeof seed !== "string" && + !(typeof seed === "number" && Number.isFinite(seed)) + ) + throw new TypeError("Seed must be a string or finite number."); + // FNV-1a over UTF-16 code units. Type tagging distinguishes 1 from "1". + const input = `${key}:${typeof seed}:${seed}`; + let hash = 2166136261; + for (let i = 0; i < input.length; i++) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 16777619) >>> 0; + } + index = hash % phrases[key].length; + } + return phrases[key][index]; +} + +/** Generate a batch without repeats until the category pool is exhausted. */ +export function excuseBatch(category = "deployment", options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Options must be an object."); + const count = options.count ?? 3; + if (!Number.isSafeInteger(count) || count < 1 || count > 20) + throw new RangeError("count must be an integer between 1 and 20."); + const first = excuse(category, { seed: options.seed }); + const pool = phrases[category.trim().toLowerCase()]; + const start = pool.indexOf(first); + return Array.from( + { length: count }, + (_, i) => pool[(start + i) % pool.length], + ); +} + +/** Read a category without consuming randomness; the returned catalog is independent. */ +export function listExcuses(category = "deployment") { + if (typeof category !== "string" || !category.trim()) + throw new TypeError("Category must be a nonempty string."); + const key = category.trim().toLowerCase(); + if (!Object.hasOwn(phrases, key)) + throw new RangeError("Unknown category: " + category); + return [...phrases[key]]; +} +export { incidentUpdate } from "./incident.js"; diff --git a/vendor/v1/ship-it-meter/LICENSE b/vendor/v1/ship-it-meter/LICENSE new file mode 100644 index 0000000..9fe7218 --- /dev/null +++ b/vendor/v1/ship-it-meter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Deploy Anyway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/v1/ship-it-meter/index.js b/vendor/v1/ship-it-meter/index.js new file mode 100644 index 0000000..5f5ff81 --- /dev/null +++ b/vendor/v1/ship-it-meter/index.js @@ -0,0 +1,304 @@ +const days = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +]; +const counts = ["tests", "failingTests", "criticalIssues", "lintFailures"]; +const allowed = [ + ...counts, + "coverage", + "build", + "uncommittedChanges", + "branch", + "day", +]; + +/** Explicit automation gate; a score alone is never a deployment approval. */ +export function releaseGate(input = {}, options = {}) { + if (!options || typeof options !== "object" || Array.isArray(options)) + throw new TypeError("Gate options must be an object."); + const minScore = options.minScore ?? 80; + if ( + typeof minScore !== "number" || + !Number.isFinite(minScore) || + minScore < 0 || + minScore > 100 + ) + throw new RangeError("minScore must be between 0 and 100."); + const result = preflight(input); + const blockers = []; + if (!input.tests) + blockers.push("Provide a positive test count. Vibes cannot pass CI."); + if (input.build !== true) blockers.push("A passing build is required."); + if (input.coverage === undefined) + blockers.push("Coverage evidence is required."); + if (input.failingTests) blockers.push("Failing tests block the gate."); + if (input.criticalIssues) blockers.push("Critical issues block the gate."); + if (input.lintFailures) blockers.push("Lint failures block the gate."); + if (input.uncommittedChanges) + blockers.push("Uncommitted changes block the gate."); + if (result.score < minScore) + blockers.push(`Score ${result.score} is below ${minScore}.`); + return { ...result, minScore, passed: blockers.length === 0, blockers }; +} + +/** + * Calculate a stateless readiness score. Missing evidence reduces the score. + * @param {{tests?: number, failingTests?: number, coverage?: number, build?: boolean, uncommittedChanges?: boolean, branch?: string, day?: string, criticalIssues?: number, lintFailures?: number}} [input] + * @returns {{score: number, verdict: string, reasons: string[]}} + */ +export function shipIt(input = {}) { + if (!input || typeof input !== "object" || Array.isArray(input)) + throw new TypeError("Input must be an object."); + for (const key of Object.keys(input)) + if (!allowed.includes(key)) throw new TypeError(`Unknown input: ${key}.`); + for (const key of counts) { + if ( + input[key] !== undefined && + (!Number.isSafeInteger(input[key]) || input[key] < 0) + ) + throw new RangeError(`${key} must be a nonnegative safe integer.`); + } + if ( + input.coverage !== undefined && + (typeof input.coverage !== "number" || + !Number.isFinite(input.coverage) || + input.coverage < 0 || + input.coverage > 100) + ) + throw new RangeError("coverage must be between 0 and 100."); + for (const key of ["build", "uncommittedChanges"]) + if (input[key] !== undefined && typeof input[key] !== "boolean") + throw new TypeError(`${key} must be a boolean.`); + for (const key of ["branch", "day"]) + if ( + input[key] !== undefined && + (typeof input[key] !== "string" || !input[key].trim()) + ) + throw new TypeError(`${key} must be a nonempty string.`); + const day = input.day?.trim().toLowerCase(); + if (day !== undefined && !days.includes(day)) + throw new RangeError("day must be a full weekday name."); + const failing = input.failingTests ?? 0; + if (failing > 0 && input.tests === undefined) + throw new RangeError("tests is required when failingTests is positive."); + if (input.tests !== undefined && failing > input.tests) + throw new RangeError("failingTests cannot exceed tests."); + let score = 100; + const reasons = []; + const deduct = (points, reason) => { + score -= points; + reasons.push(`${reason} (-${points})`); + }; + if (!input.tests) + deduct( + 25, + input.tests === 0 ? "No tests reported" : "Test count not provided", + ); + else if (failing) + deduct( + Math.min(50, 20 + Math.ceil(30 * (failing / input.tests))), + `${failing} of ${input.tests} tests are failing`, + ); + else reasons.push(`${input.tests} tests reported passing`); + if (input.coverage === undefined) deduct(15, "Coverage not provided"); + else { + const penalty = Math.ceil((100 - input.coverage) * 0.2); + if (penalty) deduct(penalty, `Coverage is ${input.coverage}%`); + else reasons.push("Coverage is 100%"); + } + if (input.build === undefined) deduct(15, "Build status not provided"); + else if (!input.build) deduct(40, "Build failed"); + else reasons.push("Build passed"); + if (input.criticalIssues) + deduct( + Math.min(50, 25 * input.criticalIssues), + `${input.criticalIssues} open critical issues`, + ); + if (input.lintFailures) + deduct( + Math.min(20, 5 * input.lintFailures), + `${input.lintFailures} lint failures`, + ); + if (input.uncommittedChanges) deduct(10, "Uncommitted changes reported"); + if ( + input.branch !== undefined && + !["main", "master"].includes(input.branch.trim()) + ) + deduct(5, `Branch is ${input.branch.trim()}`); + if (day === "friday") + deduct(3, "Friday: the weekend would like a quiet entrance"); + score = Math.max(0, Math.min(100, score)); + if (input.build === false || input.criticalIssues > 0) { + score = Math.min(score, 29); + reasons.push("Failed build or critical issues cap the score at 29"); + } else if (failing > 0) { + score = Math.min(score, 59); + reasons.push("Failing tests cap the score at 59"); + } + const verdict = + score < 30 + ? "Absolutely Not" + : score < 60 + ? "Questionable" + : score < 80 + ? "Probably Fine" + : score < 95 + ? "Ship It" + : "Suspiciously Ready"; + return { score, verdict, reasons }; +} + +/** Return actionable preflight tasks alongside the existing score. */ +export function preflight(input = {}) { + const result = shipIt(input); + const actions = []; + if (input.build !== true) + actions.push("Get a passing build. Confidence is not a build artifact."); + if (!input.tests) + actions.push("Run tests and report the count. Vibes are not assertions."); + if (input.failingTests) + actions.push("Fix failing tests. Red is not a festive deployment theme."); + if (input.coverage === undefined || input.coverage < 80) + actions.push( + "Measure coverage and review untested paths. The bugs enjoy privacy.", + ); + if (input.criticalIssues) + actions.push( + "Resolve critical issues. The incident channel deserves a quiet afternoon.", + ); + if (input.lintFailures) + actions.push("Fix lint failures. Let the semicolons retire in peace."); + if (input.uncommittedChanges) + actions.push( + "Commit or stash local changes. Your laptop is not the release archive.", + ); + if (input.day?.trim().toLowerCase() === "friday") + actions.push( + "Confirm rollback and on-call cover. The weekend has other plans.", + ); + if (!actions.length) + actions.push( + "Confirm monitoring and rollback. Even excellent evidence needs a parachute.", + ); + return { ...result, actions }; +} + +export { releaseScenarios, scenarioEvidence } from "./scenarios.js"; +/** Explain the gate as ordered work with observable completion criteria. */ +export function releasePlan(input = {}, options = {}) { + const gate = releaseGate(input, options); + const tasks = []; + const add = (id, priority, title, verify) => + tasks.push({ id, priority, title, verify }); + if (input.build !== true) + add( + "build", + "blocker", + "Get a passing build", + "Run the release build for the exact commit and record its successful result.", + ); + if (!input.tests) + add( + "tests", + "blocker", + "Run a nonempty test suite", + "Record the test count and outcome for the release commit.", + ); + if (input.failingTests) + add( + "failing-tests", + "blocker", + "Repair failing tests", + "Reproduce each failure, fix its cause and rerun the suite.", + ); + if (input.coverage === undefined) + add( + "coverage-evidence", + "blocker", + "Measure coverage", + "Generate coverage for the release commit and record the percentage.", + ); + else if (input.coverage < 80) + add( + "coverage-review", + "review", + "Review untested paths", + "Inspect uncovered critical paths and add meaningful tests where needed.", + ); + if (input.criticalIssues) + add( + "critical-issues", + "blocker", + "Resolve critical issues", + "Record the resolution and verify the affected behavior.", + ); + if (input.lintFailures) + add( + "lint", + "blocker", + "Fix lint failures", + "Run the configured lint checks successfully.", + ); + if (input.uncommittedChanges) + add( + "worktree", + "blocker", + "Reconcile local changes", + "Commit or stash intentionally and verify the exact release source.", + ); + if (input.branch && !["main", "master"].includes(input.branch.trim())) + add( + "branch", + "review", + "Confirm release branch", + "Verify that this branch is the intended release source and that its CI passed.", + ); + if (input.day?.trim().toLowerCase() === "friday") + add( + "friday", + "review", + "Confirm weekend support", + "Identify monitoring ownership and a tested rollback path.", + ); + if (gate.score < gate.minScore) + add( + "score", + "blocker", + "Improve evidence to the configured threshold", + "Address score deductions, rerun the checks and reevaluate the gate.", + ); + add( + "rollback", + "release", + "Prepare the return route", + "Verify the rollback artifact and procedure before deployment.", + ); + add( + "monitoring", + "release", + "Watch the actual release", + "Check health and error metrics after deployment and name an owner.", + ); + const summaries = { + "Absolutely Not": "The evidence says stop. The confidence hat can wait.", + Questionable: + "The release has questions. Answer them before adding confetti.", + "Probably Fine": "Close is a useful direction, not a deployment receipt.", + "Ship It": "Good evidence. Keep the parachute and watch the landing.", + "Suspiciously Ready": + "Boring, verified readiness. Give the checks their share of the applause.", + }; + const rank = { blocker: 0, review: 1, release: 2 }; + tasks.sort((a, b) => rank[a.priority] - rank[b.priority]); + return { ...gate, summary: summaries[gate.verdict], tasks }; +} +export { + parseTestReport, + parseCoverageReport, + evaluateReports, +} from "./reports.js"; diff --git a/vendor/v1/ship-it-meter/reports.js b/vendor/v1/ship-it-meter/reports.js new file mode 100644 index 0000000..95688ae --- /dev/null +++ b/vendor/v1/ship-it-meter/reports.js @@ -0,0 +1,262 @@ +import { releasePlan } from "./index.js"; +const object = (value, label) => { + if (!value || typeof value !== "object" || Array.isArray(value)) + throw new TypeError(`${label} must be an object.`); + return value; +}; +const count = (value, label) => { + if (!Number.isSafeInteger(value) || value < 0) + throw new TypeError(`${label} must be a nonnegative safe integer.`); + return value; +}; +const json = (value) => (typeof value === "string" ? JSON.parse(value) : value); +const date = (value, label) => { + if ( + typeof value !== "string" || + !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(value) || + !Number.isFinite(Date.parse(value)) || + new Date(value).toISOString().slice(0, 19) !== value.slice(0, 19) + ) + throw new TypeError(`${label} must be a UTC ISO timestamp.`); + return Date.parse(value); +}; + +/** Parse completed Node TAP or Jest JSON test results; skipped/todo tests are not passing evidence. */ +export function parseTestReport(data, format = "node-tap") { + if (format === "node-tap") { + if ( + typeof data !== "string" || + !data.startsWith("TAP version 13") || + /^Bail out!/m.test(data) + ) + throw new TypeError("Expected a completed Node TAP v13 report."); + const values = {}; + for (const key of [ + "tests", + "pass", + "fail", + "cancelled", + "skipped", + "todo", + ]) { + const matches = [ + ...data.matchAll(new RegExp(`^# ${key} (\\d+)\\r?$`, "gm")), + ]; + if (matches.length !== 1) + throw new TypeError(`TAP requires one ${key} summary.`); + values[key] = count(Number(matches[0][1]), key); + } + if ( + values.pass + + values.fail + + values.cancelled + + values.skipped + + values.todo !== + values.tests + ) + throw new TypeError("TAP counts do not reconcile."); + return { + tests: values.tests - values.skipped - values.todo, + passedTests: values.pass, + failingTests: values.fail + values.cancelled, + skippedTests: values.skipped, + todoTests: values.todo, + successful: values.fail + values.cancelled === 0, + }; + } + if (format === "jest") { + const report = object(json(data), "Jest report"); + const total = count(report.numTotalTests, "numTotalTests"), + passed = count(report.numPassedTests, "numPassedTests"), + failed = count(report.numFailedTests, "numFailedTests"), + pending = count(report.numPendingTests, "numPendingTests"), + todo = count(report.numTodoTests ?? 0, "numTodoTests"); + if ( + total !== passed + failed + pending + todo || + typeof report.success !== "boolean" + ) + throw new TypeError("Jest counts/success do not reconcile."); + if ( + report.wasInterrupted || + report.testResults?.some((result) => + result.assertionResults?.some( + (assertion) => assertion.wouldRun === true, + ), + ) + ) + throw new TypeError( + "Interrupted or collected-only Jest output is not executed evidence.", + ); + const failedSuites = count( + report.numFailedTestSuites ?? 0, + "numFailedTestSuites", + ); + return { + tests: total - pending - todo, + passedTests: passed, + failingTests: failed, + skippedTests: pending, + todoTests: todo, + successful: report.success && failed === 0 && failedSuites === 0, + }; + } + throw new RangeError("Supported test formats: node-tap, jest."); +} +/** Read Istanbul/c8 coverage-summary.json. Empty instrumented code is not evidence. */ +export function parseCoverageReport(data) { + const total = object( + object(json(data), "Coverage report").total, + "Coverage total", + ), + result = {}; + for (const key of ["lines", "statements", "functions", "branches"]) { + const metric = object(total[key], `Coverage ${key}`), + all = count(metric.total, `${key}.total`), + covered = count(metric.covered, `${key}.covered`); + if ( + (all === 0 && ["lines", "statements"].includes(key)) || + covered > all || + typeof metric.pct !== "number" || + !Number.isFinite(metric.pct) || + metric.pct < 0 || + metric.pct > 100 + ) + throw new TypeError( + `Coverage ${key} must contain measured code and a percentage.`, + ); + const computed = all === 0 ? 100 : (covered / all) * 100; + if (Math.abs(computed - metric.pct) > 0.02) + throw new TypeError(`Coverage ${key} percentage disagrees with counts.`); + result[key] = metric.pct; + } + return result; +} +/** Gate real report envelopes. Caller-supplied commit/timestamps are provenance claims, not signatures. */ +export function evaluateReports(bundle, options = {}) { + object(bundle, "Report bundle"); + object(options, "Policy"); + for (const key of Object.keys(options)) + if ( + !["minTests", "minCoverage", "minScore", "maxAgeMs", "now"].includes(key) + ) + throw new TypeError(`Unknown policy option: ${key}.`); + if (typeof bundle.commit !== "string" || !bundle.commit.trim()) + throw new TypeError("Report bundle requires a nonempty commit."); + const policy = { + minTests: 1, + minCoverage: 80, + minScore: 80, + maxAgeMs: 3600000, + ...options, + }; + for (const [key, value] of Object.entries({ + minTests: 1, + minCoverage: 80, + minScore: 80, + maxAgeMs: 3600000, + })) + if (policy[key] === undefined) policy[key] = value; + count(policy.minTests, "minTests"); + if (policy.minTests < 1) throw new RangeError("minTests must be positive."); + for (const key of ["minCoverage", "minScore"]) + if ( + typeof policy[key] !== "number" || + !Number.isFinite(policy[key]) || + policy[key] < 0 || + policy[key] > 100 + ) + throw new RangeError(`${key} must be from 0 to 100.`); + if (!Number.isSafeInteger(policy.maxAgeMs) || policy.maxAgeMs < 1) + throw new RangeError("maxAgeMs must be a positive safe integer."); + const now = options.now ?? new Date().toISOString(); + const nowMs = date(now, "now"); + const evidence = {}, + receipts = [], + blockers = []; + for (const kind of ["tests", "coverage", "build"]) { + const issues = [], + report = bundle[kind]; + if (!report) { + issues.push(`${kind}: report is missing.`); + } else + try { + object(report, kind); + const captured = date(report.capturedAt, `${kind}.capturedAt`); + if (report.commit !== bundle.commit) + issues.push(`${kind}: commit does not match ${bundle.commit}.`); + if (captured > nowMs) + issues.push(`${kind}: timestamp is in the future.`); + if (nowMs - captured > policy.maxAgeMs) + issues.push(`${kind}: report is stale.`); + if (kind === "tests") { + if (!["node-tap", "jest"].includes(report.format)) + throw new RangeError("Test format must be node-tap or jest."); + const parsed = parseTestReport(report.data, report.format); + evidence.tests = parsed.tests; + evidence.failingTests = parsed.failingTests; + if (!parsed.successful) + issues.push("tests: test execution was unsuccessful."); + if (parsed.tests < policy.minTests) + issues.push( + `tests: ${parsed.tests} executed tests is below ${policy.minTests}.`, + ); + } else if (kind === "coverage") { + if (report.format !== "istanbul-summary") + throw new RangeError("Coverage format must be istanbul-summary."); + const coverage = parseCoverageReport(report.data); + evidence.coverage = coverage.lines; + for (const [metric, value] of Object.entries(coverage)) + if (value < policy.minCoverage) + issues.push( + `coverage: ${metric} ${value}% is below ${policy.minCoverage}%.`, + ); + } else { + count(report.exitCode, "build.exitCode"); + evidence.build = report.exitCode === 0; + if (report.exitCode !== 0) + issues.push(`build: command exited ${report.exitCode}.`); + } + } catch (error) { + issues.push(`${kind}: ${error.message}`); + } + receipts.push({ + kind, + format: kind === "build" ? "exit-code" : (report?.format ?? "missing"), + commit: typeof report?.commit === "string" ? report.commit : null, + capturedAt: + typeof report?.capturedAt === "string" ? report.capturedAt : null, + accepted: issues.length === 0, + issues, + }); + blockers.push(...issues); + } + const plan = releasePlan(evidence, { minScore: policy.minScore }); + const combined = [...new Set([...blockers, ...plan.blockers])]; + const receiptTasks = receipts + .filter((item) => !item.accepted) + .map((item) => ({ + id: "receipt-" + item.kind, + priority: "blocker", + title: "Repair " + item.kind + " evidence", + verify: `Generate a completed ${item.kind} report for ${bundle.commit} with a current UTC timestamp; resolve: ${item.issues.join(" ")}`, + })); + return { + ...plan, + summary: combined.length + ? "The receipts say stop. Confidence must wait." + : plan.summary, + tasks: [...receiptTasks, ...plan.tasks], + passed: combined.length === 0, + blockers: combined, + evidence, + receipts, + commit: bundle.commit, + policy: { + minTests: policy.minTests, + minCoverage: policy.minCoverage, + minScore: policy.minScore, + maxAgeMs: policy.maxAgeMs, + }, + evaluatedAt: now, + }; +} diff --git a/vendor/v1/ship-it-meter/scenarios.js b/vendor/v1/ship-it-meter/scenarios.js new file mode 100644 index 0000000..069b8e6 --- /dev/null +++ b/vendor/v1/ship-it-meter/scenarios.js @@ -0,0 +1,133 @@ +// Demonstration inputs, never a claim about a real release. +const scenarios = { + ready: { + description: + "Passing evidence, clean worktree and main branch. Boring is a compliment.", + evidence: { + tests: 125, + coverage: 96, + build: true, + branch: "main", + day: "tuesday", + }, + }, + friday: { + description: + "Good evidence on Friday. Confirm rollback and on-call before the victory lap.", + evidence: { + tests: 125, + coverage: 92, + build: true, + branch: "main", + day: "friday", + }, + }, + "no-evidence": { + description: + "Confidence is present. Test, coverage and build evidence have not arrived.", + evidence: {}, + }, + "failed-build": { + description: + "The build is red. The deploy button is not a color correction tool.", + evidence: { + tests: 125, + coverage: 92, + build: false, + }, + }, + "failing-tests": { + description: + "Some tests fail. Green deployment lights will not repair assertions.", + evidence: { + tests: 125, + failingTests: 3, + coverage: 92, + build: true, + }, + }, + "low-coverage": { + description: + "A passing build with many untested paths. The bugs appreciate the privacy.", + evidence: { + tests: 125, + coverage: 45, + build: true, + }, + }, + "critical-issue": { + description: + "A critical issue blocks the gate even when everything else looks cheerful.", + evidence: { + tests: 125, + coverage: 96, + build: true, + criticalIssues: 1, + }, + }, + "lint-failures": { + description: + "Lint failures are still failures, however decorative the semicolons feel.", + evidence: { + tests: 125, + coverage: 96, + build: true, + lintFailures: 2, + }, + }, + "dirty-worktree": { + description: + "The laptop contains uncommitted changes. It is not the release archive.", + evidence: { + tests: 125, + coverage: 96, + build: true, + uncommittedChanges: true, + }, + }, + "feature-branch": { + description: + "Healthy evidence on a feature branch. Verify the intended release source.", + evidence: { + tests: 125, + coverage: 96, + build: true, + branch: "feature/zoomies", + }, + }, + "zero-tests": { + description: + "No tests ran. Zero failures is an especially small achievement here.", + evidence: { + tests: 0, + coverage: 0, + build: true, + }, + }, + "weekend-incident": { + description: + "A failed build, failing tests and a critical issue. Bring evidence, not a bigger confidence hat.", + evidence: { + tests: 125, + failingTests: 12, + coverage: 60, + build: false, + criticalIssues: 1, + day: "saturday", + }, + }, +}; +export function releaseScenarios() { + return Object.entries(scenarios).map(([name, value]) => ({ + name, + description: value.description, + evidence: { ...value.evidence }, + })); +} +export function scenarioEvidence(name) { + if (typeof name !== "string" || !Object.hasOwn(scenarios, name)) + throw new RangeError( + "Unknown scenario. Choose: " + Object.keys(scenarios).join(", "), + ); + return { ...scenarios[name].evidence }; +}