mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-11-09 22:13:39 +09:00
fix(adr): retain qualified relations without parsing narrative as edges
This commit is contained in:
1 parent
39ea61fda9
commit
d6fbe172dc
3 files changed
+61
-26
No files matched your search
@@ -52,6 +52,12 @@ proposed --> accepted --> deprecated
|
||||
|
||||
Relationships tracked as causal edges: `supersedes`, `amends`, `depends-on`, `related`.
|
||||
|
||||
Body relationship fields accept qualifiers such as `**Amends by scope**:` or
|
||||
`**Depends-on / confirms**:`, and `Relates` is an alias for `Related`. Wrapped
|
||||
lists may continue on lines containing only ADR references (including Markdown
|
||||
links) and separators. Narrative continuation is not scanned for relationships;
|
||||
declare additional relationships in another bold field.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **CLI:** pinned to `@claude-flow/cli` v3.6 major+minor.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { parseAdr } from '../lib/parse-adrs.mjs';
|
||||
|
||||
function links(body) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'adr-relations-'));
|
||||
try {
|
||||
const file = join(dir, 'ADR-032-example.md');
|
||||
writeFileSync(file, `# ADR-032: Example\n\n**Status**: accepted\n\n${body}\n`);
|
||||
return parseAdr(file, dir).links;
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
for (const label of ['Supersedes in part', 'Amends by scope', 'Relates', 'Depends-on / confirms']) {
|
||||
test(`retains a relation with the ${label} label`, () => {
|
||||
assert.equal(links(`**${label}**: ADR-031`).length, 1);
|
||||
});
|
||||
}
|
||||
test('narrative continuation cannot invent an ADR edge', () => {
|
||||
const edges = links('**Amends (by scope)**: ADR-031 surveyed two sites\nand missed a third.\nADR-031 itself applied a correction to ADR-030.');
|
||||
assert.deepEqual(edges, [{ from: 'ADR-032', to: 'ADR-031', relation: 'amends' }]);
|
||||
});
|
||||
test('wrapped reference-only lists still retain every reference', () => {
|
||||
const edges = links('**Related**: ADR-030,\nADR-031, [ADR-029](ADR-029.md)\n\n## Context\nADR-028 is background.');
|
||||
assert.deepEqual(edges.map(e => e.to), ['ADR-030', 'ADR-031', 'ADR-029']);
|
||||
});
|
||||
test('supports repeated fields without treating ordinary prose as metadata', () => {
|
||||
const edges = links('**Related**: ADR-030\n\n**Related (implementation)**: ADR-031\n\nRelated: ADR-029');
|
||||
assert.deepEqual(edges.map(e => e.to), ['ADR-030', 'ADR-031']);
|
||||
});
|
||||
@@ -163,32 +163,28 @@ function parseLinks(text, selfId) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Body relationship lines. #2474 Bug 3: loosened to accept both colon
|
||||
// placements (`**Supersedes:**` and `**Supersedes**:`) and an optional
|
||||
// parenthetical qualifier like `**Supersedes (partial):**` — same
|
||||
// tolerance as parseStatus.
|
||||
//
|
||||
// #2781 (Jordi-Izquierdo-DDS): a wrapped relation like
|
||||
// **Related**: ADR-124,
|
||||
// ADR-125
|
||||
// silently dropped ADR-125 because `(.+)$` under /m only captures one
|
||||
// physical line. Now capture the first line plus any continuation lines
|
||||
// that don't look like the start of a new field (bold **Label**:), a
|
||||
// heading (`##`), a horizontal rule (`---`), or a new list bullet.
|
||||
// Safe because extractAdrRefs strips anything that isn't an ADR-NNN
|
||||
// token, so over-capture into a plain-text continuation is harmless.
|
||||
const REL = (label) => new RegExp(
|
||||
`^[-*+]?\\s*\\*\\*${label}(?:\\s*\\([^)]*\\))?:?\\*\\*:?\\s*(.+(?:\\n(?!\\s*(?:\\*\\*[A-Za-z]|##|---|[-*+]\\s|\\d+\\.\\s))[^\\n]+)*)`,
|
||||
'mi',
|
||||
);
|
||||
const supersedes = REL('Supersedes').exec(text);
|
||||
if (supersedes) for (const ref of extractAdrRefs(supersedes[1])) out.push({ from: ref, to: selfId, relation: 'supersedes' });
|
||||
const amended = REL('(?:Amended[ -]by|Amends)').exec(text);
|
||||
if (amended) for (const ref of extractAdrRefs(amended[1])) out.push({ from: selfId, to: ref, relation: 'amends' });
|
||||
const related = REL('Related').exec(text);
|
||||
if (related) for (const ref of extractAdrRefs(related[1])) out.push({ from: selfId, to: ref, relation: 'related' });
|
||||
const dependsOn = REL('Depends[ -]on').exec(text);
|
||||
if (dependsOn) for (const ref of extractAdrRefs(dependsOn[1])) out.push({ from: selfId, to: ref, relation: 'depends-on' });
|
||||
// Accept qualified labels, either colon placement, and repeated fields.
|
||||
// Wrapped reference lists (#2781) remain supported, but narrative prose is
|
||||
// not a continuation: incidental ADR mentions must not invent edges (#3096).
|
||||
const values = (label) => {
|
||||
const re = new RegExp(
|
||||
`^[ \\t]*(?:[-*+]\\s+)?\\*\\*${label}(?=\\s|:|\\*\\*|/|\\()[^*\\r\\n]*\\*\\*:?[^\\S\\r\\n]*(.*)$`, 'gmi',
|
||||
);
|
||||
return [...text.matchAll(re)].map((match) => {
|
||||
let value = match[1];
|
||||
const rest = text.slice(match.index + match[0].length).split(/\r?\n/).slice(1);
|
||||
for (const line of rest) {
|
||||
const plain = line.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
|
||||
if (!/\bADR-?\d+\b/i.test(plain) || !/^[\s,+;&]*$/.test(plain.replace(/\bADR-?\d+\b/gi, ''))) break;
|
||||
value += `\n${line}`;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
for (const value of values('Supersedes')) for (const ref of extractAdrRefs(value)) out.push({ from: ref, to: selfId, relation: 'supersedes' });
|
||||
for (const value of values('(?:Amended[ -]by|Amends)')) for (const ref of extractAdrRefs(value)) out.push({ from: selfId, to: ref, relation: 'amends' });
|
||||
for (const value of values('(?:Related|Relates)')) for (const ref of extractAdrRefs(value)) out.push({ from: selfId, to: ref, relation: 'related' });
|
||||
for (const value of values('Depends[ -]on')) for (const ref of extractAdrRefs(value)) out.push({ from: selfId, to: ref, relation: 'depends-on' });
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user