dr-gen 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -19
- package/dist/cli.js +71 -2
- package/dist/generator.js +7 -1
- package/dist/interactive.js +10 -7
- package/dist/list.js +230 -0
- package/dist/messages.js +78 -11
- package/dist/parser.js +2 -0
- package/dist/template.js +2 -0
- package/dist/text.js +24 -0
- package/dist/yaml.js +2 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# decision-record-generator (dr-gen)
|
|
2
2
|
|
|
3
3
|
Generate lightweight Decision Records (DRs) from `decision.yaml`.
|
|
4
|
-
Built for fast-moving teams who need a small, reproducible “evidence trail” of decisions (why/
|
|
4
|
+
Built for fast-moving teams who need a small, reproducible “evidence trail” of decisions (why/decision) for handoffs, audits, and security questionnaires.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
日本語は下の「Japanese (日本語)」セクションにまとめています。
|
|
7
7
|
|
|
8
8
|
**Outputs (4 files)**
|
|
9
9
|
- `decision-record.md` (human-readable)
|
|
@@ -11,10 +11,54 @@ Built for fast-moving teams who need a small, reproducible “evidence trail”
|
|
|
11
11
|
- `repro.md` (reproducibility notes)
|
|
12
12
|
- `manifest.json` (SHA256 hashes for tamper detection)
|
|
13
13
|
|
|
14
|
+
## Input (decision.yaml)
|
|
15
|
+
|
|
16
|
+
All fields except `title` are optional. Empty strings are OK.
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
title: "Use PostgreSQL for user data" # required
|
|
20
|
+
date: "2024-01-15" # optional (YYYY-MM-DD)
|
|
21
|
+
decider: "Alice (PM)" # optional
|
|
22
|
+
|
|
23
|
+
# ADR-style lifecycle (optional)
|
|
24
|
+
status: "accepted" # optional: accepted | deprecated | superseded
|
|
25
|
+
|
|
26
|
+
# Tip: dr-gen is designed for post-decision records. For drafts, use `dr-gen new --skip-generate` instead of managing a "proposed" lifecycle.
|
|
27
|
+
|
|
28
|
+
# Linking DRs when a decision changes:
|
|
29
|
+
# - Recommended (minimal): set `supersedes` on the newer DR.
|
|
30
|
+
supersedes: "" # optional: link/id/path of the older DR (recommended)
|
|
31
|
+
|
|
32
|
+
context: "" # optional
|
|
33
|
+
why: "" # optional
|
|
34
|
+
rule: "" # optional (the decision/policy; rendered as "Decision" in decision-record.md)
|
|
35
|
+
alternatives: "" # optional
|
|
36
|
+
consequences: "" # optional
|
|
37
|
+
tags: [] # optional
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If a decision is reversed later, prefer creating a new DR and setting `supersedes` on the new DR (rather than rewriting history).
|
|
41
|
+
|
|
14
42
|
## Operations (Drive / Box)
|
|
15
43
|
|
|
16
|
-
If you store DRs in Drive / Box (common for non-developers), see:
|
|
17
|
-
- [DRIVE_BOX_OPERATION_GUIDE.md](DRIVE_BOX_OPERATION_GUIDE.md)
|
|
44
|
+
If you store DRs in Drive / Box (common for non-developers), see this guide (Japanese):
|
|
45
|
+
- [DRIVE_BOX_OPERATION_GUIDE.md](https://github.com/Ineeza/decision-record-generator/blob/main/DRIVE_BOX_OPERATION_GUIDE.md)
|
|
46
|
+
|
|
47
|
+
## Verify (integrity / tamper detection)
|
|
48
|
+
|
|
49
|
+
To verify that generated files were not modified after generation, run:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
dr-gen verify out/<date>__<title>__<id>/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Without installing globally:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx dr-gen@latest verify out/<date>__<title>__<id>/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This checks `decision-record.md`, `summary.json`, and `repro.md` against hashes in `manifest.json`.
|
|
18
62
|
|
|
19
63
|
## Fastest try (npm)
|
|
20
64
|
|
|
@@ -33,7 +77,15 @@ If you don't want to write YAML by hand:
|
|
|
33
77
|
dr-gen new
|
|
34
78
|
```
|
|
35
79
|
|
|
36
|
-
|
|
80
|
+
Japanese prompts:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
dr-gen new --lang ja
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can also set `DR_GEN_LANG=ja`.
|
|
87
|
+
|
|
88
|
+
This asks for the minimum useful info (Title + Why + Decision). Other fields are optional.
|
|
37
89
|
By default, `date` is set to today's date (YYYY-MM-DD). To disable this:
|
|
38
90
|
|
|
39
91
|
```bash
|
|
@@ -44,6 +96,14 @@ By default this creates:
|
|
|
44
96
|
- `in/<date>__<title>__<id>/decision.yaml`
|
|
45
97
|
- `out/<date>__<title>__<id>/` (generated files)
|
|
46
98
|
|
|
99
|
+
If you want to fill more fields (status/alternatives/etc) before generating outputs, use:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
dr-gen new --skip-generate
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then edit the generated `decision.yaml`, and run `dr-gen generate`.
|
|
106
|
+
|
|
47
107
|
## Quickstart
|
|
48
108
|
|
|
49
109
|
```bash
|
|
@@ -98,30 +158,127 @@ If you prefer a different base output directory:
|
|
|
98
158
|
dr-gen generate decision.yaml --out-dir some-dir
|
|
99
159
|
```
|
|
100
160
|
|
|
101
|
-
|
|
161
|
+
## CLI
|
|
102
162
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
需要があれば日本語表示(i18n)も検討します。
|
|
163
|
+
```bash
|
|
164
|
+
dr-gen generate decision.yaml
|
|
165
|
+
```
|
|
107
166
|
|
|
108
|
-
|
|
167
|
+
Other commands:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
dr-gen new
|
|
171
|
+
dr-gen verify out/<date>__<title>__<id>/
|
|
172
|
+
dr-gen list --from 2026-01-01 --to 2026-01-31
|
|
173
|
+
|
|
174
|
+
# Japanese report labels
|
|
175
|
+
dr-gen list --from 2026-01-01 --to 2026-01-31 --lang ja
|
|
176
|
+
|
|
177
|
+
# Optional: keep the Decision line on one terminal line
|
|
178
|
+
dr-gen list --from 2026-01-01 --to 2026-01-31 --max-decision-len 60
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Japanese (日本語)
|
|
184
|
+
|
|
185
|
+
引き継ぎ・監査対応・セキュリティに関する確認(質問対応)のために、意思決定(Why / Decision)を小さく・再現可能な形で残すDRジェネレーターです。`decision.yaml` から Markdown / JSON を生成し、`manifest.json` の SHA256 ハッシュで改ざん検知できます。
|
|
186
|
+
|
|
187
|
+
### 最速で試す(npm)
|
|
109
188
|
|
|
110
189
|
```bash
|
|
190
|
+
npm install -g dr-gen
|
|
191
|
+
|
|
192
|
+
# decision.yaml が無い場合はテンプレを作成します
|
|
111
193
|
dr-gen generate decision.yaml
|
|
112
194
|
```
|
|
113
195
|
|
|
114
|
-
|
|
196
|
+
### YAMLなし(対話形式)
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
dr-gen new
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
日本語で質問を表示する場合:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
dr-gen new --lang ja
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
環境変数で指定することもできます(例: `DR_GEN_LANG=ja`)。
|
|
209
|
+
|
|
210
|
+
Title / Why / Decision を中心に入力します(他は任意)。`--no-date` で日付自動入力を無効にできます。
|
|
211
|
+
|
|
212
|
+
`status` / `alternatives` などを追記してから生成したい場合は、まず `decision.yaml` だけ作るのがおすすめです。
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
dr-gen new --skip-generate
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
その後、生成された `decision.yaml` を編集してから `dr-gen generate` を実行します。
|
|
219
|
+
|
|
220
|
+
### 検証(改ざん検知)
|
|
221
|
+
|
|
222
|
+
生成後のファイルが変更されていないか確認するには、`manifest.json` と照合します。
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npx dr-gen@latest verify out/<date>__<title>__<id>/
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 一覧(レポート出力)
|
|
229
|
+
|
|
230
|
+
期間を指定して、コピペしやすい Markdown レポートを標準出力に出します。
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
dr-gen list --from 2026-01-01 --to 2026-01-31
|
|
234
|
+
|
|
235
|
+
# レポート文言を日本語にする
|
|
236
|
+
dr-gen list --from 2026-01-01 --to 2026-01-31 --lang ja
|
|
237
|
+
|
|
238
|
+
# 任意: Decision 行の文字数上限(デフォルト: 80)
|
|
239
|
+
dr-gen list --from 2026-01-01 --to 2026-01-31 --max-decision-len 60
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Drive / Box 運用
|
|
243
|
+
|
|
244
|
+
非エンジニアの運用で Drive / Box に保存する場合は、ガイドを参照してください。
|
|
245
|
+
|
|
246
|
+
- [DRIVE_BOX_OPERATION_GUIDE.md](https://github.com/Ineeza/decision-record-generator/blob/main/DRIVE_BOX_OPERATION_GUIDE.md)
|
|
247
|
+
|
|
248
|
+
### 入力(decision.yaml)
|
|
249
|
+
|
|
250
|
+
`title` 以外はすべて任意です。空文字でもOKです。
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
title: "ユーザーデータはPostgreSQLを使う" # 必須
|
|
254
|
+
date: "2024-01-15" # 任意(YYYY-MM-DD)
|
|
255
|
+
decider: "Alice (PM)" # 任意
|
|
256
|
+
|
|
257
|
+
# ADR風のライフサイクル管理(任意)
|
|
258
|
+
status: "accepted" # 任意: accepted | deprecated | superseded
|
|
259
|
+
|
|
260
|
+
# Tip: dr-gen は「決定後に残す」用途がメインです。下書きは `dr-gen new --skip-generate` で YAML だけ作るのがおすすめです。
|
|
261
|
+
|
|
262
|
+
# DRのリンク(判断が変わったとき):
|
|
263
|
+
# - 推奨(最小運用): 新しいDRに `supersedes` を書く
|
|
264
|
+
supersedes: "" # 任意: 以前のDRのリンク/ID/パス(推奨)
|
|
265
|
+
|
|
266
|
+
context: "" # 任意
|
|
267
|
+
why: "" # 任意
|
|
268
|
+
rule: "" # 任意(決めたこと/方針。decision-record.md では "Decision" として表示)
|
|
269
|
+
alternatives: "" # 任意
|
|
270
|
+
consequences: "" # 任意
|
|
271
|
+
tags: [] # 任意
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
後から判断が覆った場合は、既存DRを書き換えるよりも「新しいDRを作り、新DRに `supersedes` を書く」運用がおすすめです。
|
|
275
|
+
|
|
115
276
|
|
|
116
|
-
|
|
117
|
-
OSS として無料で提供しています。
|
|
277
|
+
### ライセンスについて
|
|
118
278
|
|
|
119
|
-
|
|
279
|
+
本ソフトウェアは MIT License のもとで OSS として無料で提供しています(個人・法人を問わず利用可能です)。
|
|
120
280
|
|
|
121
|
-
|
|
122
|
-
サポート、利用条件の明確化、
|
|
123
|
-
継続的な提供が必要な場合は、
|
|
124
|
-
法人契約をご検討ください。
|
|
281
|
+
組織・チームでの利用において、サポート、利用条件の明確化、継続的な提供が必要な場合は、法人契約をご検討ください。
|
|
125
282
|
|
|
126
283
|
法人契約はこちら:
|
|
127
284
|
https://www.ineeza.com/
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,9 @@ import { decisionFolderName } from './naming.js';
|
|
|
10
10
|
import { promptDecisionRecord } from './interactive.js';
|
|
11
11
|
import { renderDecisionYaml } from './yaml.js';
|
|
12
12
|
import { verifyOutDir } from './verify.js';
|
|
13
|
-
import {
|
|
13
|
+
import { areTitleAndRuleTooSimilar } from './text.js';
|
|
14
|
+
import { listDecisions, renderListReportMarkdown } from './list.js';
|
|
15
|
+
import { generatedOutputLines, missingCoreFieldsLines, signatureIgnoredLines, skippedGenerateLines, templateCreatedLines, titleRuleTooSimilarLines, wroteInputLines } from './messages.js';
|
|
14
16
|
function getToolVersion() {
|
|
15
17
|
try {
|
|
16
18
|
const require = createRequire(import.meta.url);
|
|
@@ -21,6 +23,19 @@ function getToolVersion() {
|
|
|
21
23
|
return undefined;
|
|
22
24
|
}
|
|
23
25
|
}
|
|
26
|
+
function resolveLang(cliLangRaw) {
|
|
27
|
+
const cliLang = (cliLangRaw ?? '').trim().toLowerCase();
|
|
28
|
+
const envLang = (process.env.DR_GEN_LANG ?? '').trim().toLowerCase();
|
|
29
|
+
const candidate = cliLang.length > 0 ? cliLang : envLang;
|
|
30
|
+
if (candidate === 'ja' || candidate === 'jp' || candidate === 'japanese')
|
|
31
|
+
return 'ja';
|
|
32
|
+
if (candidate === 'en' || candidate === 'english')
|
|
33
|
+
return 'en';
|
|
34
|
+
const locale = `${process.env.LC_ALL ?? ''} ${process.env.LC_MESSAGES ?? ''} ${process.env.LANG ?? ''}`.toLowerCase();
|
|
35
|
+
if (locale.includes('ja'))
|
|
36
|
+
return 'ja';
|
|
37
|
+
return 'en';
|
|
38
|
+
}
|
|
24
39
|
async function fileExists(filePath) {
|
|
25
40
|
try {
|
|
26
41
|
await fs.stat(filePath);
|
|
@@ -77,6 +92,25 @@ function warnIfMissingCoreFields(record, inputPath) {
|
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
}
|
|
95
|
+
function warnIfTitleRuleTooSimilar(record, lang) {
|
|
96
|
+
const rule = record.rule ?? '';
|
|
97
|
+
if (areTitleAndRuleTooSimilar(record.title, rule)) {
|
|
98
|
+
for (const line of titleRuleTooSimilarLines(lang)) {
|
|
99
|
+
console.warn(line);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function parsePositiveIntOrThrow(value, label) {
|
|
104
|
+
const trimmed = value.trim();
|
|
105
|
+
if (trimmed.length === 0) {
|
|
106
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
107
|
+
}
|
|
108
|
+
const n = Number.parseInt(trimmed, 10);
|
|
109
|
+
if (!Number.isFinite(n) || Number.isNaN(n) || n <= 0) {
|
|
110
|
+
throw new Error(`${label} must be a positive integer (got: ${value}).`);
|
|
111
|
+
}
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
80
114
|
async function main(argv) {
|
|
81
115
|
const program = new Command();
|
|
82
116
|
const toolVersion = getToolVersion();
|
|
@@ -107,6 +141,7 @@ async function main(argv) {
|
|
|
107
141
|
}
|
|
108
142
|
const record = await parseDecisionYaml(input);
|
|
109
143
|
warnIfMissingCoreFields(record, input);
|
|
144
|
+
warnIfTitleRuleTooSimilar(record, resolveLang(undefined));
|
|
110
145
|
const baseOutDir = options.outDir;
|
|
111
146
|
const folder = decisionFolderName(record);
|
|
112
147
|
const finalOutDir = await findAvailableDirPath(baseOutDir, folder);
|
|
@@ -134,10 +169,14 @@ async function main(argv) {
|
|
|
134
169
|
.option('--in-dir <dir>', 'Base directory for inputs', 'in')
|
|
135
170
|
.option('-p, --path <file>', 'Where to write decision.yaml (overrides --in-dir)', '')
|
|
136
171
|
.option('-o, --out-dir <dir>', 'Output directory (created if missing)', 'out')
|
|
172
|
+
.option('--lang <lang>', 'Language for prompts (en|ja). Also supports DR_GEN_LANG env var.', '')
|
|
137
173
|
.option('--no-date', 'Do not auto-fill date')
|
|
174
|
+
.option('--skip-generate', 'Only write decision.yaml and do not generate output files', false)
|
|
138
175
|
.option('--force', 'Overwrite decision.yaml if it already exists', false)
|
|
139
176
|
.action(async (options) => {
|
|
140
|
-
const
|
|
177
|
+
const lang = resolveLang(options.lang);
|
|
178
|
+
const record = await promptDecisionRecord({ includeDate: options.date, lang });
|
|
179
|
+
warnIfTitleRuleTooSimilar(record, lang);
|
|
141
180
|
const desiredFolder = decisionFolderName(record);
|
|
142
181
|
const decisionDirName = await findAvailableDecisionDirName(options.inDir, options.outDir, desiredFolder);
|
|
143
182
|
const yamlPath = options.path.trim().length > 0 ? options.path : path.join(options.inDir, decisionDirName, 'decision.yaml');
|
|
@@ -149,6 +188,12 @@ async function main(argv) {
|
|
|
149
188
|
for (const line of wroteInputLines(yamlPath)) {
|
|
150
189
|
console.log(line);
|
|
151
190
|
}
|
|
191
|
+
if (options.skipGenerate) {
|
|
192
|
+
for (const line of skippedGenerateLines(yamlPath, options.outDir, lang)) {
|
|
193
|
+
console.log(line);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
152
197
|
const finalOutDir = path.join(options.outDir, decisionDirName);
|
|
153
198
|
const command = `dr-gen new${options.date ? '' : ' --no-date'} --in-dir ${options.inDir} --out-dir ${options.outDir}`;
|
|
154
199
|
await generateDecisionRecordFiles(record, {
|
|
@@ -190,6 +235,30 @@ async function main(argv) {
|
|
|
190
235
|
}
|
|
191
236
|
process.exitCode = 2;
|
|
192
237
|
});
|
|
238
|
+
program
|
|
239
|
+
.command('list')
|
|
240
|
+
.description('List generated decision records and print a copy-pasteable report')
|
|
241
|
+
.option('-o, --out-dir <dir>', 'Base output directory to scan', 'out')
|
|
242
|
+
.option('--from <YYYY-MM-DD>', 'Start date (inclusive) based on folder name prefix', '')
|
|
243
|
+
.option('--to <YYYY-MM-DD>', 'End date (inclusive) based on folder name prefix', '')
|
|
244
|
+
.option('--lang <lang>', 'Language for report labels (en|ja). Also supports DR_GEN_LANG env var.', '')
|
|
245
|
+
.option('--max-decision-len <n>', 'Max characters for the Decision line in the report (default: 80)', '80')
|
|
246
|
+
.action(async (options) => {
|
|
247
|
+
const from = options.from.trim().length > 0 ? options.from.trim() : undefined;
|
|
248
|
+
const to = options.to.trim().length > 0 ? options.to.trim() : undefined;
|
|
249
|
+
const maxDecisionLen = parsePositiveIntOrThrow(options.maxDecisionLen, '--max-decision-len');
|
|
250
|
+
const lang = resolveLang(options.lang);
|
|
251
|
+
const items = await listDecisions({ outDir: options.outDir, from, to });
|
|
252
|
+
const report = renderListReportMarkdown(items, {
|
|
253
|
+
outDir: options.outDir,
|
|
254
|
+
from,
|
|
255
|
+
to,
|
|
256
|
+
generatedAtIso: new Date().toISOString(),
|
|
257
|
+
maxDecisionLen,
|
|
258
|
+
lang
|
|
259
|
+
});
|
|
260
|
+
console.log(report);
|
|
261
|
+
});
|
|
193
262
|
await program.parseAsync(argv);
|
|
194
263
|
}
|
|
195
264
|
main(process.argv).catch((error) => {
|
package/dist/generator.js
CHANGED
|
@@ -16,6 +16,10 @@ function renderRecordMarkdown(record) {
|
|
|
16
16
|
meta.push(`**Date**: ${record.date}`);
|
|
17
17
|
if (record.decider !== undefined && record.decider.length > 0)
|
|
18
18
|
meta.push(`**Decider**: ${record.decider}`);
|
|
19
|
+
if (record.status !== undefined && record.status.length > 0)
|
|
20
|
+
meta.push(`**Status**: ${record.status}`);
|
|
21
|
+
if (record.supersedes !== undefined && record.supersedes.length > 0)
|
|
22
|
+
meta.push(`**Supersedes**: ${record.supersedes}`);
|
|
19
23
|
if (meta.length > 0) {
|
|
20
24
|
lines.push(meta.join(' \n'));
|
|
21
25
|
lines.push('');
|
|
@@ -26,7 +30,7 @@ function renderRecordMarkdown(record) {
|
|
|
26
30
|
lines.push('## Why');
|
|
27
31
|
lines.push(record.why ?? '');
|
|
28
32
|
lines.push('');
|
|
29
|
-
lines.push('##
|
|
33
|
+
lines.push('## Decision');
|
|
30
34
|
lines.push(record.rule ?? '');
|
|
31
35
|
lines.push('');
|
|
32
36
|
lines.push('## Alternatives Considered');
|
|
@@ -148,6 +152,8 @@ export async function generateDecisionRecordFiles(record, options = {}) {
|
|
|
148
152
|
title: record.title,
|
|
149
153
|
date: record.date,
|
|
150
154
|
decider: record.decider,
|
|
155
|
+
status: record.status,
|
|
156
|
+
supersedes: record.supersedes,
|
|
151
157
|
tags: record.tags,
|
|
152
158
|
files: {
|
|
153
159
|
record: OUTPUT_FILES.record,
|
package/dist/interactive.js
CHANGED
|
@@ -8,10 +8,13 @@ function todayLocalIsoDate() {
|
|
|
8
8
|
const day = String(now.getDate()).padStart(2, '0');
|
|
9
9
|
return `${year}-${month}-${day}`;
|
|
10
10
|
}
|
|
11
|
-
function requiredNonEmpty(
|
|
11
|
+
function requiredNonEmpty(labelEn, labelJa, value, lang) {
|
|
12
12
|
const trimmed = value.trim();
|
|
13
13
|
if (trimmed.length === 0) {
|
|
14
|
-
|
|
14
|
+
if (lang === 'ja') {
|
|
15
|
+
throw new Error(`${labelJa}は必須です(空欄不可)。`);
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`${labelEn} is required (cannot be empty).`);
|
|
15
18
|
}
|
|
16
19
|
return trimmed;
|
|
17
20
|
}
|
|
@@ -23,13 +26,13 @@ export async function promptDecisionRecord(options) {
|
|
|
23
26
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
24
27
|
try {
|
|
25
28
|
const todayIsoDate = todayLocalIsoDate();
|
|
26
|
-
for (const line of newIntroLines({ includeDate: options.includeDate, todayIsoDate })) {
|
|
29
|
+
for (const line of newIntroLines({ includeDate: options.includeDate, todayIsoDate }, options.lang)) {
|
|
27
30
|
console.log(line);
|
|
28
31
|
}
|
|
29
|
-
const title = requiredNonEmpty('Title', await rl.question(newTitlePrompt()));
|
|
30
|
-
const why = requiredNonEmpty('Why', await rl.question(newWhyPrompt()));
|
|
31
|
-
const rule = requiredNonEmpty('Rule', await rl.question(newRulePrompt()));
|
|
32
|
-
const context = optionalTrimmed(await rl.question(newContextPrompt()));
|
|
32
|
+
const title = requiredNonEmpty('Title', 'タイトル', await rl.question(newTitlePrompt(options.lang)), options.lang);
|
|
33
|
+
const why = requiredNonEmpty('Why', 'Why', await rl.question(newWhyPrompt(options.lang)), options.lang);
|
|
34
|
+
const rule = requiredNonEmpty('Rule', 'Rule', await rl.question(newRulePrompt(options.lang)), options.lang);
|
|
35
|
+
const context = optionalTrimmed(await rl.question(newContextPrompt(options.lang)));
|
|
33
36
|
const date = options.includeDate ? todayIsoDate : undefined;
|
|
34
37
|
return { title, date, why, rule, context };
|
|
35
38
|
}
|
package/dist/list.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
+
export function assertIsoDateOrThrow(value, label) {
|
|
5
|
+
if (!ISO_DATE_RE.test(value)) {
|
|
6
|
+
throw new Error(`${label} must be YYYY-MM-DD (got: ${value})`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function extractFolderDate(folderName) {
|
|
10
|
+
// Folder naming is expected to start with YYYY-MM-DD__
|
|
11
|
+
if (folderName.length < 12)
|
|
12
|
+
return undefined;
|
|
13
|
+
const candidate = folderName.slice(0, 10);
|
|
14
|
+
if (!ISO_DATE_RE.test(candidate))
|
|
15
|
+
return undefined;
|
|
16
|
+
if (folderName.slice(10, 12) !== '__')
|
|
17
|
+
return undefined;
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
async function readSummaryJson(outDir) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(path.join(outDir, 'summary.json'), 'utf8');
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
25
|
+
return undefined;
|
|
26
|
+
const obj = parsed;
|
|
27
|
+
const asOptionalString = (v) => (typeof v === 'string' ? v : undefined);
|
|
28
|
+
return {
|
|
29
|
+
title: asOptionalString(obj.title),
|
|
30
|
+
date: asOptionalString(obj.date),
|
|
31
|
+
decider: asOptionalString(obj.decider),
|
|
32
|
+
status: asOptionalString(obj.status),
|
|
33
|
+
supersedes: asOptionalString(obj.supersedes)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function extractSectionFirstLine(markdown, header) {
|
|
41
|
+
const lines = markdown.split(/\r?\n/);
|
|
42
|
+
let inRule = false;
|
|
43
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
44
|
+
const line = lines[i] ?? '';
|
|
45
|
+
if (!inRule) {
|
|
46
|
+
if (line.trim() === header) {
|
|
47
|
+
inRule = true;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Stop if next section starts.
|
|
52
|
+
if (line.startsWith('## '))
|
|
53
|
+
return undefined;
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (trimmed.length === 0)
|
|
56
|
+
continue;
|
|
57
|
+
// Keep the excerpt single-line.
|
|
58
|
+
return trimmed.replace(/\s+/g, ' ');
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function extractDecisionExcerptFromRecordMarkdown(markdown) {
|
|
63
|
+
// Prefer the newer heading, but remain backward compatible.
|
|
64
|
+
return (extractSectionFirstLine(markdown, '## Decision') ??
|
|
65
|
+
extractSectionFirstLine(markdown, '## Rule'));
|
|
66
|
+
}
|
|
67
|
+
async function readRuleExcerpt(outDir) {
|
|
68
|
+
try {
|
|
69
|
+
const md = await fs.readFile(path.join(outDir, 'decision-record.md'), 'utf8');
|
|
70
|
+
return extractDecisionExcerptFromRecordMarkdown(md);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function withinRange(date, from, to) {
|
|
77
|
+
// ISO date strings compare lexicographically.
|
|
78
|
+
if (from !== undefined && date < from)
|
|
79
|
+
return false;
|
|
80
|
+
if (to !== undefined && date > to)
|
|
81
|
+
return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
export async function listDecisions(options) {
|
|
85
|
+
const from = options.from?.trim();
|
|
86
|
+
const to = options.to?.trim();
|
|
87
|
+
if (from !== undefined && from.length > 0)
|
|
88
|
+
assertIsoDateOrThrow(from, '--from');
|
|
89
|
+
if (to !== undefined && to.length > 0)
|
|
90
|
+
assertIsoDateOrThrow(to, '--to');
|
|
91
|
+
const entries = await fs.readdir(options.outDir, { withFileTypes: true });
|
|
92
|
+
const dirs = entries
|
|
93
|
+
.filter((e) => e.isDirectory())
|
|
94
|
+
.map((e) => e.name);
|
|
95
|
+
const results = [];
|
|
96
|
+
for (const folderName of dirs) {
|
|
97
|
+
const dateFromFolder = extractFolderDate(folderName);
|
|
98
|
+
if (dateFromFolder !== undefined) {
|
|
99
|
+
if (!withinRange(dateFromFolder, from && from.length > 0 ? from : undefined, to && to.length > 0 ? to : undefined)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// If the folder doesn't follow naming, skip it for date-range listing.
|
|
105
|
+
if ((from !== undefined && from.length > 0) || (to !== undefined && to.length > 0)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const fullOutDir = path.join(options.outDir, folderName);
|
|
110
|
+
const summary = await readSummaryJson(fullOutDir);
|
|
111
|
+
const ruleExcerpt = await readRuleExcerpt(fullOutDir);
|
|
112
|
+
results.push({
|
|
113
|
+
folderName,
|
|
114
|
+
outDir: fullOutDir,
|
|
115
|
+
dateFromFolder,
|
|
116
|
+
summary,
|
|
117
|
+
ruleExcerpt
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Sort newest first. If date is missing, sort last.
|
|
121
|
+
results.sort((a, b) => {
|
|
122
|
+
const ad = a.dateFromFolder ?? '';
|
|
123
|
+
const bd = b.dateFromFolder ?? '';
|
|
124
|
+
if (ad !== bd)
|
|
125
|
+
return bd.localeCompare(ad);
|
|
126
|
+
const at = a.summary?.title ?? '';
|
|
127
|
+
const bt = b.summary?.title ?? '';
|
|
128
|
+
return at.localeCompare(bt);
|
|
129
|
+
});
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
function mdEscape(value) {
|
|
133
|
+
// Keep it simple for copy/paste: escape pipes and newlines.
|
|
134
|
+
return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
135
|
+
}
|
|
136
|
+
function truncateOneLine(value, maxChars) {
|
|
137
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
138
|
+
const chars = [...normalized];
|
|
139
|
+
if (chars.length <= maxChars)
|
|
140
|
+
return normalized;
|
|
141
|
+
if (maxChars <= 1)
|
|
142
|
+
return '…';
|
|
143
|
+
return `${chars.slice(0, maxChars - 1).join('')}…`;
|
|
144
|
+
}
|
|
145
|
+
function extractFolderId(folderName) {
|
|
146
|
+
// Folder naming is expected to end with __<id>. If it doesn't, return the full name.
|
|
147
|
+
const parts = folderName.split('__');
|
|
148
|
+
const last = parts.at(-1) ?? folderName;
|
|
149
|
+
return /^[0-9a-f]{8}$/i.test(last) ? last : folderName;
|
|
150
|
+
}
|
|
151
|
+
function listLabels(lang) {
|
|
152
|
+
if (lang === 'ja') {
|
|
153
|
+
return {
|
|
154
|
+
reportTitle: '# 意思決定レポート',
|
|
155
|
+
outDir: '- 出力ディレクトリ',
|
|
156
|
+
period: '- 期間',
|
|
157
|
+
generatedAt: '- 生成日時',
|
|
158
|
+
decisionsSection: '## 一覧',
|
|
159
|
+
periodAll: '全て',
|
|
160
|
+
periodFrom: (from) => `開始 ${from}`,
|
|
161
|
+
periodTo: (to) => `終了 ${to}`,
|
|
162
|
+
id: 'ID',
|
|
163
|
+
date: '日付',
|
|
164
|
+
title: 'タイトル',
|
|
165
|
+
status: 'ステータス',
|
|
166
|
+
decider: '決定者',
|
|
167
|
+
decision: '決定事項'
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
reportTitle: '# Decision Record Report',
|
|
172
|
+
outDir: '- Out dir',
|
|
173
|
+
period: '- Period',
|
|
174
|
+
generatedAt: '- Generated at',
|
|
175
|
+
decisionsSection: '## Decisions',
|
|
176
|
+
periodAll: 'all',
|
|
177
|
+
periodFrom: (from) => `from ${from}`,
|
|
178
|
+
periodTo: (to) => `to ${to}`,
|
|
179
|
+
id: 'ID',
|
|
180
|
+
date: 'Date',
|
|
181
|
+
title: 'Title',
|
|
182
|
+
status: 'Status',
|
|
183
|
+
decider: 'Decider',
|
|
184
|
+
decision: 'Decision'
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export function renderListReportMarkdown(items, options) {
|
|
188
|
+
const lines = [];
|
|
189
|
+
const maxDecisionLen = options.maxDecisionLen ?? 80;
|
|
190
|
+
const lang = options.lang ?? 'en';
|
|
191
|
+
const labels = listLabels(lang);
|
|
192
|
+
lines.push(labels.reportTitle);
|
|
193
|
+
lines.push('');
|
|
194
|
+
const periodParts = [];
|
|
195
|
+
if (options.from !== undefined && options.from.trim().length > 0)
|
|
196
|
+
periodParts.push(labels.periodFrom(options.from));
|
|
197
|
+
if (options.to !== undefined && options.to.trim().length > 0)
|
|
198
|
+
periodParts.push(labels.periodTo(options.to));
|
|
199
|
+
const period = periodParts.length > 0 ? periodParts.join(' ') : labels.periodAll;
|
|
200
|
+
lines.push(`${labels.outDir}: ${options.outDir}`);
|
|
201
|
+
lines.push(`${labels.period}: ${period}`);
|
|
202
|
+
lines.push(`${labels.generatedAt}: ${options.generatedAtIso}`);
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(labels.decisionsSection);
|
|
205
|
+
lines.push('');
|
|
206
|
+
for (const item of items) {
|
|
207
|
+
const id = extractFolderId(item.folderName);
|
|
208
|
+
const date = item.summary?.date ?? item.dateFromFolder ?? '';
|
|
209
|
+
const title = item.summary?.title ?? '';
|
|
210
|
+
const status = item.summary?.status ?? '';
|
|
211
|
+
const decider = item.summary?.decider ?? '';
|
|
212
|
+
const rule = item.ruleExcerpt ?? '';
|
|
213
|
+
lines.push(`- ${labels.id}: ${mdEscape(id)}`);
|
|
214
|
+
if (date.length > 0)
|
|
215
|
+
lines.push(` - ${labels.date}: ${mdEscape(date)}`);
|
|
216
|
+
if (title.length > 0)
|
|
217
|
+
lines.push(` - ${labels.title}: ${mdEscape(title)}`);
|
|
218
|
+
if (status.length > 0)
|
|
219
|
+
lines.push(` - ${labels.status}: ${mdEscape(status)}`);
|
|
220
|
+
if (decider.length > 0)
|
|
221
|
+
lines.push(` - ${labels.decider}: ${mdEscape(decider)}`);
|
|
222
|
+
if (rule.length > 0) {
|
|
223
|
+
const clipped = truncateOneLine(rule, maxDecisionLen);
|
|
224
|
+
lines.push(` - ${labels.decision}: ${mdEscape(clipped)}`);
|
|
225
|
+
}
|
|
226
|
+
// Add a blank line between entries for readability and safer copy/paste.
|
|
227
|
+
lines.push('');
|
|
228
|
+
}
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
package/dist/messages.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
export function titleRuleTooSimilarLines(lang = 'en') {
|
|
2
|
+
if (lang === 'ja') {
|
|
3
|
+
return [
|
|
4
|
+
'Warning: title と rule がほぼ同じ内容に見えます。',
|
|
5
|
+
'Tip: title は短い要約、rule は「全員が守るべきルール(行動・条件・例外)」を書くと役立ちます。',
|
|
6
|
+
'Example:',
|
|
7
|
+
' title: "クラウド基盤は AWS を採用する"',
|
|
8
|
+
' rule: "原則として新規の本番環境は AWS。例外が必要なら別DRで判断する"'
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
return [
|
|
12
|
+
'Warning: title and rule look very similar.',
|
|
13
|
+
'Tip: title should be a short summary; rule should be an actionable rule (behavior/conditions/exceptions).',
|
|
14
|
+
'Example:',
|
|
15
|
+
' title: "Choose AWS as our cloud provider"',
|
|
16
|
+
' rule: "Default to AWS for new production. If an exception is needed, create a separate DR."'
|
|
17
|
+
];
|
|
18
|
+
}
|
|
1
19
|
export function templateCreatedLines(inputPath) {
|
|
2
20
|
return [
|
|
3
21
|
`Created template: ${inputPath}`,
|
|
@@ -21,37 +39,86 @@ export function missingCoreFieldsLines(inputPath, missingFields) {
|
|
|
21
39
|
export function wroteInputLines(inputPath) {
|
|
22
40
|
return [`Wrote: ${inputPath}`];
|
|
23
41
|
}
|
|
42
|
+
export function skippedGenerateLines(inputPath, baseOutDir, lang = 'en') {
|
|
43
|
+
if (lang === 'ja') {
|
|
44
|
+
return [
|
|
45
|
+
'Note: --skip-generate が指定されているため、出力ファイルの生成はスキップしました。',
|
|
46
|
+
'次に、必要なら decision.yaml を追記・編集してください。',
|
|
47
|
+
`生成するには: dr-gen generate ${inputPath} --out-dir ${baseOutDir}`
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
return [
|
|
51
|
+
'Note: outputs were not generated because --skip-generate was set.',
|
|
52
|
+
'Next: edit decision.yaml if needed.',
|
|
53
|
+
`To generate outputs: dr-gen generate ${inputPath} --out-dir ${baseOutDir}`
|
|
54
|
+
];
|
|
55
|
+
}
|
|
24
56
|
export function generatedOutputLines(outDir) {
|
|
25
57
|
return [`Generated: ${outDir}`];
|
|
26
58
|
}
|
|
27
|
-
export function newIntroLines(options) {
|
|
59
|
+
export function newIntroLines(options, lang = 'en') {
|
|
28
60
|
const lines = [];
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
if (lang === 'ja') {
|
|
62
|
+
lines.push('いくつか質問します。短い回答でOKです。');
|
|
63
|
+
lines.push('コツ: 理由(Why)とルール(Rule)に集中してください。');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
lines.push('Answer a few questions. Short answers are OK.');
|
|
67
|
+
lines.push('Tip: Focus on the reason (Why) and the rule (Rule).');
|
|
68
|
+
}
|
|
31
69
|
if (options.includeDate) {
|
|
32
|
-
|
|
70
|
+
if (lang === 'ja') {
|
|
71
|
+
lines.push(`日付は本日(${options.todayIsoDate})に設定します。無効にするには --no-date を使ってください。`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
lines.push(`Date will be set to today (${options.todayIsoDate}). Use --no-date to disable.`);
|
|
75
|
+
}
|
|
33
76
|
}
|
|
34
77
|
return lines;
|
|
35
78
|
}
|
|
36
|
-
export function newTitlePrompt() {
|
|
79
|
+
export function newTitlePrompt(lang = 'en') {
|
|
80
|
+
if (lang === 'ja') {
|
|
81
|
+
return `タイトル(必須)
|
|
82
|
+
何を決めましたか?(短い見出し・名詞句でもOK)
|
|
83
|
+
例: 「定例会議の上限を30分にする」 / 「初動返信SLAを24時間以内にする」
|
|
84
|
+
> `;
|
|
85
|
+
}
|
|
37
86
|
return `Title (required)
|
|
38
87
|
What did you decide? (one sentence)
|
|
39
88
|
Examples: "Limit recurring meetings to 30 minutes" / "Set support reply time to within 24 hours"
|
|
40
89
|
> `;
|
|
41
90
|
}
|
|
42
|
-
export function newWhyPrompt() {
|
|
91
|
+
export function newWhyPrompt(lang = 'en') {
|
|
92
|
+
if (lang === 'ja') {
|
|
93
|
+
return `Why(必須)
|
|
94
|
+
この決定をした理由・目的(なぜやるのか?)
|
|
95
|
+
例: 「セキュリティ事故を防ぐ」 / 「請求をシンプルにする」 / 「長い会議を減らす」
|
|
96
|
+
> `;
|
|
97
|
+
}
|
|
43
98
|
return `Why (required)
|
|
44
99
|
Reason / goal behind the decision (why are we doing this?).
|
|
45
100
|
Examples: "Reduce security risk" / "Make billing simpler" / "Avoid long meetings"
|
|
46
101
|
> `;
|
|
47
102
|
}
|
|
48
|
-
export function newRulePrompt() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
103
|
+
export function newRulePrompt(lang = 'en') {
|
|
104
|
+
if (lang === 'ja') {
|
|
105
|
+
return `Decision / Rule(必須)
|
|
106
|
+
今回の決定は何ですか?(方針・ルール・当面の優先順位など。行動・条件・例外まで書けると良い)
|
|
107
|
+
例: 「優先開発は list と new --skip-generate。他は後回しにする」
|
|
108
|
+
> `;
|
|
109
|
+
}
|
|
110
|
+
return `Decision / Rule (required)
|
|
111
|
+
What did you decide? (policy/rule/priority; actions are OK)
|
|
112
|
+
Examples: "Prioritize list + --skip-generate first; defer others" / "Default to AWS for new production"
|
|
52
113
|
> `;
|
|
53
114
|
}
|
|
54
|
-
export function newContextPrompt() {
|
|
115
|
+
export function newContextPrompt(lang = 'en') {
|
|
116
|
+
if (lang === 'ja') {
|
|
117
|
+
return `Context(任意)
|
|
118
|
+
背景・制約(空欄でもOK)
|
|
119
|
+
例: 「返信が遅いという声が増えた」 / 「スプレッドシートでパスワード共有していた」
|
|
120
|
+
> `;
|
|
121
|
+
}
|
|
55
122
|
return `Context (optional)
|
|
56
123
|
Background / constraints (you can leave this empty).
|
|
57
124
|
Examples: "Customers say replies are slow" / "We used to share passwords in a spreadsheet"
|
package/dist/parser.js
CHANGED
|
@@ -33,6 +33,8 @@ export async function parseDecisionYaml(filePath) {
|
|
|
33
33
|
title,
|
|
34
34
|
date: asOptionalString(loaded.date),
|
|
35
35
|
decider: asOptionalString(loaded.decider),
|
|
36
|
+
status: asOptionalString(loaded.status),
|
|
37
|
+
supersedes: asOptionalString(loaded.supersedes),
|
|
36
38
|
context: asOptionalString(loaded.context),
|
|
37
39
|
why: asOptionalString(loaded.why),
|
|
38
40
|
rule: asOptionalString(loaded.rule),
|
package/dist/template.js
CHANGED
package/dist/text.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function normalizeComparable(value) {
|
|
2
|
+
return value
|
|
3
|
+
.normalize('NFKC')
|
|
4
|
+
.trim()
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
// Remove whitespace and common punctuation so minor formatting differences don't matter.
|
|
7
|
+
.replace(/[\s\u3000]+/g, '')
|
|
8
|
+
.replace(/["'`“”‘’.,:;!?()\[\]{}<>|\\/\-_=+*~^$#@]+/g, '');
|
|
9
|
+
}
|
|
10
|
+
export function areTitleAndRuleTooSimilar(title, rule) {
|
|
11
|
+
const t = normalizeComparable(title);
|
|
12
|
+
const r = normalizeComparable(rule);
|
|
13
|
+
if (t.length === 0 || r.length === 0)
|
|
14
|
+
return false;
|
|
15
|
+
if (t === r)
|
|
16
|
+
return true;
|
|
17
|
+
// Heuristic: one contains the other and they're very close in length.
|
|
18
|
+
const shorter = t.length <= r.length ? t : r;
|
|
19
|
+
const longer = t.length <= r.length ? r : t;
|
|
20
|
+
if (!longer.includes(shorter))
|
|
21
|
+
return false;
|
|
22
|
+
const diff = longer.length - shorter.length;
|
|
23
|
+
return diff <= 8;
|
|
24
|
+
}
|
package/dist/yaml.js
CHANGED
|
@@ -4,6 +4,8 @@ export function renderDecisionYaml(record) {
|
|
|
4
4
|
title: record.title,
|
|
5
5
|
date: record.date ?? '',
|
|
6
6
|
decider: record.decider ?? '',
|
|
7
|
+
status: record.status ?? '',
|
|
8
|
+
supersedes: record.supersedes ?? '',
|
|
7
9
|
context: record.context ?? '',
|
|
8
10
|
why: record.why ?? '',
|
|
9
11
|
rule: record.rule ?? '',
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dr-gen",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Generate lightweight Decision Records from decision.yaml",
|
|
5
|
+
"homepage": "https://www.ineeza.com/",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ineeza/decision-record-generator.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/Ineeza/decision-record-generator/issues"
|
|
12
|
+
},
|
|
5
13
|
"license": "MIT",
|
|
6
14
|
"type": "module",
|
|
7
15
|
"bin": {
|