diff --git a/.reqstool-ai.yaml b/.reqstool-ai.yaml new file mode 100644 index 00000000..aa5f1f33 --- /dev/null +++ b/.reqstool-ai.yaml @@ -0,0 +1,8 @@ +# reqstool-ai configuration — see https://github.com/reqstool/reqstool-ai +# Capability-prefixed IDs (STATUS_, REPORT_, ...) are managed by hand in +# requirements.yml; no per-module prefixes are configured for this single dataset. +urn: reqstool-client +revision: "0.11.0" + +system: + path: docs/reqstool diff --git a/docs/reqstool/manual_verification_results.yml b/docs/reqstool/manual_verification_results.yml deleted file mode 100644 index a0867ce0..00000000 --- a/docs/reqstool/manual_verification_results.yml +++ /dev/null @@ -1,22 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/manual_verification_results.schema.json - -results: - - id: MVR_001 - svc_ids: ["SVC_021"] - pass: true - - id: MVR_002 - svc_ids: ["SVC_026"] - comment: "Verify the status written to stdout, structure, content etc." - pass: true - - id: MVR_003 - svc_ids: ["SVC_028"] - comment: "Verify raw JSON written to file" - pass: true - - id: MVR_004 - svc_ids: ["SVC_035"] - comment: "Verify report written to file" - pass: true - - id: MVR_005 - svc_ids: ["SVC_036"] - comment: "Verify working custom path to annotations.yml" - pass: true diff --git a/docs/reqstool/reqstool_config.yml b/docs/reqstool/reqstool_config.yml index 33271ba0..2ce65328 100644 --- a/docs/reqstool/reqstool_config.yml +++ b/docs/reqstool/reqstool_config.yml @@ -5,7 +5,6 @@ build: hatch resources: requirements: requirements.yml software_verification_cases: software_verification_cases.yml - manual_verification_results: manual_verification_results.yml annotations: ../../build/reqstool/annotations.yml test_results: - ../../build/**/*.xml diff --git a/docs/reqstool/requirements.yml b/docs/reqstool/requirements.yml index 0d68a4e3..5f244c74 100644 --- a/docs/reqstool/requirements.yml +++ b/docs/reqstool/requirements.yml @@ -7,231 +7,452 @@ metadata: url: https://github.com/reqstool/reqstool-client requirements: - - id: REQ_001 - title: Indata from local files - significance: shall - description: Reqstool should be able to read and parse *.yml files from a local disk. - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_002 - title: Indata from git location - significance: shall - description: Reqstool should be able to fetch and parse *.yml files from a git repository. - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_003 - title: Indata from maven artifacts - significance: shall - description: Reqstool should be able to fetch, unzip and parse *.yml files from a maven artifact - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_004 - title: Requirement categories from ISO standard - significance: shall - description: Requirements shall be categorized using this ISO25000 standard for product quality. See link for further information https://iso25000.com/index.php/en/iso-25000-standards/iso-25010 - categories: [reliability] - revision: 0.0.1 - - id: REQ_005 - title: Reqstool client should be able to parse all *.yml files from a microservice variant - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [compatibility] - revision: 0.0.1 - - id: REQ_006 - title: Reqstool client should be able to parse all *.yml files except the implementations part of annotations.yml from a system variant - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [compatibility] - revision: 0.0.1 - - id: REQ_007 - title: Reqstool client should be able to parse a requirements.yml file from an external variant - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [compatibility] - revision: 0.0.1 - - id: REQ_008 - title: Requirements.yml file must reside under content root - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_009 - title: Manual_verificaton_results.yml file must reside under content root - significance: shall - description: User will have to provide a manual_verificaton_results.yml file in the root of the path provided - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_010 - title: Software_verificaton_cases.yml file must reside under content root - significance: shall - description: User will have to provide a software_verificaton_cases.yml file in the root of the path provided - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_011 - title: Reqstool_config.yml file must reside under content root + # --- status capability (derived from openspec/specs/status) --- + - id: STATUS_0001 + title: Requirement status computation + significance: shall + description: The system shall compute, for every requirement, its implementation state, automated test outcome, and manual verification outcome, and classify each requirement as complete or incomplete. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: STATUS_0002 + title: Completion verdict + significance: shall + description: The system shall derive an overall verdict from the number of incomplete requirements, reporting PASS when none are incomplete and FAIL otherwise. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: STATUS_0003 + title: Console verbosity levels + significance: shall + description: The system shall provide four console verbosity levels (compact, normal, verbose, extra-verbose) presenting increasing detail, defaulting to normal. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: STATUS_0004 + title: Incomplete-only filtering + significance: shall + description: The system shall support restricting console output to only incomplete requirements. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: STATUS_0005 + title: JSON output format + significance: shall + description: The system shall be able to emit status as a structured JSON document as an alternative to console output, ignoring console verbosity when JSON is selected. + categories: ["compatibility"] + revision: "0.11.0" + - id: STATUS_0006 + title: Requirement and SVC filtering + significance: shall + description: The system shall allow JSON status output to be filtered to a specified set of requirement IDs or SVC IDs. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: STATUS_0007 + title: CI gating exit code + significance: shall + description: The system shall, when explicitly asked to enforce coverage, exit with a dedicated non-zero code if any requirement is unmet, and otherwise exit zero. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: STATUS_0008 + title: Post-build test gating + significance: shall + description: The system shall accept one or more post-build JUnit XML result files and incorporate their outcomes into the status computation. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: STATUS_0009 + title: Status output destination + significance: shall + description: The system shall write status output to a file when a path is given and to standard output otherwise. + categories: ["interaction-capability"] + revision: "0.11.0" + + # --- report capability (derived from openspec/specs/report) --- + - id: REPORT_0001 + title: Report generation + significance: shall + description: The system shall generate a report from the collected statistics that lists every requirement together with its implementation and verification status. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: REPORT_0002 + title: Output format selection + significance: shall + description: The system shall render the report in a selectable markup format, defaulting to AsciiDoc and also supporting Markdown. + categories: ["compatibility"] + revision: "0.11.0" + - id: REPORT_0003 + title: Grouping + significance: shall + description: The system shall group requirements in the report either by their initial-versus-imported origin or by requirement category. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: REPORT_0004 + title: Sorting + significance: shall + description: The system shall sort the requirements within each group by one or more of identifier, significance, or revision. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: REPORT_0005 + title: Report output destination + significance: shall + description: The system shall write the report to a file when a path is given and to standard output otherwise. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: REPORT_0006 + title: Deprecated AsciiDoc alias + significance: should + description: The system should retain a deprecated dedicated AsciiDoc report command that behaves like the report command in AsciiDoc format while warning the user to migrate. + categories: ["maintainability"] + revision: "0.11.0" + + # --- export capability (derived from openspec/specs/export) --- + - id: EXPORT_0001 + title: JSON export + significance: shall + description: The system shall export the dataset as a JSON document conforming to the export schema, as the default format. + categories: ["compatibility"] + revision: "0.11.0" + - id: EXPORT_0002 + title: Export filtering + significance: shall + description: The system shall allow the JSON export to be restricted to a specified set of requirement IDs or SVC IDs. + categories: ["flexibility"] + revision: "0.11.0" + - id: EXPORT_0003 + title: Unfiltered export + significance: shall + description: The system shall provide an option to export the dataset without applying the dataset's own import and scope filters. + categories: ["flexibility"] + revision: "0.11.0" + - id: EXPORT_0004 + title: SQLite export + significance: shall + description: The system shall be able to export the dataset as a SQLite database snapshot written to a file, requiring an output file path. + categories: ["compatibility"] + revision: "0.11.0" + - id: EXPORT_0005 + title: Export output destination + significance: shall + description: The system shall write the JSON export to a file when a path is given and to standard output otherwise. + categories: ["interaction-capability"] + revision: "0.11.0" + + # --- validate capability (derived from openspec/specs/validate) --- + - id: VALIDATE_0001 + title: SVC coverage check + significance: shall + description: The system shall report every requirement that has no software verification case defined. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: VALIDATE_0002 + title: Manual verification coverage check + significance: shall + description: The system shall report every SVC that expects a manual verification result but has none recorded. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: VALIDATE_0003 + title: Referential integrity check + significance: shall + description: The system shall report referential-integrity errors where an SVC, MVR, or annotation references a non-existent entity, and treat such errors as fatal. + categories: ["reliability"] + revision: "0.11.0" + - id: VALIDATE_0004 + title: Strict mode + significance: shall + description: The system shall treat coverage gaps as warnings by default and as errors when strict mode is enabled. + categories: ["flexibility"] + revision: "0.11.0" + - id: VALIDATE_0005 + title: Validation summary + significance: shall + description: The system shall summarize the outcome with counts of errors and warnings and an overall pass/fail result, signalling failure through its exit code. + categories: ["interaction-capability"] + revision: "0.11.0" + + # --- enrich capability (derived from openspec/specs/enrich) --- + - id: ENRICH_0001 + title: Document enrichment + significance: shall + description: The system shall enrich an input document by injecting the titles and descriptions of the requirement, SVC, and MVR identifiers it references. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: ENRICH_0002 + title: Enrichment preset selection + significance: shall + description: The system shall apply a named enrichment preset that determines how references are detected and rendered. + categories: ["flexibility"] + revision: "0.11.0" + - id: ENRICH_0003 + title: Enrichment input and output + significance: shall + description: The system shall read the document to enrich from a file or standard input and write the result to a file or standard output. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: ENRICH_0004 + title: Dataset auto-detection + significance: shall + description: The system shall auto-detect the dataset from the reqstool AI configuration file in the current or an ancestor directory when no source is provided, and report an error when none is found. + categories: ["interaction-capability"] + revision: "0.11.0" + + # --- lsp capability (derived from openspec/specs/lsp) --- + - id: LSP_0001 + title: Language server startup + significance: shall + description: The system shall start a Language Server Protocol server that serves reqstool data to a connected editor client. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: LSP_0002 + title: Language server transport selection + significance: shall + description: The system shall serve over stdio by default and support a TCP transport with a configurable host and port. + categories: ["compatibility"] + revision: "0.11.0" + - id: LSP_0003 + title: Language server log file significance: may - description: User will have to provide a reqstool_config.yml file in the root of the path provided if changes to the default path for dynamic files or project root directory are required - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_012 - title: Both static and dynamically generated files should should honor their respective json schemas - significance: shall - description: All files that reqstool requires or creates should have a json schema. Reqstool client should validate the content before parsing - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_013 - title: Reqstool client requires a annotations.yml file in order to parse and track implementation and tests + description: The system may write server logs to a file in addition to standard error. + categories: ["maintainability"] + revision: "0.11.0" + - id: LSP_0004 + title: Language server dependency guard + significance: shall + description: The system shall report a clear, actionable error when the optional dependencies required for the language server are not installed. + categories: ["reliability"] + revision: "0.11.0" + + # --- mcp capability (derived from openspec/specs/mcp) --- + - id: MCP_0001 + title: MCP server startup + significance: shall + description: The system shall start a Model Context Protocol server that exposes the reqstool dataset to connected clients through structured tools. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: MCP_0002 + title: MCP server transport selection + significance: shall + description: The system shall support stdio, SSE, and streamable-HTTP transports, defaulting to stdio, with a configurable host and port for the HTTP transports. + categories: ["compatibility"] + revision: "0.11.0" + - id: MCP_0003 + title: MCP dataset resolution + significance: shall + description: The system shall serve an explicitly provided source, or auto-detect the dataset from the reqstool AI configuration file when no source is given, and report an error when neither is available. + categories: ["interaction-capability"] + revision: "0.11.0" + - id: MCP_0004 + title: MCP server dependency guard + significance: shall + description: The system shall report a clear, actionable error when the optional dependencies required for the MCP server are not installed. + categories: ["reliability"] + revision: "0.11.0" + + # --- data-sources capability (derived from openspec/specs/data-sources) --- + - id: SOURCE_0001 + title: Local materialization contract + significance: shall + description: The system shall make every selected source available on local disk before parsing, regardless of source type. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: SOURCE_0002 + title: Local directory source + significance: shall + description: The system shall accept a path to a local directory as a data source. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: SOURCE_0003 + title: Local packaged-artifact source + significance: shall + description: The system shall accept a local packaged artifact (Maven ZIP, npm tarball, or PyPI source distribution) as a data source and extract it before parsing. + categories: ["compatibility"] + revision: "0.11.0" + - id: SOURCE_0004 + title: Git repository source + significance: shall + description: The system shall fetch data from a git repository identified by URL, a path within the repository, and a ref (branch, tag, or commit). + categories: ["functional-suitability"] + revision: "0.11.0" + - id: SOURCE_0005 + title: Maven artifact source + significance: shall + description: The system shall fetch a Maven artifact identified by group ID, artifact ID, and version, with an optional repository URL and classifier. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: SOURCE_0006 + title: npm package source + significance: shall + description: The system shall fetch an npm package identified by package name and version, defaulting to the public npm registry when no registry URL is given. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: SOURCE_0007 + title: PyPI package source + significance: shall + description: The system shall fetch a PyPI package identified by package name and version, with an optional index URL. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: SOURCE_0008 + title: Authenticated access + significance: shall + description: The system shall accept an authentication token for remote sources, supplied as a direct value with support for variable references in data files. + categories: ["security"] + revision: "0.11.0" + + # --- ingestion capability (derived from openspec/specs/ingestion) --- + - id: INGEST_0001 + title: Parse requirements + significance: shall + description: The system shall parse requirements from a requirements.yml file, producing requirement entities with their identifier, title, significance, description, categories, and revision. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: INGEST_0002 + title: Parse software verification cases + significance: shall + description: The system shall parse software verification cases from a software_verification_cases.yml file, each linked to the requirements it verifies. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: INGEST_0003 + title: Parse manual verification results + significance: shall + description: The system shall parse manual verification results from a manual_verification_results.yml file, each linked to the SVC it verifies. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: INGEST_0004 + title: Parse code annotations significance: should - description: A annotations.yml file should be provided for Reqstool client in order to check implementation ans test results. The default path (target/reqstool) could be altered with the requirements_config file. - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_014 - title: Reqstool client should be able to parse junit test reports - significance: shall - description: "Reqstool client should be able to parse test cases from xml data that follows junit xml structure. Sett this link for an example: https://github.com/testmoapp/junitxml#basic-junit-xml-structure" - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_015 - title: Reqstool client should be able to parse data from karate test reports - significance: shall - description: Reqstool client should be able to parse test reports from karate test cases - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_016 - title: Reqstool client requires directory path(s) to test report files in order to read and parse the test results + description: The system should parse code annotations from a generated annotations.yml file, capturing which code elements implement requirements and which tests verify SVCs. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: INGEST_0005 + title: Parse automated test results + significance: shall + description: The system shall parse automated test results from JUnit XML report files. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: INGEST_0006 + title: Parse Karate test reports + significance: shall + description: The system shall parse test results from Karate test reports. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: INGEST_0007 + title: Static files at the content root + significance: shall + description: The system shall expect the static input files (requirements.yml, software_verification_cases.yml, manual_verification_results.yml) at the root of the provided content path. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: INGEST_0008 + title: Configurable file locations significance: should - description: A directory path to where .xml test report files resides should be provided for Reqstool client in order to check implementation ans test results. The default paths (target/failsafe-reports | target/surefire-reports) could be altered with the requirements_config file. - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_017 - title: Parse requirements from a maven artifact - significance: shall - description: "Reqstool client should be able to parse requirements data from a Maven artifact that follows the correct structure described in the documentation. See: https://reqstool.github.io/reqstool-client/reqstool-client/0.3.0/data.html#maven-artifact-zip-directory-structure" - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_018 - title: Apply filters on imported requirements - significance: shall - description: User shall be able to filter in or out specific requirement id's from another source - categories: [interaction-capability] - revision: 0.0.1 - - id: REQ_019 - title: Apply filters on imported software verification cases - significance: shall - description: User shall be able to filter in or out specific software verification id's from another source - categories: [interaction-capability] - revision: 0.0.1 - - id: REQ_020 - title: Custom filter patterns - significance: may - description: User may apply a custom filter pattern for svc id's and requirement id's - categories: [flexibility] - revision: 0.0.1 - - id: REQ_021 - title: All yml files needed for reqstool-client shall follow their respective json schema. - significance: shall - description: Reqstool-client shall validate .yml files and prompt user it errors are detected. - categories: [functional-suitability] - revision: 0.0.1 - - id: REQ_022 - title: Reqstool client shall log a warning if duplicate requirement id's are detected during parsing - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [compatibility] - revision: 0.0.1 - - id: REQ_023 - title: Reqstool client shall log a warning if duplicate svc id's are detected during parsing - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [compatibility] - revision: 0.0.1 - - id: REQ_024 - title: Reqstool client shall log a warning if references to non existing requirement id's are detected during parsing - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [compatibility] - revision: 0.0.1 - - id: REQ_025 - title: Reqstool client shall log a warning if references to non existing svc id's are detected during parsing - significance: shall - description: User will have to provide a requirements.yml file in the root of the path provided - categories: [compatibility] - revision: 0.0.1 - - id: REQ_026 - title: Reqstool client shall exit if it cannot find a requirements.yml file from the path provided - significance: shall - description: User will be propted that a required .yml file is missing from the path provided. - categories: [interaction-capability] - revision: 0.0.1 - - id: REQ_027 - title: Generate status - significance: shall - description: Reqstool should be able to generate status from the collected statistics, listing all requirements and their status on implementation and tests. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_028 - title: Collect statistics for Requirements - significance: shall - description: Reqstool should collect statistics for each requirement (SVCs, MVRs, implementation). - categories: [maintainability] - revision: 0.4.0 - - id: REQ_029 - title: Write status to file / stdout - significance: shall - description: Reqstool should be able to write the status to file or default to stdout if no file specified. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_030 - title: Generate raw JSON from imported models - significance: shall - description: Reqstool should be able to generate imported models into raw JSON. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_031 - title: Write generated JSON to file / stdout - significance: shall - description: Reqstool should be able to write the JSON to file or default to stdout if no file specified. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_032 - title: Generate report from collected statistics - significance: shall - description: Reqstool should be able to generate a report from the collected statistics, listing all requirements and their status. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_033 - title: Group by in report - significance: shall - description: Reqstool should be able to generate a report grouped by the categories specified in the requirements or initial/imported urns. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_034 - title: Sort by in report - significance: shall - description: Reqstool should be able to sort the generated report. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_035 - title: Write report to file / stdout - significance: shall - description: Reqstool should be able to write the report to file / stdout. - categories: [functional-suitability] - revision: 0.4.0 - - id: REQ_036 - title: Reqstool shall support requirements that does not require an implementation - significance: shall - description: A user should be able to define a requirement that does not need an source code implementation. - categories: [functional-suitability, flexibility] - revision: 0.4.5 - - id: REQ_037 - title: Requirements shall have the option to be decommissioned - significance: shall - description: A user shall be able to specify the state of the requirement. Whether it is in use or not. - categories: [maintainability] - revision: 0.4.6 - - id: REQ_038 - title: SVCs shall have the option to be decommissioned - significance: shall - description: A user shall be able to specify the state of the SVC. Whether it is in use or not. - categories: [maintainability] - revision: 0.4.6 + description: The system should support an optional configuration file that overrides the default locations for generated files and test reports. + categories: ["flexibility"] + revision: "0.11.0" + + # --- imports-and-filtering capability (derived from openspec/specs/imports-and-filtering) --- + - id: IMPORT_0001 + title: Recursive import resolution + significance: shall + description: The system shall resolve a dataset's imports recursively, including the requirements of each imported dataset and the imports they declare in turn. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: IMPORT_0002 + title: Import cycle detection + significance: shall + description: The system shall detect a cycle in the import chain and reject it rather than recursing indefinitely. + categories: ["reliability"] + revision: "0.11.0" + - id: IMPORT_0003 + title: Recursive implementation resolution + significance: shall + description: The system shall resolve a dataset's declared implementations recursively, treating each implementing dataset as one that may declare its own implementations. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: IMPORT_0004 + title: Implementation cycle detection + significance: shall + description: The system shall detect a cycle in the implementation chain and reject it. + categories: ["reliability"] + revision: "0.11.0" + - id: IMPORT_0005 + title: Implementation requirements excluded from scope + significance: shall + description: The system shall exclude the requirements contributed by implementation datasets from the system's own requirement set, retaining only their verification evidence. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: IMPORT_0006 + title: Filter imported requirements + significance: shall + description: The system shall allow a dataset to include or exclude specific requirement IDs from a composed source. + categories: ["flexibility"] + revision: "0.11.0" + - id: IMPORT_0007 + title: Filter imported SVCs + significance: shall + description: The system shall allow a dataset to include or exclude specific SVC IDs from a composed source. + categories: ["flexibility"] + revision: "0.11.0" + - id: IMPORT_0008 + title: Filter expression language + significance: shall + description: The system shall support a filter expression language combining logical operators, identifier equality and inequality, and regular-expression matching. + categories: ["flexibility"] + revision: "0.11.0" + + # --- parse-validation capability (derived from openspec/specs/parse-validation) --- + - id: PARSE_0001 + title: Schema validation + significance: shall + description: The system shall validate each input file against its JSON schema before parsing and report a validation error when a file does not conform. + categories: ["functional-suitability"] + revision: "0.11.0" + - id: PARSE_0002 + title: Missing requirements file + significance: shall + description: The system shall fail with a clear error when no requirements.yml file is found at the provided content root. + categories: ["reliability"] + revision: "0.11.0" + - id: PARSE_0003 + title: Duplicate requirement identifiers + significance: shall + description: The system shall warn when duplicate requirement identifiers are detected during parsing. + categories: ["reliability"] + revision: "0.11.0" + - id: PARSE_0004 + title: Duplicate SVC identifiers + significance: shall + description: The system shall warn when duplicate SVC identifiers are detected during parsing. + categories: ["reliability"] + revision: "0.11.0" + - id: PARSE_0005 + title: Dangling requirement references + significance: shall + description: The system shall warn when a reference to a non-existent requirement identifier is detected during parsing. + categories: ["reliability"] + revision: "0.11.0" + - id: PARSE_0006 + title: Dangling SVC references + significance: shall + description: The system shall warn when a reference to a non-existent SVC identifier is detected during parsing. + categories: ["reliability"] + revision: "0.11.0" + + # --- lifecycle capability (derived from openspec/specs/lifecycle) --- + - id: LIFECYCLE_0001 + title: Non-code implementation types + significance: shall + description: The system shall allow a requirement to declare that it is satisfied by something other than source code (not-applicable, configuration, platform, or framework) and not treat such a requirement as unimplemented for lacking a code annotation. + categories: ["functional-suitability", "flexibility"] + revision: "0.11.0" + - id: LIFECYCLE_0002 + title: Requirement lifecycle state + significance: shall + description: The system shall allow a requirement to declare a lifecycle state (draft, effective, deprecated, or obsolete) with an optional reason, defaulting to effective. + categories: ["maintainability"] + revision: "0.11.0" + - id: LIFECYCLE_0003 + title: SVC lifecycle state + significance: shall + description: The system shall allow an SVC to declare a lifecycle state (draft, effective, deprecated, or obsolete) with an optional reason, defaulting to effective. + categories: ["maintainability"] + revision: "0.11.0" + - id: LIFECYCLE_0004 + title: Superseded-reference warning + significance: shall + description: The system shall warn when a deprecated or obsolete requirement or SVC is still referenced by active items. + categories: ["maintainability"] + revision: "0.11.0" diff --git a/docs/reqstool/software_verification_cases.yml b/docs/reqstool/software_verification_cases.yml index 05aaa441..5bb740ac 100644 --- a/docs/reqstool/software_verification_cases.yml +++ b/docs/reqstool/software_verification_cases.yml @@ -1,193 +1,452 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/software_verification_cases.schema.json cases: - - id: SVC_001 - requirement_ids: ["REQ_001"] - title: "Test that reads yml files from a local directory" - verification: automated-test - revision: "0.0.1" - - id: SVC_002 - requirement_ids: ["REQ_002"] - title: "Test that will checkout and read yml files from a git repository" - verification: automated-test - revision: "0.0.1" - - id: SVC_003 - requirement_ids: ["REQ_003", "REQ_017"] - title: "Test that will download an artifact, unzip the content and read the yml files from the unzipped directory" - verification: automated-test - revision: "0.0.1" - - id: SVC_004 - requirement_ids: ["REQ_005", "REQ_006", "REQ_007"] - title: "Test that will parse supported yml files from all variants" - verification: automated-test - revision: "0.0.1" - - id: SVC_005 - requirement_ids: ["REQ_008", "REQ_009", "REQ_010", "REQ_011", "REQ_012"] - title: "Verify that reqstool client reads yml files from the path provided" - verification: automated-test - revision: "0.0.1" - - id: SVC_006 - requirement_ids: ["REQ_015", "REQ_016"] - title: "Test that will parse karate test reports" - verification: automated-test - revision: "0.0.1" - - id: SVC_007 - requirement_ids: ["REQ_014", "REQ_016"] - title: "Test that will parse junit test reports" - verification: automated-test - revision: "0.0.1" - - id: SVC_008 - requirement_ids: ["REQ_017"] - title: "Test that will parse requirements from a Maven artifact" - verification: automated-test - revision: "0.0.1" - - id: SVC_009 - requirement_ids: ["REQ_018"] - title: "Test that will test that correct requirement id's are included" - verification: automated-test - revision: "0.0.1" - - id: SVC_010 - requirement_ids: ["REQ_018"] - title: "Test that will test that correct requirement id's are excluded" - verification: automated-test - revision: "0.0.1" - - id: SVC_011 - requirement_ids: ["REQ_019"] - title: "Test that will test that correct svc id's are included" - verification: automated-test - revision: "0.0.1" - - id: SVC_012 - requirement_ids: ["REQ_019"] - title: "Test that will test that correct svc id's are excluded" - verification: automated-test - revision: "0.0.1" - - id: SVC_013 - requirement_ids: ["REQ_020"] - title: "Test that will check validity of the custom filter syntax for requirements" - verification: automated-test - revision: "0.0.1" - - id: SVC_014 - requirement_ids: ["REQ_020"] - title: "Test that will check validity of the custom filter syntax for software verification cases" - verification: automated-test - revision: "0.0.1" - - id: SVC_015 - requirement_ids: ["REQ_021", "REQ_004"] - title: "Tests that will check that all files follow their respective schema files" - verification: automated-test - revision: "0.0.1" - - id: SVC_016 - requirement_ids: ["REQ_022"] - title: "Test that will check if duplicate requirement id's are detected during parsing" - verification: automated-test - revision: "0.0.1" - - id: SVC_017 - requirement_ids: ["REQ_023"] - title: "Test that will check if duplicate svc id's are detected during parsing" - verification: automated-test - revision: "0.0.1" - - id: SVC_018 - requirement_ids: ["REQ_024"] - title: "Test that will check if references to missing requirement id's are detected during parsing" - verification: automated-test - revision: "0.0.1" - - id: SVC_019 - requirement_ids: ["REQ_025"] - title: "Test that will check if references to missing requirement id's are detected during parsing" - verification: automated-test - revision: "0.0.1" - - id: SVC_020 - requirement_ids: ["REQ_026"] - title: "Test that will make sure that the application exits and the user gets a message about failing to provide a requirements.yml file" - verification: automated-test - revision: "0.0.1" - - id: SVC_021 - requirement_ids: ["REQ_027"] - title: "Test that runs status command on test data" - verification: automated-test - revision: "0.4.0" - - id: SVC_022 - requirement_ids: ["REQ_028"] - title: "Test that collects statistics for each requirement from a project with no imported requirements" - verification: automated-test - revision: "0.4.0" - - id: SVC_023 - requirement_ids: ["REQ_028"] - title: "Test that collects statistics for each requirement from a project with imported requirements" - verification: automated-test - revision: "0.4.0" - - id: SVC_024 - requirement_ids: ["REQ_028"] - title: "Test that collects statistics for each requirement from a project with only imported requirements" - verification: automated-test - revision: "0.4.0" - - id: SVC_025 - requirement_ids: ["REQ_028"] - title: "Test that adds statistics to requirement" - verification: automated-test - revision: "0.4.0" - - id: SVC_026 - requirement_ids: ["REQ_029"] - title: "Manual test of status written to stdout" - verification: manual-test - revision: "0.4.0" - - id: SVC_027 - requirement_ids: ["REQ_030"] - title: "Test that generates raw JSON from standard test data" - verification: automated-test - revision: "0.4.0" - - id: SVC_028 - requirement_ids: ["REQ_031"] - title: "Manual test of JSON written to file" - verification: manual-test - revision: "0.4.0" - - id: SVC_029 - requirement_ids: ["REQ_032"] - title: "Test that generates report from test data" - verification: automated-test - revision: "0.4.0" - - id: SVC_030 - requirement_ids: ["REQ_033"] - title: "Test that generates report grouped by categories" - verification: automated-test - revision: "0.4.0" - - id: SVC_031 - requirement_ids: ["REQ_033"] - title: "Test that generates report grouped by initial/imports" - verification: automated-test - revision: "0.4.0" - - id: SVC_032 - requirement_ids: ["REQ_034"] - title: "Test that generates report sorted by ID" - verification: automated-test - revision: "0.4.0" - - id: SVC_033 - requirement_ids: ["REQ_034"] - title: "Test that generates report sorted by significance" - verification: automated-test - revision: "0.4.0" - - id: SVC_034 - requirement_ids: ["REQ_033", "REQ_034"] - title: "Test that tests group_by and sort_by functionality for report" - verification: automated-test - revision: "0.4.0" - - id: SVC_035 - requirement_ids: ["REQ_035"] - title: "Manual test of report written to file" - verification: manual-test - revision: "0.4.0" - - id: SVC_036 - requirement_ids: ["REQ_013"] - title: "Manual test of altering the default path for annotations.yml " - verification: manual-test - revision: "0.4.1" - - id: SVC_037 - requirement_ids: ["REQ_036"] - title: "Test to make sure that reqstool client supports requirements without any source code implementation" - verification: automated-test - revision: "0.4.5" - - id: SVC_038 - requirement_ids: ["REQ_037", "REQ_038"] - title: "Test to make sure that reqstool client supports predefined lifecycle states in requirements and SVCs" - verification: automated-test - revision: "0.4.6" + # --- status --- + - id: SVC_STATUS_0001 + requirement_ids: ["STATUS_0001"] + title: "Status computation classifies requirements" + description: "GIVEN a dataset with implemented, unimplemented, failing-test, and non-code requirements WHEN status is computed THEN each is classified complete or incomplete on implementation, automated test, and manual verification state" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0002 + requirement_ids: ["STATUS_0002"] + title: "Verdict reflects incomplete count" + description: "GIVEN a computed status WHEN zero requirements are incomplete THEN the verdict is PASS, and WHEN one or more are incomplete THEN the verdict is FAIL" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0003 + requirement_ids: ["STATUS_0003"] + title: "Verbosity levels render increasing detail" + description: "GIVEN a dataset WHEN status is rendered at compact, normal, verbose, and extra-verbose THEN each level emits the expected detail, defaulting to normal" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0004 + requirement_ids: ["STATUS_0004"] + title: "Incomplete-only hides complete requirements" + description: "GIVEN complete and incomplete requirements WHEN incomplete-only output is requested THEN only incomplete requirements are listed" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0005 + requirement_ids: ["STATUS_0005"] + title: "JSON output emitted and verbosity ignored" + description: "GIVEN a dataset WHEN JSON output is selected THEN a machine-readable status document is emitted, and WHEN combined with a non-default verbosity THEN a warning is issued and JSON is still produced" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0006 + requirement_ids: ["STATUS_0006"] + title: "Filtering restricts status to selected IDs" + description: "GIVEN a dataset WHEN requirement or SVC IDs are supplied to JSON status output THEN the output is restricted to the requirements in scope of those IDs" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0007 + requirement_ids: ["STATUS_0007"] + title: "Gating exit code reflects coverage" + description: "GIVEN enforcement is enabled WHEN at least one requirement is incomplete THEN the process exits with the all-requirements-not-implemented code, and WHEN enforcement is disabled THEN it exits zero regardless" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0008 + requirement_ids: ["STATUS_0008"] + title: "Post-build results feed the computation" + description: "GIVEN post-build JUnit XML files WHEN supplied THEN their outcomes are injected and post-build gating is activated, and WHEN a supplied file is missing THEN it is reported and no status is produced" + verification: automated-test + revision: "0.11.0" + - id: SVC_STATUS_0009 + requirement_ids: ["STATUS_0009"] + title: "Status written to file or stdout" + description: "GIVEN a status run WHEN an output path is supplied THEN status is written to that file creating directories as needed, and WHEN no path is supplied THEN it is written to standard output" + verification: automated-test + revision: "0.11.0" + + # --- report --- + - id: SVC_REPORT_0001 + requirement_ids: ["REPORT_0001"] + title: "Report lists all requirements with status" + description: "GIVEN a dataset WHEN the report command is run THEN a document listing all requirements with their status is produced" + verification: automated-test + revision: "0.11.0" + - id: SVC_REPORT_0002 + requirement_ids: ["REPORT_0002"] + title: "Report renders selected format" + description: "GIVEN a dataset WHEN no format is specified THEN AsciiDoc is rendered, and WHEN Markdown is selected THEN Markdown is rendered" + verification: automated-test + revision: "0.11.0" + - id: SVC_REPORT_0003 + requirement_ids: ["REPORT_0003"] + title: "Report groups by origin or category" + description: "GIVEN a dataset WHEN grouping by initial/imports THEN initial and imported requirements are separated, and WHEN grouping by category THEN requirements are grouped by their first category" + verification: automated-test + revision: "0.11.0" + - id: SVC_REPORT_0004 + requirement_ids: ["REPORT_0004"] + title: "Report sorts within groups" + description: "GIVEN an ordered list of sort keys WHEN the report is generated THEN requirements within each group are ordered by those keys in sequence" + verification: automated-test + revision: "0.11.0" + - id: SVC_REPORT_0005 + requirement_ids: ["REPORT_0005"] + title: "Report written to file or stdout" + description: "GIVEN a report run WHEN an output path is supplied THEN it is written to that file creating directories as needed, and WHEN no path is supplied THEN it is written to standard output" + verification: automated-test + revision: "0.11.0" + - id: SVC_REPORT_0006 + requirement_ids: ["REPORT_0006"] + title: "Deprecated AsciiDoc alias warns and renders" + description: "GIVEN the deprecated AsciiDoc report command WHEN invoked THEN a deprecation warning is emitted and the AsciiDoc report is produced" + verification: automated-test + revision: "0.11.0" + + # --- export --- + - id: SVC_EXPORT_0001 + requirement_ids: ["EXPORT_0001"] + title: "JSON export emitted by default" + description: "GIVEN a dataset WHEN export is run without a format THEN a JSON document conforming to the export schema is emitted" + verification: automated-test + revision: "0.11.0" + - id: SVC_EXPORT_0002 + requirement_ids: ["EXPORT_0002"] + title: "Export restricted to selected IDs" + description: "GIVEN a dataset WHEN requirement or SVC IDs are supplied THEN the exported document is restricted to those requirements and their related entities" + verification: automated-test + revision: "0.11.0" + - id: SVC_EXPORT_0003 + requirement_ids: ["EXPORT_0003"] + title: "Unfiltered export includes all data" + description: "GIVEN a dataset WHEN an unfiltered export is requested THEN all data is included without applying filters" + verification: automated-test + revision: "0.11.0" + - id: SVC_EXPORT_0004 + requirement_ids: ["EXPORT_0004"] + title: "SQLite export written to file" + description: "GIVEN the SQLite format WHEN an output file path is supplied THEN a SQLite snapshot is written, and WHEN no output path is supplied THEN an error is reported and no output produced" + verification: automated-test + revision: "0.11.0" + - id: SVC_EXPORT_0005 + requirement_ids: ["EXPORT_0005"] + title: "JSON export written to file or stdout" + description: "GIVEN a JSON export WHEN no output path is supplied THEN the JSON is written to standard output" + verification: automated-test + revision: "0.11.0" + + # --- validate --- + - id: SVC_VALIDATE_0001 + requirement_ids: ["VALIDATE_0001"] + title: "Requirements without SVCs reported" + description: "GIVEN a requirement with no SVC referencing it WHEN validate runs THEN that requirement is reported as a coverage gap" + verification: automated-test + revision: "0.11.0" + - id: SVC_VALIDATE_0002 + requirement_ids: ["VALIDATE_0002"] + title: "Manual SVCs without MVRs reported" + description: "GIVEN an SVC requiring manual verification with no MVR WHEN validate runs THEN that SVC is reported as a coverage gap" + verification: automated-test + revision: "0.11.0" + - id: SVC_VALIDATE_0003 + requirement_ids: ["VALIDATE_0003"] + title: "Broken references reported and fatal" + description: "GIVEN an SVC, MVR, or annotation referencing a non-existent entity WHEN validate runs THEN a referential error is reported and validation fails" + verification: automated-test + revision: "0.11.0" + - id: SVC_VALIDATE_0004 + requirement_ids: ["VALIDATE_0004"] + title: "Strict mode promotes gaps to errors" + description: "GIVEN only coverage gaps WHEN strict mode is disabled THEN warnings are reported and validation succeeds, and WHEN strict mode is enabled THEN errors are reported and validation fails" + verification: automated-test + revision: "0.11.0" + - id: SVC_VALIDATE_0005 + requirement_ids: ["VALIDATE_0005"] + title: "Summary and exit code reflect outcome" + description: "GIVEN no referential errors and no coverage gaps WHEN validate runs THEN it reports all checks passed and exits successfully" + verification: automated-test + revision: "0.11.0" + + # --- enrich --- + - id: SVC_ENRICH_0001 + requirement_ids: ["ENRICH_0001"] + title: "Referenced IDs enriched with titles and descriptions" + description: "GIVEN an input document referencing requirement, SVC, or MVR IDs WHEN enriched THEN the corresponding titles and descriptions are injected" + verification: automated-test + revision: "0.11.0" + - id: SVC_ENRICH_0002 + requirement_ids: ["ENRICH_0002"] + title: "Preset governs enrichment" + description: "GIVEN a selected built-in preset WHEN enrichment runs THEN the document is enriched according to that preset's rules" + verification: automated-test + revision: "0.11.0" + - id: SVC_ENRICH_0003 + requirement_ids: ["ENRICH_0003"] + title: "Enrichment reads and writes file or standard streams" + description: "GIVEN no input file WHEN enrich runs THEN the document is read from standard input, and the result is written to the configured output or standard output" + verification: automated-test + revision: "0.11.0" + - id: SVC_ENRICH_0004 + requirement_ids: ["ENRICH_0004"] + title: "Dataset auto-detected or error reported" + description: "GIVEN no source WHEN a reqstool AI configuration file is found by walking up THEN that dataset is used, and WHEN none is found THEN an error explaining how to provide a source is reported" + verification: automated-test + revision: "0.11.0" + + # --- lsp --- + - id: SVC_LSP_0001 + requirement_ids: ["LSP_0001"] + title: "Language server starts" + description: "GIVEN the lsp command WHEN run THEN an LSP server is started ready to accept client connections" + verification: automated-test + revision: "0.11.0" + - id: SVC_LSP_0002 + requirement_ids: ["LSP_0002"] + title: "Language server transport selectable" + description: "GIVEN no transport selected WHEN the server starts THEN it communicates over stdio, and WHEN TCP with a host and port is selected THEN it listens on that host and port" + verification: automated-test + revision: "0.11.0" + - id: SVC_LSP_0003 + requirement_ids: ["LSP_0003"] + title: "Language server writes log file" + description: "GIVEN a log file path WHEN the server runs THEN logs are written to that file in addition to standard error" + verification: automated-test + revision: "0.11.0" + - id: SVC_LSP_0004 + requirement_ids: ["LSP_0004"] + title: "Language server guards missing extra" + description: "GIVEN the language-server dependencies are not installed WHEN the command runs THEN it reports how to install them and does not start the server" + verification: automated-test + revision: "0.11.0" + + # --- mcp --- + - id: SVC_MCP_0001 + requirement_ids: ["MCP_0001"] + title: "MCP server starts" + description: "GIVEN the mcp command WHEN run THEN an MCP server serving the resolved dataset is started" + verification: automated-test + revision: "0.11.0" + - id: SVC_MCP_0002 + requirement_ids: ["MCP_0002"] + title: "MCP server transport selectable" + description: "GIVEN no transport selected WHEN the server starts THEN it communicates over stdio, and WHEN an HTTP-based transport with host and port is selected THEN it serves on that host and port" + verification: automated-test + revision: "0.11.0" + - id: SVC_MCP_0003 + requirement_ids: ["MCP_0003"] + title: "MCP dataset resolved or error reported" + description: "GIVEN no source WHEN a reqstool AI configuration file is found by walking up THEN that dataset is served, and WHEN no source and no config THEN an error explaining how to provide a source is reported" + verification: automated-test + revision: "0.11.0" + - id: SVC_MCP_0004 + requirement_ids: ["MCP_0004"] + title: "MCP server guards missing extra" + description: "GIVEN the MCP dependencies are not installed WHEN the command runs THEN it reports how to install them and does not start the server" + verification: automated-test + revision: "0.11.0" + + # --- data-sources --- + - id: SVC_SOURCE_0001 + requirement_ids: ["SOURCE_0001"] + title: "Source materialized before parsing" + description: "GIVEN any source WHEN a command runs THEN the source content is placed in a local working directory before it is read" + verification: automated-test + revision: "0.11.0" + - id: SVC_SOURCE_0002 + requirement_ids: ["SOURCE_0002"] + title: "Local directory used as source" + description: "GIVEN the local source with a directory path WHEN a command runs THEN the data at that directory is used" + verification: automated-test + revision: "0.11.0" + - id: SVC_SOURCE_0003 + requirement_ids: ["SOURCE_0003"] + title: "Local artifact extracted and used" + description: "GIVEN the local source pointing at a Maven ZIP, npm tarball, or PyPI sdist WHEN a command runs THEN the artifact is extracted and the data it contains is used" + verification: automated-test + revision: "0.11.0" + - id: SVC_SOURCE_0004 + requirement_ids: ["SOURCE_0004"] + title: "Git source fetched at ref" + description: "GIVEN the git source with a URL, path, and ref WHEN a command runs THEN the repository is fetched at that ref and the data at the given path is used" + verification: automated-test + revision: "0.11.0" + - id: SVC_SOURCE_0005 + requirement_ids: ["SOURCE_0005"] + title: "Maven artifact downloaded and used" + description: "GIVEN the Maven source with group ID, artifact ID, and version WHEN a command runs THEN the matching artifact is downloaded and the data it contains is used" + verification: automated-test + revision: "0.11.0" + - id: SVC_SOURCE_0006 + requirement_ids: ["SOURCE_0006"] + title: "npm package downloaded and used" + description: "GIVEN the npm source with a package name and version WHEN a command runs THEN the package is downloaded and the data it contains is used" + verification: automated-test + revision: "0.11.0" + - id: SVC_SOURCE_0007 + requirement_ids: ["SOURCE_0007"] + title: "PyPI package downloaded and used" + description: "GIVEN the PyPI source with a package name and version WHEN a command runs THEN the package is downloaded and the data it contains is used" + verification: automated-test + revision: "0.11.0" + - id: SVC_SOURCE_0008 + requirement_ids: ["SOURCE_0008"] + title: "Token authenticates remote fetch" + description: "GIVEN a token for a git, Maven, npm, or PyPI source WHEN a command runs THEN that token is used to authenticate the fetch" + verification: automated-test + revision: "0.11.0" + + # --- ingestion --- + - id: SVC_INGEST_0001 + requirement_ids: ["INGEST_0001"] + title: "Requirements parsed into model" + description: "GIVEN a dataset containing requirements.yml WHEN parsed THEN each requirement and its metadata are loaded into the model" + verification: automated-test + revision: "0.11.0" + - id: SVC_INGEST_0002 + requirement_ids: ["INGEST_0002"] + title: "SVCs parsed into model" + description: "GIVEN a dataset containing software_verification_cases.yml WHEN parsed THEN each SVC and its requirement links are loaded into the model" + verification: automated-test + revision: "0.11.0" + - id: SVC_INGEST_0003 + requirement_ids: ["INGEST_0003"] + title: "MVRs parsed into model" + description: "GIVEN a dataset containing manual_verification_results.yml WHEN parsed THEN each manual verification result is loaded into the model" + verification: automated-test + revision: "0.11.0" + - id: SVC_INGEST_0004 + requirement_ids: ["INGEST_0004"] + title: "Annotations parsed into model" + description: "GIVEN a dataset containing a generated annotations.yml WHEN parsed THEN the implementation and test annotations are loaded into the model" + verification: automated-test + revision: "0.11.0" + - id: SVC_INGEST_0005 + requirement_ids: ["INGEST_0005"] + title: "JUnit results parsed" + description: "GIVEN JUnit XML report files WHEN parsed THEN the test outcomes are associated with the model" + verification: automated-test + revision: "0.11.0" + - id: SVC_INGEST_0006 + requirement_ids: ["INGEST_0006"] + title: "Karate reports parsed" + description: "GIVEN Karate test reports WHEN parsed THEN their outcomes are loaded into the model" + verification: automated-test + revision: "0.11.0" + - id: SVC_INGEST_0007 + requirement_ids: ["INGEST_0007"] + title: "Static files located at content root" + description: "GIVEN the static input files at the root of the content path WHEN a command runs THEN they are located and parsed without additional configuration" + verification: automated-test + revision: "0.11.0" + - id: SVC_INGEST_0008 + requirement_ids: ["INGEST_0008"] + title: "Configured paths override defaults" + description: "GIVEN a configuration file overriding default paths WHEN a command runs THEN generated files and test reports are read from the configured locations" + verification: automated-test + revision: "0.11.0" + + # --- imports-and-filtering --- + - id: SVC_IMPORT_0001 + requirement_ids: ["IMPORT_0001"] + title: "Nested imports resolved" + description: "GIVEN a dataset importing another that imports a third WHEN resolved THEN requirements from all datasets in the import chain are included" + verification: automated-test + revision: "0.11.0" + - id: SVC_IMPORT_0002 + requirement_ids: ["IMPORT_0002"] + title: "Circular import rejected" + description: "GIVEN datasets importing each other in a cycle WHEN resolved THEN a circular-import error is reported and resolution stops" + verification: automated-test + revision: "0.11.0" + - id: SVC_IMPORT_0003 + requirement_ids: ["IMPORT_0003"] + title: "Nested implementations resolved" + description: "GIVEN a dataset declaring an implementation that itself declares a further implementation WHEN resolved THEN the implementation chain is followed to full depth" + verification: automated-test + revision: "0.11.0" + - id: SVC_IMPORT_0004 + requirement_ids: ["IMPORT_0004"] + title: "Circular implementation rejected" + description: "GIVEN datasets declaring each other as implementations in a cycle WHEN resolved THEN a circular-implementation error is reported and resolution stops" + verification: automated-test + revision: "0.11.0" + - id: SVC_IMPORT_0005 + requirement_ids: ["IMPORT_0005"] + title: "Implementation requirements excluded" + description: "GIVEN an implementation dataset defining its own requirements WHEN resolved THEN those requirements are not included in the system's own requirement set" + verification: automated-test + revision: "0.11.0" + - id: SVC_IMPORT_0006 + requirement_ids: ["IMPORT_0006"] + title: "Imported requirements filtered" + description: "GIVEN a filter excluding a requirement ID from a source WHEN composed THEN the composed dataset omits that requirement" + verification: automated-test + revision: "0.11.0" + - id: SVC_IMPORT_0007 + requirement_ids: ["IMPORT_0007"] + title: "Imported SVCs filtered" + description: "GIVEN a filter selecting specific SVC IDs from a source WHEN composed THEN the composed dataset includes only the selected SVCs from that source" + verification: automated-test + revision: "0.11.0" + - id: SVC_IMPORT_0008 + requirement_ids: ["IMPORT_0008"] + title: "Filter expression selects matches" + description: "GIVEN a filter expression combining logical operators with identifier or regex matching WHEN applied THEN exactly the requirements or SVCs matching the expression are selected" + verification: automated-test + revision: "0.11.0" + + # --- parse-validation --- + - id: SVC_PARSE_0001 + requirement_ids: ["PARSE_0001"] + title: "Non-conforming file rejected" + description: "GIVEN an input file that does not conform to its JSON schema WHEN parsed THEN a schema validation error is reported and the file is not treated as valid" + verification: automated-test + revision: "0.11.0" + - id: SVC_PARSE_0002 + requirement_ids: ["PARSE_0002"] + title: "Missing requirements file fails" + description: "GIVEN a content root with no requirements.yml WHEN a command runs THEN the missing file is reported and the process exits with an error" + verification: automated-test + revision: "0.11.0" + - id: SVC_PARSE_0003 + requirement_ids: ["PARSE_0003"] + title: "Duplicate requirement IDs warned" + description: "GIVEN two requirements parsed with the same identifier WHEN parsed THEN a warning identifying the duplicate is logged" + verification: automated-test + revision: "0.11.0" + - id: SVC_PARSE_0004 + requirement_ids: ["PARSE_0004"] + title: "Duplicate SVC IDs warned" + description: "GIVEN two SVCs parsed with the same identifier WHEN parsed THEN a warning identifying the duplicate is logged" + verification: automated-test + revision: "0.11.0" + - id: SVC_PARSE_0005 + requirement_ids: ["PARSE_0005"] + title: "Dangling requirement reference warned" + description: "GIVEN an SVC or annotation referencing a non-existent requirement identifier WHEN parsed THEN a warning identifying the unresolved reference is logged" + verification: automated-test + revision: "0.11.0" + - id: SVC_PARSE_0006 + requirement_ids: ["PARSE_0006"] + title: "Dangling SVC reference warned" + description: "GIVEN an MVR or annotation referencing a non-existent SVC identifier WHEN parsed THEN a warning identifying the unresolved reference is logged" + verification: automated-test + revision: "0.11.0" + + # --- lifecycle --- + - id: SVC_LIFECYCLE_0001 + requirement_ids: ["LIFECYCLE_0001"] + title: "Non-code requirement not flagged unimplemented" + description: "GIVEN a requirement declaring a non-code implementation type WHEN status is computed THEN the declared type is shown instead of an implementation count and it is not flagged as missing an implementation" + verification: automated-test + revision: "0.11.0" + - id: SVC_LIFECYCLE_0002 + requirement_ids: ["LIFECYCLE_0002"] + title: "Requirement lifecycle state recorded" + description: "GIVEN a requirement declaring a deprecated lifecycle state WHEN parsed THEN that state and its reason are recorded for the requirement" + verification: automated-test + revision: "0.11.0" + - id: SVC_LIFECYCLE_0003 + requirement_ids: ["LIFECYCLE_0003"] + title: "SVC lifecycle state recorded" + description: "GIVEN an SVC declaring an obsolete lifecycle state WHEN parsed THEN that state and its reason are recorded for the SVC" + verification: automated-test + revision: "0.11.0" + - id: SVC_LIFECYCLE_0004 + requirement_ids: ["LIFECYCLE_0004"] + title: "Superseded reference warned" + description: "GIVEN an active item referencing a deprecated or obsolete requirement or SVC WHEN parsed THEN a warning identifying the superseded reference is logged" + verification: automated-test + revision: "0.11.0" diff --git a/openspec/openspecui.hooks.ts b/openspec/openspecui.hooks.ts new file mode 100644 index 00000000..49a96ee4 --- /dev/null +++ b/openspec/openspecui.hooks.ts @@ -0,0 +1,108 @@ +// @reqstool-openspec-hooks: 0.1.1 +import { spawn, ChildProcess } from "child_process"; +import type { OnReadDocumentHookV1 } from "openspecui/hooks"; + +// Minimal MCP client over stdio (JSON-RPC 2.0, newline-delimited). +// Uses only Node.js built-ins — no npm packages required. +class McpStdioClient { + private proc: ChildProcess; + private buf = ""; + private pending = new Map< + number, + { resolve: (v: unknown) => void; reject: (e: Error) => void } + >(); + private id = 1; + readonly ready: Promise; + + constructor(cwd: string) { + this.proc = spawn("reqstool", ["mcp"], { + cwd, + stdio: ["pipe", "pipe", "pipe"], + }); + this.proc.stdout!.on("data", (chunk: Buffer) => { + this.buf += chunk.toString(); + let nl: number; + while ((nl = this.buf.indexOf("\n")) !== -1) { + const line = this.buf.slice(0, nl).trim(); + this.buf = this.buf.slice(nl + 1); + if (line) this.handle(line); + } + }); + this.ready = this.init(); + } + + private handle(line: string) { + try { + const msg = JSON.parse(line) as { id?: number; result?: unknown; error?: { message: string } }; + if (msg.id !== undefined) { + const p = this.pending.get(msg.id); + if (p) { + this.pending.delete(msg.id); + msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result); + } + } + } catch (e) { + console.warn("[reqstool-openspec] Skipping non-JSON line from reqstool mcp:", e instanceof Error ? e.message : e); + } + } + + private send(method: string, params: unknown, expectReply = true): Promise { + if (!expectReply) { + this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n"); + return Promise.resolve(); + } + const id = this.id++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"); + }); + } + + private async init(): Promise { + await this.send("initialize", { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + clientInfo: { name: "openspecui", version: "1.0" }, + }); + this.send("notifications/initialized", {}, false); + } + + async enrich(content: string, preset: string): Promise { + await this.ready; + const result = (await this.send("tools/call", { + name: "enrich_document", + arguments: { content, preset }, + })) as { content: { text: string }[] }; + return result.content[0].text; + } + + close() { + this.proc.stdin?.end(); + this.proc.kill(); + } +} + +let client: McpStdioClient | null = null; + +export const onReadDocument: OnReadDocumentHookV1 = async (ctx, read) => { + if (!client) { + client = new McpStdioClient(ctx.projectDir); + ctx.lifecycle.onDispose(() => { + client?.close(); + client = null; + }); + } + + const result = await read(); + const preset = `openspec:${ctx.document.kind}`; + + try { + const enriched = await client.enrich(result.markdown, preset); + return { ...result, markdown: enriched, sourceLabel: `reqstool ${preset}` }; + } catch (e) { + return { + ...result, + diagnostics: [{ level: "warning", message: `reqstool enrich failed: ${e}` }], + }; + } +}; diff --git a/openspec/specs/data-sources/spec.md b/openspec/specs/data-sources/spec.md new file mode 100644 index 00000000..e63f7510 --- /dev/null +++ b/openspec/specs/data-sources/spec.md @@ -0,0 +1,57 @@ +# Data Sources Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: SOURCE_0001 +The system SHALL implement SOURCE_0001. + +#### Scenario: SVC_SOURCE_0001 +The system SHALL pass SVC_SOURCE_0001. + +### Requirement: SOURCE_0002 +The system SHALL implement SOURCE_0002. + +#### Scenario: SVC_SOURCE_0002 +The system SHALL pass SVC_SOURCE_0002. + +### Requirement: SOURCE_0003 +The system SHALL implement SOURCE_0003. + +#### Scenario: SVC_SOURCE_0003 +The system SHALL pass SVC_SOURCE_0003. + +### Requirement: SOURCE_0004 +The system SHALL implement SOURCE_0004. + +#### Scenario: SVC_SOURCE_0004 +The system SHALL pass SVC_SOURCE_0004. + +### Requirement: SOURCE_0005 +The system SHALL implement SOURCE_0005. + +#### Scenario: SVC_SOURCE_0005 +The system SHALL pass SVC_SOURCE_0005. + +### Requirement: SOURCE_0006 +The system SHALL implement SOURCE_0006. + +#### Scenario: SVC_SOURCE_0006 +The system SHALL pass SVC_SOURCE_0006. + +### Requirement: SOURCE_0007 +The system SHALL implement SOURCE_0007. + +#### Scenario: SVC_SOURCE_0007 +The system SHALL pass SVC_SOURCE_0007. + +### Requirement: SOURCE_0008 +The system SHALL implement SOURCE_0008. + +#### Scenario: SVC_SOURCE_0008 +The system SHALL pass SVC_SOURCE_0008. diff --git a/openspec/specs/enrich/spec.md b/openspec/specs/enrich/spec.md new file mode 100644 index 00000000..aef733db --- /dev/null +++ b/openspec/specs/enrich/spec.md @@ -0,0 +1,33 @@ +# Enrich Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: ENRICH_0001 +The system SHALL implement ENRICH_0001. + +#### Scenario: SVC_ENRICH_0001 +The system SHALL pass SVC_ENRICH_0001. + +### Requirement: ENRICH_0002 +The system SHALL implement ENRICH_0002. + +#### Scenario: SVC_ENRICH_0002 +The system SHALL pass SVC_ENRICH_0002. + +### Requirement: ENRICH_0003 +The system SHALL implement ENRICH_0003. + +#### Scenario: SVC_ENRICH_0003 +The system SHALL pass SVC_ENRICH_0003. + +### Requirement: ENRICH_0004 +The system SHALL implement ENRICH_0004. + +#### Scenario: SVC_ENRICH_0004 +The system SHALL pass SVC_ENRICH_0004. diff --git a/openspec/specs/export/spec.md b/openspec/specs/export/spec.md new file mode 100644 index 00000000..2a230f58 --- /dev/null +++ b/openspec/specs/export/spec.md @@ -0,0 +1,39 @@ +# Export Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: EXPORT_0001 +The system SHALL implement EXPORT_0001. + +#### Scenario: SVC_EXPORT_0001 +The system SHALL pass SVC_EXPORT_0001. + +### Requirement: EXPORT_0002 +The system SHALL implement EXPORT_0002. + +#### Scenario: SVC_EXPORT_0002 +The system SHALL pass SVC_EXPORT_0002. + +### Requirement: EXPORT_0003 +The system SHALL implement EXPORT_0003. + +#### Scenario: SVC_EXPORT_0003 +The system SHALL pass SVC_EXPORT_0003. + +### Requirement: EXPORT_0004 +The system SHALL implement EXPORT_0004. + +#### Scenario: SVC_EXPORT_0004 +The system SHALL pass SVC_EXPORT_0004. + +### Requirement: EXPORT_0005 +The system SHALL implement EXPORT_0005. + +#### Scenario: SVC_EXPORT_0005 +The system SHALL pass SVC_EXPORT_0005. diff --git a/openspec/specs/imports-and-filtering/spec.md b/openspec/specs/imports-and-filtering/spec.md new file mode 100644 index 00000000..faf0a944 --- /dev/null +++ b/openspec/specs/imports-and-filtering/spec.md @@ -0,0 +1,57 @@ +# Imports and Filtering Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: IMPORT_0001 +The system SHALL implement IMPORT_0001. + +#### Scenario: SVC_IMPORT_0001 +The system SHALL pass SVC_IMPORT_0001. + +### Requirement: IMPORT_0002 +The system SHALL implement IMPORT_0002. + +#### Scenario: SVC_IMPORT_0002 +The system SHALL pass SVC_IMPORT_0002. + +### Requirement: IMPORT_0003 +The system SHALL implement IMPORT_0003. + +#### Scenario: SVC_IMPORT_0003 +The system SHALL pass SVC_IMPORT_0003. + +### Requirement: IMPORT_0004 +The system SHALL implement IMPORT_0004. + +#### Scenario: SVC_IMPORT_0004 +The system SHALL pass SVC_IMPORT_0004. + +### Requirement: IMPORT_0005 +The system SHALL implement IMPORT_0005. + +#### Scenario: SVC_IMPORT_0005 +The system SHALL pass SVC_IMPORT_0005. + +### Requirement: IMPORT_0006 +The system SHALL implement IMPORT_0006. + +#### Scenario: SVC_IMPORT_0006 +The system SHALL pass SVC_IMPORT_0006. + +### Requirement: IMPORT_0007 +The system SHALL implement IMPORT_0007. + +#### Scenario: SVC_IMPORT_0007 +The system SHALL pass SVC_IMPORT_0007. + +### Requirement: IMPORT_0008 +The system SHALL implement IMPORT_0008. + +#### Scenario: SVC_IMPORT_0008 +The system SHALL pass SVC_IMPORT_0008. diff --git a/openspec/specs/ingestion/spec.md b/openspec/specs/ingestion/spec.md new file mode 100644 index 00000000..ed7c6542 --- /dev/null +++ b/openspec/specs/ingestion/spec.md @@ -0,0 +1,57 @@ +# Ingestion Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: INGEST_0001 +The system SHALL implement INGEST_0001. + +#### Scenario: SVC_INGEST_0001 +The system SHALL pass SVC_INGEST_0001. + +### Requirement: INGEST_0002 +The system SHALL implement INGEST_0002. + +#### Scenario: SVC_INGEST_0002 +The system SHALL pass SVC_INGEST_0002. + +### Requirement: INGEST_0003 +The system SHALL implement INGEST_0003. + +#### Scenario: SVC_INGEST_0003 +The system SHALL pass SVC_INGEST_0003. + +### Requirement: INGEST_0004 +The system SHALL implement INGEST_0004. + +#### Scenario: SVC_INGEST_0004 +The system SHALL pass SVC_INGEST_0004. + +### Requirement: INGEST_0005 +The system SHALL implement INGEST_0005. + +#### Scenario: SVC_INGEST_0005 +The system SHALL pass SVC_INGEST_0005. + +### Requirement: INGEST_0006 +The system SHALL implement INGEST_0006. + +#### Scenario: SVC_INGEST_0006 +The system SHALL pass SVC_INGEST_0006. + +### Requirement: INGEST_0007 +The system SHALL implement INGEST_0007. + +#### Scenario: SVC_INGEST_0007 +The system SHALL pass SVC_INGEST_0007. + +### Requirement: INGEST_0008 +The system SHALL implement INGEST_0008. + +#### Scenario: SVC_INGEST_0008 +The system SHALL pass SVC_INGEST_0008. diff --git a/openspec/specs/lifecycle/spec.md b/openspec/specs/lifecycle/spec.md new file mode 100644 index 00000000..2be81942 --- /dev/null +++ b/openspec/specs/lifecycle/spec.md @@ -0,0 +1,33 @@ +# Lifecycle Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: LIFECYCLE_0001 +The system SHALL implement LIFECYCLE_0001. + +#### Scenario: SVC_LIFECYCLE_0001 +The system SHALL pass SVC_LIFECYCLE_0001. + +### Requirement: LIFECYCLE_0002 +The system SHALL implement LIFECYCLE_0002. + +#### Scenario: SVC_LIFECYCLE_0002 +The system SHALL pass SVC_LIFECYCLE_0002. + +### Requirement: LIFECYCLE_0003 +The system SHALL implement LIFECYCLE_0003. + +#### Scenario: SVC_LIFECYCLE_0003 +The system SHALL pass SVC_LIFECYCLE_0003. + +### Requirement: LIFECYCLE_0004 +The system SHALL implement LIFECYCLE_0004. + +#### Scenario: SVC_LIFECYCLE_0004 +The system SHALL pass SVC_LIFECYCLE_0004. diff --git a/openspec/specs/lsp/spec.md b/openspec/specs/lsp/spec.md new file mode 100644 index 00000000..a62f34e2 --- /dev/null +++ b/openspec/specs/lsp/spec.md @@ -0,0 +1,33 @@ +# LSP Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: LSP_0001 +The system SHALL implement LSP_0001. + +#### Scenario: SVC_LSP_0001 +The system SHALL pass SVC_LSP_0001. + +### Requirement: LSP_0002 +The system SHALL implement LSP_0002. + +#### Scenario: SVC_LSP_0002 +The system SHALL pass SVC_LSP_0002. + +### Requirement: LSP_0003 +The system SHALL implement LSP_0003. + +#### Scenario: SVC_LSP_0003 +The system SHALL pass SVC_LSP_0003. + +### Requirement: LSP_0004 +The system SHALL implement LSP_0004. + +#### Scenario: SVC_LSP_0004 +The system SHALL pass SVC_LSP_0004. diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md new file mode 100644 index 00000000..4a8dd4e9 --- /dev/null +++ b/openspec/specs/mcp/spec.md @@ -0,0 +1,33 @@ +# MCP Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: MCP_0001 +The system SHALL implement MCP_0001. + +#### Scenario: SVC_MCP_0001 +The system SHALL pass SVC_MCP_0001. + +### Requirement: MCP_0002 +The system SHALL implement MCP_0002. + +#### Scenario: SVC_MCP_0002 +The system SHALL pass SVC_MCP_0002. + +### Requirement: MCP_0003 +The system SHALL implement MCP_0003. + +#### Scenario: SVC_MCP_0003 +The system SHALL pass SVC_MCP_0003. + +### Requirement: MCP_0004 +The system SHALL implement MCP_0004. + +#### Scenario: SVC_MCP_0004 +The system SHALL pass SVC_MCP_0004. diff --git a/openspec/specs/parse-validation/spec.md b/openspec/specs/parse-validation/spec.md new file mode 100644 index 00000000..ba0a92e3 --- /dev/null +++ b/openspec/specs/parse-validation/spec.md @@ -0,0 +1,45 @@ +# Parse Validation Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: PARSE_0001 +The system SHALL implement PARSE_0001. + +#### Scenario: SVC_PARSE_0001 +The system SHALL pass SVC_PARSE_0001. + +### Requirement: PARSE_0002 +The system SHALL implement PARSE_0002. + +#### Scenario: SVC_PARSE_0002 +The system SHALL pass SVC_PARSE_0002. + +### Requirement: PARSE_0003 +The system SHALL implement PARSE_0003. + +#### Scenario: SVC_PARSE_0003 +The system SHALL pass SVC_PARSE_0003. + +### Requirement: PARSE_0004 +The system SHALL implement PARSE_0004. + +#### Scenario: SVC_PARSE_0004 +The system SHALL pass SVC_PARSE_0004. + +### Requirement: PARSE_0005 +The system SHALL implement PARSE_0005. + +#### Scenario: SVC_PARSE_0005 +The system SHALL pass SVC_PARSE_0005. + +### Requirement: PARSE_0006 +The system SHALL implement PARSE_0006. + +#### Scenario: SVC_PARSE_0006 +The system SHALL pass SVC_PARSE_0006. diff --git a/openspec/specs/report/spec.md b/openspec/specs/report/spec.md new file mode 100644 index 00000000..2d76a9a5 --- /dev/null +++ b/openspec/specs/report/spec.md @@ -0,0 +1,45 @@ +# Report Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: REPORT_0001 +The system SHALL implement REPORT_0001. + +#### Scenario: SVC_REPORT_0001 +The system SHALL pass SVC_REPORT_0001. + +### Requirement: REPORT_0002 +The system SHALL implement REPORT_0002. + +#### Scenario: SVC_REPORT_0002 +The system SHALL pass SVC_REPORT_0002. + +### Requirement: REPORT_0003 +The system SHALL implement REPORT_0003. + +#### Scenario: SVC_REPORT_0003 +The system SHALL pass SVC_REPORT_0003. + +### Requirement: REPORT_0004 +The system SHALL implement REPORT_0004. + +#### Scenario: SVC_REPORT_0004 +The system SHALL pass SVC_REPORT_0004. + +### Requirement: REPORT_0005 +The system SHALL implement REPORT_0005. + +#### Scenario: SVC_REPORT_0005 +The system SHALL pass SVC_REPORT_0005. + +### Requirement: REPORT_0006 +The system SHALL implement REPORT_0006. + +#### Scenario: SVC_REPORT_0006 +The system SHALL pass SVC_REPORT_0006. diff --git a/openspec/specs/status/spec.md b/openspec/specs/status/spec.md new file mode 100644 index 00000000..e8031de4 --- /dev/null +++ b/openspec/specs/status/spec.md @@ -0,0 +1,63 @@ +# Status Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: STATUS_0001 +The system SHALL implement STATUS_0001. + +#### Scenario: SVC_STATUS_0001 +The system SHALL pass SVC_STATUS_0001. + +### Requirement: STATUS_0002 +The system SHALL implement STATUS_0002. + +#### Scenario: SVC_STATUS_0002 +The system SHALL pass SVC_STATUS_0002. + +### Requirement: STATUS_0003 +The system SHALL implement STATUS_0003. + +#### Scenario: SVC_STATUS_0003 +The system SHALL pass SVC_STATUS_0003. + +### Requirement: STATUS_0004 +The system SHALL implement STATUS_0004. + +#### Scenario: SVC_STATUS_0004 +The system SHALL pass SVC_STATUS_0004. + +### Requirement: STATUS_0005 +The system SHALL implement STATUS_0005. + +#### Scenario: SVC_STATUS_0005 +The system SHALL pass SVC_STATUS_0005. + +### Requirement: STATUS_0006 +The system SHALL implement STATUS_0006. + +#### Scenario: SVC_STATUS_0006 +The system SHALL pass SVC_STATUS_0006. + +### Requirement: STATUS_0007 +The system SHALL implement STATUS_0007. + +#### Scenario: SVC_STATUS_0007 +The system SHALL pass SVC_STATUS_0007. + +### Requirement: STATUS_0008 +The system SHALL implement STATUS_0008. + +#### Scenario: SVC_STATUS_0008 +The system SHALL pass SVC_STATUS_0008. + +### Requirement: STATUS_0009 +The system SHALL implement STATUS_0009. + +#### Scenario: SVC_STATUS_0009 +The system SHALL pass SVC_STATUS_0009. diff --git a/openspec/specs/validate/spec.md b/openspec/specs/validate/spec.md new file mode 100644 index 00000000..27c21458 --- /dev/null +++ b/openspec/specs/validate/spec.md @@ -0,0 +1,39 @@ +# Validate Specification + +## Purpose + +Requirement and SVC content is owned by reqstool (single source of truth). This spec references +reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via +`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`. + +## Requirements + +### Requirement: VALIDATE_0001 +The system SHALL implement VALIDATE_0001. + +#### Scenario: SVC_VALIDATE_0001 +The system SHALL pass SVC_VALIDATE_0001. + +### Requirement: VALIDATE_0002 +The system SHALL implement VALIDATE_0002. + +#### Scenario: SVC_VALIDATE_0002 +The system SHALL pass SVC_VALIDATE_0002. + +### Requirement: VALIDATE_0003 +The system SHALL implement VALIDATE_0003. + +#### Scenario: SVC_VALIDATE_0003 +The system SHALL pass SVC_VALIDATE_0003. + +### Requirement: VALIDATE_0004 +The system SHALL implement VALIDATE_0004. + +#### Scenario: SVC_VALIDATE_0004 +The system SHALL pass SVC_VALIDATE_0004. + +### Requirement: VALIDATE_0005 +The system SHALL implement VALIDATE_0005. + +#### Scenario: SVC_VALIDATE_0005 +The system SHALL pass SVC_VALIDATE_0005. diff --git a/pyproject.toml b/pyproject.toml index bf4264a6..c7edb821 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,10 @@ source = "vcs" local_scheme = "no-local-version" [tool.hatch.build.hooks.reqstool] -sources = ["src", "tests"] +# Scan only the project's own code and tests for @Requirements/@SVCs. Exclude tests/fixtures and +# tests/resources — those carry annotations for their own fixture datasets (e.g. SVC_040/050), +# which must not leak into this project's annotations.yml. +sources = ["src", "tests/unit", "tests/integration", "tests/e2e"] test_results = "build/junit.xml" dataset_directory = "docs/reqstool" output_directory = "build/reqstool" diff --git a/src/reqstool/command.py b/src/reqstool/command.py index 1f2d45d3..6f507b00 100755 --- a/src/reqstool/command.py +++ b/src/reqstool/command.py @@ -484,7 +484,7 @@ def _get_initial_source(self, args_source: argparse.Namespace) -> LocationInterf return location - @Requirements("REQ_035") + @Requirements("REPORT_0005", "REPORT_0006") def command_report(self, report_args: argparse.Namespace): initial_source = self._get_initial_source(report_args) @@ -499,6 +499,7 @@ def command_report(self, report_args: argparse.Namespace): output.write(result.result) + @Requirements("EXPORT_0004", "EXPORT_0005") def command_export(self, export_args: argparse.Namespace): initial_source = self._get_initial_source(export_args) fmt = getattr(export_args, "format", "json") @@ -539,7 +540,7 @@ def command_validate(self, validate_args: argparse.Namespace) -> int: output.write(result.result) return result.exit_code - @Requirements("REQ_029") + @Requirements("STATUS_0007", "STATUS_0009") def command_status(self, status_args: argparse.Namespace) -> int: initial_source = self._get_initial_source(status_args) output = status_args.output @@ -569,6 +570,7 @@ def command_status(self, status_args: argparse.Namespace) -> int: else 0 ) + @Requirements("LSP_0001", "LSP_0002", "LSP_0003", "LSP_0004") def command_lsp(self, lsp_args: argparse.Namespace): try: from reqstool.lsp.server import start_server @@ -584,6 +586,7 @@ def command_lsp(self, lsp_args: argparse.Namespace): logging.fatal("reqstool LSP server crashed: %s", exc) sys.exit(1) + @Requirements("MCP_0001", "MCP_0002", "MCP_0003", "MCP_0004") def command_mcp(self, mcp_args: argparse.Namespace): try: from reqstool.mcp.server import start_server @@ -628,7 +631,7 @@ def command_mcp(self, mcp_args: argparse.Namespace): logging.fatal("reqstool MCP server crashed: %s", exc) sys.exit(1) - @Requirements("REQ_039") + @Requirements("ENRICH_0003", "ENRICH_0004") def command_enrich(self, enrich_args: argparse.Namespace): if getattr(enrich_args, "source", None) is None: from pathlib import Path diff --git a/src/reqstool/commands/enrich/enrich.py b/src/reqstool/commands/enrich/enrich.py index 6478b0dd..662cbf86 100644 --- a/src/reqstool/commands/enrich/enrich.py +++ b/src/reqstool/commands/enrich/enrich.py @@ -10,7 +10,7 @@ from reqstool.storage.requirements_repository import RequirementsRepository -@Requirements("REQ_039") +@Requirements("ENRICH_0001", "ENRICH_0002") class EnrichCommand: def __init__(self, location: LocationInterface, input_content: str, config: EnrichmentConfig): self.__initial_location: LocationInterface = location diff --git a/src/reqstool/commands/generate_json/generate_json.py b/src/reqstool/commands/generate_json/generate_json.py index 471d520d..64ef4c4c 100644 --- a/src/reqstool/commands/generate_json/generate_json.py +++ b/src/reqstool/commands/generate_json/generate_json.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -@Requirements("REQ_030") +@Requirements("EXPORT_0001", "EXPORT_0002", "EXPORT_0003") class GenerateJsonCommand: def __init__( self, diff --git a/src/reqstool/commands/report/criterias/group_by.py b/src/reqstool/commands/report/criterias/group_by.py index 433ba2dd..16938a91 100644 --- a/src/reqstool/commands/report/criterias/group_by.py +++ b/src/reqstool/commands/report/criterias/group_by.py @@ -22,7 +22,7 @@ class GroupbyOptions(Enum): CATEGORY = "category" -@Requirements("REQ_033") +@Requirements("REPORT_0003") class GroupByOrganizor(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) @@ -44,7 +44,7 @@ def __iter__(self) -> Iterator[Tuple[str, List[UrnId]]]: def _add_req_to_group(self, group: str, urn_id: UrnId): self.grouped_requirements[group].append(urn_id) - @Requirements("REQ_034") + @Requirements("REPORT_0004") def _sort(self): if len(self.sort_by) == 0: return diff --git a/src/reqstool/commands/report/criterias/sort_by.py b/src/reqstool/commands/report/criterias/sort_by.py index cc848506..8d505de0 100644 --- a/src/reqstool/commands/report/criterias/sort_by.py +++ b/src/reqstool/commands/report/criterias/sort_by.py @@ -2,10 +2,7 @@ from enum import Enum, unique -from reqstool_python_decorators.decorators.decorators import Requirements - -@Requirements("REQ_034") @unique class SortByOptions(Enum): ID = "id" diff --git a/src/reqstool/commands/report/report.py b/src/reqstool/commands/report/report.py index 6d0bc8b5..9b763dda 100644 --- a/src/reqstool/commands/report/report.py +++ b/src/reqstool/commands/report/report.py @@ -43,7 +43,7 @@ def __new__(cls, value, filename): return obj -@Requirements("REQ_032") +@Requirements("REPORT_0001", "REPORT_0002") class ReportCommand: def __init__( self, diff --git a/src/reqstool/commands/status/status.py b/src/reqstool/commands/status/status.py index 2ea65fcb..d706b18e 100644 --- a/src/reqstool/commands/status/status.py +++ b/src/reqstool/commands/status/status.py @@ -68,7 +68,7 @@ def _render(*renderables) -> str: return cap.get() -@Requirements("REQ_027") +@Requirements("STATUS_0003", "STATUS_0004", "STATUS_0005", "STATUS_0006") class StatusCommand: def __init__( self, @@ -128,6 +128,7 @@ def __status_result(self) -> tuple[str, int]: ts.total_requirements - ts.completed_requirements, ) + @Requirements("STATUS_0008") @staticmethod def __inject_post_tests(db: RequirementsDatabase, initial_urn: str, paths: list[str]) -> None: resolved = [Path(p).resolve() for p in paths] @@ -147,6 +148,7 @@ def _filtered_status_dict(stats_service: StatisticsService, kept_req_ids: set | return full +@Requirements("STATUS_0002") def _status_verdict(incomplete_count: int) -> str: return "PASS" if incomplete_count == 0 else "FAIL" diff --git a/src/reqstool/commands/validate/validate.py b/src/reqstool/commands/validate/validate.py index eb37e6ef..ee82fe45 100644 --- a/src/reqstool/commands/validate/validate.py +++ b/src/reqstool/commands/validate/validate.py @@ -1,6 +1,7 @@ # Copyright © LFV +from reqstool_python_decorators.decorators.decorators import Requirements from reqstool.common.validator_error_holder import ValidationErrorHolder from reqstool.common.validators.semantic_validator import SemanticValidator from reqstool.locations.location import LocationInterface @@ -9,6 +10,7 @@ from reqstool.storage.requirements_repository import RequirementsRepository +@Requirements("VALIDATE_0003", "VALIDATE_0004", "VALIDATE_0005") class ValidateCommand: """Validate spec completeness: every requirement has ≥1 SVC; every manual SVC has an MVR. @@ -67,6 +69,7 @@ def __run(self) -> tuple[str, int]: return "\n".join(lines), exit_code + @Requirements("VALIDATE_0001", "VALIDATE_0002") def _check_coverage(self, repo: RequirementsRepository) -> list[str]: warnings = [] all_reqs = repo.get_all_requirements() diff --git a/src/reqstool/common/validators/lifecycle_validator.py b/src/reqstool/common/validators/lifecycle_validator.py index 01dee66e..b31b3543 100644 --- a/src/reqstool/common/validators/lifecycle_validator.py +++ b/src/reqstool/common/validators/lifecycle_validator.py @@ -17,7 +17,7 @@ Warning = namedtuple("Warning", ["state", "message"]) -@Requirements("REQ_037", "REQ_038") +@Requirements("LIFECYCLE_0004") class LifecycleValidator: """ Logs warnings if any requirement or SVC is used despite being marked deprecated or obsolete. diff --git a/src/reqstool/common/validators/semantic_validator.py b/src/reqstool/common/validators/semantic_validator.py index 895ecf48..004651b5 100644 --- a/src/reqstool/common/validators/semantic_validator.py +++ b/src/reqstool/common/validators/semantic_validator.py @@ -82,7 +82,7 @@ def _log_all_errors(self): logging.info(cap.get()) - @Requirements("REQ_022") + @Requirements("PARSE_0003") def _validate_no_duplicate_requirement_ids(self, data: RequirementData) -> bool: # if there are no requirements or systems defined, add a validation error if "requirements" not in data and "systems" not in data: @@ -105,7 +105,7 @@ def _validate_no_duplicate_requirement_ids(self, data: RequirementData) -> bool: return self._validation_error_holder.get_no_of_errors() > 0 - @Requirements("REQ_023") + @Requirements("PARSE_0004") def _validate_no_duplicate_svc_ids(self, data: SVCData) -> bool: if "cases" not in data: self._validation_error_holder.add_error(ValidationError(msg="No svc cases found!")) @@ -126,7 +126,7 @@ def _validate_no_duplicate_svc_ids(self, data: SVCData) -> bool: return self._validation_error_holder.get_no_of_errors() > 0 - @Requirements("REQ_024") + @Requirements("PARSE_0005") def _validate_svc_refers_to_existing_requirement_ids( self, # NOSONAR combined_raw_dataset: CombinedRawDataset, @@ -155,7 +155,7 @@ def _validate_svc_refers_to_existing_requirement_ids( return errors - @Requirements("REQ_024") + @Requirements("PARSE_0005") def _validate_annotation_impls_refers_to_existing_requirement_ids( self, combined_raw_dataset: CombinedRawDataset, @@ -183,7 +183,7 @@ def _validate_annotation_impls_refers_to_existing_requirement_ids( return errors - @Requirements("REQ_025") + @Requirements("PARSE_0006") def _validate_annotation_tests_refers_to_existing_svc_ids( self, combined_raw_dataset: CombinedRawDataset, @@ -201,7 +201,7 @@ def _validate_annotation_tests_refers_to_existing_svc_ids( return errors - @Requirements("REQ_025") + @Requirements("PARSE_0006") def _validate_mvr_refers_to_existing_svc_ids( self, combined_raw_dataset: CombinedRawDataset ) -> List[ValidationError]: diff --git a/src/reqstool/common/validators/syntax_validator.py b/src/reqstool/common/validators/syntax_validator.py index f1297152..0f852b07 100644 --- a/src/reqstool/common/validators/syntax_validator.py +++ b/src/reqstool/common/validators/syntax_validator.py @@ -41,7 +41,7 @@ class SyntaxValidator: registry = resource @ registry registry = registry.with_resource(uri="common.schema.json", resource=resource) - @Requirements("REQ_012", "REQ_021") + @Requirements("PARSE_0001") @staticmethod def is_valid_data(json_schema_type: JsonSchemaTypes, data: dict, urn: str) -> bool: jsonvalidator_draft202012 = Draft202012Validator( diff --git a/src/reqstool/location_resolver/location_resolver.py b/src/reqstool/location_resolver/location_resolver.py index a6b2fbd6..ff7fc162 100644 --- a/src/reqstool/location_resolver/location_resolver.py +++ b/src/reqstool/location_resolver/location_resolver.py @@ -4,6 +4,7 @@ from typing import Optional from pydantic import BaseModel, ConfigDict +from reqstool_python_decorators.decorators.decorators import Requirements from reqstool.locations.local_location import LocalLocation from reqstool.locations.location import LocationInterface @@ -45,5 +46,6 @@ def __resolve_resolved(self) -> LocationInterface: return resolved + @Requirements("SOURCE_0001") def make_available_on_localdisk(self, dst_path: str) -> str: return self.current._make_available_on_localdisk(dst_path=dst_path) diff --git a/src/reqstool/locations/git_location.py b/src/reqstool/locations/git_location.py index 5c2830dc..97f68bad 100644 --- a/src/reqstool/locations/git_location.py +++ b/src/reqstool/locations/git_location.py @@ -16,7 +16,7 @@ _VALID_REF_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._/\-]*$") -@Requirements("REQ_002") +@Requirements("SOURCE_0004", "SOURCE_0008") class GitLocation(LocationInterface): url: str ref: str diff --git a/src/reqstool/locations/local_location.py b/src/reqstool/locations/local_location.py index 06f47959..4ccb9ad9 100644 --- a/src/reqstool/locations/local_location.py +++ b/src/reqstool/locations/local_location.py @@ -7,7 +7,7 @@ from reqstool.locations.location import LocationInterface, make_safe_tmpdir_suffix -@Requirements("REQ_001") +@Requirements("SOURCE_0002") class LocalLocation(LocationInterface): path: str diff --git a/src/reqstool/locations/local_maven_location.py b/src/reqstool/locations/local_maven_location.py index ad2a1258..875d4205 100644 --- a/src/reqstool/locations/local_maven_location.py +++ b/src/reqstool/locations/local_maven_location.py @@ -3,9 +3,11 @@ import os from reqstool.common.utils import Utils +from reqstool_python_decorators.decorators.decorators import Requirements from reqstool.locations.location import LocationInterface, make_safe_tmpdir_suffix +@Requirements("SOURCE_0003") class LocalMavenLocation(LocationInterface): path: str # path to a local Maven ZIP artifact (.zip) diff --git a/src/reqstool/locations/maven_location.py b/src/reqstool/locations/maven_location.py index da7d67dd..39cf6bbd 100644 --- a/src/reqstool/locations/maven_location.py +++ b/src/reqstool/locations/maven_location.py @@ -12,7 +12,7 @@ from reqstool.locations.location import LocationInterface, make_safe_tmpdir_suffix -@Requirements("REQ_003", "REQ_017") +@Requirements("SOURCE_0005") class MavenLocation(LocationInterface): url: Optional[str] = "https://repo.maven.apache.org/maven2" group_id: str diff --git a/src/reqstool/locations/npm_location.py b/src/reqstool/locations/npm_location.py index 39dcd09f..63cec638 100644 --- a/src/reqstool/locations/npm_location.py +++ b/src/reqstool/locations/npm_location.py @@ -10,12 +10,14 @@ from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError from reqstool.common.utils import Utils +from reqstool_python_decorators.decorators.decorators import Requirements from reqstool.locations.location import LocationInterface, make_safe_tmpdir_suffix _METADATA_MAX_BYTES = 10 * 1024 * 1024 # 10 MB _REQUEST_TIMEOUT = 30 # seconds +@Requirements("SOURCE_0006") class NpmLocation(LocationInterface): url: str = "https://registry.npmjs.org" package: str diff --git a/src/reqstool/locations/pypi_location.py b/src/reqstool/locations/pypi_location.py index 036ef37c..397a2573 100644 --- a/src/reqstool/locations/pypi_location.py +++ b/src/reqstool/locations/pypi_location.py @@ -11,9 +11,11 @@ from pydantic import SecretStr from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError from reqstool.common.utils import Utils +from reqstool_python_decorators.decorators.decorators import Requirements from reqstool.locations.location import LocationInterface, make_safe_tmpdir_suffix +@Requirements("SOURCE_0007") class PypiLocation(LocationInterface): url: str = "https://pypi.org/simple" package: str diff --git a/src/reqstool/model_generators/combined_raw_datasets_generator.py b/src/reqstool/model_generators/combined_raw_datasets_generator.py index 40fd7379..437cbe8c 100644 --- a/src/reqstool/model_generators/combined_raw_datasets_generator.py +++ b/src/reqstool/model_generators/combined_raw_datasets_generator.py @@ -30,7 +30,6 @@ from reqstool.storage.database import RequirementsDatabase -@Requirements("REQ_005", "REQ_006", "REQ_007") class CombinedRawDatasetsGenerator: def __init__( self, @@ -166,6 +165,7 @@ def __handle_initial_imports(self, raw_datasets: Dict[str, RawDataset], rd: Requ for ms_urn in parsed_microservices: self._parsing_graph[ms_urn].append((rd.metadata.urn, "implementation")) + @Requirements("IMPORT_0001", "IMPORT_0002") def __import_systems( self, raw_datasets: Dict[str, RawDataset], @@ -207,6 +207,7 @@ def __import_systems( return parsed_urns + @Requirements("IMPORT_0003", "IMPORT_0004") def __import_implementations( self, raw_datasets: Dict[str, RawDataset], @@ -246,7 +247,7 @@ def __import_implementations( return parsed_urns - @Requirements("REQ_008", "REQ_026") + @Requirements("INGEST_0007", "PARSE_0002") def __parse_source(self, current_location_handler: LocationResolver) -> RawDataset: annotations_data = None svcs_data = None @@ -346,7 +347,7 @@ def __extract_source_paths(location: LocationInterface, requirements_indata: Req source_paths["annotations"] = paths.annotations_yml.path return source_paths - @Requirements("REQ_009", "REQ_010", "REQ_013") + @Requirements("INGEST_0002", "INGEST_0003", "INGEST_0004") def __parse_source_other( self, actual_tmp_path: str, requirements_indata: RequirementsIndata, rmg: RequirementsModelGenerator ): diff --git a/src/reqstool/model_generators/requirements_model_generator.py b/src/reqstool/model_generators/requirements_model_generator.py index 71af6850..d507bd1b 100644 --- a/src/reqstool/model_generators/requirements_model_generator.py +++ b/src/reqstool/model_generators/requirements_model_generator.py @@ -299,7 +299,7 @@ def __capture_source_lines(text: str) -> Dict[str, tuple[int, int, int]]: result[id_text] = (id_line, id_col, id_col + len(id_text)) return result - @Requirements("REQ_004", "REQ_036") + @Requirements("INGEST_0001", "LIFECYCLE_0001", "LIFECYCLE_0002") def __parse_requirements(self, model, data, source_lines: Dict[str, tuple[int, int, int]]): # NOSONAR r_reqs = {} diff --git a/src/reqstool/model_generators/svcs_model_generator.py b/src/reqstool/model_generators/svcs_model_generator.py index d12b67b0..0dc9dbc5 100644 --- a/src/reqstool/model_generators/svcs_model_generator.py +++ b/src/reqstool/model_generators/svcs_model_generator.py @@ -4,6 +4,7 @@ from typing import Dict from ruamel.yaml import YAML +from reqstool_python_decorators.decorators.decorators import Requirements from reqstool.commands.exit_codes import EXIT_CODE_SYNTAX_VALIDATION_ERROR from reqstool.common.filter_parser import parse_filters @@ -76,6 +77,7 @@ def __capture_source_lines(text: str) -> Dict[str, tuple[int, int, int]]: result[id_text] = (id_line, id_col, id_col + len(id_text)) return result + @Requirements("LIFECYCLE_0003") def __parse_svcs( self, validated: SVCsPydanticModel, diff --git a/src/reqstool/model_generators/testdata_model_generator.py b/src/reqstool/model_generators/testdata_model_generator.py index 86271ca1..37bb14f2 100644 --- a/src/reqstool/model_generators/testdata_model_generator.py +++ b/src/reqstool/model_generators/testdata_model_generator.py @@ -34,7 +34,7 @@ def __generate(self, test_result_files: List[Path], urn: str) -> TestsData: return TestsData(tests=tests) - @Requirements("REQ_014", "REQ_015") + @Requirements("INGEST_0005", "INGEST_0006") def __parse_test_data(self, test_result_files: List[Path], urn: str) -> Dict[UrnId, TestData]: r_testdata: Dict[UrnId, TestData] = {} diff --git a/src/reqstool/requirements_indata/requirements_indata.py b/src/reqstool/requirements_indata/requirements_indata.py index 66fac503..f61456fe 100644 --- a/src/reqstool/requirements_indata/requirements_indata.py +++ b/src/reqstool/requirements_indata/requirements_indata.py @@ -39,7 +39,7 @@ def model_post_init(self, __context): self._handle_requirements_config() self._ensure_absolute_paths_and_check_existance() - @Requirements("REQ_011") + @Requirements("INGEST_0008") def _handle_requirements_config(self): if os.path.exists(os.path.join(self.dst_path, "reqstool_config.yml")): diff --git a/src/reqstool/requirements_indata/requirements_indata_paths.py b/src/reqstool/requirements_indata/requirements_indata_paths.py index d43efc22..b3b22ef6 100644 --- a/src/reqstool/requirements_indata/requirements_indata_paths.py +++ b/src/reqstool/requirements_indata/requirements_indata_paths.py @@ -2,15 +2,12 @@ from pydantic import BaseModel, Field -from reqstool_python_decorators.decorators.decorators import Requirements - class RequirementsIndataPathItem(BaseModel): path: str exists: bool = False -@Requirements("REQ_016") class RequirementsIndataPaths(BaseModel): # static requirements_yml: RequirementsIndataPathItem = Field( diff --git a/src/reqstool/services/statistics_service.py b/src/reqstool/services/statistics_service.py index 0a3459e8..8a61f93f 100644 --- a/src/reqstool/services/statistics_service.py +++ b/src/reqstool/services/statistics_service.py @@ -95,7 +95,7 @@ def code_completed(self) -> int: return self.completed_requirements - self.non_code_completed -@Requirements("REQ_028") +@Requirements("STATUS_0001") class StatisticsService: def __init__(self, repository: RequirementsRepository, include_post_build: bool = False): self._repo = repository diff --git a/src/reqstool/storage/database_filter_processor.py b/src/reqstool/storage/database_filter_processor.py index 1c6a9725..4939e094 100644 --- a/src/reqstool/storage/database_filter_processor.py +++ b/src/reqstool/storage/database_filter_processor.py @@ -3,6 +3,7 @@ import logging +from reqstool_python_decorators.decorators.decorators import Requirements from reqstool.common.models.urn_id import UrnId from reqstool.filters.id_filters import IDFilters from reqstool.models.raw_datasets import RawDataset @@ -12,6 +13,7 @@ logger = logging.getLogger(__name__) +@Requirements("IMPORT_0005", "IMPORT_0006", "IMPORT_0007", "IMPORT_0008") class DatabaseFilterProcessor: def __init__(self, db: RequirementsDatabase, raw_datasets: dict[str, RawDataset]): self._db = db diff --git a/tests/integration/reqstool/model_generators/test_included_models_generator.py b/tests/integration/reqstool/model_generators/test_included_models_generator.py index c34b89ec..8690b913 100644 --- a/tests/integration/reqstool/model_generators/test_included_models_generator.py +++ b/tests/integration/reqstool/model_generators/test_included_models_generator.py @@ -3,7 +3,10 @@ import os import pytest -from reqstool_python_decorators.decorators.decorators import SVCs + +# Note: SVC_SOURCE_0004 (git) and SVC_SOURCE_0005 (maven) are exercised here, but are linked to the +# unit tests (test_git_location.py / test_maven_location.py) instead — this integration test is +# skipped without GITHUB/GITLAB tokens, so its result cannot satisfy those SVCs in credential-less CI. from reqstool.common.validator_error_holder import ValidationErrorHolder from reqstool.common.validators.semantic_validator import SemanticValidator @@ -16,7 +19,6 @@ def choose_token(): return os.getenv("GITHUB_TOKEN") or os.getenv("GITLAB_TOKEN") -@SVCs("SVC_002") @pytest.mark.integration @pytest.mark.skipif( not (os.getenv("GITHUB_TOKEN") or os.getenv("GITLAB_TOKEN")), @@ -36,7 +38,6 @@ def test_basic_git(): ) -@SVCs("SVC_003", "SVC_008") @pytest.mark.integration @pytest.mark.skipif( not (os.getenv("GITHUB_TOKEN") or os.getenv("GITLAB_TOKEN")), diff --git a/tests/unit/reqstool/commands/enrich/test_enrich.py b/tests/unit/reqstool/commands/enrich/test_enrich.py index d26eda5a..34573338 100644 --- a/tests/unit/reqstool/commands/enrich/test_enrich.py +++ b/tests/unit/reqstool/commands/enrich/test_enrich.py @@ -1,11 +1,15 @@ # Copyright © LFV +import argparse +import io from pathlib import Path +from unittest.mock import MagicMock, patch import pytest from reqstool_python_decorators.decorators.decorators import SVCs +from reqstool.command import Command from reqstool.commands.enrich.enrich import EnrichCommand from reqstool.common.enrichment.enricher import BUILT_IN_PRESETS from reqstool.locations.local_location import LocalLocation @@ -23,49 +27,85 @@ def ms101(local_testdata_resources_rootdir_w_path): return LocalLocation(path=local_testdata_resources_rootdir_w_path("test_basic/baseline/ms-101")) -@SVCs("SVC_039") +@SVCs("SVC_ENRICH_0001") def test_spec_all_fields(ms101): input_content, expected = _load("spec_all_fields") result = EnrichCommand(location=ms101, input_content=input_content, config=BUILT_IN_PRESETS["openspec:spec"]) assert result.result == expected -@SVCs("SVC_039") +@SVCs("SVC_ENRICH_0001") def test_spec_no_description(ms101): input_content, expected = _load("spec_no_description") result = EnrichCommand(location=ms101, input_content=input_content, config=BUILT_IN_PRESETS["openspec:spec"]) assert result.result == expected -@SVCs("SVC_039") +@SVCs("SVC_ENRICH_0002") def test_inline_title_only(ms101): input_content, expected = _load("inline_title_only") result = EnrichCommand(location=ms101, input_content=input_content, config=BUILT_IN_PRESETS["openspec:design"]) assert result.result == expected -@SVCs("SVC_039") +@SVCs("SVC_ENRICH_0001") def test_inline_code_spans_skipped(ms101): input_content, expected = _load("inline_code_spans") result = EnrichCommand(location=ms101, input_content=input_content, config=BUILT_IN_PRESETS["openspec:design"]) assert result.result == expected -@SVCs("SVC_039") +@SVCs("SVC_ENRICH_0001") def test_no_ids_passthrough(ms101): input_content, expected = _load("no_ids") result = EnrichCommand(location=ms101, input_content=input_content, config=BUILT_IN_PRESETS["openspec:spec"]) assert result.result == expected -@SVCs("SVC_039") +@SVCs("SVC_ENRICH_0003") +def test_command_enrich_reads_stdin_writes_output(monkeypatch): + """ENRICH_0003: with no input file, the document is read from stdin and the result written to output.""" + out = io.StringIO() + args = argparse.Namespace( + source="local", + path="/x", + maven=None, + npm=None, + pypi=None, + preset="openspec:spec", + input=None, + output=out, + ) + monkeypatch.setattr("sys.stdin", io.StringIO("document referencing REQ_X")) + with ( + patch.object(Command, "_get_initial_source", return_value=MagicMock()), + patch("reqstool.command.EnrichCommand") as mock_enrich, + ): + mock_enrich.return_value.result = "ENRICHED-OUTPUT" + Command().command_enrich(args) + assert out.getvalue() == "ENRICHED-OUTPUT" + assert mock_enrich.call_args.kwargs["input_content"] == "document referencing REQ_X" + + +@SVCs("SVC_ENRICH_0004") +def test_command_enrich_no_source_no_config_exits(capsys): + """ENRICH_0004: with neither an explicit source nor a config file, the command errors out.""" + args = argparse.Namespace(source=None, preset="openspec:spec", input=None, output=io.StringIO()) + with patch("reqstool.common.reqstool_ai_config.find_config", return_value=None): + with pytest.raises(SystemExit) as exc: + Command().command_enrich(args) + assert exc.value.code == 2 + assert "reqstool enrich:" in capsys.readouterr().err + + +@SVCs("SVC_ENRICH_0001") def test_mvr_enrichment(ms101): input_content, expected = _load("mvr") result = EnrichCommand(location=ms101, input_content=input_content, config=BUILT_IN_PRESETS["openspec:spec"]) assert result.result == expected -@SVCs("SVC_039") +@SVCs("SVC_ENRICH_0001") def test_spec_multiline_description(ms101): input_content, expected = _load("spec_multiline_description") result = EnrichCommand(location=ms101, input_content=input_content, config=BUILT_IN_PRESETS["openspec:spec"]) diff --git a/tests/unit/reqstool/commands/generate_json/test_generate_json.py b/tests/unit/reqstool/commands/generate_json/test_generate_json.py index 5c4d8a9f..4790ca9a 100644 --- a/tests/unit/reqstool/commands/generate_json/test_generate_json.py +++ b/tests/unit/reqstool/commands/generate_json/test_generate_json.py @@ -1,14 +1,17 @@ # Copyright © LFV import json +from importlib.resources import files +import jsonschema from reqstool_python_decorators.decorators.decorators import SVCs +import reqstool.resources.schemas.v1 from reqstool.commands.generate_json.generate_json import GenerateJsonCommand from reqstool.locations.local_location import LocalLocation -@SVCs("SVC_027") +@SVCs("SVC_EXPORT_0001") def test_generate_json(local_testdata_resources_rootdir_w_path): gjc = GenerateJsonCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -16,7 +19,11 @@ def test_generate_json(local_testdata_resources_rootdir_w_path): ) assert gjc.result + export_schema = json.loads(files(reqstool.resources.schemas.v1).joinpath("export_output.schema.json").read_text()) + jsonschema.validate(json.loads(gjc.result), export_schema) + +@SVCs("SVC_EXPORT_0003") def test_generate_json_no_filter_unchanged(local_testdata_resources_rootdir_w_path): gjc = GenerateJsonCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -28,6 +35,7 @@ def test_generate_json_no_filter_unchanged(local_testdata_resources_rootdir_w_pa assert "ms-001:SVC_010" in result["svcs"] +@SVCs("SVC_EXPORT_0002") def test_generate_json_filter_req_ids(local_testdata_resources_rootdir_w_path): gjc = GenerateJsonCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), diff --git a/tests/unit/reqstool/commands/report/criterias/test_criterias.py b/tests/unit/reqstool/commands/report/criterias/test_criterias.py index 6c7c2420..222b9aec 100644 --- a/tests/unit/reqstool/commands/report/criterias/test_criterias.py +++ b/tests/unit/reqstool/commands/report/criterias/test_criterias.py @@ -12,8 +12,8 @@ from reqstool.storage.requirements_repository import RequirementsRepository -@SVCs("SVC_034") -def test_basic_baseline(resource_funcname_rootdir, local_testdata_resources_rootdir_w_path): +@SVCs("SVC_REPORT_0003", "SVC_REPORT_0004") +def test_basic_baseline(local_testdata_resources_rootdir_w_path): db = RequirementsDatabase() semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) CombinedRawDatasetsGenerator( @@ -23,15 +23,32 @@ def test_basic_baseline(resource_funcname_rootdir, local_testdata_resources_root ) repo = RequirementsRepository(db) - gbc = GroupByOrganizor( + # REPORT_0003: all requirements come from the initial dataset, so INITIAL_IMPORTS grouping + # puts them all into a single "Initial URN (...)" group. + gbc_initial = GroupByOrganizor( repo=repo, group_by=GroupbyOptions.INITIAL_IMPORTS, sort_by=[SortByOptions.ID, SortByOptions.REVISION, SortByOptions.SIGNIFICANCE], ) + initial_groups = dict(gbc_initial) + assert set(initial_groups.keys()) == {f"Initial URN ({repo.get_initial_urn()})"} - requirements = repo.get_all_requirements() - for key, value in gbc: - print(f"{key}: {[requirements[urn_id] for urn_id in value]}") + # REPORT_0004: within the group, requirements are sorted by id. + ordered_ids = [urn_id.id for urn_id in next(iter(initial_groups.values()))] + assert ordered_ids == ["REQ_101", "REQ_102", "REQ_201", "REQ_202"] + + # REPORT_0003: CATEGORY grouping buckets requirements by their first stored category + # (categories are read back from the database in alphabetical order). + gbc_category = GroupByOrganizor( + repo=repo, + group_by=GroupbyOptions.CATEGORY, + sort_by=[SortByOptions.ID], + ) + category_groups = {key: [urn_id.id for urn_id in value] for key, value in gbc_category} + assert category_groups == { + "functional-suitability": ["REQ_101", "REQ_201"], + "maintainability": ["REQ_102"], + "reliability": ["REQ_202"], + } - assert gbc is not None db.close() diff --git a/tests/unit/reqstool/commands/report/test_report.py b/tests/unit/reqstool/commands/report/test_report.py index 237455b5..e73a371d 100644 --- a/tests/unit/reqstool/commands/report/test_report.py +++ b/tests/unit/reqstool/commands/report/test_report.py @@ -8,7 +8,7 @@ from reqstool.locations.local_location import LocalLocation -@SVCs("SVC_029", "SVC_030", "SVC_032") +@SVCs("SVC_REPORT_0001") def test_get_template_medium_ms001(local_testdata_resources_rootdir_w_path): rc = report.ReportCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -16,9 +16,12 @@ def test_get_template_medium_ms001(local_testdata_resources_rootdir_w_path): sort_by=[SortByOptions.ID], ) assert rc.result + assert "= REQUIREMENTS DOCUMENTATION" in rc.result + assert "== TOTAL STATISTICS" in rc.result + assert "|===" in rc.result -@SVCs("SVC_029", "SVC_031", "SVC_033") +@SVCs("SVC_REPORT_0001") def test_get_template_standard_sys001(local_testdata_resources_rootdir_w_path): rc = report.ReportCommand( location=LocalLocation( @@ -28,8 +31,12 @@ def test_get_template_standard_sys001(local_testdata_resources_rootdir_w_path): sort_by=[SortByOptions.SIGNIFICANCE], ) assert rc.result + assert "= REQUIREMENTS DOCUMENTATION" in rc.result + assert "== TOTAL STATISTICS" in rc.result + assert "|===" in rc.result +@SVCs("SVC_REPORT_0002") def test_get_template_markdown_ms001(local_testdata_resources_rootdir_w_path): rc = report.ReportCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), diff --git a/tests/unit/reqstool/commands/status/test_status.py b/tests/unit/reqstool/commands/status/test_status.py index 0e9d5328..3d8f4a1d 100644 --- a/tests/unit/reqstool/commands/status/test_status.py +++ b/tests/unit/reqstool/commands/status/test_status.py @@ -1,13 +1,16 @@ # Copyright © LFV import json +import pytest + from reqstool_python_decorators.decorators.decorators import SVCs from reqstool.commands.status.status import StatusCommand from reqstool.locations.local_location import LocalLocation +from reqstool.storage.database import RequirementsDatabase -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0001") def test_status_incomplete_implementation(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")) @@ -18,7 +21,7 @@ def test_status_incomplete_implementation(local_testdata_resources_rootdir_w_pat assert nr_of_incomplete_requirements == 5 -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0001") def test_status_report_generation_sys_ms(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/empty_ms/ms-001")) @@ -29,7 +32,7 @@ def test_status_report_generation_sys_ms(local_testdata_resources_rootdir_w_path assert nr_of_incomplete_requirements == 5 -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0005") def test_status_json_format(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -57,3 +60,33 @@ def test_status_json_format(local_testdata_resources_rootdir_w_path): assert req_stats["implementation_type"] in ["in-code", "N/A"] assert nr_of_incomplete_requirements == 5 + + +@SVCs("SVC_STATUS_0008") +def test_with_post_tests_incorporates_junit_outcomes(local_testdata_resources_rootdir_w_path): + """STATUS_0008: outcomes from a post-build JUnit XML file are inserted into the status DB. + + test_results has no FK, so an empty database plus any URN is enough to observe the injection — + no full parse needed. + """ + db = RequirementsDatabase() + junit = local_testdata_resources_rootdir_w_path( + "test_basic/no_impls/basic/ms-101/test_results/surefire/TEST-com.example.RequirementsExampleTests.xml" + ) + + # Reach the name-mangled static helper that performs the injection. + StatusCommand._StatusCommand__inject_post_tests(db, "ms-101", [str(junit)]) + + fqns = [row["fqn"] for row in db.connection.execute("SELECT fqn FROM test_results").fetchall()] + assert any("RequirementsExampleTests" in fqn for fqn in fqns) + db.close() + + +@SVCs("SVC_STATUS_0008") +def test_with_post_tests_missing_file_raises(local_testdata_resources_rootdir_w_path): + """STATUS_0008: a non-existent post-build file is rejected up-front.""" + with pytest.raises(FileNotFoundError): + StatusCommand( + location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), + with_post_tests=["/nonexistent/post-tests.xml"], + ) diff --git a/tests/unit/reqstool/commands/status/test_status_verbosity.py b/tests/unit/reqstool/commands/status/test_status_verbosity.py index 5d5621c0..ba874f3d 100644 --- a/tests/unit/reqstool/commands/status/test_status_verbosity.py +++ b/tests/unit/reqstool/commands/status/test_status_verbosity.py @@ -96,7 +96,7 @@ def test_incomplete_reasons_not_applicable_stats_ignored(): # --------------------------------------------------------------------------- -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0003") def test_status_compact_format(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -110,7 +110,7 @@ def test_status_compact_format(local_testdata_resources_rootdir_w_path): assert len(status.strip().splitlines()) == 1 -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0003") def test_status_compact_single_line(local_testdata_resources_rootdir_w_path): """Compact output is exactly one non-empty line regardless of fixture state.""" result = StatusCommand( @@ -128,7 +128,7 @@ def test_status_compact_single_line(local_testdata_resources_rootdir_w_path): # --------------------------------------------------------------------------- -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0003") def test_status_normal_has_complete_and_incomplete(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -140,7 +140,7 @@ def test_status_normal_has_complete_and_incomplete(local_testdata_resources_root assert "FAIL" in status -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0003") def test_status_normal_complete_before_incomplete(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -152,7 +152,7 @@ def test_status_normal_complete_before_incomplete(local_testdata_resources_rootd assert complete_pos < incomplete_pos, "COMPLETE section must appear before INCOMPLETE" -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0002") def test_status_normal_verdict_at_bottom(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -163,7 +163,7 @@ def test_status_normal_verdict_at_bottom(local_testdata_resources_rootdir_w_path assert "FAIL" in lines[-1] -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0003") def test_status_normal_has_reason_text(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -178,7 +178,7 @@ def test_status_normal_has_reason_text(local_testdata_resources_rootdir_w_path): # --------------------------------------------------------------------------- -@SVCs("SVC_021") +@SVCs("SVC_STATUS_0004") def test_status_incomplete_flag_hides_complete_section(local_testdata_resources_rootdir_w_path): result = StatusCommand( location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_standard/baseline/ms-001")), @@ -193,7 +193,7 @@ def test_status_incomplete_flag_hides_complete_section(local_testdata_resources_ assert not re.search(r"(? 0 + + +@SVCs("SVC_MCP_0002") +def test_start_server_streamable_http_configures_settings(local_testdata_resources_rootdir_w_path): + """MCP_0002: the streamable-HTTP transport is served as stateless JSON responses on the + configured host and port.""" + location = LocalLocation(path=local_testdata_resources_rootdir_w_path("test_basic/baseline/ms-101")) + + with patch.object(mcp.server.fastmcp, "FastMCP", _FakeFastMCP): + mcp_server.start_server(location=location, transport="streamable-http", host="0.0.0.0", port=9000) + + fake_mcp = _FakeFastMCP.instances[-1] + assert fake_mcp.run_transport == "streamable-http" + assert fake_mcp.settings.host == "0.0.0.0" + assert fake_mcp.settings.port == 9000 + assert fake_mcp.settings.json_response is True + assert fake_mcp.settings.stateless_http is True diff --git a/tests/unit/reqstool/model_generators/test_combined_raw_datasets_generator.py b/tests/unit/reqstool/model_generators/test_combined_raw_datasets_generator.py index fa0ff915..ffd5ca64 100644 --- a/tests/unit/reqstool/model_generators/test_combined_raw_datasets_generator.py +++ b/tests/unit/reqstool/model_generators/test_combined_raw_datasets_generator.py @@ -21,16 +21,29 @@ from reqstool.models.raw_datasets import CombinedRawDataset -@SVCs("SVC_001") +@SVCs("SVC_INGEST_0001", "SVC_INGEST_0004", "SVC_INGEST_0007") def test_basic_local(resource_funcname_rootdir, local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) - combined_raw_datasets_generator.CombinedRawDatasetsGenerator( + crd: CombinedRawDataset = combined_raw_datasets_generator.CombinedRawDatasetsGenerator( initial_location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_basic/baseline/ms-101")), semantic_validator=semantic_validator, - ) + ).combined_raw_datasets + + rd = crd.raw_datasets["ms-101"] + + # INGEST_0007: the static input files at the content root are parsed + assert rd.requirements_data is not None and len(rd.requirements_data.requirements) > 0 + assert rd.svcs_data is not None and len(rd.svcs_data.cases) > 0 + + # INGEST_0004: code annotations are parsed, capturing implementation and test links + assert rd.annotations_data is not None + impl_ids = {urn_id.id for urn_id in rd.annotations_data.implementations} + test_ids = {urn_id.id for urn_id in rd.annotations_data.tests} + assert "REQ_101" in impl_ids + assert "SVC_101" in test_ids -@SVCs("SVC_001") +@SVCs("SVC_INGEST_0008") def test_basic_requirements_config(resource_funcname_rootdir, local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) combined_raw_datasets_generator.CombinedRawDatasetsGenerator( @@ -41,7 +54,7 @@ def test_basic_requirements_config(resource_funcname_rootdir, local_testdata_res ) -@SVCs("SVC_001", "SVC_004") +@SVCs("SVC_INGEST_0002", "SVC_INGEST_0003", "SVC_IMPORT_0001") def test_standard_ms001_initial(local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) @@ -74,7 +87,6 @@ def test_standard_ms001_initial(local_testdata_resources_rootdir_w_path): assert crd.raw_datasets["ext-002"].mvrs_data is None -@SVCs("SVC_001") def test_standard_sys001_initial(local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) combined_raw_datasets_generator.CombinedRawDatasetsGenerator( @@ -83,7 +95,7 @@ def test_standard_sys001_initial(local_testdata_resources_rootdir_w_path): ) -@SVCs("SVC_020") +@SVCs("SVC_PARSE_0002") def test_missing_requirements_file(local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) with pytest.raises(MissingRequirementsFileError) as excinfo: @@ -96,7 +108,7 @@ def test_missing_requirements_file(local_testdata_resources_rootdir_w_path): assert "this/path/does/not/have/a/requirements/file" in str(excinfo.value) -@SVCs("SVC_020") +@SVCs("SVC_IMPORT_0002") def test_circular_import_raises(local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) with pytest.raises(CircularImportError) as excinfo: @@ -108,7 +120,7 @@ def test_circular_import_raises(local_testdata_resources_rootdir_w_path): assert "Circular import detected" in str(excinfo.value) -@SVCs("SVC_020") +@SVCs("SVC_IMPORT_0004") def test_circular_implementation_raises(local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) with pytest.raises(CircularImplementationError) as excinfo: @@ -120,7 +132,7 @@ def test_circular_implementation_raises(local_testdata_resources_rootdir_w_path) assert "Circular implementation detected" in str(excinfo.value) -@SVCs("SVC_001") +@SVCs("SVC_IMPORT_0003") def test_implementation_traversal_recursive(local_testdata_resources_rootdir_w_path): semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) @@ -163,7 +175,7 @@ def _assert_safe_suffix(suffix, expected_prefix, uri_fragment): assert re.match(r"^[a-zA-Z0-9._-]+$", suffix), f"Suffix contains unsafe chars: {suffix!r}" -@SVCs("SVC_020") +@SVCs("SVC_PARSE_0002") def test_tmpdir_suffix_local_uses_local_prefix(): with _capture_suffix_calls() as captured_suffixes: with pytest.raises(MissingRequirementsFileError): @@ -175,7 +187,7 @@ def test_tmpdir_suffix_local_uses_local_prefix(): _assert_safe_suffix(captured_suffixes[0], "local_", "nonexistent") -@SVCs("SVC_020") +@SVCs("SVC_PARSE_0002") @pytest.mark.parametrize( "location,expected_prefix,uri_fragment", [ diff --git a/tests/unit/reqstool/model_generators/test_testdata_model_generator.py b/tests/unit/reqstool/model_generators/test_testdata_model_generator.py index 16176b65..dea3ea88 100644 --- a/tests/unit/reqstool/model_generators/test_testdata_model_generator.py +++ b/tests/unit/reqstool/model_generators/test_testdata_model_generator.py @@ -22,7 +22,7 @@ ] -@SVCs("SVC_006") +@SVCs("SVC_INGEST_0006") @pytest.mark.parametrize("method_name", karate_method_names) def test_karate_method_identifier_regex(method_name): karate_match = re.match(TestDataModelGenerator.KARATE_METHOD_IDENTIFIER_REGEX, method_name) @@ -30,7 +30,7 @@ def test_karate_method_identifier_regex(method_name): assert karate_match.group(1) == "Create a subscripiton with filter and receive messages" -@SVCs("SVC_007") +@SVCs("SVC_INGEST_0005") @pytest.mark.parametrize("method_name", unit_method_names) def test_unit_method_identifier_regex(method_name): unit_match = re.match(TestDataModelGenerator.UNIT_METHOD_IDENTIFIER_REGEX, method_name) diff --git a/tests/unit/reqstool/resources/schemas/v1/test_json_schemas.py b/tests/unit/reqstool/resources/schemas/v1/test_json_schemas.py index 0459318d..c92056ed 100644 --- a/tests/unit/reqstool/resources/schemas/v1/test_json_schemas.py +++ b/tests/unit/reqstool/resources/schemas/v1/test_json_schemas.py @@ -2,36 +2,29 @@ from jsonschema import Draft202012Validator -from reqstool_python_decorators.decorators.decorators import SVCs from reqstool.common.validators.syntax_validator import JsonSchemaTypes -@SVCs("SVC_015") def test_validate_annotations_schema_json(): Draft202012Validator.check_schema(JsonSchemaTypes.ANNOTATIONS.value.schema) -@SVCs("SVC_015") def test_validate_common_schema_json(): Draft202012Validator.check_schema(JsonSchemaTypes.COMMON.value.schema) -@SVCs("SVC_015") def test_validate_manual_verification_results_schema_json(): Draft202012Validator.check_schema(JsonSchemaTypes.MANUAL_VERIFICATION_RESULTS.value.schema) -@SVCs("SVC_015") def test_validate_requirements_config_schema_json(): Draft202012Validator.check_schema(JsonSchemaTypes.REQSTOOL_CONFIG.value.schema) -@SVCs("SVC_015") def test_validate_requirements_schema_json(): Draft202012Validator.check_schema(JsonSchemaTypes.REQUIREMENTS.value.schema) -@SVCs("SVC_015") def test_validate_software_verification_cases_schema_json(): Draft202012Validator.check_schema(JsonSchemaTypes.SOFTWARE_VERIFICATION_CASES.value.schema) diff --git a/tests/unit/reqstool/storage/test_database_filter_processor.py b/tests/unit/reqstool/storage/test_database_filter_processor.py index 397f4b90..3a7aa95d 100644 --- a/tests/unit/reqstool/storage/test_database_filter_processor.py +++ b/tests/unit/reqstool/storage/test_database_filter_processor.py @@ -1,10 +1,15 @@ # Copyright © LFV import pytest +from reqstool_python_decorators.decorators.decorators import SVCs from reqstool.common.models.urn_id import UrnId +from reqstool.common.validator_error_holder import ValidationErrorHolder +from reqstool.common.validators.semantic_validator import SemanticValidator from reqstool.filters.requirements_filters import RequirementFilter from reqstool.filters.svcs_filters import SVCFilter +from reqstool.locations.local_location import LocalLocation +from reqstool.model_generators.combined_raw_datasets_generator import CombinedRawDatasetsGenerator from reqstool.models.mvrs import MVRData, MVRsData from reqstool.models.raw_datasets import RawDataset from reqstool.models.requirements import ( @@ -364,3 +369,26 @@ def test_svc_filter_excludes(): assert remaining_svcs == {"SVC_A"} db.close() + + +@SVCs("SVC_IMPORT_0005") +def test_implementation_requirements_excluded_from_scope(local_testdata_resources_rootdir_w_path): + """IMPORT_0005: requirements contributed by implementation datasets (lib-a/b/c) are removed + from the final requirement set, while the root's own requirements remain.""" + db = RequirementsDatabase() + semantic_validator = SemanticValidator(validation_error_holder=ValidationErrorHolder()) + crd = CombinedRawDatasetsGenerator( + initial_location=LocalLocation(path=local_testdata_resources_rootdir_w_path("test_recursive_impl/root")), + semantic_validator=semantic_validator, + database=db, + ).combined_raw_datasets + + DatabaseFilterProcessor(db, crd.raw_datasets).apply_filters() + + remaining = {row["id"] for row in db.connection.execute("SELECT id FROM requirements").fetchall()} + assert remaining == {"REQ_ROOT_001"} + assert "REQ_LA_001" not in remaining + assert "REQ_LB_001" not in remaining + assert "REQ_LC_001" not in remaining + + db.close() diff --git a/tests/unit/reqstool/test_command.py b/tests/unit/reqstool/test_command.py index e1d228a9..0eee28bf 100644 --- a/tests/unit/reqstool/test_command.py +++ b/tests/unit/reqstool/test_command.py @@ -1,8 +1,10 @@ # Copyright © LFV -from unittest.mock import patch +from unittest.mock import MagicMock, patch import argparse +import io +import sys import pytest @@ -18,6 +20,7 @@ from reqstool.locations.pypi_location import PypiLocation from reqstool.locations.local_npm_location import LocalNpmLocation from reqstool.locations.npm_location import NpmLocation +from reqstool_python_decorators.decorators.decorators import SVCs # --------------------------------------------------------------------------- @@ -25,6 +28,7 @@ # --------------------------------------------------------------------------- +@SVCs("SVC_REPORT_0005") def test_report_subcommand_routes_to_command_report(): with ( patch.object(Command, "command_report") as mock_report, @@ -35,6 +39,7 @@ def test_report_subcommand_routes_to_command_report(): mock_report.assert_called_once() +@SVCs("SVC_REPORT_0006") def test_report_asciidoc_deprecated_warning_printed_to_stderr(capsys): with ( patch.object(Command, "command_report"), @@ -57,6 +62,7 @@ def test_report_asciidoc_still_calls_command_report(): mock_report.assert_called_once() +@SVCs("SVC_EXPORT_0005") def test_export_subcommand_routes_to_command_export(): with ( patch.object(Command, "command_export") as mock_export, @@ -102,6 +108,7 @@ def test_missing_requirements_error_exits_with_correct_code(): mock_exit.assert_any_call(EXIT_CODE_MISSING_REQUIREMENTS_FILE) +@SVCs("SVC_STATUS_0007") def test_status_nonzero_exit_code_is_propagated(): with ( patch.object(Command, "command_status", return_value=EXIT_CODE_ALL_REQS_NOT_IMPLEMENTED), @@ -344,3 +351,137 @@ def test_status_with_post_tests_multiple_paths(): def test_status_without_post_tests_defaults_to_none(): args = _make_command_and_parse(["reqstool", "status", "local", "-p", "/tmp"]) assert args.with_post_tests is None + + +# --------------------------------------------------------------------------- +# LSP / MCP server option + dependency-guard tests +# --------------------------------------------------------------------------- + + +@SVCs("SVC_LSP_0002") +def test_lsp_tcp_transport_args_parsed(): + args = _make_command_and_parse(["reqstool", "lsp", "--tcp", "--host", "0.0.0.0", "--port", "9999"]) + assert args.tcp is True + assert args.host == "0.0.0.0" + assert args.port == 9999 + + +def test_lsp_log_file_arg_parsed(): + args = _make_command_and_parse(["reqstool", "lsp", "--log-file", "/tmp/lsp.log"]) + assert args.log_file == "/tmp/lsp.log" + + +@SVCs("SVC_LSP_0003") +def test_lsp_log_file_passed_to_server(): + """LSP_0003: the configured log-file path is forwarded to the language server.""" + cmd = Command() + lsp_args = argparse.Namespace(tcp=False, host="127.0.0.1", port=2087, log_file="/tmp/lsp.log") + fake_server = MagicMock() + with patch.dict(sys.modules, {"reqstool.lsp.server": MagicMock(start_server=fake_server.start_server)}): + cmd.command_lsp(lsp_args) + assert fake_server.start_server.call_args.kwargs["log_file"] == "/tmp/lsp.log" + + +@SVCs("SVC_LSP_0004") +def test_lsp_missing_extra_reports_and_exits(capsys): + cmd = Command() + lsp_args = argparse.Namespace(tcp=False, host="127.0.0.1", port=2087, log_file=None) + with patch.dict(sys.modules, {"reqstool.lsp.server": None}): + with pytest.raises(SystemExit) as exc: + cmd.command_lsp(lsp_args) + assert exc.value.code == 1 + assert "pip install reqstool[lsp]" in capsys.readouterr().err + + +@SVCs("SVC_MCP_0002") +def test_mcp_transport_args_parsed(): + args = _make_command_and_parse(["reqstool", "mcp", "--transport", "sse", "--host", "h", "--port", "1234"]) + assert args.transport == "sse" + assert args.host == "h" + assert args.port == 1234 + + +@SVCs("SVC_MCP_0004") +def test_mcp_missing_extra_reports_and_exits(capsys): + cmd = Command() + mcp_args = argparse.Namespace( + source="local", path="/tmp", transport="stdio", host="127.0.0.1", port=8000, maven=None, npm=None, pypi=None + ) + with patch.dict(sys.modules, {"reqstool.mcp.server": None}): + with pytest.raises(SystemExit) as exc: + cmd.command_mcp(mcp_args) + assert exc.value.code == 1 + assert "pip install 'mcp>=1.0'" in capsys.readouterr().err + + +@SVCs("SVC_MCP_0003") +def test_mcp_auto_detects_dataset_from_config(): + """MCP_0003: with no explicit source, the dataset is resolved from the reqstool AI config file.""" + cmd = Command() + mcp_args = argparse.Namespace(source=None, transport="stdio", host="127.0.0.1", port=8000) + mock_server = MagicMock() + with ( + patch.dict(sys.modules, {"reqstool.mcp.server": mock_server}), + patch("reqstool.common.reqstool_ai_config.find_config", return_value="/proj/.reqstool-ai.yaml"), + patch("reqstool.common.reqstool_ai_config.resolve_system_path", return_value="/proj/docs/reqstool"), + ): + cmd.command_mcp(mcp_args) + location = mock_server.start_server.call_args.kwargs["location"] + assert location.path == "/proj/docs/reqstool" + + +@SVCs("SVC_MCP_0003") +def test_mcp_no_source_no_config_exits(capsys): + """MCP_0003: with neither an explicit source nor a config file, the command errors out.""" + cmd = Command() + mcp_args = argparse.Namespace(source=None, transport="stdio", host="127.0.0.1", port=8000) + with ( + patch.dict(sys.modules, {"reqstool.mcp.server": MagicMock()}), + patch("reqstool.common.reqstool_ai_config.find_config", return_value=None), + ): + with pytest.raises(SystemExit) as exc: + cmd.command_mcp(mcp_args) + assert exc.value.code == 2 + assert "reqstool mcp:" in capsys.readouterr().err + + +@SVCs("SVC_STATUS_0009") +def test_command_status_writes_result_to_output_destination(): + """STATUS_0009: status content is written to the provided output handle rather than only stdout.""" + out = io.StringIO() + # command_status reads only output and check_all_reqs_met directly (everything else goes + # through getattr defaults or the mocked StatusCommand / _get_initial_source). + args = argparse.Namespace(output=out, check_all_reqs_met=False) + with ( + patch.object(Command, "_get_initial_source", return_value=MagicMock()), + patch("reqstool.command.StatusCommand") as mock_status, + ): + mock_status.return_value.result = ("STATUS-BODY", 0) + exit_code = Command().command_status(args) + assert out.getvalue() == "STATUS-BODY" + assert exit_code == 0 + + +@SVCs("SVC_STATUS_0007") +def test_command_status_returns_nonzero_when_enforcing_and_incomplete(): + """STATUS_0007: with --check-all-reqs-met and incomplete requirements, exit with the + all-requirements-not-implemented code; the same incomplete result exits zero otherwise.""" + out = io.StringIO() + args = argparse.Namespace(output=out, check_all_reqs_met=True) + with ( + patch.object(Command, "_get_initial_source", return_value=MagicMock()), + patch("reqstool.command.StatusCommand") as mock_status, + ): + mock_status.return_value.result = ("STATUS-BODY", 2) + exit_code = Command().command_status(args) + assert exit_code == EXIT_CODE_ALL_REQS_NOT_IMPLEMENTED + + out = io.StringIO() + args = argparse.Namespace(output=out, check_all_reqs_met=False) + with ( + patch.object(Command, "_get_initial_source", return_value=MagicMock()), + patch("reqstool.command.StatusCommand") as mock_status, + ): + mock_status.return_value.result = ("STATUS-BODY", 2) + exit_code = Command().command_status(args) + assert exit_code == 0