Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/broken_links_checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure broken links checker
run: |
mkdir -p ./target
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
java: 21
- os: ubuntu-latest
java: 25
- os: ubuntu-latest
java: 26

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-os-${{ matrix.os }}-java-${{ matrix.java }}
Expand All @@ -38,6 +40,7 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false

- uses: actions/setup-java@v5
name: Set up Java ${{ matrix.java }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: false

- uses: actions/setup-java@v5
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Pages
uses: actions/configure-pages@v6
- name: Build with Jekyll
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Fail if not running on main branch
if: ${{ github.ref != 'refs/heads/main' }}
Expand Down
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"source.generate.finalModifiers": "explicit",
"source.fixAll": "explicit"
},
"java.saveActions.organizeImports": true,
"java.sources.organizeImports.starThreshold": 3,
"java.sources.organizeImports.staticStarThreshold": 3,
"java.configuration.updateBuildConfiguration": "automatic",
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ You are an expert Java developer specializing in requirement tracing and softwar
- Review all changes with `./oft-self-trace.sh` to ensure tracing completeness.
- Follow the branching strategy: `<type>/<number>_<short-description-lower-snake-case>` (e.g., `feature/533_update_agents_md`).
- Place coverage markers at the narrowest possible scope (method or class).
- Follow the quality requirements in `doc/spec/design/quality_requirements.md`.
- **Ask First**:
- Before adding new external dependencies to `pom.xml`.
- Before changing existing architectural patterns in `openfasttrace-core`.
Expand Down
156 changes: 156 additions & 0 deletions doc/changesets/563-support-gherkin-feature-specification-documents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# GH-563 Support Gherkin `.feature` Files As OpenFastTrace Specification Documents

## Goal

Allow OpenFastTrace to import specification items from Gherkin `Scenario` and
`Scenario Outline` blocks in `.feature` files. Preserve legacy coverage tags
when they are written in Gherkin comments, while avoiding coverage-tag regular
expression evaluation for executable Gherkin lines.

## Scope

In scope:

* Extract shared line scanning and long/short coverage-tag parsing into a
prerequisite module.
* Add a dedicated Gherkin importer that composes the shared tag parser.
* Import OFT scenario items identified by `@id:<oft-id>`.
* Parse scoped `# Covers:` and `# Needs:` metadata strictly.
* Preserve comment-based legacy coverage tags in `.feature` files and all
existing tag importer behavior for non-`.feature` inputs.
* Update traced requirements, design, tests, and user documentation.

Out of scope:

* Importing `Feature`, `Rule`, `Background`, or `Examples` as specification
items.
* Supporting coverage tags in executable Gherkin lines.
* Adding an external Gherkin parser dependency.
* Moving Gherkin grammar or validation into the shared parser module.

## Design References

* [System Requirements](../spec/system_requirements.md)
* [Design](../spec/design.md)
* [Quality Requirements](../spec/design/quality_requirements.md)
* [User Guide](../user_guide.md)

## Strategy

1. Merge a behavior-preserving refactoring PR that introduces a shared module
for line scanning and long/short coverage-tag parsing. It retains `.feature`
support in the tag importer.
2. Atomically move `.feature` ownership from the tag importer to a new Gherkin
importer with higher precedence when the Gherkin importer is registered.
3. Implement Gherkin parsing as a single-pass state machine. It receives every
line but forwards only comment lines to the shared coverage-tag parser.
4. Keep all Gherkin syntax, state, validation, and `ImportEventListener`
mapping in the Gherkin importer.

## Gherkin Syntax And Behavior

* A Gherkin scenario is an OFT item only when its immediately preceding,
contiguous Gherkin tag region contains exactly one
`@id:<SpecificationItemId>` tag. Other Gherkin tags are ignored.
* Directives are recognized only in `#` comment lines after that ID tag region
and before the associated `Scenario:` or `Scenario Outline:` header.
Comments elsewhere are ignored by Gherkin metadata parsing.
* `# Covers:` and `# Needs:` are case-sensitive. Each is optional but may occur
at most once. When present, it must contain a non-empty comma-separated list;
duplicate values and malformed IDs or artifact types are errors.
* A repeated or invalid `@id` tag, or a scoped directive without exactly one
valid ID, fails the import with an `ImporterException` containing the file,
line, and reason. Scenarios without OFT metadata remain ignored.
* The scenario header line is the item location. Text after `Scenario:` or
`Scenario Outline:` is the title. The importer streams the scenario-step
block into the description, excluding comments and `Examples`; it ends the
item at the next `Scenario`, `Scenario Outline`, `Feature`, `Rule`,
`Background`, `Examples`, or end of file.
* The importer retains only active metadata and previously imported Gherkin IDs
for duplicate detection. It does not buffer a complete file or description.

## Task List

- [ ] Create and checkout branch
`feature/563_support_gherkin_feature_specification_documents`.

### PR 1: Shared Coverage-Tag Parser Refactoring

- [ ] Add `importer/tag-importer-common` with artifact ID
`openfasttrace-importer-tag-importer-common` to the Maven reactor.
- [ ] Add JPMS module `org.itsallcode.openfasttrace.importer.tag.common` and
export only `LineReader` (including its line-consumer contract) and
`CoverageTagParser` from
`org.itsallcode.openfasttrace.importer.tag.common`.
- [ ] Move line scanning, line-handler composition, regex matching, long/short
coverage-tag parsing, and CRC32 ID generation from `importer/tag` into
the shared module without changing parsing semantics, generated IDs,
listener events, logging, or exception wrapping.
- [ ] Define `CoverageTagParser.create(PathConfig, InputFile,
ImportEventListener)` to compose the long-tag parser and, when a path
configuration is present, the short-tag parser into one line consumer.
Keep parser implementation classes encapsulated.
- [ ] Refactor `openfasttrace-importer-tag` into a thin adapter that creates
the shared parser and scans its input once with the shared `LineReader`.
Keep all supported extensions, including `.feature`, unchanged in this
refactoring PR.
- [ ] Move scanner tests to the shared module and add focused shared-parser
tests for representative long and configured short tags, asserting
listener events, locations, generated IDs, coverage links, and needed
artifact types.
- [ ] Keep the existing tag-importer parsing and factory/configuration tests as
regression tests, including `.feature` support, to prove that the
refactoring is behavior-preserving.

### Requirements And Design

- [ ] Add requirements for importing Gherkin scenarios and outlines, strict
scoped metadata validation, Gherkin importer selection, and comment-only
legacy coverage-tag compatibility.
- [ ] Stop and ask user for review of the updated system requirements.
- [ ] Add design items for factory precedence, the streaming Gherkin state
machine, metadata scope, event mapping, and shared-parser delegation.
- [ ] Stop and ask user for review of the updated design.

### PR 2: Gherkin Importer

- [ ] Add `importer/gherkin` with artifact ID
`openfasttrace-importer-gherkin`; register it in the Maven reactor and
product dependencies.
- [ ] Provide a Gherkin importer factory for `.feature` files with priority
`9000`, ahead of the tag importer's priority `10000`.
- [ ] Implement the defined single-pass Gherkin state machine and map imported
scenario fragments to `ImportEventListener` events.
- [ ] Inject the shared coverage-tag parser and forward only lines whose
trimmed form starts with `#` to it.
- [ ] Implement the specified `@id:`, `Covers`, `Needs`, title, description,
boundary, duplicate-ID, and error behavior.
- [ ] Preserve the shared parser's existing `ImporterException` behavior for
legacy coverage tags; do not introduce a new shared validation exception.

### Verification

- [ ] Add Gherkin importer unit tests for valid scenarios and outlines,
location/title/description extraction, coverage metadata, non-OFT tags,
and ignored ordinary scenarios.
- [ ] Add validation tests for invalid or multiple IDs, orphan directives,
repeated or empty directives, malformed list entries, duplicate metadata
values, and duplicate Gherkin IDs. Assert exception type and relevant
message content.
- [ ] Add regression tests proving comment coverage tags import in `.feature`
files, non-comment coverage-tag text is ignored in `.feature` files, and
tag importer behavior for non-`.feature` inputs is unchanged.
- [ ] Add pipeline tests proving each Gherkin file is scanned once and only
comment lines reach the shared coverage-tag parser.
- [ ] Add product-level tests for Gherkin importer precedence and mixed
scenario specifications with comment-based legacy coverage tags.
- [ ] Run `./oft-self-trace.sh` and ensure the trace stays clean.
- [ ] Run `mvn -T 1C verify` and ensure all quality gates pass.

### Documentation And Changelog

- [ ] Extend [doc/user_guide.md](../user_guide.md) with the `.feature` syntax,
placement rules, validation behavior, and examples.
- [ ] Update [.agents/skills/openfasttrace/SKILL.md](../../.agents/skills/openfasttrace/SKILL.md)
with the Gherkin syntax and comment-only compatibility rule.
- [ ] Add the GH-563 entry to [doc/changes/changes_4.6.0.md](../changes/changes_4.6.0.md).
6 changes: 6 additions & 0 deletions doc/spec/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ The plugin loader discovers and loads available plugins.
## Importers
For each specification artifact type OFT uses an importer. The importer uses the specification artifact as data source and reads specification items from it.

### Shared Coverage Tag Parser

The `importer/tag-importer-common` module provides the reusable line scanning and coverage-tag parsing used by importers. Its public API consists of `LineReader`, which forwards input lines to a consumer, and `CoverageTagParser`, which recognizes full coverage tags and optionally configured short coverage tags.

The tag importer remains responsible for selecting its input files and creating the shared parser. Parsing implementation classes remain encapsulated in the shared module so that future importers can reuse the same coverage-tag semantics without depending on tag-importer internals.

## Import Event Listener
Importers emit events if they find parts of a [specification item](#specification-item) in the artifact they are importing.

Expand Down
2 changes: 2 additions & 0 deletions doc/spec/design/quality_requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ The following rules are written for both human contributors and coding agents. T
3. Declare method parameters as `final`.
4. Output parameters are only allowed when required by external libraries.
5. Prefer explicit types over `var`.
6. Limit code lines to 120 characters. Do not wrap code earlier solely to fit a shorter line length.
7. Limit comment lines, including JavaDoc, to 80 characters.

### Java Test Rules

Expand Down
27 changes: 27 additions & 0 deletions importer/tag-importer-common/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>openfasttrace-importer-tag-importer-common</artifactId>
<name>OpenFastTrace Tag Importer Common</name>
<parent>
<relativePath>../../openfasttrace-mc-deployable-parent/pom.xml</relativePath>
<groupId>org.itsallcode.openfasttrace</groupId>
<artifactId>openfasttrace-mc-deployable-parent</artifactId>
<version>${revision}</version>
</parent>
<properties>
<project.build.outputTimestamp>${reproducible.build.timestamp}</project.build.outputTimestamp>
</properties>
<dependencies>
<dependency>
<groupId>org.itsallcode.openfasttrace</groupId>
<artifactId>openfasttrace-api</artifactId>
</dependency>
<dependency>
<groupId>org.itsallcode.openfasttrace</groupId>
<artifactId>openfasttrace-testutil</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
9 changes: 9 additions & 0 deletions importer/tag-importer-common/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Shared line scanning and coverage-tag parsing support for importers.
*/
module org.itsallcode.openfasttrace.importer.tag.common {
requires java.logging;
requires transitive org.itsallcode.openfasttrace.api;

exports org.itsallcode.openfasttrace.importer.tag.common;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.itsallcode.openfasttrace.importer.tag.common;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer;

abstract class AbstractRegexLineConsumer implements LineConsumer {
private final Pattern pattern;

AbstractRegexLineConsumer(final String patternRegex) {
this(Pattern.compile(patternRegex));
}

private AbstractRegexLineConsumer(final Pattern pattern) {
this.pattern = pattern;
}

@Override
public void readLine(final int lineNumber, final String line) {
final Matcher matcher = this.pattern.matcher(line);
int counter = 0;
while (matcher.find()) {
processMatch(matcher, lineNumber, counter);
counter++;
}
}

abstract void processMatch(Matcher matcher, int lineNumber, int lineMatchCount);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.itsallcode.openfasttrace.importer.tag;
package org.itsallcode.openfasttrace.importer.tag.common;

import java.nio.charset.StandardCharsets;
import java.util.zip.CRC32;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.itsallcode.openfasttrace.importer.tag.common;

import java.util.ArrayList;
import java.util.List;

import org.itsallcode.openfasttrace.api.importer.ImportEventListener;
import org.itsallcode.openfasttrace.api.importer.input.InputFile;
import org.itsallcode.openfasttrace.api.importer.tag.config.PathConfig;
import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer;

/**
* Creates line consumers that import full and configured short coverage tags.
*/
public final class CoverageTagParser {
private CoverageTagParser() {
// Prevent instantiation.
}

/**
* Create a line consumer for coverage tags in an input file.
*
* @param config
* optional configuration for short coverage tags
* @param file
* input file that contains the tags
* @param listener
* listener receiving imported specification items
* @return line consumer that imports full and configured short coverage tags
*/
public static LineConsumer create(final PathConfig config, final InputFile file,
final ImportEventListener listener) {
final List<LineConsumer> parsers = new ArrayList<>();
parsers.add(new LongTagImportingLineConsumer(file, listener));
if (config != null) {
parsers.add(new ShortTagImportingLineConsumer(config, file, listener));
}
return new DelegatingLineConsumer(parsers);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package org.itsallcode.openfasttrace.importer.tag;
package org.itsallcode.openfasttrace.importer.tag.common;

import java.util.Collections;
import java.util.List;

import org.itsallcode.openfasttrace.importer.tag.LineReader.LineConsumer;
import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer;

class DelegatingLineConsumer implements LineConsumer
{
Expand Down
Loading
Loading