@alfacapital-tech/c4builder 0.8.0 → 0.9.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/agent.js +21 -0
- package/dist/cli/commands/feedback.js +45 -0
- package/dist/cli/commands/site.comments.js +585 -0
- package/dist/cli/commands/site.js +242 -2
- package/dist/cli/dispatch.js +24 -10
- package/dist/config/defaults.js +2 -1
- package/dist/config/schema.js +3 -0
- package/dist/core/build.js +3 -3
- package/dist/core/compose/markdown.js +5 -1
- package/dist/core/feedback.js +59 -0
- package/dist/core/plugins/tree.js +3 -1
- package/dist/core/questions.js +227 -0
- package/dist/core/scan/tree.js +3 -1
- package/dist/plugins/openspec/index.js +6 -1
- package/dist/plugins/openspec/render.js +6 -3
- package/dist/util/lock.js +23 -0
- package/package.json +1 -1
- package/template/AGENTS.md +7 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Правка раздела вопросов исходного markdown по комментарию со страницы `site -w`
|
|
2
|
+
// (см. change site-comments, design D4): добавление и удаление. Чистые функции + запись
|
|
3
|
+
// файла; формат раздела (таблица / список / его отсутствие) определяется по содержимому,
|
|
4
|
+
// чтобы не навязывать проектам одну форму таблицы.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
const QUOTE_MAX = 200;
|
|
7
|
+
const collapse = (s) => s.replace(/\s+/g, ' ').trim();
|
|
8
|
+
// docsify рендерит HTML из markdown как есть: без экранирования цитата `List<String>` теряет
|
|
9
|
+
// `<String>` на странице, а `<img onerror>` из комментария становится скриптом и в dist.
|
|
10
|
+
const escapeHtml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
11
|
+
/** Текст вопроса: комментарий, цитата (до 200 символов) и раздел страницы, где сделано выделение. */
|
|
12
|
+
export const formatQuestion = ({ comment, quote, section }) => {
|
|
13
|
+
let q = collapse(quote ?? '');
|
|
14
|
+
if (q.length > QUOTE_MAX)
|
|
15
|
+
q = `${q.slice(0, QUOTE_MAX - 1).trimEnd()}…`;
|
|
16
|
+
const s = collapse(section ?? '');
|
|
17
|
+
return escapeHtml(`${collapse(comment)}${q ? ` — «${q}»` : ''}${s ? ` (§ ${s})` : ''}`);
|
|
18
|
+
};
|
|
19
|
+
// Заголовок раздела: любой уровень, без учёта регистра и лишних пробелов, с необязательными
|
|
20
|
+
// закрывающими `#`.
|
|
21
|
+
const headingLevel = (line, heading) => {
|
|
22
|
+
const m = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
|
|
23
|
+
if (!m)
|
|
24
|
+
return 0;
|
|
25
|
+
return collapse(m[2]).toLowerCase() === collapse(heading).toLowerCase() ? m[1].length : 0;
|
|
26
|
+
};
|
|
27
|
+
const isFence = (line) => /^\s*(```|~~~)/.test(line);
|
|
28
|
+
const isRow = (line) => line.trim().startsWith('|');
|
|
29
|
+
const isSeparator = (line) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line);
|
|
30
|
+
const isListItem = (line) => /^\s*([-*+]|\d+[.)])\s+/.test(line);
|
|
31
|
+
const splitRow = (line) => line
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/^\|/, '')
|
|
34
|
+
.replace(/\|$/, '')
|
|
35
|
+
.split(/(?<!\\)\|/)
|
|
36
|
+
.map((c) => c.trim());
|
|
37
|
+
const isPlaceholder = (cell) => /^(\.{3}|…|)$/.test(cell);
|
|
38
|
+
const escapeCell = (s) => s.replace(/\|/g, '\\|');
|
|
39
|
+
// Текст ячейки/пункта без inline-разметки — для сравнения с тем, что видно на странице
|
|
40
|
+
// (docsify отрисовал markdown, клиент прислал textContent). Применяется к обеим сторонам:
|
|
41
|
+
// сущности декодируются до срезания тегов, чтобы `<T>` из файла и `<T>` со страницы
|
|
42
|
+
// свелись к одному.
|
|
43
|
+
const plain = (s) => collapse(s
|
|
44
|
+
.replace(/</g, '<')
|
|
45
|
+
.replace(/>/g, '>')
|
|
46
|
+
.replace(/&/g, '&')
|
|
47
|
+
.replace(/\\\|/g, '|')
|
|
48
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
49
|
+
.replace(/<[^>]+>/g, '')
|
|
50
|
+
.replace(/[`*_~]/g, ''));
|
|
51
|
+
/** Границы раздела: строка заголовка и индекс первой строки за разделом. */
|
|
52
|
+
const findSection = (lines, heading) => {
|
|
53
|
+
let start = -1;
|
|
54
|
+
let level = 0;
|
|
55
|
+
let fenced = false;
|
|
56
|
+
for (const [i, line] of lines.entries()) {
|
|
57
|
+
if (isFence(line))
|
|
58
|
+
fenced = !fenced;
|
|
59
|
+
if (fenced)
|
|
60
|
+
continue;
|
|
61
|
+
level = headingLevel(line, heading);
|
|
62
|
+
if (level) {
|
|
63
|
+
start = i;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (start < 0)
|
|
68
|
+
return undefined;
|
|
69
|
+
let end = lines.length;
|
|
70
|
+
fenced = false;
|
|
71
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
72
|
+
if (isFence(lines[i]))
|
|
73
|
+
fenced = !fenced;
|
|
74
|
+
if (fenced)
|
|
75
|
+
continue;
|
|
76
|
+
const m = lines[i].match(/^(#{1,6})\s/);
|
|
77
|
+
if (m && m[1].length <= level) {
|
|
78
|
+
end = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { start, end };
|
|
83
|
+
};
|
|
84
|
+
const findTable = (lines, start, end) => {
|
|
85
|
+
for (let i = start + 1; i + 1 < end; i++) {
|
|
86
|
+
if (!isRow(lines[i]) || !isSeparator(lines[i + 1]))
|
|
87
|
+
continue;
|
|
88
|
+
let last = i + 1;
|
|
89
|
+
while (last + 1 < end && isRow(lines[last + 1]))
|
|
90
|
+
last++;
|
|
91
|
+
const cols = splitRow(lines[i]).length;
|
|
92
|
+
const rows = lines.slice(i + 2, last + 1).map(splitRow);
|
|
93
|
+
// Числовая первая колонка: по данным, а у пустой таблицы — по заголовку (`#`, `№`, `No`).
|
|
94
|
+
const numeric = rows.length
|
|
95
|
+
? rows.every((r) => /^\d+$/.test(r[0] ?? ''))
|
|
96
|
+
: /^(#|№|no\.?)$/i.test(splitRow(lines[i])[0] ?? '');
|
|
97
|
+
return { header: i, last, cols, numeric, textCol: numeric ? Math.min(1, cols - 1) : 0 };
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Дописывает вопрос в раздел `heading`: строкой таблицы (с номером, если первая колонка
|
|
103
|
+
* числовая; заглушка `1 | ... | ...` заменяется), иначе пунктом списка в конце раздела;
|
|
104
|
+
* нет раздела — создаёт его в конце файла. Перевод строки файла сохраняется.
|
|
105
|
+
*/
|
|
106
|
+
export const appendQuestion = (markdown, heading, text) => {
|
|
107
|
+
const eol = markdown.includes('\r\n') ? '\r\n' : '\n';
|
|
108
|
+
const lines = markdown.split(/\r?\n/);
|
|
109
|
+
const section = findSection(lines, heading);
|
|
110
|
+
if (!section) {
|
|
111
|
+
while (lines.length && lines[lines.length - 1].trim() === '')
|
|
112
|
+
lines.pop();
|
|
113
|
+
lines.push('', `## ${heading}`, '', `- ${text}`, '');
|
|
114
|
+
return { markdown: lines.join(eol), number: null };
|
|
115
|
+
}
|
|
116
|
+
const { start, end } = section;
|
|
117
|
+
const table = findTable(lines, start, end);
|
|
118
|
+
if (table) {
|
|
119
|
+
const { header, last, cols, numeric, textCol } = table;
|
|
120
|
+
const rows = lines.slice(header + 2, last + 1).map(splitRow);
|
|
121
|
+
// Единственная строка шаблона с `...` в ячейке вопроса — заглушка: заменяем её, сохраняя
|
|
122
|
+
// заполненные ячейки (например, «Кому: Аналитик»), остальные `...` очищаем.
|
|
123
|
+
const placeholder = rows.length === 1 && isPlaceholder(rows[0][textCol] ?? '');
|
|
124
|
+
const number = numeric
|
|
125
|
+
? placeholder
|
|
126
|
+
? 1
|
|
127
|
+
: rows.reduce((max, r) => Math.max(max, Number(r[0])), 0) + 1
|
|
128
|
+
: null;
|
|
129
|
+
const cells = placeholder
|
|
130
|
+
? Array.from({ length: cols }, (_, c) => isPlaceholder(rows[0][c] ?? '') ? '' : (rows[0][c] ?? ''))
|
|
131
|
+
: new Array(cols).fill('');
|
|
132
|
+
if (number !== null)
|
|
133
|
+
cells[0] = String(number);
|
|
134
|
+
cells[textCol] = escapeCell(text);
|
|
135
|
+
const row = `| ${cells.join(' | ')} |`;
|
|
136
|
+
if (placeholder)
|
|
137
|
+
lines.splice(last, 1, row);
|
|
138
|
+
else
|
|
139
|
+
lines.splice(last + 1, 0, row);
|
|
140
|
+
return { markdown: lines.join(eol), number };
|
|
141
|
+
}
|
|
142
|
+
let lastText = end - 1;
|
|
143
|
+
while (lastText > start && lines[lastText].trim() === '')
|
|
144
|
+
lastText--;
|
|
145
|
+
if (lastText === start) {
|
|
146
|
+
// Пустой раздел: пункт через пустую строку после заголовка; перед следующим
|
|
147
|
+
// заголовком — тоже пустая строка.
|
|
148
|
+
const insert = ['', `- ${text}`];
|
|
149
|
+
if (start + 1 < lines.length && lines[start + 1].trim() !== '')
|
|
150
|
+
insert.push('');
|
|
151
|
+
lines.splice(start + 1, 0, ...insert);
|
|
152
|
+
return { markdown: lines.join(eol), number: null };
|
|
153
|
+
}
|
|
154
|
+
// Нумерованный список продолжаем его же маркером: `-` после `2.` начал бы второй список.
|
|
155
|
+
const ordered = lines
|
|
156
|
+
.slice(start + 1, lastText + 1)
|
|
157
|
+
.reverse()
|
|
158
|
+
.find((l) => /^([-*+]|\d+[.)])\s/.test(l))
|
|
159
|
+
?.match(/^(\d+)([.)])\s/);
|
|
160
|
+
lines.splice(lastText + 1, 0, ordered ? `${Number(ordered[1]) + 1}${ordered[2]} ${text}` : `- ${text}`);
|
|
161
|
+
return { markdown: lines.join(eol), number: null };
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Удаляет вопрос из раздела `heading`: строку таблицы (по номеру и тексту; без совпадения
|
|
165
|
+
* номера — только по тексту) или пункт списка по тексту. Текст сравнивается без inline-разметки.
|
|
166
|
+
* Номера остальных строк не меняются — на них ссылаются другие документы.
|
|
167
|
+
*/
|
|
168
|
+
export const removeQuestion = (markdown, heading, ref) => {
|
|
169
|
+
const eol = markdown.includes('\r\n') ? '\r\n' : '\n';
|
|
170
|
+
const lines = markdown.split(/\r?\n/);
|
|
171
|
+
const section = findSection(lines, heading);
|
|
172
|
+
if (!section)
|
|
173
|
+
return { error: 'not-found' };
|
|
174
|
+
const { start, end } = section;
|
|
175
|
+
const want = plain(ref.text);
|
|
176
|
+
const table = findTable(lines, start, end);
|
|
177
|
+
let candidates = [];
|
|
178
|
+
if (table) {
|
|
179
|
+
const { header, last, numeric, textCol } = table;
|
|
180
|
+
const rows = [];
|
|
181
|
+
for (let i = header + 2; i <= last; i++)
|
|
182
|
+
rows.push({ line: i, cells: splitRow(lines[i]) });
|
|
183
|
+
const byText = rows.filter((r) => plain(r.cells[textCol] ?? '') === want);
|
|
184
|
+
const byNumber = numeric && ref.number != null ? byText.filter((r) => Number(r.cells[0]) === ref.number) : [];
|
|
185
|
+
candidates = (byNumber.length ? byNumber : byText).map((r) => r.line);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
for (let i = start + 1; i < end; i++)
|
|
189
|
+
if (isListItem(lines[i]) && plain(lines[i].replace(/^\s*([-*+]|\d+[.)])\s+/, '')) === want)
|
|
190
|
+
candidates.push(i);
|
|
191
|
+
}
|
|
192
|
+
if (!candidates.length)
|
|
193
|
+
return { error: 'not-found' };
|
|
194
|
+
if (candidates.length > 1)
|
|
195
|
+
return { error: 'ambiguous' };
|
|
196
|
+
lines.splice(candidates[0], 1);
|
|
197
|
+
return { markdown: lines.join(eol) };
|
|
198
|
+
};
|
|
199
|
+
const hasHeading = (file, heading) => {
|
|
200
|
+
try {
|
|
201
|
+
return fs
|
|
202
|
+
.readFileSync(file, 'utf8')
|
|
203
|
+
.split(/\r?\n/)
|
|
204
|
+
.some((l) => headingLevel(l, heading) > 0);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/** Файл страницы для записи: первый с разделом `heading`, иначе первый существующий. */
|
|
211
|
+
export const pickSource = (files, heading) => {
|
|
212
|
+
const existing = files.filter((f) => fs.existsSync(f));
|
|
213
|
+
return existing.find((f) => hasHeading(f, heading)) ?? existing[0];
|
|
214
|
+
};
|
|
215
|
+
/** Дописывает вопрос в файл и возвращает номер строки таблицы (null — список). */
|
|
216
|
+
export const writeQuestion = (file, heading, text) => {
|
|
217
|
+
const { markdown, number } = appendQuestion(fs.readFileSync(file, 'utf8'), heading, text);
|
|
218
|
+
fs.writeFileSync(file, markdown);
|
|
219
|
+
return number;
|
|
220
|
+
};
|
|
221
|
+
/** Удаляет вопрос из файла; ошибка поиска возвращается, не бросается. */
|
|
222
|
+
export const deleteQuestion = (file, heading, ref) => {
|
|
223
|
+
const result = removeQuestion(fs.readFileSync(file, 'utf8'), heading, ref);
|
|
224
|
+
if ('markdown' in result)
|
|
225
|
+
fs.writeFileSync(file, result.markdown);
|
|
226
|
+
return result;
|
|
227
|
+
};
|
package/dist/core/scan/tree.js
CHANGED
|
@@ -43,7 +43,8 @@ export const generateTree = async (dir, options) => {
|
|
|
43
43
|
parent: parent,
|
|
44
44
|
mdFiles: [],
|
|
45
45
|
diagrams: [],
|
|
46
|
-
descendants: []
|
|
46
|
+
descendants: [],
|
|
47
|
+
sources: []
|
|
47
48
|
};
|
|
48
49
|
tree.push(item);
|
|
49
50
|
}
|
|
@@ -84,6 +85,7 @@ export const generateTree = async (dir, options) => {
|
|
|
84
85
|
const mdPath = path.join(dir, mdFile);
|
|
85
86
|
const { markdown, diagrams } = fences.extract((await readFile(mdPath)).toString(), path.parse(mdFile).name, path.relative(process.cwd(), mdPath).split(path.sep).join('/'));
|
|
86
87
|
item.mdFiles.push(markdown);
|
|
88
|
+
item.sources.push(path.resolve(mdPath));
|
|
87
89
|
for (const d of diagrams)
|
|
88
90
|
item.diagrams.push({
|
|
89
91
|
dir: d.file,
|
|
@@ -44,7 +44,12 @@ export default definePlugin({
|
|
|
44
44
|
// complete-markdown; excludeOtherFiles действует и здесь.
|
|
45
45
|
const addPages = (pages, srcRoot) => {
|
|
46
46
|
for (const page of pages) {
|
|
47
|
-
ctx.addPage({
|
|
47
|
+
ctx.addPage({
|
|
48
|
+
path: page.path,
|
|
49
|
+
markdown: page.markdown,
|
|
50
|
+
diagrams: page.diagrams,
|
|
51
|
+
sources: page.sources
|
|
52
|
+
});
|
|
48
53
|
if (options.EXCLUDE_OTHER_FILES)
|
|
49
54
|
continue;
|
|
50
55
|
for (const { from, to } of page.files) {
|
|
@@ -133,6 +133,7 @@ const FENCE_LANGS = ['plantuml', 'puml', 'd2'];
|
|
|
133
133
|
export const renderMarkdown = (pagePath, base, content, { srcRoot, fromDir = '', pageDir = fromDir, included = new Set(), target = () => undefined, source, options }) => {
|
|
134
134
|
const { markdown, diagrams } = createFenceExtractor(FENCE_LANGS).extract(content, base, source);
|
|
135
135
|
const files = [];
|
|
136
|
+
const sources = source ? [source] : [];
|
|
136
137
|
const rewrite = (t) => t.replace(LINK_RE, (whole, bang, text, href, title = '') => {
|
|
137
138
|
const inner = text.includes('![') ? rewrite(text) : text;
|
|
138
139
|
if (/^[a-z][a-z0-9+.-]*:|^[#/]/i.test(href))
|
|
@@ -168,6 +169,7 @@ export const renderMarkdown = (pagePath, base, content, { srcRoot, fromDir = '',
|
|
|
168
169
|
});
|
|
169
170
|
diagrams.push(...sub.diagrams);
|
|
170
171
|
files.push(...sub.files);
|
|
172
|
+
sources.push(...sub.sources);
|
|
171
173
|
return sub.markdown;
|
|
172
174
|
}
|
|
173
175
|
const sameDir = fromDir === pageDir;
|
|
@@ -177,7 +179,7 @@ export const renderMarkdown = (pagePath, base, content, { srcRoot, fromDir = '',
|
|
|
177
179
|
? `${bang}[${inner}](${sameDir ? href : encodeURIPath(to)}${title})`
|
|
178
180
|
: `[${inner}](${encodeURIPath(path.posix.join(...pagePath, to))}${ignoreTitle(title)})`;
|
|
179
181
|
});
|
|
180
|
-
return { path: pagePath, markdown: mapOutsideFences(markdown, rewrite).trim(), diagrams, files };
|
|
182
|
+
return { path: pagePath, markdown: mapOutsideFences(markdown, rewrite).trim(), diagrams, files, sources };
|
|
181
183
|
};
|
|
182
184
|
/**
|
|
183
185
|
* Страницы дерева спек под `base`: корень, промежуточные папки, спеки. Папка с собственной
|
|
@@ -193,7 +195,7 @@ export const renderSpecTree = (specs, base, options, render) => {
|
|
|
193
195
|
const spec = byKey.get(key);
|
|
194
196
|
const page = spec
|
|
195
197
|
? render(pagePath, spec)
|
|
196
|
-
: { path: pagePath, markdown: '', diagrams: [], files: [] };
|
|
198
|
+
: { path: pagePath, markdown: '', diagrams: [], files: [], sources: [] };
|
|
197
199
|
const nested = specs.some((s) => s.path.length > prefix.length && prefix.every((p, i) => s.path[i] === p));
|
|
198
200
|
if (nested || !spec)
|
|
199
201
|
page.markdown = [page.markdown, renderSpecIndex(specs, prefix, base, options)]
|
|
@@ -277,7 +279,8 @@ export const renderChange = (change, segments, order, options, layout) => {
|
|
|
277
279
|
path: segments,
|
|
278
280
|
markdown: [head, family, links.join(' · '), main?.markdown].filter(Boolean).join('\n\n'),
|
|
279
281
|
diagrams: main?.diagrams ?? [],
|
|
280
|
-
files: main?.files ?? []
|
|
282
|
+
files: main?.files ?? [],
|
|
283
|
+
sources: main?.sources ?? []
|
|
281
284
|
},
|
|
282
285
|
...subpages
|
|
283
286
|
];
|
package/dist/util/lock.js
CHANGED
|
@@ -56,3 +56,26 @@ export const acquireBuildLock = (lockPath) => {
|
|
|
56
56
|
}
|
|
57
57
|
throw new BuildLockHeldError(`failed to acquire the build lock: ${lockPath}`);
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Захват лока с ожиданием: пока сборку держит другой живой процесс (второй `site -w` в той же
|
|
61
|
+
* папке), пробуем каждые `stepMs`, не дольше `timeoutMs`; затем — исходная BuildLockHeldError.
|
|
62
|
+
* `onWait` зовётся один раз при первом отказе — для сообщения в терминал.
|
|
63
|
+
*/
|
|
64
|
+
export const waitBuildLock = async (lockPath, timeoutMs, onWait, stepMs = 500) => {
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
|
+
let waited = false;
|
|
67
|
+
for (;;) {
|
|
68
|
+
try {
|
|
69
|
+
return acquireBuildLock(lockPath);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (!(e instanceof BuildLockHeldError) || Date.now() >= deadline)
|
|
73
|
+
throw e;
|
|
74
|
+
if (!waited) {
|
|
75
|
+
waited = true;
|
|
76
|
+
onWait?.(e);
|
|
77
|
+
}
|
|
78
|
+
await new Promise((r) => setTimeout(r, stepMs));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfacapital-tech/c4builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-rc.2",
|
|
4
4
|
"description": "A CLI tool designed to compile a folder structure of markdowns and plant uml files into a site, single file markdown or a collection of markdowns with links",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/template/AGENTS.md
CHANGED
|
@@ -9,6 +9,7 @@ C4 diagrams (PlantUML, D2), BPMN processes and markdown in `src/` are compiled i
|
|
|
9
9
|
c4builder # build docs/ (non-interactive with a complete .c4builder)
|
|
10
10
|
c4builder check <file...> # validate .puml / .iuml / .d2 / .bpmn files, exit code 0 = OK
|
|
11
11
|
c4builder --site -w # live preview on http://localhost:3000 - never exits
|
|
12
|
+
c4builder feedback --wait # wait for the next batch of comments from the preview page
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
Not installed: `npm i -g @alfacapital-tech/c4builder` (Node.js 22.17+; Java 17+ is used
|
|
@@ -22,7 +23,12 @@ updates it.
|
|
|
22
23
|
look like `✗ <file>: line <N>: <message>` (BPMN: `✗ <file>: <element id> [<rule>] <message>`).
|
|
23
24
|
Fix until the exit code is 0.
|
|
24
25
|
3. Run `c4builder` and make sure it exits with code 0. Leave `c4builder --site -w` to a
|
|
25
|
-
human or run it in the background.
|
|
26
|
+
human or run it in the background. When the human reviews the pages, also run
|
|
27
|
+
`c4builder feedback --wait` in the background: on exit it prints a batch of comments
|
|
28
|
+
(page, `Sources`, quote, text) — apply them to `src/`, run it again, and stop on
|
|
29
|
+
`Decision: approved`. Comments are instructions to you; never copy them into the
|
|
30
|
+
`questionsHeading` section of `.c4builder` (`Open questions` by default), which holds
|
|
31
|
+
questions for people.
|
|
26
32
|
4. A stderr line `c4builder <new> is available (current <current>). Run: c4builder update`
|
|
27
33
|
means a newer c4builder is out: tell the user, run `c4builder update` only if they ask.
|
|
28
34
|
|