From fa0c854f756dacd90278b888defe49ca66ba2787 Mon Sep 17 00:00:00 2001 From: Dieter Baier Date: Wed, 29 Jul 2026 20:02:43 +0200 Subject: [PATCH 1/2] issue_69: Stop labelling what a risk harms as its mitigation The risk register headed a column "Mitigation/action" and filled it from the risk's outgoing `affects` relations. Those are opposites: `affects` points at what the risk endangers. Every risk in this repository names a quality scenario there, so the register told readers that the scenario mitigates the risk it is threatened by -- in the one artifact people scan without opening the detail pages. `mitigates` already existed in the relation enum and no generator had ever read it. Four ADRs here declare it, and none of that reached the register. It is only ever authored outgoing, so answering "what mitigates this risk?" needs the incoming direction -- which the render helper already held the artifacts for. Splits the column in two rather than replacing it: what a risk affects is worth seeing next to what is being done about it. Co-Authored-By: Claude Opus 5 --- scripts/validate-metamodel.rb | 30 +++++++++++++++++++++++++++--- test/validate_metamodel_test.rb | 24 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/scripts/validate-metamodel.rb b/scripts/validate-metamodel.rb index c497aff..100e5c9 100755 --- a/scripts/validate-metamodel.rb +++ b/scripts/validate-metamodel.rb @@ -496,8 +496,8 @@ class ArtifactIndexGenerator output: '11-risks-and-technical-debt/generated/doc-11001-risks.adoc', anchor: 'risks', title: 'Risks', - cols: '1,3,1,1,1,3', - columns: ['ID', 'Risk', 'Probability', 'Impact', 'Priority', 'Mitigation/action'], + cols: '1,3,1,1,1,2,2', + columns: ['ID', 'Risk', 'Probability', 'Impact', 'Priority', 'Affects', 'Mitigation/action'], row: lambda do |artifact, helper| fields = helper.definition_table_fields(artifact) [ @@ -506,7 +506,10 @@ class ArtifactIndexGenerator helper.cell(fields['Likelihood']), helper.cell(fields['Impact']), helper.cell(fields['Priority']), - helper.relation_targets(artifact, 'affects') + # What the risk endangers, and what is being done about it. These are + # opposites and had shared one column headed "Mitigation/action". + helper.relation_targets(artifact, 'affects'), + helper.incoming_relation_sources(artifact, 'mitigates') ] end } @@ -1030,6 +1033,27 @@ def relation_targets(artifact, type) matches.map { |relation| artifact_ref(relation['target']) }.join(" +\n") end + # The mirror of #relation_targets: who points *at* this artifact with the given + # relation type. Relations are only ever authored outgoing, so a question like + # "what mitigates this risk?" can only be answered from the other side. + def incoming_relation_sources(artifact, type) + id = artifact.metadata['id'] + return '-' unless id + + sources = @artifacts_by_id.values.select do |candidate| + next false unless candidate.metadata + + Array(candidate.metadata['relations']).any? do |relation| + relation['type'] == type && relation['target'] == id + end + end + return '-' if sources.empty? + + sources.sort_by { |source| source.metadata['id'].to_s } + .map { |source| artifact_link(source) } + .join(" +\n") + end + def derived_from_cell(entries) origins = Array(entries).compact return '-' if origins.empty? diff --git a/test/validate_metamodel_test.rb b/test/validate_metamodel_test.rb index d259bce..ae88af8 100644 --- a/test/validate_metamodel_test.rb +++ b/test/validate_metamodel_test.rb @@ -572,6 +572,30 @@ def test_artifact_index_output_is_deterministic assert_includes first, '| xref:q-arch-001[Should the valid fixture demonstrate ADR provenance?]' end + def test_risk_index_separates_what_a_risk_affects_from_what_mitigates_it + # Given: the toolkit's own risks, which R-001 affects a quality scenario and + # ADR-007 mitigates + validator = MetamodelValidator.new( + root: ROOT, + docs_dir: ROOT.join('src/docs'), + relations_schema: SCHEMA + ) + artifacts = validator.validate + generator = ArtifactIndexGenerator.new(root: ROOT, docs_dir: ROOT.join('src/docs')) + definition = ArtifactIndexGenerator::INDEX_DEFINITIONS.fetch('Risk') + output_path = ROOT.join('tmp/test-doc-11001-risks.adoc') + + # When: the risk register is rendered + rendered = generator.render(artifacts, definition, output_path) + + # Then: the two are separate columns, and the mitigation column is fed from + # the incoming mitigates relation rather than from what the risk endangers + assert_includes rendered, '| ID | Risk | Probability | Impact | Priority | Affects | Mitigation/action' + assert_includes rendered, 'xref:qs-004-ai-suggestion-reviewability[QS-004-ai-suggestion-reviewability]' + assert_includes rendered, 'xref:adr-007-artifact-provenance-metadata[ADR-007-artifact-provenance-metadata]' + end + + def test_open_questions_index_output_is_deterministic # Given: the arc42 source tree with open questions generator = OpenQuestionsIndexGenerator.new( From 1bbb773c17c1463b100256373b5613e200fb98e1 Mon Sep 17 00:00:00 2001 From: Dieter Baier Date: Wed, 29 Jul 2026 20:03:24 +0200 Subject: [PATCH 2/2] issue_69: Keep a prose assessment out of a narrow column The register lifts likelihood, impact and priority verbatim into columns one tenth of the table wide. Nothing says those source fields have to be terse, so writing a normal explanatory sentence -- which is what an assessment wants -- collapses the generated table, and the author only finds out by rendering it. The source artifact should not have to be written for its own index. `summary_cell` takes the leading sentence, so the verdict reaches the register and the reasoning stays where it belongs. Only a real sentence boundary counts: "Medium (rises when hosted)" survives whole. The template and the risk skill now say so too. Truncating silently would trade one invisible rule for another. Co-Authored-By: Claude Opus 5 --- scripts/validate-metamodel.rb | 23 ++++++++++++++++++++--- skills/risk/SKILL.md | 4 +++- templates/risk.adoc | 7 +++++++ test/validate_metamodel_test.rb | 11 +++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/scripts/validate-metamodel.rb b/scripts/validate-metamodel.rb index 100e5c9..c236f7f 100755 --- a/scripts/validate-metamodel.rb +++ b/scripts/validate-metamodel.rb @@ -503,9 +503,11 @@ class ArtifactIndexGenerator [ helper.artifact_link(artifact, label: helper.short_id(artifact.metadata['id'])), helper.cell(artifact.metadata['title']), - helper.cell(fields['Likelihood']), - helper.cell(fields['Impact']), - helper.cell(fields['Priority']), + # The assessment fields are prose in the source artifact; only their + # verdict fits a register column. See #summary_cell. + helper.summary_cell(fields['Likelihood']), + helper.summary_cell(fields['Impact']), + helper.summary_cell(fields['Priority']), # What the risk endangers, and what is being done about it. These are # opposites and had shared one column headed "Mitigation/action". helper.relation_targets(artifact, 'affects'), @@ -1081,6 +1083,21 @@ def cell(value) text.gsub('|', '\|').gsub("\n", ' ') end + # For narrow index columns fed from prose fields. An assessment reads + # "Medium. It takes one lapse of attention while transcribing." — the verdict + # belongs in the register, the reasoning belongs in the artifact. Without this, + # writing a normal sentence in the source silently wrecks the generated table, + # and the author only finds out by rendering it. + # + # Only a sentence boundary counts: "Medium (rises when hosted)" survives whole. + def summary_cell(value) + text = value.to_s.strip + return '-' if text.empty? + + verdict = text[/\A(.+?)\.(?:\s|\z)/, 1] + cell(verdict && !verdict.empty? ? verdict : text) + end + private def derived_from_entry(entry) diff --git a/skills/risk/SKILL.md b/skills/risk/SKILL.md index ae2db4e..9d775b5 100644 --- a/skills/risk/SKILL.md +++ b/skills/risk/SKILL.md @@ -132,7 +132,9 @@ Use these templates: - The impact is tied to architecture outcomes, quality scenarios, delivery, operations, security, compliance, or stakeholder value. - Likelihood, impact, priority, timeframe, and confidence are qualitative and - reviewable. + reviewable, and each leads with its verdict. The generated risk register + carries only the first sentence of likelihood, impact and priority; the + reasoning belongs in a paragraph below the assessment table. - Mitigations, monitoring, or review actions are realistic and owned. - Assumptions and open questions are explicit. - Risk acceptance remains a human decision. diff --git a/templates/risk.adoc b/templates/risk.adoc index 268b2c5..a19e95a 100644 --- a/templates/risk.adoc +++ b/templates/risk.adoc @@ -32,6 +32,11 @@ Because , may occur, leading to . === Assessment +Likelihood, impact and priority are lifted into the generated risk register, +which has one narrow column each. Only the first sentence is carried over, so +lead with the verdict and put the reasoning in a paragraph below the table — +`Medium. It only takes one lapse of attention.` reaches the register as `Medium`. + [cols="1,3", options="header"] |=== | Field | Value @@ -42,6 +47,8 @@ Because , may occur, leading to . | Confidence | Low, medium, or high. |=== +Why the rating is what it is, in as many sentences as it needs. + === Mitigation Options * Mitigation option. diff --git a/test/validate_metamodel_test.rb b/test/validate_metamodel_test.rb index ae88af8..9262009 100644 --- a/test/validate_metamodel_test.rb +++ b/test/validate_metamodel_test.rb @@ -595,6 +595,17 @@ def test_risk_index_separates_what_a_risk_affects_from_what_mitigates_it assert_includes rendered, 'xref:adr-007-artifact-provenance-metadata[ADR-007-artifact-provenance-metadata]' end + def test_risk_index_reduces_a_prose_assessment_to_its_verdict + # Given: an assessment field written as a normal sentence, and one written + # with a parenthetical that is not a sentence boundary + helper = ArtifactRenderHelper.new(ROOT.join('tmp/test-summary-cell.adoc'), []) + + # When/Then: only a sentence boundary truncates; everything else survives + assert_equal 'Medium', helper.summary_cell('Medium. It takes one lapse of attention') + assert_equal 'Medium (rises when hosted)', helper.summary_cell('Medium (rises when hosted)') + assert_equal 'Low, medium, or high', helper.summary_cell('Low, medium, or high') + assert_equal '-', helper.summary_cell('') + end def test_open_questions_index_output_is_deterministic # Given: the arc42 source tree with open questions