uva-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ import { select, text, confirm, isCancel, log } from '@clack/prompts'
2
+ import { bannerIntro, bannerOutro, bannerCancelled } from '../lib/banner.mjs'
3
+ import { getGitRoot } from '../lib/git.mjs'
4
+ import { slugify } from '../lib/slugify.mjs'
5
+ import { execSync } from 'child_process'
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
7
+ import { join, dirname } from 'path'
8
+ import { fileURLToPath } from 'url'
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+
12
+ // ── Template registry ──────────────────────────────────────────────────────
13
+
14
+ const CATEGORIES = {
15
+ docs: {
16
+ label: 'Docs — documentation files',
17
+ types: [
18
+ {
19
+ value: 'adr',
20
+ label: 'ADR Architecture Decision Record',
21
+ folder: 'docs/decisions',
22
+ },
23
+ {
24
+ value: 'decision-log',
25
+ label: 'Decision Log Lightweight decision entry',
26
+ folder: 'docs/decisions',
27
+ },
28
+ {
29
+ value: 'meeting-notes',
30
+ label: 'Meeting Notes Agenda, notes, and action items',
31
+ folder: 'docs/meetings',
32
+ },
33
+ { value: 'rfc', label: 'RFC Technical proposal / spec', folder: 'docs/rfcs' },
34
+ {
35
+ value: 'runbook',
36
+ label: 'Runbook Operational step-by-step guide',
37
+ folder: 'docs/runbooks',
38
+ },
39
+ ],
40
+ },
41
+ git: {
42
+ label: 'Git — GitHub workflow files',
43
+ types: [
44
+ {
45
+ value: 'pull-request',
46
+ label: 'PR Template GitHub pull request template',
47
+ folder: '.github',
48
+ fixedName: 'pull_request_template.md',
49
+ },
50
+ ],
51
+ },
52
+ }
53
+
54
+ const LANGUAGES = [
55
+ { value: 'en', label: 'English' },
56
+ { value: 'pt-br', label: 'Português (BR)' },
57
+ ]
58
+
59
+ // ── Helpers ────────────────────────────────────────────────────────────────
60
+
61
+ function getAuthor() {
62
+ try {
63
+ return execSync('git config user.name', { encoding: 'utf-8' }).trim() || 'unknown'
64
+ } catch {
65
+ return 'unknown'
66
+ }
67
+ }
68
+
69
+ function getDate() {
70
+ return new Date().toISOString().split('T')[0]
71
+ }
72
+
73
+ function applyTemplate(content, vars) {
74
+ return content
75
+ .replace(/\{\{title\}\}/g, vars.title)
76
+ .replace(/\{\{date\}\}/g, vars.date)
77
+ .replace(/\{\{author\}\}/g, vars.author)
78
+ }
79
+
80
+ // ── Command ────────────────────────────────────────────────────────────────
81
+
82
+ // Called by commander as runNewFile(options, command)
83
+ export async function runNewFile(opts = {}) {
84
+ bannerIntro('new-file')
85
+
86
+ // ── Language ─────────────────────────────────────────────────────────────
87
+ let lang
88
+ if (opts.lang) {
89
+ const valid = LANGUAGES.map((l) => l.value)
90
+ if (!valid.includes(opts.lang)) {
91
+ log.error(`Invalid --lang "${opts.lang}". Valid values: ${valid.join(', ')}`)
92
+ process.exit(1)
93
+ }
94
+ lang = opts.lang
95
+ } else {
96
+ lang = await select({ message: 'Language', options: LANGUAGES })
97
+ if (isCancel(lang)) {
98
+ bannerCancelled()
99
+ process.exit(0)
100
+ }
101
+ }
102
+
103
+ // ── Category ─────────────────────────────────────────────────────────────
104
+ let category
105
+ if (opts.category) {
106
+ const valid = Object.keys(CATEGORIES)
107
+ if (!valid.includes(opts.category)) {
108
+ log.error(`Invalid --category "${opts.category}". Valid values: ${valid.join(', ')}`)
109
+ process.exit(1)
110
+ }
111
+ category = opts.category
112
+ } else {
113
+ category = await select({
114
+ message: 'Category',
115
+ options: Object.entries(CATEGORIES).map(([value, { label }]) => ({ value, label })),
116
+ })
117
+ if (isCancel(category)) {
118
+ bannerCancelled()
119
+ process.exit(0)
120
+ }
121
+ }
122
+
123
+ // ── Type ─────────────────────────────────────────────────────────────────
124
+ const types = CATEGORIES[category].types
125
+ let type
126
+ if (opts.type) {
127
+ const valid = types.map((t) => t.value)
128
+ if (!valid.includes(opts.type)) {
129
+ log.error(
130
+ `Invalid --type "${opts.type}" for category "${category}". Valid values: ${valid.join(', ')}`,
131
+ )
132
+ process.exit(1)
133
+ }
134
+ type = opts.type
135
+ } else {
136
+ type = await select({
137
+ message: 'File type',
138
+ options: types.map((t) => ({ value: t.value, label: t.label })),
139
+ })
140
+ if (isCancel(type)) {
141
+ bannerCancelled()
142
+ process.exit(0)
143
+ }
144
+ }
145
+
146
+ const def = types.find((t) => t.value === type)
147
+ const templatePath = join(__dirname, '../templates', lang, category, `${type}.md`)
148
+
149
+ if (!existsSync(templatePath)) {
150
+ log.error(`Template not found: templates/${lang}/${category}/${type}.md`)
151
+ process.exit(1)
152
+ }
153
+
154
+ const author = getAuthor()
155
+ const date = getDate()
156
+
157
+ // ── Git templates: fixed output path, no name needed ─────────────────────
158
+ if (def.fixedName) {
159
+ const root = getGitRoot()
160
+ const absFolder = join(root, def.folder)
161
+ const absFile = join(absFolder, def.fixedName)
162
+ const relPath = join(def.folder, def.fixedName)
163
+
164
+ if (existsSync(absFile)) {
165
+ const overwrite = await confirm({ message: `"${relPath}" already exists. Overwrite?` })
166
+ if (isCancel(overwrite) || !overwrite) {
167
+ bannerCancelled()
168
+ process.exit(0)
169
+ }
170
+ }
171
+
172
+ const content = applyTemplate(readFileSync(templatePath, { encoding: 'utf-8' }), {
173
+ title: '',
174
+ date,
175
+ author,
176
+ })
177
+ if (!existsSync(absFolder)) mkdirSync(absFolder, { recursive: true })
178
+ writeFileSync(absFile, content, { encoding: 'utf-8' })
179
+ bannerOutro(`Created: ${relPath}`)
180
+ return
181
+ }
182
+
183
+ // ── Docs templates: need a title and output folder ────────────────────────
184
+ let rawName
185
+ if (opts.name) {
186
+ rawName = opts.name
187
+ } else {
188
+ const input = await text({
189
+ message: 'Title',
190
+ placeholder: 'Switch auth library to Lucia',
191
+ validate: (v) => (v.trim() ? undefined : 'Title cannot be empty.'),
192
+ })
193
+ if (isCancel(input)) {
194
+ bannerCancelled()
195
+ process.exit(0)
196
+ }
197
+ rawName = input.trim()
198
+ }
199
+
200
+ const fileName = `${slugify(rawName)}.md`
201
+
202
+ let outputFolder
203
+ if (opts.folder) {
204
+ outputFolder = opts.folder
205
+ } else {
206
+ const input = await text({
207
+ message: 'Output folder',
208
+ initialValue: def.folder,
209
+ validate: (v) => (v.trim() ? undefined : 'Folder cannot be empty.'),
210
+ })
211
+ if (isCancel(input)) {
212
+ bannerCancelled()
213
+ process.exit(0)
214
+ }
215
+ outputFolder = input.trim()
216
+ }
217
+
218
+ const root = getGitRoot()
219
+ const absFolder = join(root, outputFolder)
220
+ const absFile = join(absFolder, fileName)
221
+ const relPath = join(outputFolder, fileName)
222
+
223
+ if (existsSync(absFile)) {
224
+ const overwrite = await confirm({ message: `"${relPath}" already exists. Overwrite?` })
225
+ if (isCancel(overwrite) || !overwrite) {
226
+ bannerCancelled()
227
+ process.exit(0)
228
+ }
229
+ }
230
+
231
+ const content = applyTemplate(readFileSync(templatePath, { encoding: 'utf-8' }), {
232
+ title: rawName,
233
+ date,
234
+ author,
235
+ })
236
+
237
+ if (!existsSync(absFolder)) mkdirSync(absFolder, { recursive: true })
238
+ writeFileSync(absFile, content, { encoding: 'utf-8' })
239
+ bannerOutro(`Created: ${relPath}`)
240
+ }
@@ -0,0 +1,31 @@
1
+ import { confirm, isCancel, log } from '@clack/prompts'
2
+ import { bannerIntro, bannerOutro, bannerCancelled } from '../lib/banner.mjs'
3
+ import { getCurrentBranch } from '../lib/git.mjs'
4
+ import { spawnSync } from 'child_process'
5
+
6
+ // Called by commander as runPush(options, command)
7
+ export async function runPush(opts = {}) {
8
+ bannerIntro('push')
9
+
10
+ const branch = getCurrentBranch()
11
+
12
+ if (!opts.yes) {
13
+ const confirmed = await confirm({
14
+ message: `Push current branch: ${branch}?`,
15
+ })
16
+ if (isCancel(confirmed) || !confirmed) {
17
+ bannerCancelled()
18
+ process.exit(0)
19
+ }
20
+ }
21
+
22
+ const result = spawnSync('git', ['push', 'origin', branch], {
23
+ stdio: ['pipe', 'inherit', 'inherit'],
24
+ })
25
+ if (result.status !== 0) {
26
+ log.error('Push failed — see the output above.')
27
+ process.exit(1)
28
+ }
29
+
30
+ bannerOutro(`Pushed: origin/${branch}`)
31
+ }
@@ -0,0 +1,63 @@
1
+ import { select, isCancel, intro, outro } from '@clack/prompts'
2
+ import pc from 'picocolors'
3
+ import { banner as brandBanner, uva, folha } from '../lib/colors.mjs'
4
+ import { runInit } from './init.mjs'
5
+ import { runCommit } from './commit.mjs'
6
+ import { runBranch } from './branch.mjs'
7
+ import { runNewFile } from './new-file.mjs'
8
+ import { runPush } from './push.mjs'
9
+
10
+ export async function runStart() {
11
+ intro(brandBanner('UVA CLI'))
12
+
13
+ console.log('')
14
+ console.log(uva(' uva-cli') + pc.dim(' — Git workflow automation'))
15
+ console.log('')
16
+ console.log(pc.dim(' Guides your team through branch creation and commits'))
17
+ console.log(pc.dim(' following whatever conventions your project defines.'))
18
+ console.log('')
19
+
20
+ const action = await select({
21
+ message: 'What do you want to do?',
22
+ options: [
23
+ {
24
+ value: 'init',
25
+ label: folha('uva init') + ' Set up UVA CLI for this project',
26
+ hint: 'configure commit and branch patterns',
27
+ },
28
+ {
29
+ value: 'commit',
30
+ label: uva('uva commit') + ' Create an interactive commit',
31
+ hint: 'select files, type, ticket and message',
32
+ },
33
+ {
34
+ value: 'branch',
35
+ label: uva('uva branch') + ' Create a new branch',
36
+ hint: 'checks out source and pulls automatically',
37
+ },
38
+ {
39
+ value: 'new-file',
40
+ label: uva('uva new-file') + ' Scaffold a file from a template',
41
+ hint: 'docs, frontend (React), or backend (Express)',
42
+ },
43
+ {
44
+ value: 'push',
45
+ label: uva('uva push') + ' Push the current branch to origin',
46
+ hint: 'confirms the branch and runs git push',
47
+ },
48
+ ],
49
+ })
50
+
51
+ if (isCancel(action)) {
52
+ outro(pc.dim('Operation cancelled.'))
53
+ process.exit(0)
54
+ }
55
+
56
+ console.log('')
57
+
58
+ if (action === 'init') return runInit()
59
+ if (action === 'commit') return runCommit()
60
+ if (action === 'branch') return runBranch()
61
+ if (action === 'new-file') return runNewFile()
62
+ if (action === 'push') return runPush()
63
+ }
package/lib/banner.mjs ADDED
@@ -0,0 +1,15 @@
1
+ import { intro, outro } from '@clack/prompts'
2
+ import pc from 'picocolors'
3
+ import { banner as brandBanner, folha } from './colors.mjs'
4
+
5
+ export function bannerIntro(command) {
6
+ intro(brandBanner('UVA CLI') + ' ' + pc.dim(command))
7
+ }
8
+
9
+ export function bannerOutro(message) {
10
+ outro(folha('+ ' + message))
11
+ }
12
+
13
+ export function bannerCancelled() {
14
+ outro(pc.dim('Operation cancelled.'))
15
+ }
package/lib/colors.mjs ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Brand colors — ANSI true color (24-bit).
3
+ * Falls back to basic ANSI if the terminal reports no true-color support.
4
+ *
5
+ * Dark-mode palette used for terminal output:
6
+ * uva #A78BFA (light purple — readable on dark bg)
7
+ * folha #34D399 (light green)
8
+ * bg #6D28D9 (deep purple — banner background)
9
+ * tinta #1C1B22 (near-black ink)
10
+ */
11
+
12
+ const TRUE_COLOR =
13
+ process.env.COLORTERM === 'truecolor' ||
14
+ process.env.COLORTERM === '24bit' ||
15
+ process.env.TERM_PROGRAM === 'iTerm.app' ||
16
+ process.env.TERM_PROGRAM === 'vscode'
17
+
18
+ function hexRgb(hex) {
19
+ const n = parseInt(hex.replace('#', ''), 16)
20
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
21
+ }
22
+
23
+ /** Apply foreground and/or background hex color to text. */
24
+ function paint(text, { fg, bg } = {}) {
25
+ if (!TRUE_COLOR) return text
26
+ let codes = ''
27
+ if (bg) {
28
+ const [r, g, b] = hexRgb(bg)
29
+ codes += `\x1b[48;2;${r};${g};${b}m`
30
+ }
31
+ if (fg) {
32
+ const [r, g, b] = hexRgb(fg)
33
+ codes += `\x1b[38;2;${r};${g};${b}m`
34
+ }
35
+ return `${codes}${text}\x1b[0m`
36
+ }
37
+
38
+ // ── Brand helpers ──────────────────────────────────────────────────────────
39
+
40
+ /** Light purple text — for general UVA highlights. */
41
+ export const uva = (text) => paint(text, { fg: '#A78BFA' })
42
+
43
+ /** Light green text — for success messages and outros. */
44
+ export const folha = (text) => paint(text, { fg: '#34D399' })
45
+
46
+ /** Deep-purple background + light-purple text — for the intro banner pill. */
47
+ export const banner = (text) => paint(` ${text} `, { fg: '#A78BFA', bg: '#6D28D9' })
48
+
49
+ /** Light-purple dim text — for secondary info (falls back to the raw string). */
50
+ export const dim = (text) => paint(text, { fg: '#6D4FA8' })
package/lib/config.mjs ADDED
@@ -0,0 +1,34 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
2
+ import { join } from 'path'
3
+ import { log } from '@clack/prompts'
4
+ import { getGitRoot } from './git.mjs'
5
+
6
+ function getConfigPath() {
7
+ const root = getGitRoot()
8
+ return join(root, 'uva.config.json')
9
+ }
10
+
11
+ export function loadConfig() {
12
+ const path = getConfigPath()
13
+ if (!existsSync(path)) return null
14
+ try {
15
+ return JSON.parse(readFileSync(path, { encoding: 'utf-8' }))
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ export function saveConfig(config) {
22
+ const path = getConfigPath()
23
+ writeFileSync(path, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf-8' })
24
+ return path
25
+ }
26
+
27
+ export function requireConfig() {
28
+ const config = loadConfig()
29
+ if (!config) {
30
+ log.error('No configuration found. Run `uva init` to set up your project.')
31
+ process.exit(1)
32
+ }
33
+ return config
34
+ }
package/lib/format.mjs ADDED
@@ -0,0 +1,26 @@
1
+ import { slugify } from './slugify.mjs'
2
+
3
+ export function buildCommitMessage(format, { type, ticket, message }) {
4
+ switch (format) {
5
+ case 'conventional-ticket':
6
+ return `${type}(${ticket}): ${message}`
7
+ case 'ticket-conventional':
8
+ return `[${ticket}] ${type}: ${message}`
9
+ case 'conventional':
10
+ default:
11
+ return `${type}: ${message}`
12
+ }
13
+ }
14
+
15
+ export function buildBranchName(format, { type, ticket, name }) {
16
+ const slug = slugify(name)
17
+ switch (format) {
18
+ case 'type-ticket-name':
19
+ return `${type}/${ticket}_${slug}`
20
+ case 'ticket-type-name':
21
+ return `${ticket}/${type}/${slug}`
22
+ case 'type-name':
23
+ default:
24
+ return `${type}/${slug}`
25
+ }
26
+ }
package/lib/git.mjs ADDED
@@ -0,0 +1,67 @@
1
+ import { execSync, spawnSync } from 'child_process'
2
+
3
+ function exec(cmd) {
4
+ return execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim()
5
+ }
6
+
7
+ export function getCurrentBranch() {
8
+ return exec('git rev-parse --abbrev-ref HEAD')
9
+ }
10
+
11
+ export function getGitRoot() {
12
+ return exec('git rev-parse --show-toplevel')
13
+ }
14
+
15
+ export function getChangedFiles() {
16
+ const output = exec('git status --porcelain')
17
+ if (!output) return []
18
+ return output
19
+ .split('\n')
20
+ .filter((line) => line.trim())
21
+ .map((line) => {
22
+ // format: XY<space>path (3 chars) or X<space>path (2 chars when Y is space)
23
+ const pathStart = line[2] === ' ' ? 3 : 2
24
+ return {
25
+ status: line.slice(0, pathStart - 1).trim(),
26
+ path: line.slice(pathStart).trim(),
27
+ }
28
+ })
29
+ }
30
+
31
+ export function hasUncommittedChanges() {
32
+ return getChangedFiles().length > 0
33
+ }
34
+
35
+ export function checkout(branch) {
36
+ exec(`git checkout ${branch}`)
37
+ }
38
+
39
+ export function pull() {
40
+ exec('git pull')
41
+ }
42
+
43
+ export function createBranch(name) {
44
+ exec(`git checkout -b ${name}`)
45
+ }
46
+
47
+ export function addFiles(paths) {
48
+ // spawnSync with an array of args avoids any shell quoting issues
49
+ const result = spawnSync('git', ['add', '--', ...paths], {
50
+ encoding: 'utf-8',
51
+ stdio: 'pipe',
52
+ })
53
+ if (result.status !== 0) {
54
+ throw new Error(result.stderr || 'git add falhou')
55
+ }
56
+ }
57
+
58
+ export function commit(message) {
59
+ // pipe stdin to avoid conflicts with the interactive terminal of clack
60
+ // inherit stdout/stderr so that lefthook/commitlint appear normally
61
+ const result = spawnSync('git', ['commit', '-m', message], {
62
+ stdio: ['pipe', 'inherit', 'inherit'],
63
+ })
64
+ if (result.status !== 0) {
65
+ throw new Error('git commit falhou — veja a saída acima')
66
+ }
67
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Converts free text to kebab-case without accents.
3
+ * Example: "Add Login Screen" -> "add-login-screen"
4
+ */
5
+ export function slugify(text) {
6
+ return text
7
+ .normalize('NFD')
8
+ .replace(/[\u0300-\u036f]/g, '') // remove diacritics
9
+ .toLowerCase()
10
+ .replace(/\s+/g, '-')
11
+ .replace(/[^a-z0-9-]/g, '')
12
+ .replace(/-+/g, '-')
13
+ .replace(/^-|-$/g, '')
14
+ }
package/lib/types.mjs ADDED
@@ -0,0 +1,45 @@
1
+ export const COMMIT_TYPES = [
2
+ { value: 'feat', label: 'feat · New feature' },
3
+ { value: 'fix', label: 'fix · Bug fix' },
4
+ { value: 'docs', label: 'docs · Documentation change' },
5
+ { value: 'style', label: 'style · Formatting, no logic change' },
6
+ { value: 'refactor', label: 'refactor · Refactor without behavior change' },
7
+ { value: 'test', label: 'test · Add or fix tests' },
8
+ { value: 'chore', label: 'chore · Build tasks, configs, dependencies' },
9
+ ]
10
+
11
+ export const COMMIT_FORMATS = [
12
+ {
13
+ value: 'conventional-ticket',
14
+ label: 'feat(PROJ-42): add login screen',
15
+ hint: 'Conventional Commits with ticket',
16
+ },
17
+ {
18
+ value: 'conventional',
19
+ label: 'feat: add login screen',
20
+ hint: 'Conventional Commits, no ticket',
21
+ },
22
+ {
23
+ value: 'ticket-conventional',
24
+ label: '[PROJ-42] feat: add login screen',
25
+ hint: 'Ticket first, then conventional type',
26
+ },
27
+ ]
28
+
29
+ export const BRANCH_FORMATS = [
30
+ {
31
+ value: 'type-ticket-name',
32
+ label: 'feat/PROJ-42_add-login-screen',
33
+ hint: 'type/ticket_name',
34
+ },
35
+ {
36
+ value: 'type-name',
37
+ label: 'feat/add-login-screen',
38
+ hint: 'type/name (no ticket in branch)',
39
+ },
40
+ {
41
+ value: 'ticket-type-name',
42
+ label: 'PROJ-42/feat/add-login-screen',
43
+ hint: 'ticket/type/name',
44
+ },
45
+ ]
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "uva-cli",
3
+ "version": "1.0.0",
4
+ "description": "UVA CLI — Interactive Git workflow automation for teams",
5
+ "type": "module",
6
+ "bin": {
7
+ "uva": "./bin.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "files": [
13
+ "bin.mjs",
14
+ "commands/",
15
+ "lib/",
16
+ "templates/"
17
+ ],
18
+ "keywords": [
19
+ "cli",
20
+ "git",
21
+ "conventional-commits",
22
+ "workflow",
23
+ "automation",
24
+ "interactive",
25
+ "branching"
26
+ ],
27
+ "license": "MIT",
28
+ "scripts": {
29
+ "test": "node --test tests/slugify.test.mjs tests/format.test.mjs",
30
+ "lint": "eslint .",
31
+ "format": "prettier --write .",
32
+ "format:check": "prettier --check .",
33
+ "prepare": "lefthook install",
34
+ "prepublishOnly": "npm run format:check && npm run lint && npm test"
35
+ },
36
+ "dependencies": {
37
+ "@clack/prompts": "^0.9.0",
38
+ "commander": "^12.0.0",
39
+ "picocolors": "^1.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^10.0.1",
43
+ "eslint": "^10.10.0",
44
+ "lefthook": "^2.1.12",
45
+ "prettier": "^3.9.6"
46
+ }
47
+ }
@@ -0,0 +1,45 @@
1
+ # ADR: {{title}}
2
+
3
+ **Date:** {{date}}
4
+ **Author:** {{author}}
5
+ **Status:** Proposed
6
+
7
+ ---
8
+
9
+ ## Context
10
+
11
+ <!-- What is the situation? What forces are at play — technical, product, team, or otherwise?
12
+ Include enough background for someone unfamiliar with the problem to understand it. -->
13
+
14
+ ## Decision
15
+
16
+ <!-- State the decision clearly and directly. -->
17
+
18
+ ## Rationale
19
+
20
+ <!-- Why this option over the others? What trade-offs were accepted? -->
21
+
22
+ ## Consequences
23
+
24
+ ### Positive
25
+
26
+ -
27
+
28
+ ### Negative
29
+
30
+ -
31
+
32
+ ### Risks
33
+
34
+ -
35
+
36
+ ## Alternatives Considered
37
+
38
+ | Alternative | Why it was not chosen |
39
+ | :---------- | :-------------------- |
40
+ | | |
41
+ | | |
42
+
43
+ ## References
44
+
45
+ -