Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .agents/skills/openfasttrace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ Coverage in a YMAL file (e.g., GitHub workflow)
Require coverage:

```plantuml
# [req -> dsn~hash-sum-calculation~1 >> impl, utest]
' [req -> dsn~hash-sum-calculation~1 >> impl, utest]
```

Multiple coverage:

```Java
// [dsn -> req~local-stability~1,arch~dimensional-input~1]
```

## Tracing
Expand Down
4 changes: 4 additions & 0 deletions doc/changes/changes_4.6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ We moved some GitHub action permissions from workflow-level to job-level.
* #546: Replaced `OsDetector` with JUnit5's `EnabledOnOs` annotation.
* #544: Replaced optional parameter with null check
* #543: Made `CliException` a `RuntimeException`

## Features

* #553 Tag importer supports multiple covered ids
18 changes: 18 additions & 0 deletions doc/spec/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,24 @@ Covers:

Needs: impl, utest

#### Full Coverage Tag Format Allows Multiple Coverage
`dsn~import.full-coverage-tag-multiple-needed-coverage~1`

OFT imports full coverage tags with multiple need coverage ids:

full-tag-multiple-coverage-id =
"[" *WSP reference *WSP "->" *WSP requirement-id *WSP *("," *WSP requirement-id) *WSP "]"

Rationale:

An item can cover multiple IDs. This avoids creating multiple IDs for the same item solely to represent multiple coverage relations. It also reduces the number of IDs that related items must reference for complete coverage, improving readability and maintainability.

Covers:

* `req~import.full-coverage-tag-format~1`

Needs: impl, utest

#### Full Coverage Tag Format Allows Specifying a Revision
`dsn~import.full-coverage-tag-with-revision~1`

Expand Down
4 changes: 2 additions & 2 deletions doc/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ To avoid conflict with the formats actual contents, you embed these definitions
Tags have the following format:

```
[ <covered-artifact-type> -> <specification-object-id> ]
[ <covered-artifact-type> -> <list-of-specification-object-ids> ]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[ <covered-artifact-type> -> <list-of-specification-object-ids> ]
[ <covered-artifact-type> -> <specification-object-id>]
If a piece of code covers multiple specification items, you can also list them separated by comma.
[ <covered-artifact-type> -> <specification-object-id>, <specification-object-id>]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Davidius86 for the PR. I am started reviewing it. I will update main to 2.6.0 and create the release letter in another PR, so that we have a place where we can mention the change.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completeness: Please also add a short example of the syntax to .agents/skills/openfasttrace/SKILL.md.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completeness: Please add an short entry for this in the change log too (we are currently working towards 4.6.0).

```

Spaces above were only added for readability. They are optional. In fact usually people prefer a more compact form.
Expand Down Expand Up @@ -737,7 +737,7 @@ Examples:
// [impl~validate-password~2->dsn~validate-authentication-request~1]
```

##### Forwarding Requirements
##### Needed Coverage

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find. 👍

Compliance: I am guessing you are using an LLM to help with the work. Please note that according to our contribution guideline, LLM-assisted PR must state that.


When using UML models as design document files like UML models it is useful to add needed coverage as well. To do this, you can use the following format:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import static java.util.Collections.emptyList;

import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.regex.Matcher;
Expand All @@ -23,18 +25,21 @@ class LongTagImportingLineConsumer extends AbstractRegexLineConsumer
private static final String OPTIONAL_WHITESPACE = "\\s*";
private static final String TAG_PREFIX = "\\[";
private static final String TAG_SUFFIX = "\\]";
private static final String NEEDS_COVERAGE = ">>" + OPTIONAL_WHITESPACE + "(\\p{Alpha}+(?:"
+ OPTIONAL_WHITESPACE
+ "," + OPTIONAL_WHITESPACE + "\\p{Alpha}+)*)";
private static final String COVERED_IDS = SpecificationItemId.ID_PATTERN + "(?:"
+ OPTIONAL_WHITESPACE + "," + OPTIONAL_WHITESPACE + SpecificationItemId.ID_PATTERN
+ ")*";
private static final String NEEDS_COVERAGE = ">>" + OPTIONAL_WHITESPACE
+ "(?<neededArtifactTypes>\\p{Alpha}+(?:" + OPTIONAL_WHITESPACE + ","
+ OPTIONAL_WHITESPACE + "\\p{Alpha}+)*)";
private static final String TAG_REGEX = TAG_PREFIX + OPTIONAL_WHITESPACE//
+ "(" + COVERING_ARTIFACT_TYPE_PATTERN + ")"
+ "(?<artifactType>" + COVERING_ARTIFACT_TYPE_PATTERN + ")"
+ "(?:" + SpecificationItemId.ARTIFACT_TYPE_SEPARATOR
// [impl->dsn~import.full-coverage-tag-with-name-and-revision~1]
+ "(" + SpecificationItemId.ITEM_NAME_PATTERN + ")?"
+ "(?<customName>" + SpecificationItemId.ITEM_NAME_PATTERN + ")?"
+ SpecificationItemId.REVISION_SEPARATOR
+ SpecificationItemId.ITEM_REVISION_PATTERN + ")?" //
+ "(?<revision>" + SpecificationItemId.ITEM_REVISION_PATTERN + "))?" //
+ OPTIONAL_WHITESPACE + "->" + OPTIONAL_WHITESPACE //
+ "(" + SpecificationItemId.ID_PATTERN + ")" //
+ "(?<coveredIds>" + COVERED_IDS + ")" //
+ OPTIONAL_WHITESPACE + "(?:" + NEEDS_COVERAGE + OPTIONAL_WHITESPACE + ")?" //
+ TAG_SUFFIX;

Expand All @@ -50,21 +55,40 @@ class LongTagImportingLineConsumer extends AbstractRegexLineConsumer

@Override
public void processMatch(final Matcher matcher, final int lineNumber, final int lineMatchCount)
{
final List<SpecificationItemId> coveredIds = parseCoveredIds(matcher.group("coveredIds"));
final List<String> neededArtifactTypes = parseNeededArtifactTypes(matcher.group("neededArtifactTypes"));

final List<SpecificationItemId> generatedIds = createItemIds(matcher, lineNumber, lineMatchCount, coveredIds,
neededArtifactTypes);

if (generatedIds.size() > 1)
{
assert generatedIds.size() == coveredIds.size();
for (int i = 0; i < generatedIds.size(); i++)
{
addSpecificationItem(lineNumber, generatedIds.get(i), List.of(coveredIds.get(i)), neededArtifactTypes);
}
}
else
{
addSpecificationItem(lineNumber, generatedIds.get(0), coveredIds, neededArtifactTypes);
}
}

private void addSpecificationItem(final int lineNumber, final SpecificationItemId generatedId,
final List<SpecificationItemId> coveredIds, final List<String> neededArtifactTypes)
{
this.listener.beginSpecificationItem();
this.listener.setLocation(this.file.getPath(), lineNumber);
final SpecificationItemId coveredId = SpecificationItemId.parseId(matcher.group(5));
final List<String> neededArtifactTypes = parseCommaSeparatedList(matcher.group(9));
final SpecificationItemId generatedId = createItemId(matcher, lineNumber, lineMatchCount, coveredId,
neededArtifactTypes);
logItem(lineNumber, coveredId, neededArtifactTypes, generatedId);
this.listener.setId(generatedId);
this.listener.addCoveredId(coveredId);
neededArtifactTypes.forEach(listener::addNeededArtifactType);
coveredIds.forEach(this.listener::addCoveredId);
neededArtifactTypes.forEach(this.listener::addNeededArtifactType);
this.listener.endSpecificationItem();
logItem(lineNumber, coveredIds, neededArtifactTypes, generatedId);
}

private static List<String> parseCommaSeparatedList(final String input)
private static List<SpecificationItemId> parseCoveredIds(final String input)
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safety: Add null handling, please.

if (input == null)
{
Expand All @@ -74,32 +98,56 @@ private static List<String> parseCommaSeparatedList(final String input)
return Arrays.stream(input.split(","))
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.map(SpecificationItemId::parseId)
.toList();
}

private SpecificationItemId createItemId(final Matcher matcher, final int lineNumber, final int lineMatchCount,
final SpecificationItemId coveredId, final List<String> neededArtifactTypes)
private static List<String> parseNeededArtifactTypes(final String input)
{
final String artifactType = matcher.group(1);
final String customName = matcher.group(2);
final String revision = matcher.group(4);
final String name = customName != null ? customName
: getItemName(lineNumber, lineMatchCount, coveredId, neededArtifactTypes);
return SpecificationItemId.createId(artifactType, name, parseRevision(revision));
if (input == null)
{
return emptyList();
}

return Arrays.stream(input.split(","))
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.toList();
}

private List<SpecificationItemId> createItemIds(final Matcher matcher, final int lineNumber,
final int lineMatchCount,
final List<SpecificationItemId> coveredIds, final List<String> neededArtifactTypes)
{
final String artifactType = matcher.group("artifactType");
final String customName = matcher.group("customName");
final String revision = matcher.group("revision");
if (customName != null)
{
return List.of(SpecificationItemId.createId(artifactType, customName, parseRevision(revision)));
}

final List<SpecificationItemId> result = new java.util.ArrayList<>(coveredIds.size());
for (final SpecificationItemId coveredId : coveredIds)
{
final String name = getItemName(lineNumber, lineMatchCount, coveredId, neededArtifactTypes);
result.add(SpecificationItemId.createId(artifactType, name, parseRevision(revision)));
}
return result;
}

private void logItem(final int lineNumber, final SpecificationItemId coveredId,
private void logItem(final int lineNumber, final List<SpecificationItemId> coveredIds,
final List<String> neededArtifactTypes, final SpecificationItemId generatedId)
{
if (neededArtifactTypes.isEmpty())
{
LOG.finest(() -> "File " + this.file + ":" + lineNumber + ": found '" + generatedId
+ "' covering id '" + coveredId);
+ "' covering ids " + coveredIds);
}
else
{
LOG.finest(() -> "File " + this.file + ":" + lineNumber + ": found '" + generatedId
+ "' covering id '" + coveredId + "', needs artifact types "
+ "' covering ids " + coveredIds + ", needs artifact types "
+ neededArtifactTypes);
}
}
Expand Down

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completeness: Please also add tests that the features with requiring coverage and partial spec item definition still work in combination with the comma-separated covered IDs.

Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,64 @@ static Stream<Arguments> tagImporterTests()
itemACoveringB("implA~name1-2943155783~0", "dsn~name1~2"),
itemACoveringB("implB~name2-1099447527~0", "dsn~name2~3"),
itemACoveringB("implC~name3-2846888323~0", "dsn~name3~4")),
parsedItem("[impl~combined~1->dsn~name1~2,dsn~name2~3]",
itemBuilder().id(SpecificationItemId.parseId("impl~combined~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))),
parsedItem("[ impl~combined~1 -> dsn~name1~2 , dsn~name2~3 >> test ]",
itemBuilder().id(SpecificationItemId.parseId("impl~combined~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))
.addNeedsArtifactType("test")),
parsedItem("[ impl~combined~1 -> dsn~name1~2 , dsn~name2~3 >> utest,itest ]",
itemBuilder().id(SpecificationItemId.parseId("impl~combined~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))
.addNeedsArtifactType("utest")
.addNeedsArtifactType("itest")),

parsedItems("[ impl~~1 -> dsn~name1~2 , dsn~name2~3 ]",
itemBuilder().id(SpecificationItemId.parseId("impl~name1-2943155783~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2")),
itemBuilder().id(SpecificationItemId.parseId("impl~name2-3660411016~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))),
parsedItems("[ impl -> dsn~name1~2 , dsn~name2~3 ]",
itemBuilder().id(SpecificationItemId.parseId("impl~name1-2943155783~0"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2")),
itemBuilder().id(SpecificationItemId.parseId("impl~name2-3660411016~0"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))),
parsedItems("[ impl~~1 -> dsn~name1~2 , dsn~name2~3 >> test ]",
itemBuilder().id(SpecificationItemId.parseId("impl~name1~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2"))
.addNeedsArtifactType("test"),
itemBuilder().id(SpecificationItemId.parseId("impl~name2~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))
.addNeedsArtifactType("test")),
parsedItems("[ impl -> dsn~name1~2 , dsn~name2~3 >> test ]",
itemBuilder().id(SpecificationItemId.parseId("impl~name1~0"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2"))
.addNeedsArtifactType("test"),
itemBuilder().id(SpecificationItemId.parseId("impl~name2~0"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))
.addNeedsArtifactType("test")),
parsedItems("[ impl~~1 -> dsn~name1~2 , dsn~name2~3 >> utest,itest ]",
itemBuilder().id(SpecificationItemId.parseId("impl~name1~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2"))
.addNeedsArtifactType("utest")
.addNeedsArtifactType("itest"),
itemBuilder().id(SpecificationItemId.parseId("impl~name2~1"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))
.addNeedsArtifactType("utest")
.addNeedsArtifactType("itest")),
parsedItems("[ impl -> dsn~name1~2 , dsn~name2~3 >> utest,itest ]",
itemBuilder().id(SpecificationItemId.parseId("impl~name1~0"))
.addCoveredId(SpecificationItemId.parseId("dsn~name1~2"))
.addNeedsArtifactType("utest")
.addNeedsArtifactType("itest"),
itemBuilder().id(SpecificationItemId.parseId("impl~name2~0"))
.addCoveredId(SpecificationItemId.parseId("dsn~name2~3"))
.addNeedsArtifactType("utest")
.addNeedsArtifactType("itest")),

parsedItems("[implA->dsn~name1~2" + "]" + UNIX_NEWLINE + "[implB->dsn~name2~3" + "]",
itemACoveringB("implA~name1-2943155783~0", "dsn~name1~2"),
Expand Down Expand Up @@ -163,6 +221,7 @@ static Stream<Arguments> tagImporterTests()
noItemDetected("[impl~missing-revision~->dsn~name2~2]"),
noItemDetected("[impl~illegal?char~1->dsn~name2~2]"),
noItemDetected("[impl~negative-revision~-1->dsn~name2~2]"),
noItemDetected("[impl~missing-covered-id~1->dsn~name2~2,]"),
noItemDetected("[impl~missing-forward~1->dsn~name2~2>>]"),
noItemDetected("[impl~trailing-comma~1->dsn~name2~2>>test,]"),
noItemDetected("[impl~duplicate-comma~1->dsn~name2~2>>test,,other]"),
Expand Down