checkQuestionRegister function
RegisterReport
checkQuestionRegister(
- String blueprintYaml,
- String clarifications, {
- QuestionRegisterConfig config = QuestionRegisterConfig.defaults,
Checks clarifications (the index file's text) against the question blocks
of blueprintYaml, by the project's config (default:
QuestionRegisterConfig.defaults).
Implementation
RegisterReport checkQuestionRegister(
String blueprintYaml,
String clarifications, {
QuestionRegisterConfig config = QuestionRegisterConfig.defaults,
}) {
final report = RegisterReport();
final idOrder = _idOrder(config.prefixes);
// --- blocks in the Blueprint --------------------------------------------
final annotations = SpecReviewAnnotations(
SpecDocument()..setContent('blueprint', blueprintYaml),
);
for (final e in annotations.validate()) {
if (e.code == SpecValidationCode.annotationMalformed &&
e.message.contains('question header')) {
report.findings.add('malformed question block: ${e.message}');
}
}
final blocks = <String, QuestionBlock>{};
for (final a in annotations.list()) {
final id = a.id;
if (a.kind != SpecReviewAnnotationKind.question ||
!config.isQuestionId(id!)) {
continue;
}
if (blocks.containsKey(id)) {
report.findings.add(
'duplicate block: $id at lines ${blocks[id]!.line} and ${a.line + 1}',
);
}
blocks[id] = QuestionBlock(
id,
a.line + 1,
answered: a.answer.isNotEmpty && !a.answer.startsWith('see '),
);
}
report.blocks = blocks.length;
for (final b in blocks.values) {
if (b.answered) report.answeredNotApplied.add('${b.id} (line ${b.line})');
}
// --- the index ---------------------------------------------------------
String section(String from, String to) {
final a = clarifications.indexOf(from);
final b = clarifications.indexOf(to, a < 0 ? 0 : a);
if (a < 0) return '';
return clarifications.substring(a, b < 0 ? clarifications.length : b);
}
final index = <String, IndexRow>{};
final order = <String>[];
for (final line in section(
'## Index',
'## Not in the Blueprint',
).split('\n')) {
if (config.rowId(line) == null) continue;
final cells = line.trim().split(_pipe).map((c) => c.trim()).toList();
// A row is `| id | ★ | where | topic | status | note |`: seven pieces
// around the six cells, the first and last empty.
if (cells.length != 8 || cells.first.isNotEmpty || cells.last.isNotEmpty) {
report.findings.add(
'malformed index row: ${line.length > 80 ? line.substring(0, 80) : line}…',
);
continue;
}
final row = IndexRow(
cells[1],
cells[2],
cells[3],
cells[4],
cells[5],
cells[6],
);
if (index.containsKey(row.id)) {
report.findings.add('index row twice: ${row.id}');
}
final known = config.statuses.any(
(st) => st.key == 'duplicate'
? (row.status.startsWith('${st.label} → ') &&
config.isQuestionId(row.status.substring(st.label.length + 3)))
: row.status == st.label,
);
if (!known) {
report.findings.add('${row.id}: unknown status "${row.status}"');
}
index[row.id] = row;
order.add(row.id);
}
for (var i = 1; i < order.length; i++) {
if (idOrder(order[i - 1], order[i]) > 0) {
report.findings.add(
'index out of order: ${order[i - 1]} before ${order[i]}',
);
}
}
report.rows = index.length;
final notInBlueprint = <String, String>{};
for (final line in section('## Not in the Blueprint', '\n## ').split('\n')) {
if (config.rowId(line) == null) continue;
final cells = line.trim().split(_pipe).map((c) => c.trim()).toList();
notInBlueprint[cells[1]] = cells.length > 2 ? cells[2] : '';
}
// --- the two against each other -----------------------------------------
for (final id in blocks.keys) {
if (!index.containsKey(id)) {
report.findings.add('$id: block in the Blueprint, no index row');
}
}
index.forEach((id, row) {
final inBlueprint = blocks.containsKey(id);
final waiting = config.statuses.any(
(st) => st.key == row.statusKey && st.waiting,
);
switch (waiting ? 'waiting' : row.statusKey) {
case 'waiting':
if (!inBlueprint) {
report.findings.add(
'$id: ${row.status} in the index, but no block in the Blueprint',
);
}
case 'duplicate':
if (!inBlueprint && notInBlueprint[id] != row.status) {
report.findings.add(
'$id: duplicate without a pointer block, and not under "Not in the Blueprint"',
);
}
default:
if (inBlueprint) {
report.findings.add(
'$id: ${row.status} in the index, but its block is still in the Blueprint',
);
}
if (notInBlueprint[id] != row.status) {
report.findings.add(
'$id: ${row.status} in the index, but "Not in the Blueprint" '
'${notInBlueprint.containsKey(id) ? 'says ${notInBlueprint[id]}' : 'lacks it'}',
);
}
}
});
notInBlueprint.forEach((id, status) {
if (!index.containsKey(id)) {
report.findings.add(
'$id: under "Not in the Blueprint" but not in the index',
);
}
});
// --- the counts ----------------------------------------------------------
final maxOf = <String, int>{};
for (final id in index.keys) {
final dash = id.lastIndexOf('-');
final prefix = id.substring(0, dash);
final number = int.parse(id.substring(dash + 1));
if (number > (maxOf[prefix] ?? 0)) maxOf[prefix] = number;
}
final groups = config.groups.isNotEmpty
? config.groups
: [
for (final prefix
in (maxOf.keys.toList()..sort((x, y) => idOrder('$x-0', '$y-0'))))
QuestionRegisterGroup(
key: prefix,
label: '$prefix-01 to $prefix-{max}',
prefix: prefix,
),
];
String labelOf(QuestionRegisterGroup g) =>
g.label.replaceAll('{max}', '${maxOf[g.prefix] ?? 0}');
final count = <String, int>{};
final star = <String, int>{};
for (final row in index.values) {
final key = '${row.statusKey}/${config.groupOf(row.id)}';
count[key] = (count[key] ?? 0) + 1;
if (row.star.isNotEmpty) {
star[row.statusKey] = (star[row.statusKey] ?? 0) + 1;
}
}
int c(String s, String g) => count['$s/$g'] ?? 0;
final header =
'| Status | ${[for (final g in groups) labelOf(g)].join(' | ')} | All | ★ |';
final table = <String>[
header,
'|--------|${[for (final _ in groups) '------:'].join('|')}|----:|--:|',
];
final totals = List<int>.filled(groups.length, 0);
var ts = 0;
for (final st in config.statuses) {
final cells = [for (final g in groups) c(st.key, g.key)];
for (var i = 0; i < cells.length; i++) {
totals[i] += cells[i];
}
ts += star[st.key] ?? 0;
final starCell = st.waiting ? '${star[st.key] ?? 0}' : '—';
table.add(
'| ${st.label} | ${cells.join(' | ')} | ${cells.fold(0, (x, y) => x + y)} | $starCell |',
);
}
table.add(
'| **all** | ${[for (final t in totals) '**$t**'].join(' | ')} | '
'**${totals.fold(0, (x, y) => x + y)}** | **$ts** |',
);
final waitingKeys = [
for (final st in config.statuses)
if (st.waiting) st.key,
];
report.waiting = [
for (final s in waitingKeys)
for (final g in groups) c(s, g.key),
].fold(0, (a, b) => a + b);
report.waitingStarred = waitingKeys.fold(0, (a, s) => a + (star[s] ?? 0));
// Write the counts into the file, then compare with what it said.
var out = clarifications;
final tableStart = groups.isEmpty
? -1
: out.indexOf('| Status | ${labelOf(groups.first).split('{max}').first}');
if (tableStart < 0) {
report.findings.add('the status table is missing');
} else {
var tableEnd = tableStart;
for (var n = 0; n < table.length; n++) {
tableEnd = out.indexOf('\n', tableEnd) + 1;
}
out = out.replaceRange(tableStart, tableEnd, '${table.join('\n')}\n');
}
out = out.replaceFirstMapped(
RegExp(
r'\d+ questions and proposals wait for an answer, \d+ of them marked ★;\n\d+ blocks are in the Blueprint',
),
(_) =>
'${report.waiting} questions and proposals wait for an answer, '
'${report.waitingStarred} of them marked ★;\n${report.blocks} blocks are in the Blueprint',
);
maxOf.forEach((prefix, max) {
out = out.replaceAll(
RegExp(
'${RegExp.escape(prefix)}-01 to ${RegExp.escape(prefix)}-\\d+\\*\\*',
),
'$prefix-01 to $prefix-$max**',
);
});
report.rewritten = out;
if (out != clarifications) {
report.findings.add(
'the status counts, the sentence below them or the id range are out of date '
'(run with --write)',
);
}
return report;
}