From d8cc3876fa3191517d8998dc68fb5667fcba7f6a Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 27 Oct 2026 19:12:46 +0100 Subject: [PATCH 1/2] feat(api): bootstrap fail-closed cross-component contract verification --- .github/workflows/api-contracts.yml | 56 + .github/workflows/marreq-ci.yml | 3 + .github/workflows/release-artifacts.yml | 57 + .gitignore | 4 + Cargo.lock | 1 + docker/Dockerfile | 2 + docker/frontend/Dockerfile | 2 + docs/developer/api-contract-routes.json | 1958 +++++ .../developer/api-contract-source-review.json | 378 + docs/developer/http-api-contract.md | 177 + docs/developer/mcp-setup.md | 12 + docs/developer/openapi.yaml | 7079 ++++++++++++++++- docs/developer/use-cases-api.md | 11 + docs/releases.md | 128 + frontend/src/api/generated-contract.ts | 450 ++ frontend/src/api/types.ts | 120 +- .../src/hooks/__tests__/useBuildInfo.test.ts | 2 +- .../src/pages/__tests__/AccountPage.test.tsx | 1 + .../src/pages/__tests__/AdminPage.test.tsx | 2 + .../src/pages/__tests__/HelpPage.test.tsx | 2 +- .../src/pages/__tests__/LoginPage.test.tsx | 2 + marreq-cloud/Cargo.toml | 1 + marreq-cloud/tests/api_contract_inventory.rs | 56 + ...ed_oauth_mcp_tools_postgres_integration.rs | 63 +- mcp-server/Dockerfile | 2 + mcp-server/compatibility.json | 4 + mcp-server/src/client.ts | 27 +- mcp-server/src/generated-contract.ts | 450 ++ mcp-server/src/remote.ts | 9 +- scripts/check-api-compatibility.sh | 3 + scripts/contracts/bundle.py | 59 + scripts/contracts/check.py | 80 + scripts/contracts/check_baseline.py | 26 + scripts/contracts/compatibility.py | 294 + scripts/contracts/generate.py | 84 + scripts/contracts/inventory.py | 26 + scripts/contracts/requirements.txt | 3 + scripts/contracts/smoke-consumers.mjs | 49 + scripts/contracts/test-backend.sh | 66 + scripts/contracts/test_compatibility.py | 227 + scripts/contracts/test_contract_tooling.py | 70 + scripts/contracts/verify_transcript.py | 90 + 42 files changed, 11850 insertions(+), 286 deletions(-) create mode 100644 .github/workflows/api-contracts.yml create mode 100644 docs/developer/api-contract-routes.json create mode 100644 docs/developer/api-contract-source-review.json create mode 100644 frontend/src/api/generated-contract.ts create mode 100644 marreq-cloud/tests/api_contract_inventory.rs create mode 100644 mcp-server/compatibility.json create mode 100644 mcp-server/src/generated-contract.ts create mode 100755 scripts/check-api-compatibility.sh create mode 100644 scripts/contracts/bundle.py create mode 100644 scripts/contracts/check.py create mode 100644 scripts/contracts/check_baseline.py create mode 100644 scripts/contracts/compatibility.py create mode 100644 scripts/contracts/generate.py create mode 100644 scripts/contracts/inventory.py create mode 100644 scripts/contracts/requirements.txt create mode 100644 scripts/contracts/smoke-consumers.mjs create mode 100755 scripts/contracts/test-backend.sh create mode 100644 scripts/contracts/test_compatibility.py create mode 100644 scripts/contracts/test_contract_tooling.py create mode 100644 scripts/contracts/verify_transcript.py diff --git a/.github/workflows/api-contracts.yml b/.github/workflows/api-contracts.yml new file mode 100644 index 00000000..da7ff175 --- /dev/null +++ b/.github/workflows/api-contracts.yml @@ -0,0 +1,56 @@ +name: API contracts + +on: + workflow_call: + +permissions: + contents: read + +jobs: + contracts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: pip + cache-dependency-path: scripts/contracts/requirements.txt + - name: Install contract validation tooling + run: python -m pip install -r scripts/contracts/requirements.txt + - name: Validate OpenAPI, source review, metadata and generated consumers + run: python scripts/contracts/check.py + - name: Exercise compatible and breaking historical combinations + run: python -m unittest discover -s scripts/contracts -v + - name: Check audited contracts against PR base + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + git show "$BASE_SHA:docs/developer/openapi.yaml" > /tmp/base-openapi.yaml + python scripts/contracts/check_baseline.py /tmp/base-openapi.yaml + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 + with: + toolchain: stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: api-contracts + - name: Install PostgreSQL link dependency + run: sudo apt-get update && sudo apt-get install -y libpq-dev + - name: Verify actual compiled Server and Cloud route mounts + run: cargo test --locked -p marreq-cloud --test api_contract_inventory + - uses: actions/setup-node@v7 + with: + node-version: '22' + - name: Typecheck and test consumers of generated wire types + run: | + npm ci --prefix frontend + npm ci --prefix mcp-server + npm run build --prefix frontend + npm run build --prefix mcp-server + npm test --prefix frontend + npm test --prefix mcp-server + - name: Verify real Rocket and HTTP consumers with isolated migrated PostgreSQL + run: scripts/contracts/test-backend.sh diff --git a/.github/workflows/marreq-ci.yml b/.github/workflows/marreq-ci.yml index ffe282f4..b0b478f7 100644 --- a/.github/workflows/marreq-ci.yml +++ b/.github/workflows/marreq-ci.yml @@ -14,6 +14,9 @@ concurrency: cancel-in-progress: true jobs: + api-contracts: + uses: ./.github/workflows/api-contracts.yml + lint: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 8f9af2bc..cefd10bc 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -12,7 +12,62 @@ permissions: packages: write jobs: + contract-tests: + uses: ./.github/workflows/api-contracts.yml + + compatibility: + needs: contract-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - run: python -m pip install -r scripts/contracts/requirements.txt + - name: Require complete producer contracts before publication + run: python scripts/contracts/check.py --require-complete + - name: Require immutable deployment-plan revision + id: plan + env: + PLAN_SHA: ${{ vars.MARREQ_RELEASE_CONTRACTS_SHA }} + run: | + if [[ ! "$PLAN_SHA" =~ ^[a-f0-9]{40}$ ]]; then + echo "Set MARREQ_RELEASE_CONTRACTS_SHA to an immutable source revision containing deployment.json and exact retained contract bundles." >&2 + exit 1 + fi + echo "sha=$PLAN_SHA" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v7 + with: + ref: ${{ steps.plan.outputs.sha }} + path: .release-contracts + - name: Verify exact candidate and unchanged deployment artifacts + env: + CANDIDATE_TAG: ${{ github.ref_name }} + CANDIDATE_SHA: ${{ github.sha }} + run: | + case "$CANDIDATE_TAG" in + marreq-server-v*) component=backend; version=${CANDIDATE_TAG#marreq-server-v} ;; + marreq-frontend-v*) component=frontend; version=${CANDIDATE_TAG#marreq-frontend-v} ;; + marreq-mcp-v*) component=mcp; version=${CANDIDATE_TAG#marreq-mcp-v} ;; + *) echo "unsupported component tag" >&2; exit 1 ;; + esac + if [[ "$component" == backend ]]; then + python scripts/contracts/bundle.py --component backend --variant server --version "$version" --output /tmp/candidate-contract.json + else + python scripts/contracts/bundle.py --component "$component" --version "$version" --output /tmp/candidate-contract.json + fi + scripts/check-api-compatibility.sh \ + --manifest .release-contracts/deployment.json \ + --candidate-component "$component" --candidate-version "$version" \ + --candidate-source-sha "$CANDIDATE_SHA" \ + --candidate-bundle /tmp/candidate-contract.json + - uses: actions/upload-artifact@v7 + with: + name: candidate-api-contract + path: /tmp/candidate-contract.json + backend: + needs: compatibility if: startsWith(github.ref_name, 'marreq-server-v') runs-on: ubuntu-latest steps: @@ -51,6 +106,7 @@ jobs: MARREQ_GIT_SHA=${{ github.sha }} frontend: + needs: compatibility if: startsWith(github.ref_name, 'marreq-frontend-v') runs-on: ubuntu-latest steps: @@ -88,6 +144,7 @@ jobs: MARREQ_GIT_SHA=${{ github.sha }} mcp: + needs: compatibility if: startsWith(github.ref_name, 'marreq-mcp-v') runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 7c21b673..479399d5 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,7 @@ _bmad-output/ .cursorignore AGENTS.md + +# Python contract tooling +__pycache__/ +*.pyc diff --git a/Cargo.lock b/Cargo.lock index a07a0881..9130bfbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2994,6 +2994,7 @@ dependencies = [ "lettre", "marreq-core", "rand 0.10.3", + "regex", "rocket", "serde", "serde_json", diff --git a/docker/Dockerfile b/docker/Dockerfile index 0de03a8e..303f7ff3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -85,6 +85,8 @@ COPY ${MARREQ_BIN}/Rocket.toml ./Rocket.toml # diesel CLI uses /app/diesel.toml + /app/migrations (see docker-entrypoint.sh) COPY marreq-core/diesel.toml ./diesel.toml +COPY docs/developer/openapi.yaml /app/contracts/openapi.yaml +COPY marreq-core/frontend_compatibility.json /app/contracts/frontend_compatibility.json # Attachment storage (MARREQ_ATTACHMENTS_DIR). Created here so a new named # volume mounted on it inherits the marreq owner. diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index 868d4453..a644685a 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -28,6 +28,8 @@ ENV MARREQ_UMAMI_WEBSITE_ID= ENV MARREQ_UMAMI_HOST=umami ENV MARREQ_UMAMI_PORT=3000 COPY --from=build /build/frontend/dist /usr/share/nginx/html +COPY frontend/compatibility.json /usr/share/nginx/html/contracts/compatibility.json +COPY docs/developer/openapi.yaml /usr/share/nginx/html/contracts/openapi.yaml COPY docker/frontend/nginx.conf.template /etc/nginx/templates/default.conf.template COPY --chmod=755 docker/frontend/40-marreq-umami.sh /docker-entrypoint.d/40-marreq-umami.sh EXPOSE 80 diff --git a/docs/developer/api-contract-routes.json b/docs/developer/api-contract-routes.json new file mode 100644 index 00000000..fdfce3d3 --- /dev/null +++ b/docs/developer/api-contract-routes.json @@ -0,0 +1,1958 @@ +[ + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{id}/activity", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications/{id}/activity", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/applicability", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/applicability", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/applicability/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/applicability/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/applicability/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/attachments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/attachments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/attachments/{attachment_id}/download", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/attachments/{attachment_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/storage", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/storage/quota", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/attachments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/attachments/{attachment_id}/download", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/auth/providers", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/auth/external/{provider}/start", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/auth/external/{provider}/link", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/auth/external/{provider}/callback", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/auth/csrf", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/auth/login", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/auth/identities", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/auth/identities/{identity_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/auth/logout", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/auth/change-password", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/auth/me", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/auth/me", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/admin/backup/info", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/admin/backup", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/baselines", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/requirements", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/traceability", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/verifications", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/diff", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/cache/stats", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/cache/clear", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/cache/cleanup", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/cache/performance", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/cache/recommendations", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/cache/reset-counters", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/cache/health", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/catalog", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/categories", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/categories", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/categories/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/categories/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/categories/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{requirement_id}/comments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/requirements/{requirement_id}/comments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/requirements/{requirement_id}/comments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/requirements/{requirement_id}/comments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/custom_fields", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/custom_fields", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/custom_fields/{field_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/custom_fields/{field_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/custom_fields/{field_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/dashboard", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/dsm", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/requirements.xlsx", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/verifications.xlsx", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/matrix.xlsx", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/dsm.xlsx", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/matrix-links.xlsx", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/requirements.pdf", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/report.pdf", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/requirements.reqif", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/requirements.reqifz", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/baselines/{filename}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/bundle.json", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/exports/bundle.zip", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/groups", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/groups", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/groups/creatable", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/groups/{group_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PATCH", + "path": "/api/groups/{group_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/groups/{group_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/groups/{group_id}/projects", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/groups/{group_id}/members", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/groups/{group_id}/members/{user_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/groups/{group_id}/members/{user_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/home/attention", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/search", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/imports/excel/preview", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/imports/excel", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/imports/reqif", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/imports/bundle", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/admin/logs", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/admin/logs/stats", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/admin/logs/export.json", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/admin/logs/cleanup", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/matrix", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/matrix", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications/{verification_id}/matrix", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/verifications/{verification_id}/matrix", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/mcp/principal", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/mcp/audit", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/mcp/internal/audit", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/me/permissions", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/members", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/members/{user_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/members/{user_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/reviewers", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/reviewers", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/meta/health", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/meta/deployment", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/meta/build", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/notifications", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/notifications/unread-count", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PATCH", + "path": "/api/notifications/{id}/read", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/notifications/read-all", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/notifications/preferences", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/notifications/preferences/{project_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/notifications/preferences/{project_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/.well-known/oauth-authorization-server", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/.well-known/oauth-protected-resource", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/.well-known/oauth-protected-resource/mcp", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/oauth/register", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/oauth/authorize", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/oauth/authorize", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/oauth/token", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/oauth/grants", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/oauth/grants/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PATCH", + "path": "/api/projects/{project_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/archive", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/unarchive", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/project-from-path/{slug}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/reports/types", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/report_templates", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/report_templates", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/report_templates/{template_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PATCH", + "path": "/api/projects/{project_id}/report_templates/{template_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/report_templates/{template_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/reports/{file}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/requirements/{req_id}/versions/{v1}/diff/{v2}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{req_id}/versions/{v1}/diff/{v2}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/requirements/{req_id}/diff/current", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/requirement-version-links", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirement-version-links", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/requirement-version-links/{link_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirement-version-links/link-types", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/requirements", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/requirements", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/requirements", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/requirements/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/requirements/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PATCH", + "path": "/api/requirements/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PATCH", + "path": "/api/projects/{project_id}/requirements/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/requirements/{id}/versions", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{id}/versions", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/requirements/{req_id}/versions/{version_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{req_id}/versions/{version_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/requirements/{id}/impacted_tests", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{id}/impacted_tests", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/requirements/{req_id}/versions/{version_id}/approval", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/requirements/{req_id}/versions/{version_id}/approval", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/saved_views", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/saved_views", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/saved_views/{view_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PATCH", + "path": "/api/projects/{project_id}/saved_views/{view_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/saved_views/{view_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/semantic_search", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/requirements/ask", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/requirements/reindex", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/index_status", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/semantic_search/status", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/status", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/status", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/status/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/status/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/status/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{id}/trace_up", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{id}/trace_down", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/coverage_report", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/traceability/clear_suspect", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/traceability/clear_suspect", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/use-cases", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/next-code", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/use-cases/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}/draft", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/use-cases/{id}/draft", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/use-cases/{id}/draft", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/use-cases/{id}/draft/operations", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/use-cases/{id}/draft/publish", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}/versions", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}/versions/{version_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/use-cases/{id}/versions/{version_id}/approval", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}/links", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/use-cases/{id}/links", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/use-cases/{id}/links/{link_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/use-cases/{id}/links/{link_id}/clear_suspect", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{requirement_id}/use-cases", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications/{verification_id}/use-cases", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}/draft/diff", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}/versions/{a}/diff/{b}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/use-cases/{id}/comments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/use-cases/{id}/comments", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/use-cases", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/use-cases/{id}/diff/current", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/users", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/users", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/users/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/users/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/users/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/users/{id}/password", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications/{verification_id}/control", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/verifications/{verification_id}/control", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/requirements/{requirement_id}/close_out", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/close_out", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/requirements/{requirement_id}/compliance", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications/{id}/snapshots", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications/{id}/snapshots/{v1}/diff/{v2}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/baselines/{baseline_id}/verifications/{id}/diff/current", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verification-methods", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/verification-methods", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/verification-methods/{method_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/projects/{project_id}/verification-methods/{method_id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/verification-status", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/verification-status", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/verification-status/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/verification-status/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/verification-status/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/verifications", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/verifications", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/verifications", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/projects/{project_id}/verifications/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "PUT", + "path": "/api/projects/{project_id}/verifications/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/verifications/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "DELETE", + "path": "/api/verifications/{id}", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/verifications/{id}/field", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/projects/{project_id}/verifications/{id}/field", + "variants": [ + "server", + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/auth/register", + "variants": [ + "cloud" + ] + }, + { + "method": "GET", + "path": "/api/auth/verify-email", + "variants": [ + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/auth/forgot-password", + "variants": [ + "cloud" + ] + }, + { + "method": "POST", + "path": "/api/auth/reset-password", + "variants": [ + "cloud" + ] + } +] diff --git a/docs/developer/api-contract-source-review.json b/docs/developer/api-contract-source-review.json new file mode 100644 index 00000000..7f587699 --- /dev/null +++ b/docs/developer/api-contract-source-review.json @@ -0,0 +1,378 @@ +{ + "frontend/compatibility.json": "0d9bcff76abe3a44b59c5bbcd3b73acd29266394e25c16b7c4b069bec28b9472", + "frontend/src/api/attachments.ts": "3ab5a6039eea83e9a84c1198e686802edbac7812821eadad84050da3a0b40089", + "frontend/src/api/auth.ts": "8b43e8187605037285550f751588ab4ed3b36b7813babd0e7ea397e9cb893f0c", + "frontend/src/api/baselines.ts": "2048ad67ce7d5bb56d6762e13344423e599b7dc71b287b26ae9dd0c07c2be022", + "frontend/src/api/catalog.ts": "9b80cbd031a50fedc09abc09f9784506582d68715ee92b56b4066faa1ab9abcf", + "frontend/src/api/client.ts": "ab4787005a7a4139996d857c4bf06414d8884d4462e3702452f9c1cdcf24df66", + "frontend/src/api/dashboard.ts": "2de1b33077d0de43ff695c222b90870bbda4916595332f80249796aac2f93c1c", + "frontend/src/api/exports.ts": "8dc5817e40727ced7ffc9b9f485b92bbf716764f22c3a48d0925a07a5cc48a19", + "frontend/src/api/groups.ts": "3e58295865acecb4f0fa18be7a5c490d7a011a4eab1bd771f2e6b7cfaa07fb2f", + "frontend/src/api/home.ts": "2454252b15757c234a781b6d14559d10f73ec0a468defc77c0407fa8877137ff", + "frontend/src/api/imports.ts": "72a998ce604f6b23158786b5992cf7add1ab2be532100be4b22ed9e77e6cd4d3", + "frontend/src/api/logs.ts": "9822d3be5e25aeb397fc6993740b9eeeaebca68262619e13e35f7c5d9ec95b44", + "frontend/src/api/meta.ts": "e4441b4ea3a89e65589e2d837b1f4a4e404061d3c8182cfc6b7839fe40c46e9d", + "frontend/src/api/notifications.ts": "35f6cdb54f1f1ebb229368d1a667ef1a3038600cf5f11b0614b5cc83a8e1ce89", + "frontend/src/api/projects.ts": "6bdf2aab505c5c422450fb6d375307d3fc6e01de4a5430f898ec84253a3fff40", + "frontend/src/api/publicAuth.ts": "207d53b750d9065911365f2a96c4d3a4a110983123b264e19b96da3ea0fd74ed", + "frontend/src/api/reports.ts": "1fc085f17031c28a011288dc6d85c5a87bc723b5007132f1769b886f41dbe560", + "frontend/src/api/requirements.ts": "9935a13bcc618a5b95be7d4a0783456d9f5fd9d32c3391964cbb2d3731471d78", + "frontend/src/api/savedViews.ts": "95a7dc49f2b7b217fc63d1032afc064b9a6aa852e513bf468a0f983073197760", + "frontend/src/api/traceability.ts": "7dcdab37cd865af634081bc8df0b05dab225e4df124f59fa8c9b526f2f99b22b", + "frontend/src/api/transport.ts": "25db6fdcde2a1c5c65e08dfce1a92f21e04b7b4b9a8ec2f613bbc0d2b63bf59a", + "frontend/src/api/types.ts": "9b4b6b7b71730b6ea0d214dbe087c264b864df91930f78ba88a7ed77be361489", + "frontend/src/api/useCases.ts": "1867c3aba24d4883fa243299067fff54abfcdb77195f771d4cd6bfef24c7832f", + "frontend/src/api/users.ts": "7155f0bc8f6768753d6a766ca25145259e0fd59c1321870f2967821e4c6151c6", + "frontend/src/api/verificationControl.ts": "f372bfaad0f15c7b112798ba54298776cb8993a9bfb41abde4dd3490974e5ad2", + "frontend/src/api/verifications.ts": "09315bcd631942a15aa01cf6d35e951bdf09c38f045572a50d86fade5c6b4042", + "frontend/src/components/AttachmentsPanel.tsx": "864c91baffb45c90703a7d6f5f4e7dd7245887c684e24b4931470dba39e42195", + "frontend/src/components/CommentBody.tsx": "ca3be1bd63cdac8761218a24e35445d33877e8d201ba56a4c99a7e82a11da5fe", + "frontend/src/components/CompatibilityBanner.tsx": "802755e5d6078469f55bc2fae084046d262709f1895e771e5f705a1efa766dac", + "frontend/src/components/DeleteProjectSection.tsx": "bed2e4b648e36d1a5fbf8a109a14279b87bb26d6ffeef445a971558c8d04734d", + "frontend/src/components/HierarchyGraph.tsx": "ea11fbe1e1eb949f23bc5913b9b182dabe796d9e0d8a79adb1a02d7474e7c7e1", + "frontend/src/components/MentionTextarea.tsx": "228f4f7b836b129a5a9d1e223b6c90f1d015fafee9c34d9626b61d569acc8446", + "frontend/src/components/NotificationPanel.tsx": "d787fad22b8bce38598991541da458f8b4c263131e07227e7d51df412f2b5c9b", + "frontend/src/components/ProjectCreateForm.tsx": "4432de404b310f026d9ad59eeca998b9b5502fccdf985f23a7afee95b560a0d3", + "frontend/src/components/ProjectGeneralSettings.tsx": "151ca068c4911be56f42ad14fb12530b95ac5c353939e432f2f5c18e54750358", + "frontend/src/components/ProjectStorageSettings.tsx": "0b77e7fbeb87ca2fdf29c4b316d87499c0505b38af6f7ab743f57ee1d2d7b60e", + "frontend/src/components/RequirementCloseOutPanel.tsx": "c9dab4f0c3ed9a4477414faaf869adecbc934ed39c216542c2c8a75ad90e45a6", + "frontend/src/components/RequirementCommentComposer.tsx": "ebe202796ba001fee0d63b492236a358ec8ef7d80fea330f5c2f342a2742b531", + "frontend/src/components/RequirementMatrixPicker.tsx": "814990316c34b18594959134663b4b1ffc27f0f3b12acf1c4d7a39db746d6400", + "frontend/src/components/RequirementReviewCard.tsx": "2cb83ac3cd3cc78a407c40dbcd56a127aa4426d6eaecb70a02a336331c2a7118", + "frontend/src/components/RequirementVersionDiffDialog.tsx": "80a6c1aa4b30da3693ccfb9614b05fad9a57fab40cbb9b652308b3235345bbb7", + "frontend/src/components/RequirementsTable.tsx": "c88067b10d2debecb6a1e887d1c5eff8be40a08f70191d69d1bcfe8590a377a0", + "frontend/src/components/SavedViewsToolbar.tsx": "b0dec9ba27a8055cb07bfd79d1c54f512432d1de67e314312125e8292425cfad", + "frontend/src/components/TraceabilityGraph.tsx": "6c74f0abc03db92a55c8b1d670185ae69fbbc3f5999d9cee409ac6c5c044de23", + "frontend/src/components/VerificationControlPanel.tsx": "994b105d7a29e3d8fb9bbe9d0dd78f97377f416415e00ba12a90a64027e94550", + "frontend/src/components/VerificationStatusMetrics.tsx": "0cf513fadec90402790797faf3a158b7c03c696c621fe0c11a3dac9c1fb21428", + "frontend/src/components/VerificationVersionDiffDialog.tsx": "532ff63107b952943f516cccd08e4bb2547b101db8b61c6cfb8814a7dcaa074f", + "frontend/src/components/VerificationsTable.tsx": "7712820673fea66961898d70ab98f60b46e823fec0370db209bce54ca7119398", + "frontend/src/components/dsm/DsmGrid.tsx": "417d102c6f81f220f026dfac83181262688d45cefab60a24ead6c4fd6104a4c4", + "frontend/src/components/dsm/DsmSidePanel.tsx": "795b7d6b208b8ac7bc65c473deaf58f2e19ff40a2d5496b35e82da481c00c186", + "frontend/src/components/dsm/DsmView.tsx": "f478d1cebd6d1ff7cad418eb9953c6dd7619b412ca33219176a1fb69eb02ab1d", + "frontend/src/components/matrix/MatrixGrid.tsx": "2fff2d35fb634a7f5e1e2f08a8e63e75b12ae91e0fdddc6e8abba263d40cd759", + "frontend/src/components/matrix/MatrixSidePanel.tsx": "e12b080999f75d1e5b93f4f0ad77c22582807a06e41807ed86cd78c7540327ee", + "frontend/src/components/matrix/MatrixView.tsx": "8b173ad6c53f9082e8a4c28db5d6d65cdb26faf947d07426b28eaebfe3dcaf0d", + "frontend/src/components/reports/DocumentSettingsForm.tsx": "5e5bda5e256db2cb0df6f6745a0f56059cfdb9df8cda31d668103cc3a6919d1e", + "frontend/src/components/reports/ReportDocumentsCard.tsx": "ea4599e550fc798fd6c55b62788f01f503c5b0482834e7e2d4128bfc0caec4b3", + "frontend/src/components/reports/SectionList.tsx": "7adb1c33b62f3ef05e0db15af981219799f1e993ebf4da5c73d3aa1f81907bc4", + "frontend/src/components/start/StartSearch.tsx": "0cba50200262e52bda2fbf22e7c57803e1d2db0c2faa60eb5587d8af4af2a167", + "frontend/src/components/useCases/BaselineUseCasesSection.tsx": "b4d20fb9a89f5c730379aab6c3e4417133b38f700f39e79a4e64c7cad202a822", + "frontend/src/components/useCases/BranchCard.tsx": "2c16c584c566340617833503e180d2be175e7415761d8455b9457061834fd4f2", + "frontend/src/components/useCases/LinkChip.tsx": "0ca3ea3f0c024c85eec9d5d6dd2ca4525f4d06b5b74d021b2a72222295590ca6", + "frontend/src/components/useCases/LinkDialog.tsx": "0db09520c092df0410e9e65231e94e4005daa64c41fd56b7ba180c758e3fa782", + "frontend/src/components/useCases/RelatedArtifactsPanel.tsx": "03fbafa70a47258bbe715c9d2005bf26a6bf677b6d3926bb98f6c9582661cfdb", + "frontend/src/components/useCases/RequirementPeek.tsx": "09d3b1de5afa65ab1ba3e3add96fe3cd9ca12aef944c5ac29adffa6a91a351d6", + "frontend/src/components/useCases/UseCaseDiscussion.tsx": "73f54e3c66cc786226fce62764c7221d34895dcb46fd004d60dd0901a7954544", + "frontend/src/components/useCases/UseCaseDocumentView.tsx": "46957ce3466f1a0d4ac45112c1229a2c68fd428fbe515d1681faad3fced4b405", + "frontend/src/components/useCases/UseCaseFlowView.tsx": "ffb2b6acfdebc2e4fdd5df4b6f1ba974d07aa45556b57e4124d505257dedfe22", + "frontend/src/components/useCases/UseCaseReviewView.tsx": "f642c462715da9f95cbe0f0791ebdbe77bab97eabd791ba41abce87048dbf583", + "frontend/src/components/useCases/UsedInUseCasesCard.tsx": "cd65c36495771faac75eb6104cc5cc35c50c22c42247d3c8db7d6208bdd2a427", + "frontend/src/components/versionDiffUi.tsx": "288799d997ee305283411d9637910a4a9eafa6d57d6416f3ad7b14b1f7915a72", + "frontend/src/context/DashboardContext.tsx": "fc5b37e22e5c758062ade4f23474186d06fbfab39830ddd6caf4b56340762a81", + "frontend/src/hooks/useBuildInfo.ts": "444733da6fffd5734a5511370d1ed3253ea6d05b2425d91bbba0aae4fed12b77", + "frontend/src/hooks/useNotifications.ts": "ff1cc4fbabddfe4f91c1dd98231dd082c5eb812249020d577ac8962abb383124", + "frontend/src/pages/AccountPage.tsx": "2c6b72c059ed8585e92a7bfb3cbd6ec01066801d27a00fe1ced770671cfaebec", + "frontend/src/pages/AdminPage.tsx": "7cac5d4dc76bafc498831509c34c8870f774bb75cc9a816d63e48f3fab2d1a98", + "frontend/src/pages/BackupPage.tsx": "aa5912d7f6e4b76f7759b790951dfd4fb5b0748261ca928488cb590465abe8cf", + "frontend/src/pages/BaselineDetailPage.tsx": "590af60312aa864a7bda51be8f85e04c2af90edcd3a1e1bead7e74c497ba53a2", + "frontend/src/pages/BaselinesPage.tsx": "efec445a6c5f8b8062b0817cd49673519a6da4e401d353d81b802dcd95b80fd9", + "frontend/src/pages/ChangePasswordPage.tsx": "0ff12abd9c144cfc2fa1528cdc3b4076b00b686bba48c7ddf567ee6a0b4846ad", + "frontend/src/pages/CreateRequirementPage.tsx": "b7b87777c9438ed186c9bdca1f7533e4b8d68520efa3560c6edf13f64cb75cef", + "frontend/src/pages/CreateVerificationPage.tsx": "c638e2d1e8be63854f0acd4b167aa575fa85b18119ef10b2684d0dbd3f2c204a", + "frontend/src/pages/DashboardPage.tsx": "5ec394dbe016542d4b85a031aa589aa360e9ab2e336e6eab6389fb763c65e806", + "frontend/src/pages/EditRequirementPage.tsx": "acd9281739516a859e32111db4bb5a224e529ad6b3f77e77d6a5790b036278b4", + "frontend/src/pages/EditVerificationPage.tsx": "f9f7a455dd564269e3d02be7e7f156aaa27a37c9b41e0520c42f389891d145d4", + "frontend/src/pages/ForgotPasswordPage.tsx": "c8beb4f6ab9869c01f6fc3f08f2b624c6f2828e41c9433d285e0b2827dacde45", + "frontend/src/pages/ImportPage.tsx": "3d64cecd82bb0ac326202940401155aeaee3e24da2b6fa1f2f9f6d557db8cb07", + "frontend/src/pages/LogAnalyticsPage.tsx": "b5e389677fe998fdd60e0daca5290de48edc17904cd07876c51ca317d4ae3e28", + "frontend/src/pages/LoginPage.tsx": "574ca76f3b7d20021aa66ca786fd7a46b088e340710dc50f658b0ff0c14a99be", + "frontend/src/pages/ProjectBundleImportPage.tsx": "4162d83cad0c9633ba392bf065edbfd93acb9d99ae64d9ddb4506828f2a2d525", + "frontend/src/pages/ProjectCreatePage.tsx": "17e145d53aebdbde58753a27b093c5b7a70cb109ca507d6a4f2c504fa0743317", + "frontend/src/pages/ProjectLayout.tsx": "3bfc38ff8fe368dc8ff25ab05f5f6acb5ec37a33aabfb4e10d5ba8e849a9322f", + "frontend/src/pages/RegisterPage.tsx": "1e9e057dde4e9faf2c39e2b5a1f8aa4ea8f6c01ee802006464c78bc2f5e40f88", + "frontend/src/pages/ReportBuilderPage.tsx": "8f5d61e7801a1a2deb1b5e51108fec0380bd4d83dc6cc839820d818696d50f3d", + "frontend/src/pages/ReportsPage.tsx": "1b37d6a6b5ec8b8a5fe116f7f8fc070a95524f36b1b5373a9aa76e7fc1abaf94", + "frontend/src/pages/ResetPasswordPage.tsx": "bf60116c75c4178d98cb52a25e4c74fce8cfe7b01f3868357a055f95262ddf36", + "frontend/src/pages/StartPage.tsx": "92e1ab484cf0c151118aef27ab0278f9f9a57a141e7230f2572a493830591205", + "frontend/src/pages/SystemLogsPage.tsx": "cd505938abd22298c9eff02316c2e1d12fc58aeaecd0ba07c2d2fb8640a4f537", + "frontend/src/pages/UseCasesPage.tsx": "8b7bd84008cdd1005f7177912e23306951fb58f04ca070f217a230fcb2e6c80e", + "frontend/src/pages/VerifyEmailPage.tsx": "d12d0483bef2b7f1242b70cd49de89671ec39e04e557e5183788cf3b0e85a0b6", + "frontend/src/pages/ViewRequirementPage.tsx": "32cd5e22c24aeae559c87f06a7f5daf7bced700fb1cbbbfd489c98e84739b2aa", + "frontend/src/pages/ViewUseCasePage.tsx": "1cf82735d28145d518dc3d7b9f4970c6a9afe735707000c511a2fee85e3c65af", + "frontend/src/pages/ViewVerificationPage.tsx": "333753fc8f69e2c69579097191e1118fdcc2b51309c0c915e394775aa7560a2c", + "frontend/src/pages/account/ProfileSection.tsx": "4853064f8b07d51cf97c2437b021fec43c5e7bc21ae54f1979242021cfda9d0c", + "frontend/src/pages/admin/AdminLayout.tsx": "fcd781952701cf7b14a34077669d15e0daad07aae8f7318c9f5d61969c617849", + "frontend/src/pages/admin/SetPasswordDialog.tsx": "66dec75ff74ae92c46ff6539786caccdd7075b5182b85ff022772dc2a5493729", + "frontend/src/pages/admin/UserFormDialog.tsx": "7b90758cf2349f92173da92a31a250aa0cb7146e4888dadf862cff611b5b9571", + "frontend/src/pages/catalog/CatalogApplicabilityPage.tsx": "3e0322240e4da2a13854fa061207987dd97f65d6efb016102a6335a6977ff804", + "frontend/src/pages/catalog/CatalogCategoriesPage.tsx": "c4819f09441e4d435f6b2970ccfde47bba93aa9a8bc27c2f6741962d8e8e2176", + "frontend/src/pages/catalog/CatalogCustomFieldsPage.tsx": "48009a19ac670cc45c79894fa425eb9c6b1e9e7162dec18fb7a9ac1f59c3e390", + "frontend/src/pages/catalog/CatalogRequirementStatusesPage.tsx": "2e49517a487c93a32bb493ef5455073da81000b8378cb2a7a80638f95185d4c3", + "frontend/src/pages/catalog/CatalogVerificationMethodsPage.tsx": "5034a088d804caf187109a9709cea94e1773166b6e55b48ae9a2c1c83c2f3fe4", + "frontend/src/pages/catalog/CatalogVerificationStatusesPage.tsx": "8a24da86a3a6d98d0dafa4f1ddd201dda014880ef420502ae61f2c02796cc231", + "frontend/src/pages/groups/GroupCreatePage.tsx": "ef599da27c8b439dd77899b5b6c7e9378be83af68114da5cf321c3d5f7f6edfa", + "frontend/src/pages/groups/GroupEditPage.tsx": "16c192ed8e7c060f5e32a701c2e93ca4cd03e0431e381c13c999c127996ea995", + "frontend/src/pages/groups/GroupMembersPage.tsx": "29a3f2c44b8c374b8569e49dd341e8e403cfb0125e40124409f469ffe9ef4aca", + "frontend/src/pages/groups/GroupViewPage.tsx": "ab42dc3302004af815427e247da6c66ed9485d3190b56690eb8687a84d72884c", + "frontend/src/pages/groups/GroupsListPage.tsx": "8ade08bf0e4815ee866e4bbbe1de1a52592de1cba57659e52c69a9b44f6d4530", + "frontend/src/pages/settings/MembersSettingsPage.tsx": "8a9089ac90817ad47a4306b12081e7b262142082c9a545f864f45700629e44b2", + "frontend/src/pages/settings/NotificationSettingsPage.tsx": "45a06bcbc7c68580e11a1dfc10d92a71fa07d63e8636c2f70db0d2e9fa37db4f", + "frontend/src/pages/settings/ProjectSettingsLayout.tsx": "59eaadac194a13684ffb2285cf2f6d9bcef3d781e9433eb3a6ac439c5b188383", + "frontend/src/pages/settings/settingsContext.ts": "43047b5fda62fa4131d7928842ddaf6e7600c90b684abdc38b8c71ee918765f3", + "frontend/src/statusAuthorDefaults.ts": "fbcbdbfa7e1402ce194a0796a665bb8fc7b22da209cdda630b6e19798c67306b", + "frontend/src/test/reportFixtures.ts": "126979fb70781d2c29175510b5162c37eb62442ea66a2f60243821aca68fce60", + "frontend/src/utils/dsm.ts": "f9248410826e11d2eba0acbadac7877675e91acf7d9e17607409c8db4e561c52", + "frontend/src/utils/duplicateRequirement.ts": "dd9ae9e091d773f2dbe4edd75afbc548e8d90f71c85dfda9be974073703ee4bb", + "frontend/src/utils/mentions.ts": "ac1f4b8a4ffc2086f945315e40b7001d03c48ffa42a93875d3f09ea06cb110d2", + "frontend/src/utils/parseUser.ts": "528441744ab75019bb683f3cd2e5d142abe425e63d4407592c1cbcdd37957a7d", + "frontend/src/utils/projectUrl.ts": "146123b4a0a460339350dc7ab96dde0649623c7a67cff68997fe005a6e4cbf2f", + "frontend/src/utils/reportDefinition.ts": "f39b5528889f7bd0fbd5668e5cc8eabc296db77cae433526dccb036df913167b", + "frontend/src/utils/savedViewDefinition.ts": "a1d1251f7d60ea03c4b3a8a55f75722e936bdf30dd6f370b836fa42fba3a0f7c", + "frontend/src/utils/useCaseDocument.ts": "a51556ac25f9e5bd3c4c92328a86738bacf2c45a0a42608dd858821f3c492de4", + "marreq-cloud/src/api/auth_public.rs": "b080c11b42d95b02a21b6f49226bd1c1c9f72a6eec4f2056f6a7f29ef4def378", + "marreq-cloud/src/api/mod.rs": "0a570340b80f76ced32d7f30c057c16d95dad39ef433180911f7a8225da18919", + "marreq-cloud/src/deployment.rs": "6833286dc0ceb2933e95a1bb884a5174548bd801b645b97ee05d0d7ca042bd66", + "marreq-cloud/src/fairings/cloud_admin_bootstrap.rs": "d9cad7b7e1c71c071696fee5bbabd387ca438e741a821e577b26fa0a8c65ea01", + "marreq-cloud/src/fairings/mod.rs": "6c14ec5bd25e8835c4b19cbdb1674a3540ad7cf106dcd43c136e9ddc263db272", + "marreq-cloud/src/lib.rs": "8f873d14051b1b1c177bb9b66eb81b9430f970073219d8cff8deb7834c2cf4fc", + "marreq-cloud/src/main.rs": "45f07e434e587a3c8ec95fec94b127d43fcbb395ae9da6351ea4115ba07dcdcf", + "marreq-cloud/src/routes.rs": "36ba1db8aa241bbd96757d341bf2058e2bf650019242ee2a09ee90d03b833905", + "marreq-cloud/src/services/mod.rs": "4607f9a7c5c8651472b8016b6ac8f68ca3431939ee4c3f9d0e944422f7348a25", + "marreq-cloud/src/services/registration_service.rs": "433de11216e66c659fca1572d2c9326c7f27b58076a6c65bbc92d4bb876eef41", + "marreq-core/frontend_compatibility.json": "3e569883e23b5411ca13570743096c893cf876362060007d0a17a8ee5623d2a7", + "marreq-core/src/api/activity.rs": "94a3fd9c6aa73ac989935925f108d35db55b148cebbc6dfdcde62b8872d1809e", + "marreq-core/src/api/applicability.rs": "aa88a53ccdeda77df4d83f7bffde00668bf6152ef1e7b598ca9179ecb472b081", + "marreq-core/src/api/attachments.rs": "a7f600d5e6293038ee668ca673ed093be06714e1191a06b640bb99cd11f56124", + "marreq-core/src/api/auth.rs": "42c42fe249ee62a7e5bbceb05030bb63f50806ba4c8b9ca36ceaa49b5e372ba1", + "marreq-core/src/api/backup.rs": "a364be4c85072f311d0a8e0930ec42dae25182ac035cbe4ee9576e2d1d74cbe6", + "marreq-core/src/api/baselines.rs": "53bee136e032cb6f167d639c06d9feb7f230378a88a5fee6de5819611b3c239c", + "marreq-core/src/api/cache.rs": "63410e53b7e2c01758bf565af66981dbd5bd046fd222d5ea9c6b5e4a6f571c75", + "marreq-core/src/api/catalog.rs": "8a7017fff0a5faefe8d124aacc0ed3f1a68c5289398969391bec3d40c9cb53ae", + "marreq-core/src/api/categories.rs": "af9ebb58df5c50e7d1a590006400ce2f5113d846b481cc5e32b6a1a0d0f070b5", + "marreq-core/src/api/comments.rs": "4d5429530e90a4f9fb4cf5b7c224c7744a5c89b4dd14d6c5aa11c4b4c09e161c", + "marreq-core/src/api/custom_fields.rs": "7138b0fee7f5c2a938c277413f7bd8289c4ffaf9ecf70635f9eecdd4d3c5b48b", + "marreq-core/src/api/dashboard.rs": "d262e1de00a6b180951ac80891c6ed2574a5b96f939b2cee4bf21c4a80952665", + "marreq-core/src/api/dsm.rs": "609ccf610c64d69438c28fdce7623cf81ff68a1f91f0f7f9755ad0985d81c474", + "marreq-core/src/api/error.rs": "f8703993e7a5c266b2cb7b946246a5cf285a46ddda9d4f976e8a1d0e4f6c3d0e", + "marreq-core/src/api/exports.rs": "ed842ab4cff67fd556e779bf65744e4bf1d42e8bd3513a10be4cd3a1731ae40b", + "marreq-core/src/api/groups.rs": "3d8f31614abdc70bb2f9dddb08d1b8464556d5a04fccffd16c337692d91a60ad", + "marreq-core/src/api/guards.rs": "ba11edb5858a725a36e1988a7ea76d1ac0642945bb6c3f8406eee31d00e7deda", + "marreq-core/src/api/home.rs": "3b8f325d7f1096734378702bf42c72e8c7adb0793b7c77ce51478b8f6a18dfbd", + "marreq-core/src/api/idempotency.rs": "7586819b8488daed5c7fb51495c70c9369a8cb069d1f9aed76cdb0ebc12e6f8a", + "marreq-core/src/api/imports.rs": "d76d3923cd34f58f02b55a600c56e2491b4b14c8269bad23c9ddd137343e518b", + "marreq-core/src/api/logs.rs": "126db136b1bee0f78227cf7be98fd330309700ecf6981acd4fd1dfeac74bfc2a", + "marreq-core/src/api/matrix.rs": "951a83ff3feaef5b94b902b8101779873d5075eda609946376a7668338d02f22", + "marreq-core/src/api/mcp.rs": "01451ffb92308c53634718d763433e60b1714c42f2f02a33d3e1a7c46338a0cd", + "marreq-core/src/api/members.rs": "398c1bd74615b611c6fd71cb3d54b2c26c077b90d0db5b52ea8197fe1fb99a83", + "marreq-core/src/api/meta.rs": "13ff923c9635214132f8e5ac6be236fa9e0dc0eddca7ea826993655c4ce0fb2f", + "marreq-core/src/api/mod.rs": "a8b106368e1fed4766fc75c8135f5ec63e867452e18afa096afabd66012d1a2c", + "marreq-core/src/api/notifications.rs": "ca1f959e06e5f8e8d8e55cb4d83342006378be4c778ac92cf67e1c885d9b0a74", + "marreq-core/src/api/oauth.rs": "ce1a150b3c3b3ef2118de60767f95ac502000c29ae94520b5bbf1d14202eb801", + "marreq-core/src/api/prelude.rs": "3aca18c037023d5076101771d0c32045fb8cd2dda3d8410bdda9f2b6241ef9f2", + "marreq-core/src/api/projects.rs": "e607a66955222dad5ad6fb29daf8c881fc730d12fbc4c391bdebf7664d97ca2d", + "marreq-core/src/api/projects_session.rs": "06a1689822c8956ff4570931cdc056a198e0bfeae85b6e171ea11546d8c3c840", + "marreq-core/src/api/reports.rs": "09c31e4c57508f6aec4b042d86e87765690f82ef64a5c6b0fde233fc2a190e10", + "marreq-core/src/api/requirement_diff.rs": "39a662d096001564033e2069c67265b797d5b7c5eee45bfe26cdeee1c04cf637", + "marreq-core/src/api/requirement_version_links.rs": "a2790428840fb0c99a874f26ea524b6649ab3760b5218b8bf6ea20c0d6510848", + "marreq-core/src/api/requirements.rs": "bf2317f5637bedd763f6a37a9b015fce0e064c58315530a0f6430724606df93a", + "marreq-core/src/api/saved_views.rs": "ed3a13e3d95d56b1a6a2f41e123a7dc6944ab1b534845c3c8309de6c9f111d6f", + "marreq-core/src/api/semantic_search.rs": "6e4da1230a89137f3339fab09750ed9b8b773a3eb2722680909b32fd69a92ccf", + "marreq-core/src/api/status.rs": "4c1ed40c20c74cf541677f8573b60af5afd4eb32decda9a4d9d10b69082c6299", + "marreq-core/src/api/traceability.rs": "b45e4e0ab57e9fb8da0d4bb58e32945b4cb8cf9d2f175d548502360f411807e4", + "marreq-core/src/api/use_cases.rs": "3355a964773df4500bb173bac7b1d78ce9e2110e48c6e628bfa3f6f249b1e766", + "marreq-core/src/api/users.rs": "98bc2ed2ce937ef745399178f78b59517ec10a8677497ce4f7c8a5c6374bbe66", + "marreq-core/src/api/verification_control.rs": "5f49241dc2ceae386381c3c5a088e75e7e67d5c5140ef8cbd0540c021e83882c", + "marreq-core/src/api/verification_diff.rs": "3c1957bddba35bf6a3c88e91e02353584aaf52dd8fbb853ccdef812fd2fd2fc7", + "marreq-core/src/api/verification_methods.rs": "092ce0a633249acfb9f387ab859bf832512bdbb68277c906084e8929c11ba58e", + "marreq-core/src/api/verification_status.rs": "91fe8437410198511618bbc7638480be7c0e6dfa1c63ef67ea45733dc070b746", + "marreq-core/src/api/verifications.rs": "77e5d01401363a2d0a26664b9015ca836261a108aba631d9d2a6b5468b285f71", + "marreq-core/src/app.rs": "f506f1951a8db3660348f69630a7e77fbe5109a0365f0a26fb49fc0049b50cbd", + "marreq-core/src/auth/config.rs": "fe55c128b1c8886451a45d5b0863840005de6afda5f16b77573512885e33fbde", + "marreq-core/src/auth/csrf.rs": "a988dc55f7cdbc390072fcdd91fda6f43233023c85d05a04c036152f95ad9b0a", + "marreq-core/src/auth/delegated.rs": "61a549874674294a037394625ac81b05844f88627b5531cabf812bf15174c3dc", + "marreq-core/src/auth/errors.rs": "acb72478d2c6bd08f59efeed13d01d484c46a7024dd791c65910948abd7f022d", + "marreq-core/src/auth/external.rs": "bf0c4a33b3b849de41a31c7761f9478eed37f55e6e320fa5640a6b050f0b9bf5", + "marreq-core/src/auth/guards/admin.rs": "bdc1c1654926cb9b5425897371c87b402393d2d5c1fae29cc8fc37f40f3d1df1", + "marreq-core/src/auth/guards/api.rs": "ffdebb6107730290213f249fceea8a5796089339ae5a8d8a61014d074917150c", + "marreq-core/src/auth/guards/bearer.rs": "138e9c96e04372e8f2c3f33c298146bfbe128bf53ddd94e147a27856b41ee7b7", + "marreq-core/src/auth/guards/group.rs": "2042fe1d4af6350d0eb8a42e23ca5a35452cc8e9bd80535cae13154f0cc78f48", + "marreq-core/src/auth/guards/html.rs": "21529d3a0d23ac112f57d8cf7be81bde9824a42a9d44ad48bd91a0a2798b2389", + "marreq-core/src/auth/guards/mod.rs": "09e01eb7522a013007167447d0102fd7173d8588e1803fbc406748eeb5dda7c8", + "marreq-core/src/auth/guards/project.rs": "3a03e405327e02f35f1b69348a03b0ae1762d0e19f24c9f46acd0bb69da341d0", + "marreq-core/src/auth/guards/route_params.rs": "e076f960e7316ab45e33d78419296dc56ffa1020dcce95711f9865bbc4620fd1", + "marreq-core/src/auth/guards/session.rs": "b290b71d9d88f340f955b205cefc9c726728ff887b4014b803573fb173c39563", + "marreq-core/src/auth/login.rs": "48b258c2467fa4b7b82f07d9fba566be8efad302fb92ec445580ee6cc3705326", + "marreq-core/src/auth/logout.rs": "04e469e64e7bde1c21e5aca119f2ca166e28c747976b7ba494550275b65e6d03", + "marreq-core/src/auth/mod.rs": "ba9b96813124d3c439033a0fb5ff4815d48f0383bc05de9878bbfca8cf92938b", + "marreq-core/src/auth/password.rs": "1babc63db0d5adbccbc5aa94d378ec1b5a9ea962a1a40b642f308a44f75c441d", + "marreq-core/src/auth/password_policy.rs": "038967fae772a36137fc236b4e618776ce7a21ce53b80e20c1b5746919b6b9f0", + "marreq-core/src/auth/provider.rs": "cf7dd4fb37f566af283bd876cc0ac492b5c4d1e275e70c8f3b170e682ce8ebb2", + "marreq-core/src/auth/rate_limiter/config.rs": "9c225c383fd2fdb444199103a3dfc8ad892ae387c073162badfe64dd1df7713c", + "marreq-core/src/auth/rate_limiter/mod.rs": "f57a3c416486907adf7340d86058352046b553a1fc5a8de5b9d4e2d45913e8e3", + "marreq-core/src/auth/rate_limiter/postgres.rs": "4324ccc8fa90bfaf1e79bd44907f758bc2e07d72a4d34839b71afa7ade845428", + "marreq-core/src/auth/rate_limiter/store.rs": "7b8e861c56f62331d05996b7dd74d8f0e970fb1ebfadfc136e3a4d5e9220c0fd", + "marreq-core/src/auth/session.rs": "5c1df56bf8fc67ef2782d9e2781074ee6a366add00f997ca4e67fea5d06485b2", + "marreq-core/src/auth/session_config.rs": "732703ffde7f9f651b51c47ff544089edfd4fdda6fcd11ddef54e37968c2f605", + "marreq-core/src/auth/transaction_cookie.rs": "de91b4949f98267b232a3b3152460053ab16f3315e8821bb7186e28deaa4b069", + "marreq-core/src/authorization.rs": "4facd135c6f2b3c9c16249d9ab63fe4cf9d1fed8afe2190fb9d403d671f7d44d", + "marreq-core/src/backup.rs": "7e0223d93afbfd52dc0395e023c4be7156cb2aff193d27ec00557731754b19fb", + "marreq-core/src/build_info.rs": "3f602ae76288c24acc8efd5e445a299ca3c671c4b69b1c2695adc68c184c142b", + "marreq-core/src/config.rs": "44b3bfd89821e6965da447dde30045b181afbdd51e6540e980ce0912d6d40621", + "marreq-core/src/cors.rs": "31aa625f8ed94d43eb41e4d29c2ce35a30b61256b0388abf53dc30d580fc23a3", + "marreq-core/src/db_types.rs": "080883843e7ae0facaf671951d3985fbf8614f956dbb24db4c3f1624cfb206af", + "marreq-core/src/deployment/mod.rs": "7ca21f6bb56fc06009f4193c4509eb4b44622825ba33287a002e0a2f6f9a3873", + "marreq-core/src/deployment/test_support.rs": "3678a6ea0d4b989b14e630c3a9283b9a88b949e4b256e74b428df076308659bf", + "marreq-core/src/diff/mod.rs": "de6112f9c9731040d1af1afb2637863ae16c814105caebef2ba16bb9efdde7f7", + "marreq-core/src/fairings/cache_control.rs": "3a27895c3f0e2f5c9d41f922ac786ed59848ba5911434212c784daf270f1516e", + "marreq-core/src/fairings/csrf.rs": "be695fea17b384d081b2aa3dafa2fc0934c11b33cee48ef2a16139cb8a396d9f", + "marreq-core/src/fairings/mod.rs": "bb30f3b994c05075094bd4d2b91e96145a9f4daff1fcf0dc2b06206029854cdb", + "marreq-core/src/fairings/oauth_challenge.rs": "d09b75d9b7a82bdf232700dc4f8eb66a30a5a524031558da32f5c3f4b22859e4", + "marreq-core/src/fairings/request_log.rs": "0c1e067a3977e5c9d7ab77f58f188724d5d8414ac362e10a61822c8dd4ccf839", + "marreq-core/src/fairings/security_headers.rs": "f97b003cfb9a73d42aee51c4e2dcf94064b779f638a424be917a95c4ef89deed", + "marreq-core/src/fairings/semantic_index.rs": "498f6e3a73719bc92b3818b855a837da300380dd8c868ae9562c401069fa3abd", + "marreq-core/src/generators/excel.rs": "6a1ff8ca77c14227e5e93b086c5cc051601d1b47305dfc0f781055915a8ae57b", + "marreq-core/src/generators/mod.rs": "c1ccb1039a1282c40f4813cd5cd1ea598449f539e303784f7ac27754fb7c42b8", + "marreq-core/src/generators/reports.rs": "97cd2c6d58572e65f18dd20cd3bf68a193b610173d962e37781e2f4a04550958", + "marreq-core/src/generators/tests.rs": "d75f80eb06e806fe094672c361284267128c5400e9ad986f7f30fadf8a09c1f5", + "marreq-core/src/helper_functions/decorators.rs": "a1e66680f74295c681054eeb2b00dfdf859c6fc41fbe77e1fba6d9d3b70b1f32", + "marreq-core/src/helper_functions/filters.rs": "cba543dae55a85d7e45c304483856c3adfc7be988d6268f7bc63e1d6e5300976", + "marreq-core/src/helper_functions/mod.rs": "4c8fa19cde6104e8df42dbc0e5d4393d3ee1af7b2c83777d2add317f880991bf", + "marreq-core/src/helper_functions/reports.rs": "2b7cbd806ac46dfed4a836fa62cc6ac3b14bdbf8f8e6bd7019e1b1b96562154f", + "marreq-core/src/helper_functions/utils.rs": "ca5c701668ca6859527d185cbcb5574b528ada66e6bde154f1200eeaff6b7bc0", + "marreq-core/src/importers/excel.rs": "6fdea2824c9650724056618c4d17f5e27b347f6169be2365e33df4e32c4d71ec", + "marreq-core/src/importers/mod.rs": "c737520bbde0e3ae1d3f7e8b2f66bad2a9508f1da01116f8c190d2ab3c8543e8", + "marreq-core/src/importers/project_bundle.rs": "538e47b3b288ac938c7f6063e8058524b80f7cbdef16ba4ac8a648ffaabc93fb", + "marreq-core/src/importers/tests.rs": "e0f5331e87fd77c7f468199e75f4d2437ba5726bc8c975a957be734ec9399098", + "marreq-core/src/lib.rs": "1455ce36de9647931b03fd18c1f4a1c82a6df41e2df40948805996fbc4a7da6e", + "marreq-core/src/logger.rs": "b6a29960712e7fa9ad9ebfa33c041daab6e5a80d350615237903921e17491e85", + "marreq-core/src/models/decorators.rs": "40bb2f62a4037fbaf9f23aef4400606f4640e5f37471a4a8923e0dc46f7bf3b5", + "marreq-core/src/models/entities.rs": "1eabe06f2e169a5300cef45f85e897a9611233b1fdc0ac743a9bbbee705ac5fe", + "marreq-core/src/models/forms.rs": "7dae5b25d0cda8f5d943df5e546cd3e12fe691872052d475dd8f023ed6402c20", + "marreq-core/src/models/mod.rs": "649f0f88778ac1a4145ab00a07d685277cc365ce4be75801b1f462ad1ef3cc1c", + "marreq-core/src/models/semantic_search.rs": "3221de526e270bcfac49df625d129d93df0849137c201ecd58b0b0afcc9d8370", + "marreq-core/src/models/tests.rs": "66760a34dd08e200871c6bcddf0cd3572cebfea7ea3c3a11e5d7ff5b85c9641b", + "marreq-core/src/namespaces.rs": "0b7c8e3d7c437e8ee50daaaa81d578d21f20543496801bea7032fb9a4b8d12c0", + "marreq-core/src/permissions.rs": "aba57beb9f1ae6583e3c79c07530085328befd1b2cba7a2d925cead7eca10a3d", + "marreq-core/src/reports/data.rs": "2a3611d90d886b15dd8fae7f054cddd79aa42898d7a8855d5c0c34ad1533f3f5", + "marreq-core/src/reports/definition.rs": "8131924beb38d768c09b2e8c64c535de325a215eee0a1104e1b055da5c1a550c", + "marreq-core/src/reports/mod.rs": "dd04e70a3dd01e41561d0cc986655d08b1c70707dac2100803a4e4d64f5c8671", + "marreq-core/src/reports/model.rs": "8bd9b9d2b37f50d6e5058b507734d50c5685b98f8620cbfaaa418316d5eb916c", + "marreq-core/src/reports/odt.rs": "1990b981599d0c99bb296877e8806c4635098497903a50b60cc8d58ef98d4bfd", + "marreq-core/src/reports/pdf.rs": "05dd88da08ca9bd9dbceafd09c1c4a978505701e9b60cbe3c99ca83a41ae0b46", + "marreq-core/src/reports/sections.rs": "edc74cc6432a9ae0b0fece0f3fe2590173cd50f491e691e2161a30a1f5b266c4", + "marreq-core/src/repository/cache/keys.rs": "4480d4a36550e69a86162d029b9fbd18cf0f1ef7d2a9ad9ec4fb7fef0dba63f5", + "marreq-core/src/repository/cache/mod.rs": "70280d28b9fe0df543ba951fac7249ad498b49c206536f2dda32ca076df98349", + "marreq-core/src/repository/cache/repository.rs": "06eaec21cd1b7aaf219b39790076fbf400e07d0172bbf4ea10cfc0b84742d60d", + "marreq-core/src/repository/cache/stats.rs": "6f7c20f039b29f6060a1d76c3e8f11aece1828406a347dbdc6f303d06c8182f9", + "marreq-core/src/repository/cache_middleware.rs": "700959d0e2161082f1e2df92b1fd7309f8869fcb05679fd22b12c8e76f504269", + "marreq-core/src/repository/diesel_repo/attachments.rs": "995a5bf18dd274109d235de35a9d9655cc5aaa0dbaaf734aff6e47ec3054f6e8", + "marreq-core/src/repository/diesel_repo/baselines.rs": "1b636f5785c91ae8801608da4af8b5de2991f38edfd447f3e3285810035ddb75", + "marreq-core/src/repository/diesel_repo/comments.rs": "d2fe0edc50e95a4975a804ac68576bfee6166d91ae6d393b736a690a77f1162d", + "marreq-core/src/repository/diesel_repo/custom_fields.rs": "2c79f7da0dd3329712dc526d9f275ae3b5529b60ce2c5a38b3823c341aaaf7f9", + "marreq-core/src/repository/diesel_repo/groups.rs": "dcf3ecbbf2366a732fe72fc722afac9b4584772b58dbbaf7dc3903add4da75e6", + "marreq-core/src/repository/diesel_repo/idempotency.rs": "b8245707ed7c35683d461cc14ee8e8952bb654b63d9f87da2959ce2b371f9959", + "marreq-core/src/repository/diesel_repo/identities.rs": "29d8cba985744fb801f643585cd81f2d6e762bd60d2a748857dcdbed45d95769", + "marreq-core/src/repository/diesel_repo/logs.rs": "828817d0a2163df7b0d22176eadea4757731dd7ae4b6e88549b6fc68b0c848f2", + "marreq-core/src/repository/diesel_repo/lookups.rs": "35bfeba0d637674d912b97c72100ef58b09cf419eec25c368d7065a3354f491a", + "marreq-core/src/repository/diesel_repo/matrix.rs": "e7a75f4d2dfe71183ff0938c101130403343ecb21283f6c490d1543ab46d04e1", + "marreq-core/src/repository/diesel_repo/mod.rs": "3bef7d6ac3c6a21da409b8a9eec67d819f7ecf81ec340383f997148a11c76082", + "marreq-core/src/repository/diesel_repo/notifications.rs": "c49f845b11f9fa64e23a637a682919140133c761d2a9f049ebbfe0d3bc24b618", + "marreq-core/src/repository/diesel_repo/oauth.rs": "7c616112874226213fa2ddaf66cde5012d5c1398c279403ceb3e7ef506c99eba", + "marreq-core/src/repository/diesel_repo/projects.rs": "5f1a9a0081a7c6674e74e9bfe35cb3d0114e434bf97944453b41eeb1958fb1a0", + "marreq-core/src/repository/diesel_repo/report_templates.rs": "3d4542ced77375a9dc9e7066f288338d3dc042fcec5193c2b5181c140d470f64", + "marreq-core/src/repository/diesel_repo/requirement_links.rs": "6775b67d323497e444149a3c45c8ccec11d87a602add7d001ecec4963e88239b", + "marreq-core/src/repository/diesel_repo/requirements.rs": "24a0e00ddc48062949761b431dd46741ed8d261a0f228df2727c99708a85b75d", + "marreq-core/src/repository/diesel_repo/saved_views.rs": "6051322dcc26e2f6c194243460f3f292d12893035deb844b098a48cee980b9ca", + "marreq-core/src/repository/diesel_repo/use_cases.rs": "fcec4e43d0d65a467daa6abc5fcce97582ca71fc78c2f0ef4461b70094c3bfa1", + "marreq-core/src/repository/diesel_repo/users.rs": "883cf4fe2a69a882fbf71b37ddbed93191cb20910c2fc5d75d8c2aed7d92197d", + "marreq-core/src/repository/diesel_repo/verification_control.rs": "29c63e62ab0a445eaf11e28756a301d1ede358005eeaf12fc95769e4e5911f90", + "marreq-core/src/repository/diesel_repo/verifications.rs": "4493d652fd759c63e6220e8b2ef17a08e547283180da82aba189b03f9a9caa9c", + "marreq-core/src/repository/diesel_repo/workspaces.rs": "4ebe8759cc12391bc6b0b7679f1bfba462518c467aaf5576f50e777d338d7622", + "marreq-core/src/repository/diesel_repo_mock.rs": "2af62bd62ae078aafff93e01e7be20096e5b86eec28e034f0b81f2673e348e37", + "marreq-core/src/repository/errors.rs": "9cf994d8f428567cd8f0ca5206ba0b363e555e4dfb018561968e2cc7081dce3c", + "marreq-core/src/repository/mod.rs": "13f6e6696619adef56e4b541399be903837c7b6a31df20f7d552055fe1694dde", + "marreq-core/src/repository/tests.rs": "fe1dd6ba26d7ebf588d4fe16b33d030303af5875d3e2017d30154125bfe84413", + "marreq-core/src/reqif/archive.rs": "0bb0e69455933ae03303ddcd53c50573aad11f176e603bdd32e1db28cb573d36", + "marreq-core/src/reqif/export.rs": "66cf5c01957bf4eba237b59c0cfb7863e9ae35d0648f3bd87a1668263a0234cd", + "marreq-core/src/reqif/import.rs": "0dbbdfac7dfe711840d1e83a38bec5b74334ef89da844928f9b0cc6207d6666e", + "marreq-core/src/reqif/mapping.rs": "1cb83d2f18e1c6161c05680d22ef16dfa055531519043247ff2685e8ea122189", + "marreq-core/src/reqif/mod.rs": "48a98c711cb01e94f40dd29a7b7f9abcf02d42e601f436e4961247c3aa4bba1a", + "marreq-core/src/reqif/schema.rs": "fc5601a2d63f35312efa437e19b14dfa197ab0bb0b4c38e731b52c0683ad0ff3", + "marreq-core/src/reqif/vendor_import_tests.rs": "bf74ae53531b3a6ad1f725e26be528b6f3332a00127c3e3edf924a3843bbaec4", + "marreq-core/src/rich_text.rs": "ff4e2c4f60e15f52e933fb6226cecf193f4bac9c47d45b1a0e2158d28751936e", + "marreq-core/src/routes/api_info.rs": "1994ccb506523391848cc2b5202bffffff5362c9f3e3937c833a4d07944f7467", + "marreq-core/src/routes/catchers.rs": "ed18bab7d2bc2bcfd36ca96a49fe1071acb08747d09cf010b01d53dc3db5e65e", + "marreq-core/src/routes/helpers.rs": "396527d619b65b32880b59d5f75b195372d2396164536d6b8224bbb78103a3ea", + "marreq-core/src/routes/mod.rs": "c25096024e4b6fd5c63ec7e0b658a34bb3faabad2b6a5c08825b37af1909c2a2", + "marreq-core/src/saved_view_definition.rs": "a222e3e907f7ed5dacc759961eba299389eae4bd79387b8370772b6a526da4c3", + "marreq-core/src/schema.rs": "4a8a5e2aa8a44ad974e1cb0737ebf609f80f0c729c103ec5476adb039f98f536", + "marreq-core/src/services/applicability_service.rs": "da755f23a963182b284686c1d4987356449e96764780646b33b80e804cf7e719", + "marreq-core/src/services/attachment_service.rs": "ba06b3dde76eca6730fe17db100b335d03daac762f953efffe8969af274b7338", + "marreq-core/src/services/base_service.rs": "e649209878f6920588beebca68188171e423bbadec50a5e8a3ead9d16ce0ad1c", + "marreq-core/src/services/baseline_service.rs": "726dbce5864593256f856cdbcdf4a3ce37faf627642cbb5968fb82438a767a07", + "marreq-core/src/services/cache_service.rs": "8a94395ef50b6b369cd935f3614e6aacdb24a4ad1e1237581214122afa9acb7a", + "marreq-core/src/services/category_service.rs": "9c5c14bfe03c398b2dde0d22aee7567c6dc5ef325c66d8598be7e46a6441b35c", + "marreq-core/src/services/comment_service.rs": "b7ead06f324840fea8c0ab35812b45c9a954d5a72c44fee340331c86b647db02", + "marreq-core/src/services/config_authorization_tests.rs": "b1ec6c9fbdf28769258f8d23c8aaaf1e4dbba62621c97e5b037b03ce1642d4ce", + "marreq-core/src/services/custom_field_service.rs": "7778d7e2a95560e608efe5ca4dd33353cf73d30a125f7eb7a679b38a24d295ec", + "marreq-core/src/services/decorated_requirement_service.rs": "e7a04e3beccbe151b1d936b53cbbd2df2d9a0405f649d6d02e868d6e842815b3", + "marreq-core/src/services/decorated_test_service.rs": "37c82e2843540b78367f7337988d347cedb03b25e7005edcb0b45972c07d4bd6", + "marreq-core/src/services/dsm_service.rs": "9409bdb79b7269305a04381cf77d6d8329de8fec54d033766d6cb344504331ed", + "marreq-core/src/services/email_sender.rs": "1a556590e3b034839fde6adc634dd563ca93a1642af070c5646b148690cf5a73", + "marreq-core/src/services/external_auth_service.rs": "1a03d40e81701a094f16b1010c1d90af5246d565f8ce35b5ecabb654a81b2dfe", + "marreq-core/src/services/group_service.rs": "bdb286fead51cde5d91a669f6f3aca77a1cabc86ef1389e287b3d193cebd3e90", + "marreq-core/src/services/log_service.rs": "b63aee03c8fde42e9143c2f4f19e7eb5df37a1299380829d093c0e44dcde5e5c", + "marreq-core/src/services/matrix_service.rs": "57ace88167fccefe005e4dabb852a44d4c333107308208f0bec6e7bfc82cd1bf", + "marreq-core/src/services/mentions.rs": "37338f744e659c1424409c3375e71fdc8df51c44a2ee67815f0b293383198483", + "marreq-core/src/services/mod.rs": "419b0ffb22da94c32ae983a7a1fd9cfe83cf8c6066d995d2afbebdab378f2af9", + "marreq-core/src/services/notification_service.rs": "959bf356384407505d9c13158072aea84767d32834b807c681667db6ab3bd575", + "marreq-core/src/services/project_service.rs": "cb0ba576c9e4a33700e99a07c2d57d9ed26f4be54f2c700a2c55380e402c8a6d", + "marreq-core/src/services/reqif_service.rs": "1c4134dbaeacb09b259507159528a593acaf36a0a182ec7ba6aa4aefbec92f24", + "marreq-core/src/services/reqifz_service.rs": "f838756002594afbedbc94346e1c143394b2e63c2fd1d071f3de49db972b2e31", + "marreq-core/src/services/requirement_analytics_service.rs": "1388c3f6b506802ff4fb9255e5047ad66b66701b2645750d460c647e287544f8", + "marreq-core/src/services/requirement_diff_service.rs": "33cd9c92855de59df501e8343b508e316275c03852703012967bfe74b53ea2d9", + "marreq-core/src/services/requirement_service.rs": "3e8d9dc89253780582981b3086094e66dfb69aeb329575ad810a68e32e57e084", + "marreq-core/src/services/saved_view_service.rs": "0c8376fdd9fc2d6eb402382a3338cac73f0507e1fb1ba1c64b305ec9a4bb778f", + "marreq-core/src/services/semantic_search/config.rs": "5203461fa16be63887069fb6ebf3ed0833096ee6823f050ffa73d93ca4300198", + "marreq-core/src/services/semantic_search/document_builder.rs": "81f6f8d224d5595eb6e57bc3604ecfc338bb634fca682cb5bc43e7d48332d52f", + "marreq-core/src/services/semantic_search/embedding_provider.rs": "3f8e5d3e66bf78ae3216cadc76ab4e4c7480226830abba4a2e722268fbd765a4", + "marreq-core/src/services/semantic_search/indexing_service.rs": "8ea2d41ab9a64a8958e78163ab76d1a960220a1f5786077f1c75cc6c69d62a08", + "marreq-core/src/services/semantic_search/llm_provider.rs": "b7318a4e6e8ae2e794a51a2d644d88cd6a6674b1de4fca7098d83cf85ffe4fa3", + "marreq-core/src/services/semantic_search/mod.rs": "2cf968e31b72125552698ad1beebd8648b2cc628410c3aff0046303470fd6200", + "marreq-core/src/services/semantic_search/search_service.rs": "3603273c894ddb0d2f2980ab8b6290f70664c3d313011ba0a082ea0f2a393c54", + "marreq-core/src/services/status_service.rs": "b85e510fc9deffa37c51106d4d56d033aba5f8499cd7a53fc94452708e081b23", + "marreq-core/src/services/tests.rs": "244cb904f099f380754efab68ed8632a350ed5c27b333670e7d057705004bcaa", + "marreq-core/src/services/use_case_service.rs": "0e1e3f42e63b5990389bc62ee9765dc3bd71d619908897d5ad036a52f85b60b4", + "marreq-core/src/services/user_provisioning_service.rs": "ed4a1d192b3a40e66f689f80730985cb9acb6e88bc6cb6307c875103257a42d9", + "marreq-core/src/services/user_service.rs": "855663cc391eda53582ca2bf8160bdbe5dfe6e803e257a5b377c7d2b6d2acb2d", + "marreq-core/src/services/verification_control_service.rs": "b2015f9051fdcbfabfec9330a890ba303feb55c252df6143c49a0cde4310fee7", + "marreq-core/src/services/verification_diff_service.rs": "63f1bdb62d3e1092bf45a5907fe3a393707e832f390bdc839dc7aae4c1804308", + "marreq-core/src/services/verification_method_service.rs": "8491d252d76cbf3f20fa20c2850e45d962ca2a55715247a8b25b781ae03d4a66", + "marreq-core/src/services/verification_service.rs": "75eb4a790eb92dd866e91037d6d4f8ccb3db80b61958014d5d2a3751e4ec10f2", + "marreq-core/src/status_enums.rs": "0b3794e640c934f10584780bb703cbd8dd54bf1fd218841c70e5975ee85e20a3", + "marreq-core/src/storage/content_type.rs": "e324695785b60c63fc14a76d53d4ab5a1891fb623ea6485502f92cb892f6c423", + "marreq-core/src/storage/local.rs": "112afb565a8eff8007ee499baaf5751a96dec78df5805b23d40effc8ce099801", + "marreq-core/src/storage/mod.rs": "79653093398c728e8a2be8270e0ccadcc14bf3c8ac8dbce62d0fe344c42de66b", + "marreq-core/src/use_case_document/diff.rs": "9059f723d9f14704f80e1901be32483d0c3de552dd61f8a45d5dcc12e5032dd9", + "marreq-core/src/use_case_document/mod.rs": "d27cff5c38cfc5a88a47b3e8615f7bf2ce19b11345b603dbd6e46fff125e72e7", + "marreq-core/src/use_case_document/operations.rs": "78ab1e3750a7ace36991660e23bc5fc9e05e88de2452d90ecbd4d7fa8095595c", + "marreq-core/src/validation/error.rs": "e80f3f225c3300a9e49f78c2a07ebbe66fb0ce7237e1010f8f51341380f0805d", + "marreq-core/src/validation/mod.rs": "41643b71b016e8a1d07384979ec9cd54c5ed860ab9a8a78326485251638f3cd0", + "marreq-core/src/validation/tests.rs": "589456901adce6f94a66db2b3ee33ea01f1db596a9be51680abdeef8b05fed1b", + "marreq-server/src/api/mod.rs": "5707e1cbc3c10b9e33e62bc7b3b244a7a565024a1c7147562634c0da7b1d3331", + "marreq-server/src/deployment.rs": "840d3a3324402d64a2448d888ea2f569656f2fc07c8d38dfc75b862b0e2714d9", + "marreq-server/src/main.rs": "b7c82e45b16873d46e9a4c178e1311d62b2d7a90a15d4e0d39a6848fe3c859b6", + "marreq-server/src/routes.rs": "348dbf2079a01db04f81209371c33c01e20ddab4ed4f1c8b34108e172c360058", + "mcp-server/compatibility.json": "0d9bcff76abe3a44b59c5bbcd3b73acd29266394e25c16b7c4b069bec28b9472", + "mcp-server/src/check-duplicates.ts": "ccdf3f47bb2d562a316b4d957cae8786f00c4255d1100e5f8bfb08202ae15d75", + "mcp-server/src/client.ts": "6011376ae4200afd83a8110d5c997df604791b3d058726b977e0270100c80306", + "mcp-server/src/context.ts": "7a0666e3f92ce56f55442425dbc1c917f1670665bd8c30fc363036d2479b09f4", + "mcp-server/src/credentials.ts": "e7697f096ccc60de4709e9b94c8bb0091c23fb090234ae4c2a36fc5f1011639c", + "mcp-server/src/gap-tools.ts": "c0453c6dac1ed6c6beb2460c47849ed5b6bf70e790b4ffc7b4fdc3f35b19f6a8", + "mcp-server/src/index.ts": "8ae7b012c087c4d522e688d1119b555d252ad2199553e8829b4445a73f46c272", + "mcp-server/src/pagination.ts": "64d0dcdec7dc6ba268f44745e281da0a2a52839804d328261be336686f8f5a5d", + "mcp-server/src/prompts.ts": "e80f45676aa1e29db25b1c637366a99c04bd0b7dd2aa402d050c000ea93ca698", + "mcp-server/src/remote.ts": "db6a0bee20bc73d3c6635d5364dc5a98e06a0b30aae5aa07482a664cef4d328b", + "mcp-server/src/resources.ts": "0c68424f0676391ecdf66df5ae8d78f026e4b82d8683e6fe0759ab33d6720fd6" +} diff --git a/docs/developer/http-api-contract.md b/docs/developer/http-api-contract.md index 79cdc045..4bb86309 100644 --- a/docs/developer/http-api-contract.md +++ b/docs/developer/http-api-contract.md @@ -212,3 +212,180 @@ A minimal **OpenAPI 3** sketch for auth and session project listing lives in [`o - **Adminer:** host port **8081** (to avoid clashing with the frontend). See [`../../docker/README.md`](../../docker/README.md). + +## P0 contract-hardening audit and current coverage (#423) + +**This is an incomplete bootstrap, not completion of the P0 prerequisite.** The +OpenAPI inventory lists all 245 compiled HTTP API operations: 241 on Server and +245 on Cloud, including origin-root OAuth routes. Currently 39 operations have +explicit wire schemas; 206 are marked `x-marreq-unverified` and are inventory +entries only. The release completeness gate deliberately rejects them. A passing +OpenAPI validator or PR check does **not** establish full API compatibility. + +Architecture assessment: + +| Boundary | Implementation and observations | +| --- | --- | +| Shared backend | `marreq-core/src/app.rs::declared_mounts` composes policy-classified API and root OAuth routes. Rocket guards enforce sessions, Bearer scopes, membership and permissions. Diesel models are not automatically wire schemas: flattened DTOs, computed JSON, omitted fields and custom responders matter. | +| Server | `marreq-server/src/routes.rs` currently adds no endpoints. User creation/admin promotion and database backup are shared endpoints with deployment-mode restrictions, not absent Server/Cloud routes. | +| Cloud | Four additional public account endpoints: register, verify-email, forgot-password and reset-password. Their presence differs from Server; shared capabilities also differ at runtime. | +| Frontend | `frontend/src/api/*.ts`, plus direct fetches in pages/components. Session credentials and CSRF are essential; binary downloads, multipart imports and redirects cannot be modeled as ordinary JSON. Only metadata/provider discovery and selected use-case response models have migrated to generated types so far. | +| MCP | `mcp-server/src/client.ts` wraps project-scoped REST calls; `index.ts` defines tools, `remote.ts` owns MCP HTTP sessions and delegated credentials. Principal identity has nullable `client_id`/`grant_id`, even though the previous TypeScript definitions described optional strings/numbers. The Node MCP transport and `/healthz` are outside the Rust HTTP API. | +| Authentication | `/api/home/attention` now accepts `ProjectsRead` scoped credentials; the older session-only description above is stale. `/api/search` remains session-only. `/oauth/*` and `/.well-known/*` are mounted at the origin root. Guard/fairing responses can be empty or framework-generated; consumers must not assume every error is an `ApiError` JSON object. | +| Release | Existing component tags remain the publishing triggers. Independent artifact versions are allowed. Current `main` is never substituted for an unavailable historical contract. No deployment or global release migration is implemented here. | + +The explicitly modeled surface includes public build/deployment/CSRF/provider +metadata, current user, MCP principal, the four Cloud account routes, three OAuth +discovery routes, and all 25 use-case operations (including draft operations, +publishing, versions, approvals, comments, links, baseline content and diffs). +Use-case document requests distinguish omitted defaults from response properties +that Serde always emits. Unmodeled inventory entries do not promise fabricated +response bodies/status codes. Origin-root UI/HTML and internal CSRF-denial routes +are excluded from the API inventory comparison. + +### Source of truth and change workflow + +`openapi.yaml` is the wire contract. This retains Rocket/Diesel and introduces no +backend runtime dependency. `api-contract-routes.json` and both +`generated-contract.ts` files are reproducible derivatives, not independently +maintained contracts. The source-review digest records which producer/consumer +sources were audited. It detects changes requiring review, but **does not prove +serialization correctness** and is not a replacement for integration tests. + +When changing an endpoint: + +1. Inspect the actual Rocket mount, guards, Serde serialization/default/flatten/ + skip behavior, response status, and both consumers. Update OpenAPI accordingly. +2. Add or extend producer exchanges and consumer tests. Remove + `x-marreq-unverified` only after auditing the complete operation, not merely + because its route exists. Preserve unknown error-body handling. +3. Regenerate derived artifacts and explicitly record the source review: + + ```sh + python3 scripts/contracts/generate.py + python3 scripts/contracts/check.py --record-source-review + python3 scripts/contracts/check.py + python3 -m unittest discover -s scripts/contracts -v + ``` + +4. Run the compiled route check and isolated real-backend checks: + + ```sh + cargo test --locked -p marreq-cloud --test api_contract_inventory + npm ci --prefix frontend + npm ci --prefix mcp-server + scripts/contracts/test-backend.sh + ``` + +Install Python tooling with `python3 -m pip install -r +scripts/contracts/requirements.txt` in a virtual environment. Backend builds need +`libpq-dev`; Docker and Node 22 are needed for the isolated smoke check. The script +creates a new loopback-only PostgreSQL container with tmpfs storage, applies +migrations, seeds synthetic OAuth credentials through the integration test, and +starts a real `marreq-server` process. It exercises the actual frontend transport +and built MCP client over HTTP. It records requests/responses, never Authorization +headers or cookies. It ignores existing database URLs when creating its test DB; +it never connects to a production database. Cleanup runs on exit. Missing/skipped +critical producer exchanges fail transcript validation. + +### Remaining risks before P0 can be accepted + +- Audit the remaining **206** operations: authentication protocol/session flows, + projects/groups, requirements/verifications, traceability, matrices, baselines, + catalogs, administration, search, imports, exports, reports and attachments. + Presence in the inventory is not schema coverage. +- Complete per-consumer operation selection and generated request/response type + adoption, including direct frontend fetches. Bundles currently conservatively + require the entire backend surface; this can reject unrelated changes. +- Add exhaustive consumer request checks and live Cloud/session/OAuth protocol + coverage. Current executable producer/consumer coverage is representative, + not exhaustive, and no historical release is retrospectively certified. +- Retain verified bundles for real released artifact versions and bootstrap an + accurate deployment plan. Versions in package manifests alone are insufficient. +- Close existing test/security failures before making this a release-ready change. + +Until these are resolved, leave the PR in draft and keep publication blocked by +`check.py --require-complete`. Do not remove that gate to make a release green. + +### Implementation validation record (2026-10-27) + +All npm installs used the committed lockfiles (`npm ci` at root, in `frontend`, +and in `mcp-server`). Rust commands on the development host used temporary +`LIBRARY_PATH`/`LD_LIBRARY_PATH` settings pointing to an extracted `libpq` package; +no system/production database configuration was changed. + +| Exact command | Result | +| --- | --- | +| `python3 scripts/contracts/check.py` | PASS; 245 inventoried operations, 39 modeled operations, 206 explicit gaps | +| `python3 scripts/contracts/generate.py --check` | PASS; generated types and compiled-route expectations reproducible | +| `python3 -m unittest discover -s scripts/contracts -v` | PASS; 39 positive/negative tests, including historical plans and process exit codes | +| `python3 scripts/contracts/check.py --require-complete` | EXPECTED FAIL (exit 1); incomplete producer contract plus 206 unverified operations | +| `python3 scripts/contracts/compatibility.py --manifest /tmp/marreq-nonexistent-plan.json` | EXPECTED FAIL (exit 1); structured missing-evidence error | +| `cargo test --locked -p marreq-cloud --test api_contract_inventory` | PASS; compiled Server and Cloud routes match derived expectations | +| `scripts/contracts/test-backend.sh` | PASS; disposable migrated PostgreSQL, production Rocket handlers, real Server HTTP process and actual frontend/MCP clients; 15 validated exchanges, 33 explicitly inventory-only exchanges, three corrupted-traffic negative controls rejected | +| `cargo test --workspace` | PASS; 2,061 passed, one ignored. Default workspace feature unification enables test helpers; DB-specific tests without explicit test DB may skip internally. The separate isolated production-path run above is required evidence. | +| `cargo fmt --all -- --check` | PASS | +| `cargo clippy --workspace --all-targets -- -D warnings` | PASS | +| `npm run build --prefix frontend` | PASS; existing large-chunk warning | +| `npm run build --prefix mcp-server` | PASS | +| `npm test --prefix mcp-server` | PASS; 51 tests | +| `npm run test:coverage --prefix frontend` | PASS; 108 suites, 704 tests, 70.36% line coverage. An earlier ordinary full run had one report-builder failure; its isolated rerun and full coverage rerun passed. | +| `npx vitest run marreq-core/tests/js` | PASS; 411 tests, seven skipped | +| `npx --no-install stylelint 'frontend/static/**/*.css' --config .stylelintrc.json` | PASS | +| `npm run check:unused-css` | PASS (exit 0); existing warning reports 379 unused selectors | +| `npm audit --prefix frontend --omit=dev --audit-level=high` | PASS; no production vulnerabilities reported | +| `npm audit --prefix mcp-server --omit=dev --audit-level=high` | FAIL; existing lockfile reports one high and three moderate advisories | +| `cargo deny check advisories` | FAIL; existing locked dependencies trigger RUSTSEC-2026-0384, RUSTSEC-2026-0385 and RUSTSEC-2026-0387, plus yanked-crate warnings | +| `bash -n scripts/check-api-compatibility.sh scripts/contracts/test-backend.sh` | PASS | +| `git diff --check` | PASS | + +Dependency/security failures are reported, not suppressed or allowlisted. Their +remediation remains necessary before release readiness. The source-review digest +is deliberately conservative: backend service/model/guard changes and direct +frontend calls outside API modules require renewed review as well. This does not +replace the remaining serialization/consumer verification work. + +Changed files in this draft implementation (including generated artifacts): + +- `.github/workflows/api-contracts.yml` +- `.github/workflows/marreq-ci.yml` +- `.github/workflows/release-artifacts.yml` +- `.gitignore` +- `Cargo.lock` +- `docker/Dockerfile` +- `docker/frontend/Dockerfile` +- `docs/developer/api-contract-routes.json` +- `docs/developer/api-contract-source-review.json` +- `docs/developer/http-api-contract.md` +- `docs/developer/mcp-setup.md` +- `docs/developer/openapi.yaml` +- `docs/developer/use-cases-api.md` +- `docs/releases.md` +- `frontend/src/api/generated-contract.ts` +- `frontend/src/api/types.ts` +- `frontend/src/hooks/__tests__/useBuildInfo.test.ts` +- `frontend/src/pages/__tests__/AccountPage.test.tsx` +- `frontend/src/pages/__tests__/AdminPage.test.tsx` +- `frontend/src/pages/__tests__/HelpPage.test.tsx` +- `frontend/src/pages/__tests__/LoginPage.test.tsx` +- `marreq-cloud/Cargo.toml` +- `marreq-cloud/tests/api_contract_inventory.rs` +- `marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs` +- `mcp-server/Dockerfile` +- `mcp-server/compatibility.json` +- `mcp-server/src/client.ts` +- `mcp-server/src/generated-contract.ts` +- `mcp-server/src/remote.ts` +- `scripts/check-api-compatibility.sh` +- `scripts/contracts/bundle.py` +- `scripts/contracts/check.py` +- `scripts/contracts/check_baseline.py` +- `scripts/contracts/compatibility.py` +- `scripts/contracts/generate.py` +- `scripts/contracts/inventory.py` +- `scripts/contracts/requirements.txt` +- `scripts/contracts/smoke-consumers.mjs` +- `scripts/contracts/test-backend.sh` +- `scripts/contracts/test_compatibility.py` +- `scripts/contracts/test_contract_tooling.py` +- `scripts/contracts/verify_transcript.py` diff --git a/docs/developer/mcp-setup.md b/docs/developer/mcp-setup.md index 7210b7c5..1cc120ba 100644 --- a/docs/developer/mcp-setup.md +++ b/docs/developer/mcp-setup.md @@ -359,3 +359,15 @@ Reference: shared route list in `marreq-core/src/api/mod.rs` (plus deployment-sp - **Forbidden (403)**: The token’s project scope (if set) must match `MARREQ_PROJECT_ID`; or the user must be a member of the project with permission for the action. - **Connection refused**: Ensure Marreq is running and `MARREQ_BASE_URL` is correct (e.g. `http://localhost:8000`). - **Tool not found**: Confirm `MARREQ_MODE` and `MARREQ_TRACE_WRITE` — extended and trace tools are only registered when those settings enable them. + +## Backend compatibility metadata + +`mcp-server/compatibility.json` records the build-time supported backend range, +independently of the MCP component's version. It is retained in the MCP image. +CI validates its shape and release verification combines it with the exact +released MCP contract bundle; ranges alone do not establish compatibility. +See [release contract verification](../releases.md#api-compatibility-gate-and-retained-artifact-contracts-423-p0). +The current bootstrap still has unverified API operations and intentionally cannot +certify an independently deployed production combination. Existing mocked MCP +unit tests remain; `scripts/contracts/test-backend.sh` additionally executes the +actual client against a real loopback Rocket server and an isolated database. diff --git a/docs/developer/openapi.yaml b/docs/developer/openapi.yaml index a01ccfde..932e8797 100644 --- a/docs/developer/openapi.yaml +++ b/docs/developer/openapi.yaml @@ -1,216 +1,6989 @@ -openapi: 3.0.3 -info: - title: Marreq API (partial contract) - description: | - Subset of the Marreq JSON API for interchangeable frontends. - All application routes are under `/api`. Many more paths exist; see `marreq-core/src/api/mod.rs` and README. - version: 0.1.0 +openapi: 3.1.0 +info: {title: Marreq HTTP API, version: 0.1.0, description: Rocket route inventory plus explicitly audited wire + schemas. Operations marked x-marreq-unverified are NOT complete contracts and cause release verification to + fail closed. Root OAuth paths are distinct from /api. The Node MCP transport is not a Rust REST endpoint. See + http-api-contract.md for scope and remaining work.} servers: - - url: /api - description: Same-origin (nginx proxy or Vite dev proxy) - - url: http://localhost:8000/api - description: Direct Rocket (local) - +- {url: /} paths: - /auth/providers: + /api/projects/{project_id}/requirements/{id}/activity: get: - summary: Discover enabled browser authentication methods - operationId: authProviders + operationId: activity_requirement_activity_by_project + tags: [activity] + summary: activity / requirement activity by project + x-marreq-variants: [server, cloud] + x-marreq-handler: activity::requirement_activity_by_project + x-marreq-source: marreq-core/src/api/activity.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/verifications/{id}/activity: + get: + operationId: activity_verification_activity_by_project + tags: [activity] + summary: activity / verification activity by project + x-marreq-variants: [server, cloud] + x-marreq-handler: activity::verification_activity_by_project + x-marreq-source: marreq-core/src/api/activity.rs + x-marreq-guards: [ProjectVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/applicability: + get: + operationId: applicability_list + tags: [applicability] + summary: applicability / list + x-marreq-variants: [server, cloud] + x-marreq-handler: applicability::list + x-marreq-source: marreq-core/src/api/applicability.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: applicability_create + tags: [applicability] + summary: applicability / create + x-marreq-variants: [server, cloud] + x-marreq-handler: applicability::create + x-marreq-source: marreq-core/src/api/applicability.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/applicability/{id}: + get: + operationId: applicability_get + tags: [applicability] + summary: applicability / get + x-marreq-variants: [server, cloud] + x-marreq-handler: applicability::get + x-marreq-source: marreq-core/src/api/applicability.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + put: + operationId: applicability_update + tags: [applicability] + summary: applicability / update + x-marreq-variants: [server, cloud] + x-marreq-handler: applicability::update + x-marreq-source: marreq-core/src/api/applicability.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: applicability_delete + tags: [applicability] + summary: applicability / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: applicability::delete + x-marreq-source: marreq-core/src/api/applicability.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/attachments: + get: + operationId: attachments_list + tags: [attachments] + summary: attachments / list + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::list + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [ProjectRequirementsAndVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: attachments_upload + tags: [attachments] + summary: attachments / upload + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::upload + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/attachments/{attachment_id}/download: + get: + operationId: attachments_download + tags: [attachments] + summary: attachments / download + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::download + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: attachment_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/attachments/{attachment_id}: + delete: + operationId: attachments_delete + tags: [attachments] + summary: attachments / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::delete + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: attachment_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/storage: + get: + operationId: attachments_get_storage + tags: [attachments] + summary: attachments / get storage + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::get_storage + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/storage/quota: + put: + operationId: attachments_set_quota + tags: [attachments] + summary: attachments / set quota + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::set_quota + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [AdminOnly] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}/attachments: + get: + operationId: attachments_list_for_baseline + tags: [attachments] + summary: attachments / list for baseline + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::list_for_baseline + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}/attachments/{attachment_id}/download: + get: + operationId: attachments_download_for_baseline + tags: [attachments] + summary: attachments / download for baseline + x-marreq-variants: [server, cloud] + x-marreq-handler: attachments::download_for_baseline + x-marreq-source: marreq-core/src/api/attachments.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + - in: path + name: attachment_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/auth/providers: + get: + operationId: auth_auth_providers + tags: [auth] + summary: auth / auth providers + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_providers + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [] + security: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/external_ProviderDiscovery_response'} + /api/auth/external/{provider}/start: + get: + operationId: auth_auth_external_start + tags: [auth] + summary: auth / auth external start + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_external_start + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [] + parameters: + - in: path + name: provider + required: true + schema: {type: string} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + /api/auth/external/{provider}/link: + post: + operationId: auth_auth_external_link_start + tags: [auth] + summary: auth / auth external link start + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_external_link_start + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [OptionalSessionUser] + parameters: + - in: path + name: provider + required: true + schema: {type: string} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/auth/external/{provider}/callback: + get: + operationId: auth_auth_external_callback + tags: [auth] + summary: auth / auth external callback + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_external_callback + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [OptionalSessionUser] + parameters: + - in: path + name: provider + required: true + schema: {type: string} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + /api/auth/csrf: + get: + operationId: auth_auth_csrf + tags: [auth] + summary: auth / auth csrf + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_csrf + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [] + security: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + csrf_token: &id005 {type: string} + required: [csrf_token] + additionalProperties: true + /api/auth/login: + post: + operationId: auth_auth_login + tags: [auth] + summary: auth / auth login + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_login + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + /api/auth/identities: + get: + operationId: auth_auth_identities + tags: [auth] + summary: auth / auth identities + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_identities + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [OptionalSessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/auth/identities/{identity_id}: + delete: + operationId: auth_auth_identity_delete + tags: [auth] + summary: auth / auth identity delete + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_identity_delete + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [OptionalSessionUser] + parameters: + - in: path + name: identity_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/auth/logout: + post: + operationId: auth_auth_logout + tags: [auth] + summary: auth / auth logout + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_logout + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/auth/change-password: + post: + operationId: auth_auth_change_password + tags: [auth] + summary: auth / auth change password + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_change_password + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [OptionalSessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/auth/me: + get: + operationId: auth_auth_me + tags: [auth] + summary: auth / auth me + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_me + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [OptionalSessionUser] + security: + - cookieAuth: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/entities_User_response'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': + description: API error + content: + application/json: + schema: {$ref: '#/components/schemas/ApiError'} + put: + operationId: auth_auth_update_me + tags: [auth] + summary: auth / auth update me + x-marreq-variants: [server, cloud] + x-marreq-handler: auth::auth_update_me + x-marreq-source: marreq-core/src/api/auth.rs + x-marreq-guards: [OptionalSessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/admin/backup/info: + get: + operationId: backup_info + tags: [backup] + summary: backup / info + x-marreq-variants: [server, cloud] + x-marreq-handler: backup::info + x-marreq-source: marreq-core/src/api/backup.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/admin/backup: + post: + operationId: backup_create + tags: [backup] + summary: backup / create + x-marreq-variants: [server, cloud] + x-marreq-handler: backup::create + x-marreq-source: marreq-core/src/api/backup.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/baselines: + get: + operationId: baselines_list + tags: [baselines] + summary: baselines / list + x-marreq-variants: [server, cloud] + x-marreq-handler: baselines::list + x-marreq-source: marreq-core/src/api/baselines.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: baselines_create + tags: [baselines] + summary: baselines / create + x-marreq-variants: [server, cloud] + x-marreq-handler: baselines::create + x-marreq-source: marreq-core/src/api/baselines.rs + x-marreq-guards: [ProjectBaselinesWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}: + get: + operationId: baselines_get + tags: [baselines] + summary: baselines / get + x-marreq-variants: [server, cloud] + x-marreq-handler: baselines::get + x-marreq-source: marreq-core/src/api/baselines.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}/requirements: + get: + operationId: baselines_get_requirements + tags: [baselines] + summary: baselines / get requirements + x-marreq-variants: [server, cloud] + x-marreq-handler: baselines::get_requirements + x-marreq-source: marreq-core/src/api/baselines.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}/traceability: + get: + operationId: baselines_get_traceability + tags: [baselines] + summary: baselines / get traceability + x-marreq-variants: [server, cloud] + x-marreq-handler: baselines::get_traceability + x-marreq-source: marreq-core/src/api/baselines.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}/verifications: + get: + operationId: baselines_get_verifications + tags: [baselines] + summary: baselines / get verifications + x-marreq-variants: [server, cloud] + x-marreq-handler: baselines::get_verifications + x-marreq-source: marreq-core/src/api/baselines.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/diff: + get: + operationId: baselines_diff_baselines + tags: [baselines] + summary: baselines / diff baselines + x-marreq-variants: [server, cloud] + x-marreq-handler: baselines::diff_baselines + x-marreq-source: marreq-core/src/api/baselines.rs + x-marreq-guards: [ProjectBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/cache/stats: + get: + operationId: cache_stats + tags: [cache] + summary: cache / stats + x-marreq-variants: [server, cloud] + x-marreq-handler: cache::stats + x-marreq-source: marreq-core/src/api/cache.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/cache/clear: + post: + operationId: cache_clear + tags: [cache] + summary: cache / clear + x-marreq-variants: [server, cloud] + x-marreq-handler: cache::clear + x-marreq-source: marreq-core/src/api/cache.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/cache/cleanup: + post: + operationId: cache_cleanup + tags: [cache] + summary: cache / cleanup + x-marreq-variants: [server, cloud] + x-marreq-handler: cache::cleanup + x-marreq-source: marreq-core/src/api/cache.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/cache/performance: + get: + operationId: cache_performance + tags: [cache] + summary: cache / performance + x-marreq-variants: [server, cloud] + x-marreq-handler: cache::performance + x-marreq-source: marreq-core/src/api/cache.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/cache/recommendations: + get: + operationId: cache_recommendations + tags: [cache] + summary: cache / recommendations + x-marreq-variants: [server, cloud] + x-marreq-handler: cache::recommendations + x-marreq-source: marreq-core/src/api/cache.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/cache/reset-counters: + post: + operationId: cache_reset_counters + tags: [cache] + summary: cache / reset counters + x-marreq-variants: [server, cloud] + x-marreq-handler: cache::reset_counters + x-marreq-source: marreq-core/src/api/cache.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/cache/health: + get: + operationId: cache_health + tags: [cache] + summary: cache / health + x-marreq-variants: [server, cloud] + x-marreq-handler: cache::health + x-marreq-source: marreq-core/src/api/cache.rs + x-marreq-guards: [] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + /api/projects/{project_id}/catalog: + get: + operationId: catalog_get + tags: [catalog] + summary: catalog / get + x-marreq-variants: [server, cloud] + x-marreq-handler: catalog::get + x-marreq-source: marreq-core/src/api/catalog.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/categories: + get: + operationId: categories_list + tags: [categories] + summary: categories / list + x-marreq-variants: [server, cloud] + x-marreq-handler: categories::list + x-marreq-source: marreq-core/src/api/categories.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: categories_create + tags: [categories] + summary: categories / create + x-marreq-variants: [server, cloud] + x-marreq-handler: categories::create + x-marreq-source: marreq-core/src/api/categories.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/categories/{id}: + get: + operationId: categories_get + tags: [categories] + summary: categories / get + x-marreq-variants: [server, cloud] + x-marreq-handler: categories::get + x-marreq-source: marreq-core/src/api/categories.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + put: + operationId: categories_update + tags: [categories] + summary: categories / update + x-marreq-variants: [server, cloud] + x-marreq-handler: categories::update + x-marreq-source: marreq-core/src/api/categories.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: categories_delete + tags: [categories] + summary: categories / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: categories::delete + x-marreq-source: marreq-core/src/api/categories.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{requirement_id}/comments: + get: + operationId: comments_list_by_project + tags: [comments] + summary: comments / list by project + x-marreq-variants: [server, cloud] + x-marreq-handler: comments::list_by_project + x-marreq-source: marreq-core/src/api/comments.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: requirement_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: comments_create_by_project + tags: [comments] + summary: comments / create by project + x-marreq-variants: [server, cloud] + x-marreq-handler: comments::create_by_project + x-marreq-source: marreq-core/src/api/comments.rs + x-marreq-guards: [ProjectRequirementsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: requirement_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements/{requirement_id}/comments: + get: + operationId: comments_list + tags: [comments] + summary: comments / list + x-marreq-variants: [server, cloud] + x-marreq-handler: comments::list + x-marreq-source: marreq-core/src/api/comments.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: requirement_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: comments_create + tags: [comments] + summary: comments / create + x-marreq-variants: [server, cloud] + x-marreq-handler: comments::create + x-marreq-source: marreq-core/src/api/comments.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: requirement_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/custom_fields: + get: + operationId: custom_fields_list_by_project + tags: [custom_fields] + summary: custom fields / list by project + x-marreq-variants: [server, cloud] + x-marreq-handler: custom_fields::list_by_project + x-marreq-source: marreq-core/src/api/custom_fields.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: custom_fields_create + tags: [custom_fields] + summary: custom fields / create + x-marreq-variants: [server, cloud] + x-marreq-handler: custom_fields::create + x-marreq-source: marreq-core/src/api/custom_fields.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/custom_fields/{field_id}: + get: + operationId: custom_fields_get + tags: [custom_fields] + summary: custom fields / get + x-marreq-variants: [server, cloud] + x-marreq-handler: custom_fields::get + x-marreq-source: marreq-core/src/api/custom_fields.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: field_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + put: + operationId: custom_fields_update + tags: [custom_fields] + summary: custom fields / update + x-marreq-variants: [server, cloud] + x-marreq-handler: custom_fields::update + x-marreq-source: marreq-core/src/api/custom_fields.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: field_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: custom_fields_delete + tags: [custom_fields] + summary: custom fields / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: custom_fields::delete + x-marreq-source: marreq-core/src/api/custom_fields.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: field_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/dashboard: + get: + operationId: dashboard_dashboard_json + tags: [dashboard] + summary: dashboard / dashboard json + x-marreq-variants: [server, cloud] + x-marreq-handler: dashboard::dashboard_json + x-marreq-source: marreq-core/src/api/dashboard.rs + x-marreq-guards: [OptionalSessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/dsm: + get: + operationId: dsm_get_dsm + tags: [dsm] + summary: dsm / get dsm + x-marreq-variants: [server, cloud] + x-marreq-handler: dsm::get_dsm + x-marreq-source: marreq-core/src/api/dsm.rs + x-marreq-guards: [ProjectRequirementsAndTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/requirements.xlsx: + get: + operationId: exports_export_requirements_xlsx + tags: [exports] + summary: exports / export requirements xlsx + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_requirements_xlsx + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/verifications.xlsx: + get: + operationId: exports_export_verifications_xlsx + tags: [exports] + summary: exports / export verifications xlsx + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_verifications_xlsx + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/matrix.xlsx: + get: + operationId: exports_export_matrix_xlsx + tags: [exports] + summary: exports / export matrix xlsx + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_matrix_xlsx + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/dsm.xlsx: + get: + operationId: exports_export_dsm_xlsx + tags: [exports] + summary: exports / export dsm xlsx + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_dsm_xlsx + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/matrix-links.xlsx: + get: + operationId: exports_export_matrix_links_xlsx + tags: [exports] + summary: exports / export matrix links xlsx + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_matrix_links_xlsx + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/requirements.pdf: + get: + operationId: exports_export_requirements_pdf + tags: [exports] + summary: exports / export requirements pdf + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_requirements_pdf + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/report.pdf: + get: + operationId: exports_export_report_pdf + tags: [exports] + summary: exports / export report pdf + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_report_pdf + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/requirements.reqif: + get: + operationId: exports_export_requirements_reqif + tags: [exports] + summary: exports / export requirements reqif + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_requirements_reqif + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/requirements.reqifz: + get: + operationId: exports_export_requirements_reqifz + tags: [exports] + summary: exports / export requirements reqifz + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_requirements_reqifz + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/baselines/{filename}: + get: + operationId: exports_export_baseline_reqif + tags: [exports] + summary: exports / export baseline reqif + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_baseline_reqif + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: filename + required: true + schema: {type: string} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/bundle.json: + get: + operationId: exports_export_project_bundle + tags: [exports] + summary: exports / export project bundle + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_project_bundle + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/exports/bundle.zip: + get: + operationId: exports_export_project_bundle_zip + tags: [exports] + summary: exports / export project bundle zip + x-marreq-variants: [server, cloud] + x-marreq-handler: exports::export_project_bundle_zip + x-marreq-source: marreq-core/src/api/exports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/groups: + get: + operationId: groups_list + tags: [groups] + summary: groups / list + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::list + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: groups_create + tags: [groups] + summary: groups / create + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::create + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/groups/creatable: + get: + operationId: groups_list_creatable + tags: [groups] + summary: groups / list creatable + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::list_creatable + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/groups/{group_id}: + get: + operationId: groups_get + tags: [groups] + summary: groups / get + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::get + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: + - in: path + name: group_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + patch: + operationId: groups_update + tags: [groups] + summary: groups / update + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::update + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: + - in: path + name: group_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: groups_delete + tags: [groups] + summary: groups / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::delete + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: + - in: path + name: group_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/groups/{group_id}/projects: + get: + operationId: groups_list_projects + tags: [groups] + summary: groups / list projects + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::list_projects + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: + - in: path + name: group_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/groups/{group_id}/members: + get: + operationId: groups_list_members + tags: [groups] + summary: groups / list members + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::list_members + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: + - in: path + name: group_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/groups/{group_id}/members/{user_id}: + put: + operationId: groups_set_member_role + tags: [groups] + summary: groups / set member role + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::set_member_role + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: + - in: path + name: group_id + required: true + schema: {type: integer} + - in: path + name: user_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: groups_remove_member + tags: [groups] + summary: groups / remove member + x-marreq-variants: [server, cloud] + x-marreq-handler: groups::remove_member + x-marreq-source: marreq-core/src/api/groups.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: + - in: path + name: group_id + required: true + schema: {type: integer} + - in: path + name: user_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/home/attention: + get: + operationId: home_attention + tags: [home] + summary: home / attention + x-marreq-variants: [server, cloud] + x-marreq-handler: home::attention + x-marreq-source: marreq-core/src/api/home.rs + x-marreq-guards: [ProjectsRead] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/search: + get: + operationId: home_search + tags: [home] + summary: home / search + x-marreq-variants: [server, cloud] + x-marreq-handler: home::search + x-marreq-source: marreq-core/src/api/home.rs + x-marreq-guards: [SessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/imports/excel/preview: + post: + operationId: imports_preview_excel + tags: [imports] + summary: imports / preview excel + x-marreq-variants: [server, cloud] + x-marreq-handler: imports::preview_excel + x-marreq-source: marreq-core/src/api/imports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/imports/excel: + post: + operationId: imports_commit_excel + tags: [imports] + summary: imports / commit excel + x-marreq-variants: [server, cloud] + x-marreq-handler: imports::commit_excel + x-marreq-source: marreq-core/src/api/imports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/imports/reqif: + post: + operationId: imports_commit_reqif + tags: [imports] + summary: imports / commit reqif + x-marreq-variants: [server, cloud] + x-marreq-handler: imports::commit_reqif + x-marreq-source: marreq-core/src/api/imports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/imports/bundle: + post: + operationId: imports_import_project_bundle + tags: [imports] + summary: imports / import project bundle + x-marreq-variants: [server, cloud] + x-marreq-handler: imports::import_project_bundle + x-marreq-source: marreq-core/src/api/imports.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/admin/logs: + get: + operationId: logs_list + tags: [logs] + summary: logs / list + x-marreq-variants: [server, cloud] + x-marreq-handler: logs::list + x-marreq-source: marreq-core/src/api/logs.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/admin/logs/stats: + get: + operationId: logs_stats + tags: [logs] + summary: logs / stats + x-marreq-variants: [server, cloud] + x-marreq-handler: logs::stats + x-marreq-source: marreq-core/src/api/logs.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/admin/logs/export.json: + get: + operationId: logs_export_json + tags: [logs] + summary: logs / export json + x-marreq-variants: [server, cloud] + x-marreq-handler: logs::export_json + x-marreq-source: marreq-core/src/api/logs.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/admin/logs/cleanup: + post: + operationId: logs_cleanup + tags: [logs] + summary: logs / cleanup + x-marreq-variants: [server, cloud] + x-marreq-handler: logs::cleanup + x-marreq-source: marreq-core/src/api/logs.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/matrix: + get: + operationId: matrix_list + tags: [matrix] + summary: matrix / list + x-marreq-variants: [server, cloud] + x-marreq-handler: matrix::list + x-marreq-source: marreq-core/src/api/matrix.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/matrix: + get: + operationId: matrix_list_by_project + tags: [matrix] + summary: matrix / list by project + x-marreq-variants: [server, cloud] + x-marreq-handler: matrix::list_by_project + x-marreq-source: marreq-core/src/api/matrix.rs + x-marreq-guards: [ProjectTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/verifications/{verification_id}/matrix: + get: + operationId: matrix_get_verification_matrix + tags: [matrix] + summary: matrix / get verification matrix + x-marreq-variants: [server, cloud] + x-marreq-handler: matrix::get_verification_matrix + x-marreq-source: marreq-core/src/api/matrix.rs + x-marreq-guards: [ProjectTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: verification_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + put: + operationId: matrix_put_verification_matrix + tags: [matrix] + summary: matrix / put verification matrix + x-marreq-variants: [server, cloud] + x-marreq-handler: matrix::put_verification_matrix + x-marreq-source: marreq-core/src/api/matrix.rs + x-marreq-guards: [ProjectTraceabilityWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: verification_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/mcp/principal: + get: + operationId: mcp_principal + tags: [mcp] + summary: mcp / principal + x-marreq-variants: [server, cloud] + x-marreq-handler: mcp::principal + x-marreq-source: marreq-core/src/api/mcp.rs + x-marreq-guards: [McpPrincipalAuth] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/McpPrincipalResponse'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + /api/mcp/audit: + post: + operationId: mcp_audit + tags: [mcp] + summary: mcp / audit + x-marreq-variants: [server, cloud] + x-marreq-handler: mcp::audit + x-marreq-source: marreq-core/src/api/mcp.rs + x-marreq-guards: [StdioMcpAuditAuth] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - bearerAuth: [] + x-marreq-authentication-note: Personal API tokens only; delegated OAuth and sessions rejected. + /api/mcp/internal/audit: + post: + operationId: mcp_internal_audit + tags: [mcp] + summary: mcp / internal audit + x-marreq-variants: [server, cloud] + x-marreq-handler: mcp::internal_audit + x-marreq-source: marreq-core/src/api/mcp.rs + x-marreq-guards: [InternalMcpAudit, McpAuditAuth] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - mcpServiceAuth: [] + /api/projects/{project_id}/me/permissions: + get: + operationId: members_get_my_permissions + tags: [members] + summary: members / get my permissions + x-marreq-variants: [server, cloud] + x-marreq-handler: members::get_my_permissions + x-marreq-source: marreq-core/src/api/members.rs + x-marreq-guards: [ProjectProjectsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/members: + get: + operationId: members_list_members + tags: [members] + summary: members / list members + x-marreq-variants: [server, cloud] + x-marreq-handler: members::list_members + x-marreq-source: marreq-core/src/api/members.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/members/{user_id}: + put: + operationId: members_set_member_role + tags: [members] + summary: members / set member role + x-marreq-variants: [server, cloud] + x-marreq-handler: members::set_member_role + x-marreq-source: marreq-core/src/api/members.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: user_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: members_remove_member + tags: [members] + summary: members / remove member + x-marreq-variants: [server, cloud] + x-marreq-handler: members::remove_member + x-marreq-source: marreq-core/src/api/members.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: user_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/reviewers: + get: + operationId: members_list_project_reviewers + tags: [members] + summary: members / list project reviewers + x-marreq-variants: [server, cloud] + x-marreq-handler: members::list_project_reviewers + x-marreq-source: marreq-core/src/api/members.rs + x-marreq-guards: [ProjectProjectsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + put: + operationId: members_put_project_reviewers + tags: [members] + summary: members / put project reviewers + x-marreq-variants: [server, cloud] + x-marreq-handler: members::put_project_reviewers + x-marreq-source: marreq-core/src/api/members.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/: + get: + operationId: meta_api_root + tags: [meta] + summary: meta / api root + x-marreq-variants: [server, cloud] + x-marreq-handler: meta::api_root + x-marreq-source: marreq-core/src/api/meta.rs + x-marreq-guards: [] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/meta/health: + get: + operationId: meta_health + tags: [meta] + summary: meta / health + x-marreq-variants: [server, cloud] + x-marreq-handler: meta::health + x-marreq-source: marreq-core/src/api/meta.rs + x-marreq-guards: [] + security: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: {const: ok, type: string} + service: {const: marreq, type: string} + required: [status, service] + additionalProperties: true + /api/meta/deployment: + get: + operationId: meta_deployment_info + tags: [meta] + summary: meta / deployment info + x-marreq-variants: [server, cloud] + x-marreq-handler: meta::deployment_info + x-marreq-source: marreq-core/src/api/meta.rs + x-marreq-guards: [] + security: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/DeploymentInfo'} + /api/meta/build: + get: + operationId: meta_build_info + tags: [meta] + summary: meta / build info + x-marreq-variants: [server, cloud] + x-marreq-handler: meta::build_info + x-marreq-source: marreq-core/src/api/meta.rs + x-marreq-guards: [] + security: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/BuildInfo'} + /api/notifications: + get: + operationId: notifications_list + tags: [notifications] + summary: notifications / list + x-marreq-variants: [server, cloud] + x-marreq-handler: notifications::list + x-marreq-source: marreq-core/src/api/notifications.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/notifications/unread-count: + get: + operationId: notifications_unread_count + tags: [notifications] + summary: notifications / unread count + x-marreq-variants: [server, cloud] + x-marreq-handler: notifications::unread_count + x-marreq-source: marreq-core/src/api/notifications.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/notifications/{id}/read: + patch: + operationId: notifications_mark_read + tags: [notifications] + summary: notifications / mark read + x-marreq-variants: [server, cloud] + x-marreq-handler: notifications::mark_read + x-marreq-source: marreq-core/src/api/notifications.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/notifications/read-all: + post: + operationId: notifications_mark_all_read + tags: [notifications] + summary: notifications / mark all read + x-marreq-variants: [server, cloud] + x-marreq-handler: notifications::mark_all_read + x-marreq-source: marreq-core/src/api/notifications.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/notifications/preferences: + get: + operationId: notifications_get_preferences + tags: [notifications] + summary: notifications / get preferences + x-marreq-variants: [server, cloud] + x-marreq-handler: notifications::get_preferences + x-marreq-source: marreq-core/src/api/notifications.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/notifications/preferences/{project_id}: + put: + operationId: notifications_set_preference + tags: [notifications] + summary: notifications / set preference + x-marreq-variants: [server, cloud] + x-marreq-handler: notifications::set_preference + x-marreq-source: marreq-core/src/api/notifications.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: notifications_delete_preference + tags: [notifications] + summary: notifications / delete preference + x-marreq-variants: [server, cloud] + x-marreq-handler: notifications::delete_preference + x-marreq-source: marreq-core/src/api/notifications.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /.well-known/oauth-authorization-server: + get: + operationId: oauth_authorization_server_metadata + tags: [oauth] + summary: oauth / authorization server metadata + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::authorization_server_metadata + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + issuer: &id001 {type: string} + authorization_endpoint: *id001 + token_endpoint: *id001 + registration_endpoint: *id001 + response_types_supported: + type: array + items: *id001 + grant_types_supported: + type: array + items: *id001 + code_challenge_methods_supported: + type: array + items: *id001 + scopes_supported: + type: array + items: *id001 + token_endpoint_auth_methods_supported: + type: array + items: *id001 + authorization_response_iss_parameter_supported: {type: boolean} + required: [issuer, authorization_endpoint, token_endpoint, registration_endpoint, response_types_supported, + grant_types_supported, code_challenge_methods_supported, scopes_supported, token_endpoint_auth_methods_supported, + authorization_response_iss_parameter_supported] + additionalProperties: true + description: See implementation and http-api-contract.md for validation rules. + security: [] + /.well-known/oauth-protected-resource: + get: + operationId: oauth_protected_resource_metadata + tags: [oauth] + summary: oauth / protected resource metadata + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::protected_resource_metadata + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + resource: *id001 + authorization_servers: + type: array + items: *id001 + scopes_supported: + type: array + items: *id001 + bearer_methods_supported: + type: array + items: *id001 + required: [resource, authorization_servers, scopes_supported, bearer_methods_supported] + additionalProperties: true + description: See implementation and http-api-contract.md for validation rules. + security: [] + /.well-known/oauth-protected-resource/mcp: + get: + operationId: oauth_protected_resource_metadata_mcp + tags: [oauth] + summary: oauth / protected resource metadata mcp + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::protected_resource_metadata_mcp + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + resource: *id001 + authorization_servers: + type: array + items: *id001 + scopes_supported: + type: array + items: *id001 + bearer_methods_supported: + type: array + items: *id001 + required: [resource, authorization_servers, scopes_supported, bearer_methods_supported] + additionalProperties: true + description: See implementation and http-api-contract.md for validation rules. + security: [] + /oauth/register: + post: + operationId: oauth_register + tags: [oauth] + summary: oauth / register + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::register + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + /oauth/authorize: + get: + operationId: oauth_authorize_page + tags: [oauth] + summary: oauth / authorize page + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::authorize_page + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [OptionalSessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + post: + operationId: oauth_authorize_decision + tags: [oauth] + summary: oauth / authorize decision + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::authorize_decision + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [SessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /oauth/token: + post: + operationId: oauth_token + tags: [oauth] + summary: oauth / token + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::token + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + /api/oauth/grants: + get: + operationId: oauth_grants + tags: [oauth] + summary: oauth / grants + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::grants + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [SessionUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/oauth/grants/{id}: + delete: + operationId: oauth_revoke_grant + tags: [oauth] + summary: oauth / revoke grant + x-marreq-variants: [server, cloud] + x-marreq-handler: oauth::revoke_grant + x-marreq-source: marreq-core/src/api/oauth.rs + x-marreq-guards: [SessionUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects: + post: + operationId: projects_create + tags: [projects] + summary: projects / create + x-marreq-variants: [server, cloud] + x-marreq-handler: projects::create + x-marreq-source: marreq-core/src/api/projects.rs + x-marreq-guards: [ApiUserOrBearer] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + get: + operationId: projects_session_list_for_session + tags: [projects_session] + summary: projects session / list for session + x-marreq-variants: [server, cloud] + x-marreq-handler: projects_session::list_for_session + x-marreq-source: marreq-core/src/api/projects_session.rs + x-marreq-guards: [ProjectsRead] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}: + patch: + operationId: projects_update + tags: [projects] + summary: projects / update + x-marreq-variants: [server, cloud] + x-marreq-handler: projects::update + x-marreq-source: marreq-core/src/api/projects.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: projects_delete + tags: [projects] + summary: projects / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: projects::delete + x-marreq-source: marreq-core/src/api/projects.rs + x-marreq-guards: [ProjectAccess] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/archive: + post: + operationId: projects_archive + tags: [projects] + summary: projects / archive + x-marreq-variants: [server, cloud] + x-marreq-handler: projects::archive + x-marreq-source: marreq-core/src/api/projects.rs + x-marreq-guards: [ProjectAccess] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/unarchive: + post: + operationId: projects_unarchive + tags: [projects] + summary: projects / unarchive + x-marreq-variants: [server, cloud] + x-marreq-handler: projects::unarchive + x-marreq-source: marreq-core/src/api/projects.rs + x-marreq-guards: [ProjectAccess] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/project-from-path/{slug}: + get: + operationId: projects_session_project_from_path + tags: [projects_session] + summary: projects session / project from path + x-marreq-variants: [server, cloud] + x-marreq-handler: projects_session::project_from_path + x-marreq-source: marreq-core/src/api/projects_session.rs + x-marreq-guards: [OptionalSessionUser] + parameters: + - in: path + name: slug + required: true + schema: {type: string} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/reports/types: + get: + operationId: reports_list_types + tags: [reports] + summary: reports / list types + x-marreq-variants: [server, cloud] + x-marreq-handler: reports::list_types + x-marreq-source: marreq-core/src/api/reports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/report_templates: + get: + operationId: reports_list_templates + tags: [reports] + summary: reports / list templates + x-marreq-variants: [server, cloud] + x-marreq-handler: reports::list_templates + x-marreq-source: marreq-core/src/api/reports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: reports_create_template + tags: [reports] + summary: reports / create template + x-marreq-variants: [server, cloud] + x-marreq-handler: reports::create_template + x-marreq-source: marreq-core/src/api/reports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/report_templates/{template_id}: + get: + operationId: reports_get_template + tags: [reports] + summary: reports / get template + x-marreq-variants: [server, cloud] + x-marreq-handler: reports::get_template + x-marreq-source: marreq-core/src/api/reports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: template_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + patch: + operationId: reports_update_template + tags: [reports] + summary: reports / update template + x-marreq-variants: [server, cloud] + x-marreq-handler: reports::update_template + x-marreq-source: marreq-core/src/api/reports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: template_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: reports_delete_template + tags: [reports] + summary: reports / delete template + x-marreq-variants: [server, cloud] + x-marreq-handler: reports::delete_template + x-marreq-source: marreq-core/src/api/reports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: template_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/reports/{file}: + post: + operationId: reports_generate + tags: [reports] + summary: reports / generate + x-marreq-variants: [server, cloud] + x-marreq-handler: reports::generate + x-marreq-source: marreq-core/src/api/reports.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: file + required: true + schema: {type: string} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements/{req_id}/versions/{v1}/diff/{v2}: + get: + operationId: requirement_diff_diff_versions + tags: [requirement_diff] + summary: requirement diff / diff versions + x-marreq-variants: [server, cloud] + x-marreq-handler: requirement_diff::diff_versions + x-marreq-source: marreq-core/src/api/requirement_diff.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: req_id + required: true + schema: {type: integer} + - in: path + name: v1 + required: true + schema: {type: integer} + - in: path + name: v2 + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{req_id}/versions/{v1}/diff/{v2}: + get: + operationId: requirement_diff_diff_versions_by_project + tags: [requirement_diff] + summary: requirement diff / diff versions by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirement_diff::diff_versions_by_project + x-marreq-source: marreq-core/src/api/requirement_diff.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: req_id + required: true + schema: {type: integer} + - in: path + name: v1 + required: true + schema: {type: integer} + - in: path + name: v2 + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}/requirements/{req_id}/diff/current: + get: + operationId: requirement_diff_diff_baseline_vs_current + tags: [requirement_diff] + summary: requirement diff / diff baseline vs current + x-marreq-variants: [server, cloud] + x-marreq-handler: requirement_diff::diff_baseline_vs_current + x-marreq-source: marreq-core/src/api/requirement_diff.rs + x-marreq-guards: [ProjectRequirementsAndBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + - in: path + name: req_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirement-version-links: + post: + operationId: requirement_version_links_create + tags: [requirement_version_links] + summary: requirement version links / create + x-marreq-variants: [server, cloud] + x-marreq-handler: requirement_version_links::create + x-marreq-source: marreq-core/src/api/requirement_version_links.rs + x-marreq-guards: [ProjectTraceabilityWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + get: + operationId: requirement_version_links_list + tags: [requirement_version_links] + summary: requirement version links / list + x-marreq-variants: [server, cloud] + x-marreq-handler: requirement_version_links::list + x-marreq-source: marreq-core/src/api/requirement_version_links.rs + x-marreq-guards: [ProjectTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirement-version-links/{link_id}: + delete: + operationId: requirement_version_links_delete + tags: [requirement_version_links] + summary: requirement version links / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: requirement_version_links::delete + x-marreq-source: marreq-core/src/api/requirement_version_links.rs + x-marreq-guards: [ProjectTraceabilityWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: link_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirement-version-links/link-types: + get: + operationId: requirement_version_links_link_types + tags: [requirement_version_links] + summary: requirement version links / link types + x-marreq-variants: [server, cloud] + x-marreq-handler: requirement_version_links::link_types + x-marreq-source: marreq-core/src/api/requirement_version_links.rs + x-marreq-guards: [ProjectTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements: + get: + operationId: requirements_list + tags: [requirements] + summary: requirements / list + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::list + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: requirements_create + tags: [requirements] + summary: requirements / create + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::create + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements: + get: + operationId: requirements_list_by_project + tags: [requirements] + summary: requirements / list by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::list_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: requirements_create_by_project + tags: [requirements] + summary: requirements / create by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::create_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements/{id}: + get: + operationId: requirements_get + tags: [requirements] + summary: requirements / get + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::get + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: requirements_delete + tags: [requirements] + summary: requirements / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::delete + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + patch: + operationId: requirements_patch_requirement + tags: [requirements] + summary: requirements / patch requirement + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::patch_requirement + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{id}: + get: + operationId: requirements_get_by_project + tags: [requirements] + summary: requirements / get by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::get_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + patch: + operationId: requirements_patch_by_project + tags: [requirements] + summary: requirements / patch by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::patch_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements/{id}/versions: + get: + operationId: requirements_list_versions + tags: [requirements] + summary: requirements / list versions + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::list_versions + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{id}/versions: + get: + operationId: requirements_list_versions_by_project + tags: [requirements] + summary: requirements / list versions by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::list_versions_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements/{req_id}/versions/{version_id}: + get: + operationId: requirements_get_version + tags: [requirements] + summary: requirements / get version + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::get_version + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: req_id + required: true + schema: {type: integer} + - in: path + name: version_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{req_id}/versions/{version_id}: + get: + operationId: requirements_get_version_by_project + tags: [requirements] + summary: requirements / get version by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::get_version_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: req_id + required: true + schema: {type: integer} + - in: path + name: version_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements/{id}/impacted_tests: + get: + operationId: requirements_get_impacted_tests + tags: [requirements] + summary: requirements / get impacted tests + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::get_impacted_tests + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{id}/impacted_tests: + get: + operationId: requirements_get_impacted_tests_by_project + tags: [requirements] + summary: requirements / get impacted tests by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::get_impacted_tests_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsAndVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/requirements/{req_id}/versions/{version_id}/approval: + put: + operationId: requirements_set_version_approval + tags: [requirements] + summary: requirements / set version approval + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::set_version_approval + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: req_id + required: true + schema: {type: integer} + - in: path + name: version_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{req_id}/versions/{version_id}/approval: + put: + operationId: requirements_set_version_approval_by_project + tags: [requirements] + summary: requirements / set version approval by project + x-marreq-variants: [server, cloud] + x-marreq-handler: requirements::set_version_approval_by_project + x-marreq-source: marreq-core/src/api/requirements.rs + x-marreq-guards: [ProjectRequirementsApprove] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: req_id + required: true + schema: {type: integer} + - in: path + name: version_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/saved_views: + get: + operationId: saved_views_list + tags: [saved_views] + summary: saved views / list + x-marreq-variants: [server, cloud] + x-marreq-handler: saved_views::list + x-marreq-source: marreq-core/src/api/saved_views.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: saved_views_create + tags: [saved_views] + summary: saved views / create + x-marreq-variants: [server, cloud] + x-marreq-handler: saved_views::create + x-marreq-source: marreq-core/src/api/saved_views.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/saved_views/{view_id}: + get: + operationId: saved_views_get + tags: [saved_views] + summary: saved views / get + x-marreq-variants: [server, cloud] + x-marreq-handler: saved_views::get + x-marreq-source: marreq-core/src/api/saved_views.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: view_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + patch: + operationId: saved_views_update + tags: [saved_views] + summary: saved views / update + x-marreq-variants: [server, cloud] + x-marreq-handler: saved_views::update + x-marreq-source: marreq-core/src/api/saved_views.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: view_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: saved_views_delete + tags: [saved_views] + summary: saved views / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: saved_views::delete + x-marreq-source: marreq-core/src/api/saved_views.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: view_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirements/semantic_search: + get: + operationId: semantic_search_semantic_search + tags: [semantic_search] + summary: semantic search / semantic search + x-marreq-variants: [server, cloud] + x-marreq-handler: semantic_search::semantic_search + x-marreq-source: marreq-core/src/api/semantic_search.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirements/ask: + post: + operationId: semantic_search_ask + tags: [semantic_search] + summary: semantic search / ask + x-marreq-variants: [server, cloud] + x-marreq-handler: semantic_search::ask + x-marreq-source: marreq-core/src/api/semantic_search.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirements/reindex: + post: + operationId: semantic_search_reindex + tags: [semantic_search] + summary: semantic search / reindex + x-marreq-variants: [server, cloud] + x-marreq-handler: semantic_search::reindex + x-marreq-source: marreq-core/src/api/semantic_search.rs + x-marreq-guards: [AdminOnly, ProjectAccess] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/index_status: + get: + operationId: semantic_search_index_status + tags: [semantic_search] + summary: semantic search / index status + x-marreq-variants: [server, cloud] + x-marreq-handler: semantic_search::index_status + x-marreq-source: marreq-core/src/api/semantic_search.rs + x-marreq-guards: [ProjectAccess] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/semantic_search/status: + get: + operationId: semantic_search_search_status + tags: [semantic_search] + summary: semantic search / search status + x-marreq-variants: [server, cloud] + x-marreq-handler: semantic_search::search_status + x-marreq-source: marreq-core/src/api/semantic_search.rs + x-marreq-guards: [ProjectAccess] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/status: + get: + operationId: status_list_requirement_statuses + tags: [status] + summary: status / list requirement statuses + x-marreq-variants: [server, cloud] + x-marreq-handler: status::list_requirement_statuses + x-marreq-source: marreq-core/src/api/status.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: status_create_requirement_status + tags: [status] + summary: status / create requirement status + x-marreq-variants: [server, cloud] + x-marreq-handler: status::create_requirement_status + x-marreq-source: marreq-core/src/api/status.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/status/{id}: + get: + operationId: status_get_requirement_status + tags: [status] + summary: status / get requirement status + x-marreq-variants: [server, cloud] + x-marreq-handler: status::get_requirement_status + x-marreq-source: marreq-core/src/api/status.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + put: + operationId: status_update_requirement_status + tags: [status] + summary: status / update requirement status + x-marreq-variants: [server, cloud] + x-marreq-handler: status::update_requirement_status + x-marreq-source: marreq-core/src/api/status.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: status_delete_requirement_status + tags: [status] + summary: status / delete requirement status + x-marreq-variants: [server, cloud] + x-marreq-handler: status::delete_requirement_status + x-marreq-source: marreq-core/src/api/status.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/requirements/{id}/trace_up: + get: + operationId: traceability_trace_up + tags: [traceability] + summary: traceability / trace up + x-marreq-variants: [server, cloud] + x-marreq-handler: traceability::trace_up + x-marreq-source: marreq-core/src/api/traceability.rs + x-marreq-guards: [ProjectRequirementsAndTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirements/{id}/trace_down: + get: + operationId: traceability_trace_down + tags: [traceability] + summary: traceability / trace down + x-marreq-variants: [server, cloud] + x-marreq-handler: traceability::trace_down + x-marreq-source: marreq-core/src/api/traceability.rs + x-marreq-guards: [ProjectRequirementsVerificationsAndTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/coverage_report: + get: + operationId: traceability_coverage_report + tags: [traceability] + summary: traceability / coverage report + x-marreq-variants: [server, cloud] + x-marreq-handler: traceability::coverage_report + x-marreq-source: marreq-core/src/api/traceability.rs + x-marreq-guards: [ProjectTraceabilityRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/traceability/clear_suspect: + post: + operationId: traceability_clear_suspect + tags: [traceability] + summary: traceability / clear suspect + x-marreq-variants: [server, cloud] + x-marreq-handler: traceability::clear_suspect + x-marreq-source: marreq-core/src/api/traceability.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/traceability/clear_suspect: + post: + operationId: traceability_clear_suspect_by_project + tags: [traceability] + summary: traceability / clear suspect by project + x-marreq-variants: [server, cloud] + x-marreq-handler: traceability::clear_suspect_by_project + x-marreq-source: marreq-core/src/api/traceability.rs + x-marreq-guards: [ProjectTraceabilityWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/use-cases: + get: + operationId: use_cases_list + tags: [use_cases] + summary: use cases / list + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::list + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: query + name: q + required: false + schema: + anyOf: + - {type: string} + - {type: 'null'} + - in: query + name: approval_state + required: false + schema: + anyOf: + - {type: string} + - {type: 'null'} + - in: query + name: actor + required: false + schema: + anyOf: + - {type: string} + - {type: 'null'} + - in: query + name: has_suspect + required: false + schema: + anyOf: + - {type: boolean} + - {type: 'null'} + - in: query + name: requirement_id + required: false + schema: + anyOf: + - {type: integer} + - {type: 'null'} + - in: query + name: verification_id + required: false + schema: + anyOf: + - {type: integer} + - {type: 'null'} + - in: query + name: sort_column + required: false + schema: + anyOf: + - {type: string} + - {type: 'null'} + - in: query + name: sort_dir + required: false + schema: + anyOf: + - {type: string} + - {type: 'null'} + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: {$ref: '#/components/schemas/use_cases_UseCaseRow_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + post: + operationId: use_cases_create + tags: [use_cases] + summary: use cases / create + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::create + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: header + name: Idempotency-Key + required: false + schema: &id002 {type: string} + description: Safe replay for the same principal, target, operation and payload; changed payload returns + 409. + responses: + '201': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_UseCaseDetail_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + description: Create the shared draft; optional reference_code, title, primary_actor and document. A missing + code is generated. Supports Idempotency-Key. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:write'] + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reference_code: + type: [string, 'null'] + title: + type: [string, 'null'] + primary_actor: + type: [string, 'null'] + document: + anyOf: + - {$ref: '#/components/schemas/UseCaseDocument'} + - {type: 'null'} + additionalProperties: true + /api/projects/{project_id}/use-cases/next-code: + get: + operationId: use_cases_next_code + tags: [use_cases] + summary: use cases / next code + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::next_code + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_NextCode_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}: + get: + operationId: use_cases_get + tags: [use_cases] + summary: use cases / get + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::get + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: query + name: version_id + required: false + schema: + anyOf: + - {type: integer} + - {type: 'null'} + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_UseCaseDetail_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + delete: + operationId: use_cases_delete + tags: [use_cases] + summary: use cases / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::delete + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: header + name: X-CSRF-Token + required: false + schema: {type: string} + description: Required for session mutations; Bearer requests use scoped authentication instead. + responses: + '204': {description: Deleted} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + x-marreq-bearer-scopes: ['use_cases:write'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}/draft: + get: + operationId: use_cases_get_draft + tags: [use_cases] + summary: use cases / get draft + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::get_draft + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_DraftView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + put: + operationId: use_cases_put_draft + tags: [use_cases] + summary: use cases / put draft + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::put_draft + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: header + name: X-CSRF-Token + required: false + schema: {type: string} + description: Required for session mutations; Bearer requests use scoped authentication instead. + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_DraftChangeView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_PutDraftRequest_request'} + x-marreq-bearer-scopes: ['use_cases:write'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + delete: + operationId: use_cases_discard_draft + tags: [use_cases] + summary: use cases / discard draft + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::discard_draft + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: header + name: X-CSRF-Token + required: false + schema: {type: string} + description: Required for session mutations; Bearer requests use scoped authentication instead. + responses: + '204': {description: Deleted} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + x-marreq-bearer-scopes: ['use_cases:write'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}/draft/operations: + post: + operationId: use_cases_draft_operations + tags: [use_cases] + summary: use cases / draft operations + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::draft_operations + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_DraftChangeView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, + idempotency, publishing, links and review permissions. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:write'] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + base_revision: + anyOf: + - &id003 {type: integer} + - {type: 'null'} + operations: + type: array + items: {$ref: '#/components/schemas/UseCaseOperation'} + additionalProperties: true + required: [operations] + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + /api/projects/{project_id}/use-cases/{id}/draft/publish: + post: + operationId: use_cases_publish + tags: [use_cases] + summary: use cases / publish + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::publish + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: header + name: Idempotency-Key + required: false + schema: *id002 + description: Safe replay for the same principal, target, operation and payload; changed payload returns + 409. + responses: + '201': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/UseCasePublishResponse'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, + idempotency, publishing, links and review permissions. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:write'] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + base_revision: *id003 + change_note: + anyOf: + - *id002 + - {type: 'null'} + additionalProperties: true + required: [base_revision] + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + /api/projects/{project_id}/use-cases/{id}/versions: + get: + operationId: use_cases_list_versions + tags: [use_cases] + summary: use cases / list versions + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::list_versions + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: {$ref: '#/components/schemas/use_cases_VersionSummary_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}/versions/{version_id}: + get: + operationId: use_cases_get_version + tags: [use_cases] + summary: use cases / get version + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::get_version + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: path + name: version_id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_VersionView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}/versions/{version_id}/approval: + put: + operationId: use_cases_set_version_approval + tags: [use_cases] + summary: use cases / set version approval + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::set_version_approval + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesApprove] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: path + name: version_id + required: true + schema: {type: integer} + - in: header + name: X-CSRF-Token + required: false + schema: {type: string} + description: Required for session mutations; Bearer requests use scoped authentication instead. + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_VersionView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_SetApprovalRequest_request'} + x-marreq-bearer-scopes: ['use_cases:approve'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}/links: + get: + operationId: use_cases_list_links + tags: [use_cases] + summary: use cases / list links + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::list_links + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: {$ref: '#/components/schemas/use_cases_LinkView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + post: + operationId: use_cases_create_link + tags: [use_cases] + summary: use cases / create link + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::create_link + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + '201': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_LinkView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, + idempotency, publishing, links and review permissions. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:write'] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + element_id: + anyOf: + - *id002 + - {type: 'null'} + requirement_id: + anyOf: + - *id003 + - {type: 'null'} + verification_id: + anyOf: + - *id003 + - {type: 'null'} + rationale: + anyOf: + - *id002 + - {type: 'null'} + additionalProperties: true + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + /api/projects/{project_id}/use-cases/{id}/links/{link_id}: + delete: + operationId: use_cases_delete_link + tags: [use_cases] + summary: use cases / delete link + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::delete_link + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: path + name: link_id + required: true + schema: {type: integer} + - in: header + name: X-CSRF-Token + required: false + schema: {type: string} + description: Required for session mutations; Bearer requests use scoped authentication instead. + responses: + '204': {description: Deleted} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + x-marreq-bearer-scopes: ['use_cases:write'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}/links/{link_id}/clear_suspect: + post: + operationId: use_cases_clear_link_suspect + tags: [use_cases] + summary: use cases / clear link suspect + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::clear_link_suspect + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: path + name: link_id + required: true + schema: {type: integer} + - in: header + name: X-CSRF-Token + required: false + schema: {type: string} + description: Required for session mutations; Bearer requests use scoped authentication instead. + responses: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_ClearSuspectResponse_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + x-marreq-bearer-scopes: ['use_cases:write'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/requirements/{requirement_id}/use-cases: + get: + operationId: use_cases_requirement_use_cases + tags: [use_cases] + summary: use cases / requirement use cases + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::requirement_use_cases + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: requirement_id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: {$ref: '#/components/schemas/use_cases_UseCaseRef_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/verifications/{verification_id}/use-cases: + get: + operationId: use_cases_verification_use_cases + tags: [use_cases] + summary: use cases / verification use cases + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::verification_use_cases + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: verification_id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: {$ref: '#/components/schemas/use_cases_UseCaseRef_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/use-cases/{id}/draft/diff: + get: + operationId: use_cases_draft_diff + tags: [use_cases] + summary: use cases / draft diff + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::draft_diff + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: query + name: include_unchanged + required: false + schema: &id004 {type: boolean} responses: '200': - description: Public provider names only; no credentials or protocol secrets + description: Success content: application/json: - schema: - type: object - required: [password_enabled, external] - properties: - password_enabled: - type: boolean - external: - type: array - items: - type: object - required: [id, display_name] - properties: - id: { type: string } - display_name: { type: string } - /auth/external/{provider}/start: + schema: {$ref: '#/components/schemas/UseCaseDiff'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, + idempotency, publishing, links and review permissions. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:read'] + /api/projects/{project_id}/use-cases/{id}/versions/{a}/diff/{b}: get: - summary: Begin an external Authorization Code + PKCE login - operationId: authExternalStart + operationId: use_cases_version_diff + tags: [use_cases] + summary: use cases / version diff + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::version_diff + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] parameters: - - in: path - name: provider - required: true - schema: { type: string } - - in: query - name: return_to - schema: { type: string, example: /projects } + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: path + name: a + required: true + schema: {type: integer} + - in: path + name: b + required: true + schema: {type: integer} + - in: query + name: include_unchanged + required: false + schema: *id004 responses: - '303': { description: Redirect to the provider } - '400': { $ref: '#/components/responses/ApiError' } - '404': { $ref: '#/components/responses/ApiError' } - /auth/external/{provider}/callback: + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/UseCaseDiff'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, + idempotency, publishing, links and review permissions. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:read'] + /api/projects/{project_id}/use-cases/{id}/comments: get: - summary: Consume a single-use external authentication callback - operationId: authExternalCallback + operationId: use_cases_list_comments + tags: [use_cases] + summary: use cases / list comments + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::list_comments + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] parameters: - - in: path - name: provider - required: true - schema: { type: string } - - in: query - name: code - schema: { type: string } - - in: query - name: state - schema: { type: string } - responses: - '303': { description: Login or link completed; redirect to the local return path } - '400': { $ref: '#/components/responses/ApiError' } - /auth/csrf: - get: - summary: Get or mint CSRF token - operationId: authCsrf + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: query + name: version_id + required: false + schema: + anyOf: + - {type: integer} + - {type: 'null'} + - in: query + name: element_id + required: false + schema: + anyOf: + - {type: string} + - {type: 'null'} responses: '200': - description: CSRF token for X-CSRF-Token header + description: Success content: application/json: schema: - type: object - required: [csrf_token] - properties: - csrf_token: - type: string - /auth/login: + type: array + items: {$ref: '#/components/schemas/use_cases_UseCaseCommentView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. post: - summary: Session login (JSON) - operationId: authLogin + operationId: use_cases_create_comment + tags: [use_cases] + summary: use cases / create comment + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::create_comment + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesWrite] parameters: - - in: header - name: X-CSRF-Token - required: true - schema: - type: string + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: header + name: Idempotency-Key + required: false + schema: *id002 + description: Safe replay for the same principal, target, operation and payload; changed payload returns + 409. + responses: + '201': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_UseCaseCommentView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, + idempotency, publishing, links and review permissions. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:write'] requestBody: required: true content: application/json: schema: type: object - required: [username, password] properties: - username: - type: string - password: - type: string + body: *id002 + use_case_version_id: + anyOf: + - *id003 + - {type: 'null'} + element_id: + anyOf: + - *id002 + - {type: 'null'} + additionalProperties: true + required: [body] + x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow + trusted Origin/Referer fallback + /api/projects/{project_id}/baselines/{baseline_id}/use-cases: + get: + operationId: use_cases_baseline_use_cases + tags: [use_cases] + summary: use cases / baseline use cases + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::baseline_use_cases + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + security: + - cookieAuth: [] + - bearerAuth: [] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: {$ref: '#/components/schemas/use_cases_BaselineUseCaseView_response'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty + or framework generated; do not require an ApiError body.'} + '500': {$ref: '#/components/responses/ApiError'} + x-marreq-bearer-scopes: ['use_cases:read'] + description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review + permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict + semantics. + /api/projects/{project_id}/baselines/{baseline_id}/use-cases/{id}/diff/current: + get: + operationId: use_cases_baseline_use_case_diff + tags: [use_cases] + summary: use cases / baseline use case diff + x-marreq-variants: [server, cloud] + x-marreq-handler: use_cases::baseline_use_case_diff + x-marreq-source: marreq-core/src/api/use_cases.rs + x-marreq-guards: [ProjectUseCasesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: query + name: include_unchanged + required: false + schema: {type: boolean} responses: '200': - description: Logged in; Set-Cookie session + csrf + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/UseCaseDiff'} + '400': {$ref: '#/components/responses/ApiError'} + '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '404': {$ref: '#/components/responses/ApiError'} + '409': {$ref: '#/components/responses/ApiError'} + '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, + idempotency, publishing, links and review permissions. + security: + - cookieAuth: [] + - bearerAuth: [] + x-marreq-bearer-scopes: ['use_cases:read'] + /api/users: + get: + operationId: users_list + tags: [users] + summary: users / list + x-marreq-variants: [server, cloud] + x-marreq-handler: users::list + x-marreq-source: marreq-core/src/api/users.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: users_create + tags: [users] + summary: users / create + x-marreq-variants: [server, cloud] + x-marreq-handler: users::create + x-marreq-source: marreq-core/src/api/users.rs + x-marreq-guards: [AdminOnly] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/users/{id}: + get: + operationId: users_get + tags: [users] + summary: users / get + x-marreq-variants: [server, cloud] + x-marreq-handler: users::get + x-marreq-source: marreq-core/src/api/users.rs + x-marreq-guards: [AdminOnly] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + put: + operationId: users_update + tags: [users] + summary: users / update + x-marreq-variants: [server, cloud] + x-marreq-handler: users::update + x-marreq-source: marreq-core/src/api/users.rs + x-marreq-guards: [AdminOnly] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: users_delete + tags: [users] + summary: users / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: users::delete + x-marreq-source: marreq-core/src/api/users.rs + x-marreq-guards: [AdminOnly] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/users/{id}/password: + put: + operationId: users_set_password + tags: [users] + summary: users / set password + x-marreq-variants: [server, cloud] + x-marreq-handler: users::set_password + x-marreq-source: marreq-core/src/api/users.rs + x-marreq-guards: [AdminOnly] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/verifications/{verification_id}/control: + get: + operationId: verification_control_get_verification_control + tags: [verification_control] + summary: verification control / get verification control + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_control::get_verification_control + x-marreq-source: marreq-core/src/api/verification_control.rs + x-marreq-guards: [ProjectVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: verification_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + put: + operationId: verification_control_put_verification_control + tags: [verification_control] + summary: verification control / put verification control + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_control::put_verification_control + x-marreq-source: marreq-core/src/api/verification_control.rs + x-marreq-guards: [ProjectVerificationsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: verification_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirements/{requirement_id}/close_out: + get: + operationId: verification_control_get_requirement_close_out + tags: [verification_control] + summary: verification control / get requirement close out + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_control::get_requirement_close_out + x-marreq-source: marreq-core/src/api/verification_control.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: requirement_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/close_out: + get: + operationId: verification_control_get_project_close_out + tags: [verification_control] + summary: verification control / get project close out + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_control::get_project_close_out + x-marreq-source: marreq-core/src/api/verification_control.rs + x-marreq-guards: [ProjectRequirementsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/requirements/{requirement_id}/compliance: + put: + operationId: verification_control_put_requirement_compliance + tags: [verification_control] + summary: verification control / put requirement compliance + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_control::put_requirement_compliance + x-marreq-source: marreq-core/src/api/verification_control.rs + x-marreq-guards: [ProjectRequirementsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: requirement_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/verifications/{id}/snapshots: + get: + operationId: verification_diff_list_snapshots_by_project + tags: [verification_diff] + summary: verification diff / list snapshots by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_diff::list_snapshots_by_project + x-marreq-source: marreq-core/src/api/verification_diff.rs + x-marreq-guards: [ProjectVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/verifications/{id}/snapshots/{v1}/diff/{v2}: + get: + operationId: verification_diff_diff_snapshots_by_project + tags: [verification_diff] + summary: verification diff / diff snapshots by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_diff::diff_snapshots_by_project + x-marreq-source: marreq-core/src/api/verification_diff.rs + x-marreq-guards: [ProjectVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + - in: path + name: v1 + required: true + schema: {type: integer} + - in: path + name: v2 + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/baselines/{baseline_id}/verifications/{id}/diff/current: + get: + operationId: verification_diff_diff_baseline_vs_current + tags: [verification_diff] + summary: verification diff / diff baseline vs current + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_diff::diff_baseline_vs_current + x-marreq-source: marreq-core/src/api/verification_diff.rs + x-marreq-guards: [ProjectVerificationsAndBaselinesRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: baseline_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/verification-methods: + get: + operationId: verification_methods_list_by_project + tags: [verification_methods] + summary: verification methods / list by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_methods::list_by_project + x-marreq-source: marreq-core/src/api/verification_methods.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: verification_methods_create_by_project + tags: [verification_methods] + summary: verification methods / create by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_methods::create_by_project + x-marreq-source: marreq-core/src/api/verification_methods.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/verification-methods/{method_id}: + put: + operationId: verification_methods_update_by_project + tags: [verification_methods] + summary: verification methods / update by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_methods::update_by_project + x-marreq-source: marreq-core/src/api/verification_methods.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: method_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + delete: + operationId: verification_methods_delete_by_project + tags: [verification_methods] + summary: verification methods / delete by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_methods::delete_by_project + x-marreq-source: marreq-core/src/api/verification_methods.rs + x-marreq-guards: [ProjectAccessOrBearer] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: method_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/verification-status: + get: + operationId: verification_status_list_verification_statuses + tags: [verification_status] + summary: verification status / list verification statuses + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_status::list_verification_statuses + x-marreq-source: marreq-core/src/api/verification_status.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: verification_status_create_verification_status + tags: [verification_status] + summary: verification status / create verification status + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_status::create_verification_status + x-marreq-source: marreq-core/src/api/verification_status.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/verification-status/{id}: + get: + operationId: verification_status_get_verification_status + tags: [verification_status] + summary: verification status / get verification status + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_status::get_verification_status + x-marreq-source: marreq-core/src/api/verification_status.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + put: + operationId: verification_status_update_verification_status + tags: [verification_status] + summary: verification status / update verification status + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_status::update_verification_status + x-marreq-source: marreq-core/src/api/verification_status.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: verification_status_delete_verification_status + tags: [verification_status] + summary: verification status / delete verification status + x-marreq-variants: [server, cloud] + x-marreq-handler: verification_status::delete_verification_status + x-marreq-source: marreq-core/src/api/verification_status.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/verifications: + get: + operationId: verifications_list + tags: [verifications] + summary: verifications / list + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::list + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + post: + operationId: verifications_create + tags: [verifications] + summary: verifications / create + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::create + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ApiUser] + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/verifications: + get: + operationId: verifications_list_by_project + tags: [verifications] + summary: verifications / list by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::list_by_project + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ProjectVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + post: + operationId: verifications_create_by_project + tags: [verifications] + summary: verifications / create by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::create_by_project + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ProjectVerificationsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/projects/{project_id}/verifications/{id}: + get: + operationId: verifications_get_by_project + tags: [verifications] + summary: verifications / get by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::get_by_project + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ProjectVerificationsRead] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + put: + operationId: verifications_update_by_project + tags: [verifications] + summary: verifications / update by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::update_by_project + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ProjectVerificationsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/verifications/{id}: + get: + operationId: verifications_get + tags: [verifications] + summary: verifications / get + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::get + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + delete: + operationId: verifications_delete + tags: [verifications] + summary: verifications / delete + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::delete + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/verifications/{id}/field: + post: + operationId: verifications_update_field + tags: [verifications] + summary: verifications / update field + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::update_field + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ApiUser] + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/verifications/{id}/field: + post: + operationId: verifications_update_field_by_project + tags: [verifications] + summary: verifications / update field by project + x-marreq-variants: [server, cloud] + x-marreq-handler: verifications::update_field_by_project + x-marreq-source: marreq-core/src/api/verifications.rs + x-marreq-guards: [ProjectVerificationsWrite] + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/auth/register: + post: + operationId: auth_public_auth_register + tags: [auth_public] + summary: auth public / auth register + x-marreq-variants: [cloud] + x-marreq-handler: auth_public::auth_register + x-marreq-source: marreq-cloud/src/api/auth_public.rs + x-marreq-guards: [] + parameters: [] + responses: + '201': + description: Success content: application/json: schema: type: object properties: - status: - type: string - example: ok - user: - type: object - '400': - $ref: '#/components/responses/ApiError' - /auth/logout: - post: - summary: End session - operationId: authLogout - parameters: - - in: header - name: X-CSRF-Token - required: true - schema: - type: string - responses: + status: {type: string, const: ok} + required: [status] + additionalProperties: true '200': - description: Logged out + description: Success content: application/json: schema: type: object properties: - status: - type: string - example: ok - /auth/me: + status: {type: string, const: ok} + required: [status] + additionalProperties: true + '400': {$ref: '#/components/responses/ApiError'} + '500': {$ref: '#/components/responses/ApiError'} + description: Cloud-only public account flow. Registration returns 201 for creation or 200 for an existing + email, preventing enumeration. Forgot-password always reports success. CSRF fairing rules still apply. + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: *id001 + name: *id001 + email: *id001 + password: *id001 + required: [username, name, email, password] + additionalProperties: true + /api/auth/verify-email: get: - summary: Current user (session) - operationId: authMe - security: - - cookieAuth: [] + operationId: auth_public_auth_verify_email + tags: [auth_public] + summary: auth public / auth verify email + x-marreq-variants: [cloud] + x-marreq-handler: auth_public::auth_verify_email + x-marreq-source: marreq-cloud/src/api/auth_public.rs + x-marreq-guards: [] + parameters: + - in: query + name: token + required: true + schema: *id001 responses: '200': - description: Current user + description: Success content: application/json: schema: type: object - '401': - $ref: '#/components/responses/ApiError' - /dashboard: - get: - summary: SPA dashboard payload (user + decorated projects + CSRF) - operationId: dashboardJson - security: - - cookieAuth: [] + properties: + status: {type: string, const: ok} + required: [status] + additionalProperties: true + '400': {$ref: '#/components/responses/ApiError'} + '500': {$ref: '#/components/responses/ApiError'} + description: Cloud-only public account flow. Registration returns 201 for creation or 200 for an existing + email, preventing enumeration. Forgot-password always reports success. CSRF fairing rules still apply. + security: [] + /api/auth/forgot-password: + post: + operationId: auth_public_auth_forgot_password + tags: [auth_public] + summary: auth public / auth forgot password + x-marreq-variants: [cloud] + x-marreq-handler: auth_public::auth_forgot_password + x-marreq-source: marreq-cloud/src/api/auth_public.rs + x-marreq-guards: [] + parameters: [] responses: '200': - description: Dashboard context for the home view + description: Success content: application/json: schema: type: object - '401': - $ref: '#/components/responses/ApiError' - /projects: - get: - summary: List projects for session user - operationId: listProjectsSession - security: - - cookieAuth: [] + properties: + status: {type: string, const: ok} + required: [status] + additionalProperties: true + '400': {$ref: '#/components/responses/ApiError'} + '500': {$ref: '#/components/responses/ApiError'} + description: Cloud-only public account flow. Registration returns 201 for creation or 200 for an existing + email, preventing enumeration. Forgot-password always reports success. CSRF fairing rules still apply. + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: *id001 + required: [email] + additionalProperties: true + /api/auth/reset-password: + post: + operationId: auth_public_auth_reset_password + tags: [auth_public] + summary: auth public / auth reset password + x-marreq-variants: [cloud] + x-marreq-handler: auth_public::auth_reset_password + x-marreq-source: marreq-cloud/src/api/auth_public.rs + x-marreq-guards: [] + parameters: [] responses: '200': - description: Project list + description: Success content: application/json: schema: - type: array - items: - type: object - '401': - $ref: '#/components/responses/ApiError' - + type: object + properties: + status: {type: string, const: ok} + required: [status] + additionalProperties: true + '400': {$ref: '#/components/responses/ApiError'} + '500': {$ref: '#/components/responses/ApiError'} + description: Cloud-only public account flow. Registration returns 201 for creation or 200 for an existing + email, preventing enumeration. Forgot-password always reports success. CSRF fairing rules still apply. + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + token: *id001 + new_password: *id001 + required: [token, new_password] + additionalProperties: true components: securitySchemes: - cookieAuth: - type: apiKey - in: cookie - name: session - description: | - Rocket private session cookie: typically `session` on HTTP (localhost) or - `__Host-session` when `MARREQ_SECURE_SESSION_COOKIE=1` over HTTPS. - Use `fetch(..., { credentials: 'include' })`. - responses: + cookieAuth: {type: apiKey, in: cookie, name: session, description: Rocket private session; __Host-session on + secure deployments. Mutations require X-CSRF-Token.} + bearerAuth: {type: http, scheme: bearer, description: 'Personal API token or delegated OAuth access token; project + membership, domain scopes and project permissions apply.'} + mcpServiceAuth: {type: apiKey, in: header, name: X-Marreq-Mcp-Audit-Secret, description: Private service-to-service + audit secret; never browser or delegated caller configuration.} + schemas: + external_ProviderDiscovery_response: + type: object + properties: + password_enabled: {type: boolean} + external: + type: array + items: {$ref: '#/components/schemas/external_PublicAuthProvider_response'} + additionalProperties: true + required: [password_enabled, external] + x-rust-model: marreq-core/src/auth/external.rs::ProviderDiscovery + external_PublicAuthProvider_response: + type: object + properties: + id: {type: string} + display_name: {type: string} + additionalProperties: true + required: [id, display_name] + x-rust-model: marreq-core/src/auth/external.rs::PublicAuthProvider + entities_User_response: + type: object + properties: + id: {type: integer} + username: {type: string} + name: {type: string} + email: {type: string} + creation_date: {type: string} + last_login: {type: string} + is_admin: {type: boolean} + email_verified: {type: boolean} + additionalProperties: true + required: [id, username, name, email, creation_date, last_login, is_admin, email_verified] + x-rust-model: marreq-core/src/models/entities.rs::User + use_cases_UseCaseRow_response: + type: object + properties: + id: {type: integer} + reference_code: {type: string} + title: {type: string} + primary_actor: + anyOf: + - {type: string} + - {type: 'null'} + step_count: {type: integer} + alternative_count: {type: integer} + exception_count: {type: integer} + approval_state: + anyOf: + - {type: string} + - {type: 'null'} + current_version_number: + anyOf: + - {type: integer} + - {type: 'null'} + has_draft: {type: boolean} + link_count: {type: integer} + suspect_count: {type: integer} + updated_at: {type: string} + additionalProperties: true + required: [id, reference_code, title, primary_actor, step_count, alternative_count, exception_count, approval_state, + current_version_number, has_draft, link_count, suspect_count, updated_at] + x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseRow + use_cases_NextCode_response: + type: object + properties: + reference_code: {type: string} + additionalProperties: true + required: [reference_code] + x-rust-model: marreq-core/src/api/use_cases.rs::NextCode + use_cases_UseCaseDetail_response: + type: object + properties: + id: {type: integer} + project_id: {type: integer} + reference_code: {type: string} + title: {type: string} + created_by: + anyOf: + - {type: integer} + - {type: 'null'} + created_at: {type: string} + updated_at: {type: string} + current_version: + anyOf: + - {$ref: '#/components/schemas/use_cases_VersionView_response'} + - {type: 'null'} + draft: + anyOf: + - {$ref: '#/components/schemas/use_cases_DraftSummary_response'} + - {type: 'null'} + additionalProperties: true + required: [id, project_id, reference_code, title, created_by, created_at, updated_at, current_version, draft] + x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseDetail + use_cases_VersionView_response: + type: object + properties: + id: {type: integer} + version_number: {type: integer} + approval_state: {type: string} + title: {type: string} + primary_actor: + anyOf: + - {type: string} + - {type: 'null'} + change_note: + anyOf: + - {type: string} + - {type: 'null'} + created_by: + anyOf: + - {type: integer} + - {type: 'null'} + created_by_name: + anyOf: + - {type: string} + - {type: 'null'} + created_at: {type: string} + reviewed_by: + anyOf: + - {type: integer} + - {type: 'null'} + reviewed_at: + anyOf: + - {type: string} + - {type: 'null'} + approved_by: + anyOf: + - {type: integer} + - {type: 'null'} + approved_at: + anyOf: + - {type: string} + - {type: 'null'} + document: {$ref: '#/components/schemas/UseCaseDocumentResponse'} + labels: + type: object + additionalProperties: {type: string} + additionalProperties: true + required: [id, version_number, approval_state, title, primary_actor, change_note, created_by, created_by_name, + created_at, reviewed_by, reviewed_at, approved_by, approved_at, document, labels] + x-rust-model: marreq-core/src/api/use_cases.rs::VersionView + use_cases_VersionSummary_response: + type: object + properties: + id: {type: integer} + version_number: {type: integer} + approval_state: {type: string} + title: {type: string} + primary_actor: + anyOf: + - {type: string} + - {type: 'null'} + change_note: + anyOf: + - {type: string} + - {type: 'null'} + created_by: + anyOf: + - {type: integer} + - {type: 'null'} + created_by_name: + anyOf: + - {type: string} + - {type: 'null'} + created_at: {type: string} + reviewed_by: + anyOf: + - {type: integer} + - {type: 'null'} + reviewed_at: + anyOf: + - {type: string} + - {type: 'null'} + approved_by: + anyOf: + - {type: integer} + - {type: 'null'} + approved_at: + anyOf: + - {type: string} + - {type: 'null'} + additionalProperties: true + required: [id, version_number, approval_state, title, primary_actor, change_note, created_by, created_by_name, + created_at, reviewed_by, reviewed_at, approved_by, approved_at] + x-rust-model: marreq-core/src/api/use_cases.rs::VersionSummary + use_cases_DraftSummary_response: + type: object + properties: + revision: {type: integer} + base_version_id: + anyOf: + - {type: integer} + - {type: 'null'} + updated_by: + anyOf: + - {type: integer} + - {type: 'null'} + updated_by_name: + anyOf: + - {type: string} + - {type: 'null'} + updated_at: {type: string} + additionalProperties: true + required: [revision, base_version_id, updated_by, updated_by_name, updated_at] + x-rust-model: marreq-core/src/api/use_cases.rs::DraftSummary + use_cases_DraftView_response: + type: object + properties: + use_case_id: {type: integer} + revision: {type: integer} + base_version_id: + anyOf: + - {type: integer} + - {type: 'null'} + updated_by: + anyOf: + - {type: integer} + - {type: 'null'} + updated_by_name: + anyOf: + - {type: string} + - {type: 'null'} + updated_at: {type: string} + document: {$ref: '#/components/schemas/UseCaseDocumentResponse'} + labels: + type: object + additionalProperties: {type: string} + additionalProperties: true + required: [use_case_id, revision, base_version_id, updated_by, updated_by_name, updated_at, document, labels] + x-rust-model: marreq-core/src/api/use_cases.rs::DraftView + use_cases_PutDraftRequest_request: + type: object + properties: + base_revision: + anyOf: + - {type: integer} + - {type: 'null'} + document: {$ref: '#/components/schemas/UseCaseDocument'} + additionalProperties: true + required: [document] + x-rust-model: marreq-core/src/api/use_cases.rs::PutDraftRequest + use_cases_DraftChangeView_response: + type: object + properties: + use_case_id: {type: integer} + revision: {type: integer} + base_version_id: + anyOf: + - {type: integer} + - {type: 'null'} + updated_by: + anyOf: + - {type: integer} + - {type: 'null'} + updated_by_name: + anyOf: + - {type: string} + - {type: 'null'} + updated_at: {type: string} + document: {$ref: '#/components/schemas/UseCaseDocumentResponse'} + labels: + type: object + additionalProperties: {type: string} + effects: + type: array + items: {$ref: '#/components/schemas/UseCaseEffect'} + additionalProperties: true + required: [use_case_id, revision, base_version_id, updated_by, updated_by_name, updated_at, document, labels, + effects] + x-rust-model: marreq-core/src/api/use_cases.rs::DraftChangeView + use_cases_SetApprovalRequest_request: + type: object + properties: + state: {type: string} + additionalProperties: true + required: [state] + x-rust-model: marreq-core/src/api/use_cases.rs::SetApprovalRequest + use_cases_LinkView_response: + type: object + properties: + id: {type: integer} + element_id: + anyOf: + - {type: string} + - {type: 'null'} + element_label: + anyOf: + - {type: string} + - {type: 'null'} + target_type: + type: string + enum: [requirement, verification] + requirement_id: + anyOf: + - {type: integer} + - {type: 'null'} + verification_id: + anyOf: + - {type: integer} + - {type: 'null'} + target_reference_code: {type: string} + target_title: {type: string} + rationale: + anyOf: + - {type: string} + - {type: 'null'} + suspect: {type: boolean} + suspect_at: + anyOf: + - {type: string} + - {type: 'null'} + suspect_reason: + anyOf: + - {type: string} + - {type: 'null'} + linked_requirement_version_id: + anyOf: + - {type: integer} + - {type: 'null'} + current_requirement_version_id: + anyOf: + - {type: integer} + - {type: 'null'} + created_by: + anyOf: + - {type: integer} + - {type: 'null'} + created_at: {type: string} + additionalProperties: true + required: [id, element_id, element_label, target_type, requirement_id, verification_id, target_reference_code, + target_title, rationale, suspect, suspect_at, suspect_reason, linked_requirement_version_id, current_requirement_version_id, + created_by, created_at] + x-rust-model: marreq-core/src/api/use_cases.rs::LinkView + use_cases_ClearSuspectResponse_response: + type: object + properties: + status: {type: string} + cleared: {type: boolean} + additionalProperties: true + required: [status, cleared] + x-rust-model: marreq-core/src/api/use_cases.rs::ClearSuspectResponse + use_cases_UseCaseRef_response: + type: object + properties: + use_case_id: {type: integer} + reference_code: {type: string} + title: {type: string} + link_id: {type: integer} + element_id: + anyOf: + - {type: string} + - {type: 'null'} + element_label: + anyOf: + - {type: string} + - {type: 'null'} + suspect: {type: boolean} + additionalProperties: true + required: [use_case_id, reference_code, title, link_id, element_id, element_label, suspect] + x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseRef + use_cases_UseCaseCommentView_response: + type: object + properties: + id: {type: integer} + use_case_id: {type: integer} + use_case_version_id: + anyOf: + - {type: integer} + - {type: 'null'} + element_id: + anyOf: + - {type: string} + - {type: 'null'} + element_label: + anyOf: + - {type: string} + - {type: 'null'} + author_id: {type: integer} + author_name: {type: string} + body: {type: string} + created_at: {type: string} + additionalProperties: true + required: [id, use_case_id, use_case_version_id, element_id, element_label, author_id, author_name, body, + created_at] + x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseCommentView + use_cases_BaselineUseCaseView_response: + type: object + properties: + use_case_id: {type: integer} + reference_code: {type: string} + title: {type: string} + primary_actor: + anyOf: + - {type: string} + - {type: 'null'} + version_id: {type: integer} + version_number: {type: integer} + approval_state: {type: string} + current_version_id: + anyOf: + - {type: integer} + - {type: 'null'} + current_version_number: + anyOf: + - {type: integer} + - {type: 'null'} + link_count: {type: integer} + additionalProperties: true + required: [use_case_id, reference_code, title, primary_actor, version_id, version_number, approval_state, + current_version_id, current_version_number, link_count] + x-rust-model: marreq-core/src/api/use_cases.rs::BaselineUseCaseView ApiError: - description: Structured error - content: - application/json: - schema: + type: object + required: [status, error, message] + properties: + status: {type: integer} + error: {type: string} + message: {type: string} + additionalProperties: true + BuildInfo: + type: object + properties: + backend_version: *id005 + backend_git_sha: *id005 + deployment_mode: + type: string + enum: [server, cloud] + frontend_compatibility: + type: object + properties: + min_version: *id005 + max_version: *id005 + required: [min_version, max_version] + additionalProperties: true + required: [backend_version, backend_git_sha, deployment_mode, frontend_compatibility] + additionalProperties: true + UseCaseElement: + type: object + properties: + id: &id006 {type: string} + text: *id006 + additionalProperties: false + required: [id, text] + UseCaseBranchEnd: + oneOf: + - type: object + properties: + type: {const: rejoin, type: string} + step: *id006 + additionalProperties: false + required: [type, step] + - type: object + properties: + type: {const: end, type: string} + outcome: + type: string + enum: [success, failure] + additionalProperties: false + required: [type, outcome] + UseCaseDocument: + type: object + properties: + schema: {type: string, const: marreq.use-case.v1} + title: *id006 + primary_actor: + anyOf: + - *id006 + - {type: 'null'} + secondary_actors: + type: array + items: *id006 + summary: + anyOf: + - *id006 + - {type: 'null'} + preconditions: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + main: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + branches: + type: array + items: {$ref: '#/components/schemas/UseCaseBranch'} + postconditions: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + additionalProperties: false + required: [title] + UseCaseBranch: + type: object + properties: + id: *id006 + kind: + type: string + enum: [alternative, exception] + title: *id006 + from_step: *id006 + end: {$ref: '#/components/schemas/UseCaseBranchEnd'} + steps: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + additionalProperties: false + required: [id, kind, title, from_step, end] + UseCaseEffect: + oneOf: + - type: object + properties: + type: {type: string, const: relabelled} + id: *id006 + from: *id006 + to: *id006 + additionalProperties: true + required: [type, id, from, to] + - type: object + properties: + type: {type: string, const: removed} + id: *id006 + additionalProperties: true + required: [type, id] + - type: object + properties: + type: {type: string, const: rebranched} + id: *id006 + from_step: *id006 + additionalProperties: true + required: [type, id, from_step] + - type: object + properties: + type: {type: string, const: created} + id: *id006 + additionalProperties: true + required: [type, id] + DeploymentInfo: + type: object + properties: + mode: + type: string + enum: [server, cloud] + allows_self_registration: &id007 {type: boolean} + requires_email_verification: *id007 + allows_admin_promotion: *id007 + assigns_personal_workspace: *id007 + allows_self_administered_user_creation: *id007 + allows_database_backup: *id007 + required: [mode, allows_self_registration, requires_email_verification, allows_admin_promotion, assigns_personal_workspace, + allows_self_administered_user_creation, allows_database_backup] + additionalProperties: true + McpPrincipalResponse: + type: object + properties: + user_id: {type: integer} + authentication_type: {type: string} + principal_id: {type: string} + client_id: + anyOf: + - {type: string} + - {type: 'null'} + grant_id: + anyOf: + - {type: integer} + - {type: 'null'} + additionalProperties: true + required: [user_id, authentication_type, principal_id, client_id, grant_id] + x-rust-model: marreq-core/src/api/mcp.rs::McpPrincipalResponse + UseCaseDocumentResponse: + type: object + properties: + schema: {type: string, const: marreq.use-case.v1} + title: &id008 {type: string} + primary_actor: + anyOf: + - *id008 + - {type: 'null'} + secondary_actors: + type: array + items: *id008 + summary: + anyOf: + - *id008 + - {type: 'null'} + preconditions: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + main: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + branches: + type: array + items: {$ref: '#/components/schemas/UseCaseBranchResponse'} + postconditions: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + additionalProperties: false + required: [schema, title, primary_actor, secondary_actors, summary, preconditions, main, branches, postconditions] + UseCaseBranchResponse: + type: object + properties: + id: &id009 {type: string} + kind: + type: string + enum: [alternative, exception] + title: *id009 + from_step: *id009 + end: {$ref: '#/components/schemas/UseCaseBranchEnd'} + steps: + type: array + items: {$ref: '#/components/schemas/UseCaseElement'} + additionalProperties: false + required: [id, kind, title, from_step, end, steps] + UseCaseListRef: + oneOf: + - type: string + enum: [preconditions, main, postconditions] + - type: object + properties: + branch: *id002 + additionalProperties: true + required: [branch] + UseCaseNewElement: + type: object + properties: + id: + anyOf: + - *id002 + - {type: 'null'} + text: *id002 + additionalProperties: false + required: [text] + UseCaseNewBranch: + type: object + properties: + id: + anyOf: + - *id002 + - {type: 'null'} + kind: + type: string + enum: [alternative, exception] + title: *id002 + from_step: *id002 + end: {$ref: '#/components/schemas/UseCaseBranchEnd'} + steps: + type: array + items: {$ref: '#/components/schemas/UseCaseNewElement'} + additionalProperties: false + required: [kind, title, from_step, end] + UseCaseDependents: + oneOf: + - type: string + enum: [cascade] + - type: object + properties: + rebranch_to: *id002 + additionalProperties: true + required: [rebranch_to] + UseCaseOperation: + oneOf: + - type: object + properties: + op: {type: string, const: set_header} + title: + anyOf: + - *id002 + - {type: 'null'} + primary_actor: + anyOf: + - *id002 + - {type: 'null'} + secondary_actors: + anyOf: + - type: array + items: *id002 + - {type: 'null'} + summary: + anyOf: + - *id002 + - {type: 'null'} + additionalProperties: false + required: [op] + - type: object + properties: + op: {type: string, const: add_element} + list: {$ref: '#/components/schemas/UseCaseListRef'} + after: + anyOf: + - *id002 + - {type: 'null'} + element: {$ref: '#/components/schemas/UseCaseNewElement'} + additionalProperties: false + required: [op, list, element] + - type: object + properties: + op: {type: string, const: update_text} + id: *id002 + text: *id002 + additionalProperties: false + required: [op, id, text] + - type: object + properties: + op: {type: string, const: move_element} + id: *id002 + list: + anyOf: + - {$ref: '#/components/schemas/UseCaseListRef'} + - {type: 'null'} + after: + anyOf: + - *id002 + - {type: 'null'} + additionalProperties: false + required: [op, id] + - type: object + properties: + op: {type: string, const: remove_element} + id: *id002 + dependents: + anyOf: + - {$ref: '#/components/schemas/UseCaseDependents'} + - {type: 'null'} + additionalProperties: false + required: [op, id] + - type: object + properties: + op: {type: string, const: add_branch} + branch: {$ref: '#/components/schemas/UseCaseNewBranch'} + additionalProperties: false + required: [op, branch] + - type: object + properties: + op: {type: string, const: update_branch} + id: *id002 + title: + anyOf: + - *id002 + - {type: 'null'} + kind: + anyOf: + - type: string + enum: [alternative, exception] + - {type: 'null'} + from_step: + anyOf: + - *id002 + - {type: 'null'} + end: + anyOf: + - {$ref: '#/components/schemas/UseCaseBranchEnd'} + - {type: 'null'} + additionalProperties: false + required: [op, id] + - type: object + properties: + op: {type: string, const: remove_branch} + id: *id002 + additionalProperties: false + required: [op, id] + UseCaseDiffSide: + type: object + properties: + list: + type: string + enum: [preconditions, main, postconditions, branch, branches] + branch_id: *id002 + label: + anyOf: + - *id002 + - {type: 'null'} + text: *id002 + from_step: *id002 + end: {$ref: '#/components/schemas/UseCaseBranchEnd'} + kind: + type: string + enum: [alternative, exception] + additionalProperties: true + required: [list, label, text] + UseCaseElementDiff: + type: object + properties: + id: *id002 + kind: + type: string + enum: [precondition, postcondition, step, branch] + changes: + type: array + items: + type: string + enum: [added, removed, edited, moved, rebranched] + old: + anyOf: + - {$ref: '#/components/schemas/UseCaseDiffSide'} + - {type: 'null'} + new: + anyOf: + - {$ref: '#/components/schemas/UseCaseDiffSide'} + - {type: 'null'} + text_diff: + type: array + items: type: object properties: - status: - type: integer - error: - type: string - message: + op: type: string + enum: [equal, delete, insert] + text: *id002 + additionalProperties: true + required: [op, text] + additionalProperties: true + required: [id, kind, changes, old, new] + UseCaseDiffEnd: + type: object + properties: + version_id: + anyOf: + - *id003 + - {type: 'null'} + version_number: + anyOf: + - *id003 + - {type: 'null'} + draft_revision: + anyOf: + - *id003 + - {type: 'null'} + additionalProperties: true + required: [version_id, version_number, draft_revision] + UseCaseDiff: + type: object + properties: + from: + anyOf: + - {$ref: '#/components/schemas/UseCaseDiffEnd'} + - {type: 'null'} + to: {$ref: '#/components/schemas/UseCaseDiffEnd'} + summary: + type: object + properties: + added: *id003 + removed: *id003 + edited: *id003 + moved: *id003 + rebranched: *id003 + additionalProperties: true + required: [added, removed, edited, moved, rebranched] + header: + type: object + additionalProperties: + type: object + properties: + old: &id010 + oneOf: + - *id002 + - {type: 'null'} + - type: array + items: *id002 + new: *id010 + additionalProperties: true + required: [old, new] + elements: + type: array + items: {$ref: '#/components/schemas/UseCaseElementDiff'} + additionalProperties: true + required: [from, to, summary, header, elements] + UseCasePublishResponse: + type: object + properties: + version: {$ref: '#/components/schemas/use_cases_VersionView_response'} + removed_links: + type: array + items: {$ref: '#/components/schemas/use_cases_LinkView_response'} + additionalProperties: true + required: [version, removed_links] + responses: + ApiError: + description: API error; use cases may include code and structured details. Guard/fairing errors can have an + empty body. + content: + application/json: + schema: {$ref: '#/components/schemas/ApiError'} +x-marreq-contract-complete: false +security: +- cookieAuth: [] diff --git a/docs/developer/use-cases-api.md b/docs/developer/use-cases-api.md index 227257de..6340723c 100644 --- a/docs/developer/use-cases-api.md +++ b/docs/developer/use-cases-api.md @@ -166,3 +166,14 @@ Comments are immutable. `@username` mentions of project members send a `mentione - **Search.** `GET /api/search` hits have `kind: "requirement" | "use_case"` and `use_case_id`; use cases match on code, title, actors and step text. - **Project bundle.** `bundle.json` has an optional `use_cases` section (see the HTTP API contract). - **MCP.** `list_use_cases`, `get_use_case`, `get_use_case_diff` and `list_use_case_links` (`use_cases:read`); `create_use_case`, `edit_use_case_draft`, `publish_use_case_draft` and `link_use_case_element` (`use_cases:write`, `MARREQ_MODE=draft_write`). Approval is not exposed to agents. See [mcp-setup.md](mcp-setup.md). + +## Machine-readable contract + +The use-case routes and wire schemas are described in `openapi.yaml`, including +request defaults, response nullability, draft-operation tagged unions, version +publishing, links, comments and diffs. Derived TypeScript models are generated by +`scripts/contracts/generate.py`; do not hand-edit `generated-contract.ts`. +The isolated backend contract smoke test covers representative list/create/draft +operations, delegated scope rejection and project boundaries. Full API contract +coverage and broader cross-version evidence remain tracked in the P0 audit in +[http-api-contract.md](http-api-contract.md#p0-contract-hardening-audit-and-current-coverage-423). diff --git a/docs/releases.md b/docs/releases.md index 9f194078..645e74e7 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -81,3 +81,131 @@ Images carry OCI label `org.opencontainers.image.version`. - Frontend: Vite `define` (`__FRONTEND_VERSION__`, `__FRONTEND_GIT_SHA__`) in `frontend/vite.config.ts`. When `MARREQ_VERSION` is unset or empty, each artifact falls back to its manifest version. A leading `v` is stripped so the value stays plain `X.Y.Z` for the compatibility check. Local images: `make docker-server MARREQ_VERSION=0.1.3`. + +## API compatibility gate and retained artifact contracts (#423 P0) + +The P0 bootstrap is **not release-ready**: 206 HTTP operations still lack audited +wire contracts. `API contracts` CI validates the document, generated artifacts, +source-review evidence, both compiled route inventories, negative historical +fixtures and representative real producer/consumer exchanges. Release publication +also requires `scripts/contracts/check.py --require-complete`, which currently +fails intentionally. This must remain a draft until the coverage gaps described in +[the HTTP contract audit](developer/http-api-contract.md#p0-contract-hardening-audit-and-current-coverage-423) +are closed. + +### Compatibility policy + +Component SemVer describes each artifact independently. Equal versions are never +required. A consumer declares `requires_backend_min` and optionally an inclusive +`requires_backend_max`; frontend and MCP metadata are validated independently. +The backend's existing frontend acceptance range is also checked. Stable +`X.Y.Z` versions are supported; prerelease and wildcard ranges fail closed. +Omitting an upper bound is appropriate only with executable retained-contract +checks; an upper bound is a temporary tested-support ceiling, not proof that +all versions inside it work. Existing runtime `CompatibilityBanner` warnings are +preserved for manually assembled deployments. + +A producer must continue accepting every previously valid consumer request and +returning responses accepted by that released consumer. Additive optional request +fields and additive response fields are compatible when the consumer accepts +unknown properties. Removing an endpoint or guaranteed response property, +changing a field type/nullability, adding a required request field, restricting a +request enum, extending a response enum used by exhaustive consumers, changing +status/media/header behavior or changing authentication/scopes is potentially +breaking. Breaking changes require a new API-compatible deployment combination +and a component SemVer breaking release; while 0.x, treat minor changes with the +same explicit care rather than assuming all 0.x releases interoperate. Unknown +changed schema constraints/unions are rejected conservatively for manual review. + +Previously released consumers retain their own contracts and policy. A newer +backend must satisfy those old contracts, not regenerated contracts from current +`main`. During rolling upgrades, check each backend/consumer combination that +can coexist, including old and new replicas; upgrade consumers first only when +the old backend also passes. No gate establishes database migration safety; +that remains a separate P0 workstream in #423. + +### Export and retain an exact component contract + +After committing audited contract inputs on the component's source commit: + +```sh +python3 scripts/contracts/bundle.py --component backend --variant server \ + --version 0.2.4 --output backend-0.2.4.json +python3 scripts/contracts/bundle.py --component frontend \ + --version 0.2.1 --output frontend-0.2.1.json +python3 scripts/contracts/bundle.py --component mcp \ + --version 0.3.0 --output mcp-0.3.0.json +``` + +Run each command on **that artifact's own source commit**, not all on current +`main`. The exporter refuses dirty audited inputs and records the exact source +SHA. Retain the JSON next to the versioned image/release with its SHA-256. Images +also carry their build-time OpenAPI and compatibility metadata. The exporter can +record incomplete bootstrap evidence for review, but the gate cannot certify it. +Old releases without retained evidence require a real audit on their source +commit; missing evidence never falls back to current sources. + +### Verify the resulting deployment, including unchanged artifacts + +Prepare a `deployment.json` with exactly these top-level fields: + +```json +{ + "format": "marreq.deployment-contract.v1", + "variant": "server", + "backend": { + "file": "backend-0.2.4.json", + "sha256": "<64 lowercase hex characters>", + "version": "0.2.4", + "source_sha": "<40 lowercase hex characters>" + }, + "frontend": { + "file": "frontend-0.2.1.json", + "sha256": "<64 lowercase hex characters>", + "version": "0.2.1", + "source_sha": "<40 lowercase hex characters>" + }, + "mcp": { + "file": "mcp-0.3.0.json", + "sha256": "<64 lowercase hex characters>", + "version": "0.3.0", + "source_sha": "<40 lowercase hex characters>" + } +} +``` + +Place referenced bundles inside the plan directory. Verify offline: + +```sh +scripts/check-api-compatibility.sh --manifest deployment.json +``` + +Exit 0 means all checked declarations/contracts are compatible. Exit 1 means +incompatibility, incomplete evidence, invalid metadata, wrong deployment variant, +a missing artifact, or a digest/source/version mismatch. Output is structured JSON +with component-specific diagnostics. The comparator checks request acceptance in +the consumer-to-producer direction and response acceptance in the reverse +direction. This command does not download artifacts, execute plans, send tokens, +publish images or deploy anything. SHA-256 pins content; trusted provenance for +retrieving artifacts remains the release/deployment orchestrator's responsibility. + +### Current tag workflow and future global releases + +`release-artifacts.yml` retains the existing component tag triggers. Every image +publisher depends on contract tests and the mandatory deployment gate. Once P0 +coverage is complete, prepare a separate immutable revision in this repository +containing `deployment.json` and all referenced bundles; set the repository variable +`MARREQ_RELEASE_CONTRACTS_SHA` to that full commit SHA. This revision is prepared +**after** the candidate source commit exists, avoiding self-referential source +hashes in a manifest committed with its own candidate. The workflow checks out +that exact plan revision, exports the candidate contract from the tagged source, +and checks its digest, version and source SHA against the plan. Unchanged +frontend/MCP/backend bundles are never replaced with the tag checkout's versions. +The candidate contract is retained as a workflow artifact before image publishing. +No default production version or fabricated historical manifest is provided. + +The future global-release/selective-deployment pipeline should retrieve trusted +retained bundles, construct every resulting deployment plan, and invoke this +same command plus the reusable `api-contracts.yml` workflow. Global version +registry/tag migration, image digest provenance, actual CD, database migrations, +rollback and VPS configuration remain in #423. Nothing here deploys to the VPS. diff --git a/frontend/src/api/generated-contract.ts b/frontend/src/api/generated-contract.ts new file mode 100644 index 00000000..5e16cda6 --- /dev/null +++ b/frontend/src/api/generated-contract.ts @@ -0,0 +1,450 @@ +// Generated by scripts/contracts/generate.py from docs/developer/openapi.yaml. +// Do not edit; this file carries wire types, not a second contract. + +export type ApiError = { + "status": number; + "error": string; + "message": string; + [key: string]: unknown; +}; + +export type BuildInfo = { + "backend_version": string; + "backend_git_sha": string; + "deployment_mode": "server" | "cloud"; + "frontend_compatibility": { + "min_version": string; + "max_version": string; + [key: string]: unknown; +}; + [key: string]: unknown; +}; + +export type DeploymentInfo = { + "mode": "server" | "cloud"; + "allows_self_registration": boolean; + "requires_email_verification": boolean; + "allows_admin_promotion": boolean; + "assigns_personal_workspace": boolean; + "allows_self_administered_user_creation": boolean; + "allows_database_backup": boolean; + [key: string]: unknown; +}; + +export type McpPrincipalResponse = { + "user_id": number; + "authentication_type": string; + "principal_id": string; + "client_id": (string | null); + "grant_id": (number | null); + [key: string]: unknown; +}; + +export type UseCaseBranch = { + "id": string; + "kind": "alternative" | "exception"; + "title": string; + "from_step": string; + "end": UseCaseBranchEnd; + "steps"?: Array; +}; + +export type UseCaseBranchEnd = ({ + "type": "rejoin"; + "step": string; +} | { + "type": "end"; + "outcome": "success" | "failure"; +}); + +export type UseCaseBranchResponse = { + "id": string; + "kind": "alternative" | "exception"; + "title": string; + "from_step": string; + "end": UseCaseBranchEnd; + "steps": Array; +}; + +export type UseCaseDependents = ("cascade" | { + "rebranch_to": string; + [key: string]: unknown; +}); + +export type UseCaseDiff = { + "from": (UseCaseDiffEnd | null); + "to": UseCaseDiffEnd; + "summary": { + "added": number; + "removed": number; + "edited": number; + "moved": number; + "rebranched": number; + [key: string]: unknown; +}; + "header": { + [key: string]: { + "old": (string | null | Array); + "new": (string | null | Array); + [key: string]: unknown; +}; +}; + "elements": Array; + [key: string]: unknown; +}; + +export type UseCaseDiffEnd = { + "version_id": (number | null); + "version_number": (number | null); + "draft_revision": (number | null); + [key: string]: unknown; +}; + +export type UseCaseDiffSide = { + "list": "preconditions" | "main" | "postconditions" | "branch" | "branches"; + "branch_id"?: string; + "label": (string | null); + "text": string; + "from_step"?: string; + "end"?: UseCaseBranchEnd; + "kind"?: "alternative" | "exception"; + [key: string]: unknown; +}; + +export type UseCaseDocument = { + "schema"?: "marreq.use-case.v1"; + "title": string; + "primary_actor"?: (string | null); + "secondary_actors"?: Array; + "summary"?: (string | null); + "preconditions"?: Array; + "main"?: Array; + "branches"?: Array; + "postconditions"?: Array; +}; + +export type UseCaseDocumentResponse = { + "schema": "marreq.use-case.v1"; + "title": string; + "primary_actor": (string | null); + "secondary_actors": Array; + "summary": (string | null); + "preconditions": Array; + "main": Array; + "branches": Array; + "postconditions": Array; +}; + +export type UseCaseEffect = ({ + "type": "relabelled"; + "id": string; + "from": string; + "to": string; + [key: string]: unknown; +} | { + "type": "removed"; + "id": string; + [key: string]: unknown; +} | { + "type": "rebranched"; + "id": string; + "from_step": string; + [key: string]: unknown; +} | { + "type": "created"; + "id": string; + [key: string]: unknown; +}); + +export type UseCaseElement = { + "id": string; + "text": string; +}; + +export type UseCaseElementDiff = { + "id": string; + "kind": "precondition" | "postcondition" | "step" | "branch"; + "changes": Array<"added" | "removed" | "edited" | "moved" | "rebranched">; + "old": (UseCaseDiffSide | null); + "new": (UseCaseDiffSide | null); + "text_diff"?: Array<{ + "op": "equal" | "delete" | "insert"; + "text": string; + [key: string]: unknown; +}>; + [key: string]: unknown; +}; + +export type UseCaseListRef = ("preconditions" | "main" | "postconditions" | { + "branch": string; + [key: string]: unknown; +}); + +export type UseCaseNewBranch = { + "id"?: (string | null); + "kind": "alternative" | "exception"; + "title": string; + "from_step": string; + "end": UseCaseBranchEnd; + "steps"?: Array; +}; + +export type UseCaseNewElement = { + "id"?: (string | null); + "text": string; +}; + +export type UseCaseOperation = ({ + "op": "set_header"; + "title"?: (string | null); + "primary_actor"?: (string | null); + "secondary_actors"?: (Array | null); + "summary"?: (string | null); +} | { + "op": "add_element"; + "list": UseCaseListRef; + "after"?: (string | null); + "element": UseCaseNewElement; +} | { + "op": "update_text"; + "id": string; + "text": string; +} | { + "op": "move_element"; + "id": string; + "list"?: (UseCaseListRef | null); + "after"?: (string | null); +} | { + "op": "remove_element"; + "id": string; + "dependents"?: (UseCaseDependents | null); +} | { + "op": "add_branch"; + "branch": UseCaseNewBranch; +} | { + "op": "update_branch"; + "id": string; + "title"?: (string | null); + "kind"?: ("alternative" | "exception" | null); + "from_step"?: (string | null); + "end"?: (UseCaseBranchEnd | null); +} | { + "op": "remove_branch"; + "id": string; +}); + +export type UseCasePublishResponse = { + "version": use_cases_VersionView_response; + "removed_links": Array; + [key: string]: unknown; +}; + +export type entities_User_response = { + "id": number; + "username": string; + "name": string; + "email": string; + "creation_date": string; + "last_login": string; + "is_admin": boolean; + "email_verified": boolean; + [key: string]: unknown; +}; + +export type external_ProviderDiscovery_response = { + "password_enabled": boolean; + "external": Array; + [key: string]: unknown; +}; + +export type external_PublicAuthProvider_response = { + "id": string; + "display_name": string; + [key: string]: unknown; +}; + +export type use_cases_BaselineUseCaseView_response = { + "use_case_id": number; + "reference_code": string; + "title": string; + "primary_actor": (string | null); + "version_id": number; + "version_number": number; + "approval_state": string; + "current_version_id": (number | null); + "current_version_number": (number | null); + "link_count": number; + [key: string]: unknown; +}; + +export type use_cases_ClearSuspectResponse_response = { + "status": string; + "cleared": boolean; + [key: string]: unknown; +}; + +export type use_cases_DraftChangeView_response = { + "use_case_id": number; + "revision": number; + "base_version_id": (number | null); + "updated_by": (number | null); + "updated_by_name": (string | null); + "updated_at": string; + "document": UseCaseDocumentResponse; + "labels": { + [key: string]: string; +}; + "effects": Array; + [key: string]: unknown; +}; + +export type use_cases_DraftSummary_response = { + "revision": number; + "base_version_id": (number | null); + "updated_by": (number | null); + "updated_by_name": (string | null); + "updated_at": string; + [key: string]: unknown; +}; + +export type use_cases_DraftView_response = { + "use_case_id": number; + "revision": number; + "base_version_id": (number | null); + "updated_by": (number | null); + "updated_by_name": (string | null); + "updated_at": string; + "document": UseCaseDocumentResponse; + "labels": { + [key: string]: string; +}; + [key: string]: unknown; +}; + +export type use_cases_LinkView_response = { + "id": number; + "element_id": (string | null); + "element_label": (string | null); + "target_type": "requirement" | "verification"; + "requirement_id": (number | null); + "verification_id": (number | null); + "target_reference_code": string; + "target_title": string; + "rationale": (string | null); + "suspect": boolean; + "suspect_at": (string | null); + "suspect_reason": (string | null); + "linked_requirement_version_id": (number | null); + "current_requirement_version_id": (number | null); + "created_by": (number | null); + "created_at": string; + [key: string]: unknown; +}; + +export type use_cases_NextCode_response = { + "reference_code": string; + [key: string]: unknown; +}; + +export type use_cases_PutDraftRequest_request = { + "base_revision"?: (number | null); + "document": UseCaseDocument; + [key: string]: unknown; +}; + +export type use_cases_SetApprovalRequest_request = { + "state": string; + [key: string]: unknown; +}; + +export type use_cases_UseCaseCommentView_response = { + "id": number; + "use_case_id": number; + "use_case_version_id": (number | null); + "element_id": (string | null); + "element_label": (string | null); + "author_id": number; + "author_name": string; + "body": string; + "created_at": string; + [key: string]: unknown; +}; + +export type use_cases_UseCaseDetail_response = { + "id": number; + "project_id": number; + "reference_code": string; + "title": string; + "created_by": (number | null); + "created_at": string; + "updated_at": string; + "current_version": (use_cases_VersionView_response | null); + "draft": (use_cases_DraftSummary_response | null); + [key: string]: unknown; +}; + +export type use_cases_UseCaseRef_response = { + "use_case_id": number; + "reference_code": string; + "title": string; + "link_id": number; + "element_id": (string | null); + "element_label": (string | null); + "suspect": boolean; + [key: string]: unknown; +}; + +export type use_cases_UseCaseRow_response = { + "id": number; + "reference_code": string; + "title": string; + "primary_actor": (string | null); + "step_count": number; + "alternative_count": number; + "exception_count": number; + "approval_state": (string | null); + "current_version_number": (number | null); + "has_draft": boolean; + "link_count": number; + "suspect_count": number; + "updated_at": string; + [key: string]: unknown; +}; + +export type use_cases_VersionSummary_response = { + "id": number; + "version_number": number; + "approval_state": string; + "title": string; + "primary_actor": (string | null); + "change_note": (string | null); + "created_by": (number | null); + "created_by_name": (string | null); + "created_at": string; + "reviewed_by": (number | null); + "reviewed_at": (string | null); + "approved_by": (number | null); + "approved_at": (string | null); + [key: string]: unknown; +}; + +export type use_cases_VersionView_response = { + "id": number; + "version_number": number; + "approval_state": string; + "title": string; + "primary_actor": (string | null); + "change_note": (string | null); + "created_by": (number | null); + "created_by_name": (string | null); + "created_at": string; + "reviewed_by": (number | null); + "reviewed_at": (string | null); + "approved_by": (number | null); + "approved_at": (string | null); + "document": UseCaseDocumentResponse; + "labels": { + [key: string]: string; +}; + [key: string]: unknown; +}; diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index d4251a25..be9cc5c0 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -187,21 +187,9 @@ export interface DashboardPayload { csrf_token: string; } -export interface DeploymentInfo { - mode: 'server' | 'cloud' | string; - allows_self_registration: boolean; - requires_email_verification: boolean; - allows_admin_promotion: boolean; - assigns_personal_workspace: boolean; - allows_self_administered_user_creation: boolean; - /** False in cloud mode, where a whole-database dump would contain every tenant. */ - allows_database_backup?: boolean; -} +export type DeploymentInfo = import('./generated-contract').DeploymentInfo; -export interface AuthProviderDiscovery { - password_enabled: boolean; - external: Array<{ id: string; display_name: string }>; -} +export type AuthProviderDiscovery = import('./generated-contract').external_ProviderDiscovery_response; export interface ConnectedIdentity { id: number; @@ -229,12 +217,7 @@ export interface FrontendCompatibility { } /** Response from `GET /api/meta/build` (issue #213). */ -export interface BuildInfo { - backend_version: string; - backend_git_sha: string; - deployment_mode: string; - frontend_compatibility: FrontendCompatibility; -} +export type BuildInfo = import('./generated-contract').BuildInfo; export type RegistrationBody = { username: string; @@ -989,52 +972,16 @@ export interface UseCaseDocument { /** Display labels by element id: `3`, `A3`, `A3.1`, `E4`, `P1`, `Q1`. */ export type UseCaseLabels = Record; -export interface UseCaseRow { - id: number; - reference_code: string; - title: string; - primary_actor: string | null; - step_count: number; - alternative_count: number; - exception_count: number; - /** Of the current version; `null` before the first publish. */ - approval_state: string | null; - current_version_number: number | null; - has_draft: boolean; - /** Links to requirements and verifications, and how many are suspect (issue #400). */ - link_count: number; - suspect_count: number; - updated_at: string; -} +export type UseCaseRow = import('./generated-contract').use_cases_UseCaseRow_response; -export interface UseCaseVersionSummary { - id: number; - version_number: number; - approval_state: string; - title: string; - primary_actor: string | null; - change_note: string | null; - created_by: number | null; - created_by_name: string | null; - created_at: string; - reviewed_by: number | null; - reviewed_at: string | null; - approved_by: number | null; - approved_at: string | null; -} +export type UseCaseVersionSummary = import('./generated-contract').use_cases_VersionSummary_response; export interface UseCaseVersion extends UseCaseVersionSummary { document: UseCaseDocument; labels: UseCaseLabels; } -export interface UseCaseDraftSummary { - revision: number; - base_version_id: number | null; - updated_by: number | null; - updated_by_name: string | null; - updated_at: string; -} +export type UseCaseDraftSummary = import('./generated-contract').use_cases_DraftSummary_response; export interface UseCaseDraft extends UseCaseDraftSummary { use_case_id: number; @@ -1096,36 +1043,10 @@ export type UseCaseOperation = | { op: 'remove_branch'; id: string }; /** A link from a use case, or one of its elements, to a requirement or verification (issue #400). */ -export interface UseCaseLink { - id: number; - /** `null` for the whole use case. */ - element_id: string | null; - element_label: string | null; - target_type: 'requirement' | 'verification'; - requirement_id: number | null; - verification_id: number | null; - target_reference_code: string; - target_title: string; - rationale: string | null; - suspect: boolean; - suspect_at: string | null; - suspect_reason: string | null; - linked_requirement_version_id: number | null; - current_requirement_version_id: number | null; - created_by: number | null; - created_at: string; -} +export type UseCaseLink = import('./generated-contract').use_cases_LinkView_response; /** A use case that links to a requirement or verification. */ -export interface UseCaseRef { - use_case_id: number; - reference_code: string; - title: string; - link_id: number; - element_id: string | null; - element_label: string | null; - suspect: boolean; -} +export type UseCaseRef = import('./generated-contract').use_cases_UseCaseRef_response; /** Structural diff of two use case documents (issue #401). */ export type UseCaseChange = 'added' | 'removed' | 'edited' | 'moved' | 'rebranched'; @@ -1163,28 +1084,7 @@ export interface UseCaseDiff { elements: UseCaseElementDiff[]; } -export interface UseCaseComment { - id: number; - use_case_id: number; - use_case_version_id: number | null; - element_id: string | null; - element_label: string | null; - author_id: number; - author_name: string; - body: string; - created_at: string; -} +export type UseCaseComment = import('./generated-contract').use_cases_UseCaseCommentView_response; /** A use case version a baseline recorded (issue #402). */ -export interface BaselineUseCase { - use_case_id: number; - reference_code: string; - title: string; - primary_actor: string | null; - version_id: number; - version_number: number; - approval_state: string; - current_version_id: number | null; - current_version_number: number | null; - link_count: number; -} +export type BaselineUseCase = import('./generated-contract').use_cases_BaselineUseCaseView_response; diff --git a/frontend/src/hooks/__tests__/useBuildInfo.test.ts b/frontend/src/hooks/__tests__/useBuildInfo.test.ts index c35c5d11..1cedf393 100644 --- a/frontend/src/hooks/__tests__/useBuildInfo.test.ts +++ b/frontend/src/hooks/__tests__/useBuildInfo.test.ts @@ -7,7 +7,7 @@ vi.mock('@/api/client'); const build = { backend_version: '0.1.3', backend_git_sha: 'abc', - deployment_mode: 'server', + deployment_mode: 'server' as const, frontend_compatibility: { min_version: '0.1.0', max_version: '0.1.99' }, }; diff --git a/frontend/src/pages/__tests__/AccountPage.test.tsx b/frontend/src/pages/__tests__/AccountPage.test.tsx index 5138986b..79cd9fa6 100644 --- a/frontend/src/pages/__tests__/AccountPage.test.tsx +++ b/frontend/src/pages/__tests__/AccountPage.test.tsx @@ -31,6 +31,7 @@ const serverDeployment = { allows_admin_promotion: true, assigns_personal_workspace: false, allows_self_administered_user_creation: true, + allows_database_backup: true, }; function renderPage() { diff --git a/frontend/src/pages/__tests__/AdminPage.test.tsx b/frontend/src/pages/__tests__/AdminPage.test.tsx index 9fc82660..b935722e 100644 --- a/frontend/src/pages/__tests__/AdminPage.test.tsx +++ b/frontend/src/pages/__tests__/AdminPage.test.tsx @@ -47,6 +47,7 @@ const serverDeployment = { allows_admin_promotion: true, assigns_personal_workspace: false, allows_self_administered_user_creation: true, + allows_database_backup: true, }; const cloudDeployment = { @@ -55,6 +56,7 @@ const cloudDeployment = { allows_self_registration: true, allows_admin_promotion: false, allows_self_administered_user_creation: false, + allows_database_backup: false, }; function renderPage() { diff --git a/frontend/src/pages/__tests__/HelpPage.test.tsx b/frontend/src/pages/__tests__/HelpPage.test.tsx index 601f467b..1eae3b01 100644 --- a/frontend/src/pages/__tests__/HelpPage.test.tsx +++ b/frontend/src/pages/__tests__/HelpPage.test.tsx @@ -28,7 +28,7 @@ vi.mock('react-router-dom', async () => { const compatibleBuild = { backend_version: '0.1.0', backend_git_sha: '56c09392abcdef', - deployment_mode: 'server', + deployment_mode: 'server' as const, frontend_compatibility: { min_version: '0.1.0', max_version: '0.1.99' }, }; diff --git a/frontend/src/pages/__tests__/LoginPage.test.tsx b/frontend/src/pages/__tests__/LoginPage.test.tsx index 273c5aa5..59db6743 100644 --- a/frontend/src/pages/__tests__/LoginPage.test.tsx +++ b/frontend/src/pages/__tests__/LoginPage.test.tsx @@ -26,6 +26,7 @@ const serverDeployment = { allows_admin_promotion: true, assigns_personal_workspace: false, allows_self_administered_user_creation: true, + allows_database_backup: true, }; const cloudDeployment = { @@ -35,6 +36,7 @@ const cloudDeployment = { allows_admin_promotion: false, assigns_personal_workspace: true, allows_self_administered_user_creation: false, + allows_database_backup: false, }; describe('LoginPage', () => { diff --git a/marreq-cloud/Cargo.toml b/marreq-cloud/Cargo.toml index 52bd6a40..114fba30 100644 --- a/marreq-cloud/Cargo.toml +++ b/marreq-cloud/Cargo.toml @@ -31,6 +31,7 @@ anyhow = { workspace = true } sha2 = { workspace = true } [dev-dependencies] +regex = { workspace = true } marreq-core = { path = "../marreq-core", features = ["test-helpers"] } tokio = { workspace = true, features = ["rt"] } serde_json = { workspace = true } diff --git a/marreq-cloud/tests/api_contract_inventory.rs b/marreq-cloud/tests/api_contract_inventory.rs new file mode 100644 index 00000000..a0d6e8c5 --- /dev/null +++ b/marreq-cloud/tests/api_contract_inventory.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +//! Compare the compiled Rocket mount inventory with the OpenAPI-derived artifact. +//! No database or test-double routing: declared_mounts is also used by production. +use std::collections::BTreeSet; + +#[test] +fn compiled_server_and_cloud_routes_match_openapi_inventory() { + let expected: Vec = serde_json::from_str(include_str!( + "../../docs/developer/api-contract-routes.json" + )) + .expect("generated route inventory"); + let parameter = regex::Regex::new(r"<([\w]+)(?:\.\.)?>").unwrap(); + for (variant, extra) in [ + ("server", Vec::new()), + ("cloud", marreq_cloud::routes::routes()), + ] { + let mounts = marreq_core::app::declared_mounts(extra); + let actual: BTreeSet<_> = [("/api", mounts.api), ("", mounts.root)] + .into_iter() + .flat_map(|(base, routes)| { + let parameter = ¶meter; + routes.into_iter().filter_map(move |(_, route)| { + let path = route.uri.path().to_owned(); + // Origin root UI and internal CSRF denial routes are not + // HTTP consumer APIs. OAuth routes under root are included. + if base.is_empty() + && !(path.starts_with("/oauth/") + || path.starts_with("/.well-known/") + || path.starts_with("/api/")) + { + return None; + } + let normalized = parameter.replace_all(&path, "{$1}"); + Some((route.method.to_string(), format!("{base}{normalized}"))) + }) + }) + .collect(); + let documented: BTreeSet<_> = expected + .iter() + .filter(|op| { + op["variants"] + .as_array() + .unwrap() + .iter() + .any(|v| v == variant) + }) + .map(|op| { + ( + op["method"].as_str().unwrap().to_owned(), + op["path"].as_str().unwrap().to_owned(), + ) + }) + .collect(); + assert_eq!(actual, documented, "{variant} route contract drift"); + } +} diff --git a/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs b/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs index f76697e3..946cb674 100644 --- a/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs +++ b/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs @@ -85,10 +85,12 @@ async fn status_of( body: Option, key: Option<&str>, ) -> (Status, String) { + let recorded_body = body.clone(); let mut request = match method { "GET" => client.get(path.to_owned()), "POST" => client.post(path.to_owned()), "DELETE" => client.delete(path.to_owned()), + "PUT" => client.put(path.to_owned()), other => panic!("unsupported method {other}"), } .header(bearer(token)); @@ -100,7 +102,22 @@ async fn status_of( } let response = request.dispatch().await; let status = response.status(); - (status, response.into_string().await.unwrap_or_default()) + let text = response.into_string().await.unwrap_or_default(); + if let Ok(output) = std::env::var("MARREQ_CONTRACT_TRANSCRIPT") { + use std::io::Write; + let record = serde_json::json!({ + "method": method, "path": path, "status": status.code, + "request": recorded_body, + "response": serde_json::from_str::(&text).unwrap_or(serde_json::Value::Null), + }); + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(output) + .expect("contract transcript"); + writeln!(file, "{record}").expect("record actual Rocket exchange"); + } + (status, text) } #[rocket::async_test] @@ -155,6 +172,8 @@ async fn delegated_scopes_gate_the_routes_wrapped_by_mcp_tools() { for (raw, scopes) in [ ("projects", vec!["projects:read"]), + ("uc-read", vec!["use_cases:read"]), + ("uc-write", vec!["use_cases:read", "use_cases:write"]), ("req-read", vec!["requirements:read"]), ("trace-read", vec!["traceability:read"]), ("trace-write", vec!["traceability:write"]), @@ -174,6 +193,48 @@ async fn delegated_scopes_gate_the_routes_wrapped_by_mcp_tools() { ); let client = Client::tracked(rocket).await.expect("Rocket client"); + // Contract verification uses the real Rocket handlers and migrated PostgreSQL. + // Credentials never enter the transcript; only synthetic payloads are recorded. + for path in ["/api/meta/build", "/api/meta/deployment"] { + let (status, _) = status_of(&client, "GET", path, "uc-read", None, None).await; + assert_eq!(status, Status::Ok); + } + let use_cases = format!("{RES_BASE}/use-cases"); + let (status, _) = status_of(&client, "GET", &use_cases, "req-read", None, None).await; + assert_eq!(status, Status::Forbidden); + let (status, created) = status_of( + &client, + "POST", + &use_cases, + "uc-write", + Some(serde_json::json!({"title": "Contract use case", "primary_actor": "Tester"})), + Some("contract-create-use-case"), + ) + .await; + assert_eq!(status, Status::Created, "{created}"); + let created: serde_json::Value = serde_json::from_str(&created).unwrap(); + let uc_id = created["id"].as_i64().unwrap(); + let revision = created["draft"]["revision"].as_i64().unwrap(); + let draft_path = format!("{use_cases}/{uc_id}/draft"); + let document = serde_json::json!({ + "schema": "marreq.use-case.v1", "title": "Contract use case", + "main": [{"id": "s_test1", "text": "Verify the API contract"}] + }); + let (status, body) = status_of( + &client, + "PUT", + &draft_path, + "uc-write", + Some(serde_json::json!({"base_revision": revision, "document": document})), + None, + ) + .await; + assert_eq!(status, Status::Ok, "{body}"); + let (status, _) = status_of(&client, "GET", &use_cases, "uc-read", None, None).await; + assert_eq!(status, Status::Ok); + let (status, _) = status_of(&client, "GET", &draft_path, "uc-read", None, None).await; + assert_eq!(status, Status::Ok); + // (method, path, token lacking the scope, token holding it, expected status with scope) let links = format!("{RES_BASE}/requirement-version-links"); let reads: Vec<(String, &str, &str, Status)> = vec![ diff --git a/mcp-server/Dockerfile b/mcp-server/Dockerfile index 08e905f7..1cafae39 100644 --- a/mcp-server/Dockerfile +++ b/mcp-server/Dockerfile @@ -13,6 +13,8 @@ COPY --from=build --chown=node:node /app/package.json ./ COPY --from=build --chown=node:node /app/node_modules ./node_modules COPY --from=build --chown=node:node /app/dist ./dist COPY --chown=node:node mcp-server/MARREQ_SKILL.md ./MARREQ_SKILL.md +COPY --chown=node:node mcp-server/compatibility.json ./compatibility.json +COPY docs/developer/openapi.yaml /app/contracts/openapi.yaml USER node EXPOSE 3000 HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \ diff --git a/mcp-server/compatibility.json b/mcp-server/compatibility.json new file mode 100644 index 00000000..776ee06e --- /dev/null +++ b/mcp-server/compatibility.json @@ -0,0 +1,4 @@ +{ + "requires_backend_min": "0.1.0", + "requires_backend_max": "0.2.99" +} diff --git a/mcp-server/src/client.ts b/mcp-server/src/client.ts index 77797c95..6e083172 100644 --- a/mcp-server/src/client.ts +++ b/mcp-server/src/client.ts @@ -1,3 +1,10 @@ +import type { + McpPrincipalResponse, + use_cases_UseCaseRow_response as UseCaseRow, + use_cases_UseCaseDetail_response as UseCaseDetail, + use_cases_DraftView_response as UseCaseDraft, + use_cases_DraftChangeView_response as UseCaseDraftChange, +} from "./generated-contract.js"; import type { SessionContext } from "./context.js"; import { currentBearer } from "./credentials.js"; @@ -18,13 +25,7 @@ export class MarreqClient { return this.request("/api/projects"); } - async getPrincipal(): Promise<{ - user_id: number; - authentication_type: string; - principal_id: string; - client_id?: string; - grant_id?: number; - }> { + async getPrincipal(): Promise { return this.request("/api/mcp/principal"); } @@ -360,7 +361,7 @@ export class MarreqClient { return `/api/projects/${this.ctx.projectId}/use-cases${path}`; } - async listUseCases(query: { q?: string; approval_state?: string; has_suspect?: boolean } = {}) { + async listUseCases(query: { q?: string; approval_state?: string; has_suspect?: boolean } = {}): Promise { const params = new URLSearchParams(); if (query.q) params.set("q", query.q); if (query.approval_state) params.set("approval_state", query.approval_state); @@ -369,14 +370,14 @@ export class MarreqClient { return this.request(this.useCases(q ? `?${q}` : "")); } - async getUseCase(id: number, versionId?: number) { + async getUseCase(id: number, versionId?: number): Promise { return this.request(this.useCases(`/${id}${versionId != null ? `?version_id=${versionId}` : ""}`)); } /** The shared draft, or null when there is none. */ - async getUseCaseDraft(id: number) { + async getUseCaseDraft(id: number): Promise { try { - return await this.request(this.useCases(`/${id}/draft`)); + return await this.request(this.useCases(`/${id}/draft`)); } catch (e) { if (e instanceof Error && e.message.startsWith("Marreq API 404")) return null; throw e; @@ -399,7 +400,7 @@ export class MarreqClient { payload: { reference_code?: string; title?: string; primary_actor?: string; document?: unknown }, idempotencyKey: string ) { - return this.request(this.useCases(), { + return this.request(this.useCases(), { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(payload), @@ -413,7 +414,7 @@ export class MarreqClient { }); } - async putUseCaseDraft(id: number, baseRevision: number | null, document: unknown) { + async putUseCaseDraft(id: number, baseRevision: number | null, document: unknown): Promise { return this.request(this.useCases(`/${id}/draft`), { method: "PUT", body: JSON.stringify({ base_revision: baseRevision, document }), diff --git a/mcp-server/src/generated-contract.ts b/mcp-server/src/generated-contract.ts new file mode 100644 index 00000000..5e16cda6 --- /dev/null +++ b/mcp-server/src/generated-contract.ts @@ -0,0 +1,450 @@ +// Generated by scripts/contracts/generate.py from docs/developer/openapi.yaml. +// Do not edit; this file carries wire types, not a second contract. + +export type ApiError = { + "status": number; + "error": string; + "message": string; + [key: string]: unknown; +}; + +export type BuildInfo = { + "backend_version": string; + "backend_git_sha": string; + "deployment_mode": "server" | "cloud"; + "frontend_compatibility": { + "min_version": string; + "max_version": string; + [key: string]: unknown; +}; + [key: string]: unknown; +}; + +export type DeploymentInfo = { + "mode": "server" | "cloud"; + "allows_self_registration": boolean; + "requires_email_verification": boolean; + "allows_admin_promotion": boolean; + "assigns_personal_workspace": boolean; + "allows_self_administered_user_creation": boolean; + "allows_database_backup": boolean; + [key: string]: unknown; +}; + +export type McpPrincipalResponse = { + "user_id": number; + "authentication_type": string; + "principal_id": string; + "client_id": (string | null); + "grant_id": (number | null); + [key: string]: unknown; +}; + +export type UseCaseBranch = { + "id": string; + "kind": "alternative" | "exception"; + "title": string; + "from_step": string; + "end": UseCaseBranchEnd; + "steps"?: Array; +}; + +export type UseCaseBranchEnd = ({ + "type": "rejoin"; + "step": string; +} | { + "type": "end"; + "outcome": "success" | "failure"; +}); + +export type UseCaseBranchResponse = { + "id": string; + "kind": "alternative" | "exception"; + "title": string; + "from_step": string; + "end": UseCaseBranchEnd; + "steps": Array; +}; + +export type UseCaseDependents = ("cascade" | { + "rebranch_to": string; + [key: string]: unknown; +}); + +export type UseCaseDiff = { + "from": (UseCaseDiffEnd | null); + "to": UseCaseDiffEnd; + "summary": { + "added": number; + "removed": number; + "edited": number; + "moved": number; + "rebranched": number; + [key: string]: unknown; +}; + "header": { + [key: string]: { + "old": (string | null | Array); + "new": (string | null | Array); + [key: string]: unknown; +}; +}; + "elements": Array; + [key: string]: unknown; +}; + +export type UseCaseDiffEnd = { + "version_id": (number | null); + "version_number": (number | null); + "draft_revision": (number | null); + [key: string]: unknown; +}; + +export type UseCaseDiffSide = { + "list": "preconditions" | "main" | "postconditions" | "branch" | "branches"; + "branch_id"?: string; + "label": (string | null); + "text": string; + "from_step"?: string; + "end"?: UseCaseBranchEnd; + "kind"?: "alternative" | "exception"; + [key: string]: unknown; +}; + +export type UseCaseDocument = { + "schema"?: "marreq.use-case.v1"; + "title": string; + "primary_actor"?: (string | null); + "secondary_actors"?: Array; + "summary"?: (string | null); + "preconditions"?: Array; + "main"?: Array; + "branches"?: Array; + "postconditions"?: Array; +}; + +export type UseCaseDocumentResponse = { + "schema": "marreq.use-case.v1"; + "title": string; + "primary_actor": (string | null); + "secondary_actors": Array; + "summary": (string | null); + "preconditions": Array; + "main": Array; + "branches": Array; + "postconditions": Array; +}; + +export type UseCaseEffect = ({ + "type": "relabelled"; + "id": string; + "from": string; + "to": string; + [key: string]: unknown; +} | { + "type": "removed"; + "id": string; + [key: string]: unknown; +} | { + "type": "rebranched"; + "id": string; + "from_step": string; + [key: string]: unknown; +} | { + "type": "created"; + "id": string; + [key: string]: unknown; +}); + +export type UseCaseElement = { + "id": string; + "text": string; +}; + +export type UseCaseElementDiff = { + "id": string; + "kind": "precondition" | "postcondition" | "step" | "branch"; + "changes": Array<"added" | "removed" | "edited" | "moved" | "rebranched">; + "old": (UseCaseDiffSide | null); + "new": (UseCaseDiffSide | null); + "text_diff"?: Array<{ + "op": "equal" | "delete" | "insert"; + "text": string; + [key: string]: unknown; +}>; + [key: string]: unknown; +}; + +export type UseCaseListRef = ("preconditions" | "main" | "postconditions" | { + "branch": string; + [key: string]: unknown; +}); + +export type UseCaseNewBranch = { + "id"?: (string | null); + "kind": "alternative" | "exception"; + "title": string; + "from_step": string; + "end": UseCaseBranchEnd; + "steps"?: Array; +}; + +export type UseCaseNewElement = { + "id"?: (string | null); + "text": string; +}; + +export type UseCaseOperation = ({ + "op": "set_header"; + "title"?: (string | null); + "primary_actor"?: (string | null); + "secondary_actors"?: (Array | null); + "summary"?: (string | null); +} | { + "op": "add_element"; + "list": UseCaseListRef; + "after"?: (string | null); + "element": UseCaseNewElement; +} | { + "op": "update_text"; + "id": string; + "text": string; +} | { + "op": "move_element"; + "id": string; + "list"?: (UseCaseListRef | null); + "after"?: (string | null); +} | { + "op": "remove_element"; + "id": string; + "dependents"?: (UseCaseDependents | null); +} | { + "op": "add_branch"; + "branch": UseCaseNewBranch; +} | { + "op": "update_branch"; + "id": string; + "title"?: (string | null); + "kind"?: ("alternative" | "exception" | null); + "from_step"?: (string | null); + "end"?: (UseCaseBranchEnd | null); +} | { + "op": "remove_branch"; + "id": string; +}); + +export type UseCasePublishResponse = { + "version": use_cases_VersionView_response; + "removed_links": Array; + [key: string]: unknown; +}; + +export type entities_User_response = { + "id": number; + "username": string; + "name": string; + "email": string; + "creation_date": string; + "last_login": string; + "is_admin": boolean; + "email_verified": boolean; + [key: string]: unknown; +}; + +export type external_ProviderDiscovery_response = { + "password_enabled": boolean; + "external": Array; + [key: string]: unknown; +}; + +export type external_PublicAuthProvider_response = { + "id": string; + "display_name": string; + [key: string]: unknown; +}; + +export type use_cases_BaselineUseCaseView_response = { + "use_case_id": number; + "reference_code": string; + "title": string; + "primary_actor": (string | null); + "version_id": number; + "version_number": number; + "approval_state": string; + "current_version_id": (number | null); + "current_version_number": (number | null); + "link_count": number; + [key: string]: unknown; +}; + +export type use_cases_ClearSuspectResponse_response = { + "status": string; + "cleared": boolean; + [key: string]: unknown; +}; + +export type use_cases_DraftChangeView_response = { + "use_case_id": number; + "revision": number; + "base_version_id": (number | null); + "updated_by": (number | null); + "updated_by_name": (string | null); + "updated_at": string; + "document": UseCaseDocumentResponse; + "labels": { + [key: string]: string; +}; + "effects": Array; + [key: string]: unknown; +}; + +export type use_cases_DraftSummary_response = { + "revision": number; + "base_version_id": (number | null); + "updated_by": (number | null); + "updated_by_name": (string | null); + "updated_at": string; + [key: string]: unknown; +}; + +export type use_cases_DraftView_response = { + "use_case_id": number; + "revision": number; + "base_version_id": (number | null); + "updated_by": (number | null); + "updated_by_name": (string | null); + "updated_at": string; + "document": UseCaseDocumentResponse; + "labels": { + [key: string]: string; +}; + [key: string]: unknown; +}; + +export type use_cases_LinkView_response = { + "id": number; + "element_id": (string | null); + "element_label": (string | null); + "target_type": "requirement" | "verification"; + "requirement_id": (number | null); + "verification_id": (number | null); + "target_reference_code": string; + "target_title": string; + "rationale": (string | null); + "suspect": boolean; + "suspect_at": (string | null); + "suspect_reason": (string | null); + "linked_requirement_version_id": (number | null); + "current_requirement_version_id": (number | null); + "created_by": (number | null); + "created_at": string; + [key: string]: unknown; +}; + +export type use_cases_NextCode_response = { + "reference_code": string; + [key: string]: unknown; +}; + +export type use_cases_PutDraftRequest_request = { + "base_revision"?: (number | null); + "document": UseCaseDocument; + [key: string]: unknown; +}; + +export type use_cases_SetApprovalRequest_request = { + "state": string; + [key: string]: unknown; +}; + +export type use_cases_UseCaseCommentView_response = { + "id": number; + "use_case_id": number; + "use_case_version_id": (number | null); + "element_id": (string | null); + "element_label": (string | null); + "author_id": number; + "author_name": string; + "body": string; + "created_at": string; + [key: string]: unknown; +}; + +export type use_cases_UseCaseDetail_response = { + "id": number; + "project_id": number; + "reference_code": string; + "title": string; + "created_by": (number | null); + "created_at": string; + "updated_at": string; + "current_version": (use_cases_VersionView_response | null); + "draft": (use_cases_DraftSummary_response | null); + [key: string]: unknown; +}; + +export type use_cases_UseCaseRef_response = { + "use_case_id": number; + "reference_code": string; + "title": string; + "link_id": number; + "element_id": (string | null); + "element_label": (string | null); + "suspect": boolean; + [key: string]: unknown; +}; + +export type use_cases_UseCaseRow_response = { + "id": number; + "reference_code": string; + "title": string; + "primary_actor": (string | null); + "step_count": number; + "alternative_count": number; + "exception_count": number; + "approval_state": (string | null); + "current_version_number": (number | null); + "has_draft": boolean; + "link_count": number; + "suspect_count": number; + "updated_at": string; + [key: string]: unknown; +}; + +export type use_cases_VersionSummary_response = { + "id": number; + "version_number": number; + "approval_state": string; + "title": string; + "primary_actor": (string | null); + "change_note": (string | null); + "created_by": (number | null); + "created_by_name": (string | null); + "created_at": string; + "reviewed_by": (number | null); + "reviewed_at": (string | null); + "approved_by": (number | null); + "approved_at": (string | null); + [key: string]: unknown; +}; + +export type use_cases_VersionView_response = { + "id": number; + "version_number": number; + "approval_state": string; + "title": string; + "primary_actor": (string | null); + "change_note": (string | null); + "created_by": (number | null); + "created_by_name": (string | null); + "created_at": string; + "reviewed_by": (number | null); + "reviewed_at": (string | null); + "approved_by": (number | null); + "approved_at": (string | null); + "document": UseCaseDocumentResponse; + "labels": { + [key: string]: string; +}; + [key: string]: unknown; +}; diff --git a/mcp-server/src/remote.ts b/mcp-server/src/remote.ts index f2ed51cc..4d517c49 100644 --- a/mcp-server/src/remote.ts +++ b/mcp-server/src/remote.ts @@ -1,3 +1,4 @@ +import type { McpPrincipalResponse } from "./generated-contract.js"; import { randomUUID } from "node:crypto"; import type { Server } from "node:http"; import type { Request, Response } from "express"; @@ -9,13 +10,7 @@ import { loadContext, type SessionContext } from "./context.js"; import { MarreqAuthenticationError, MarreqClient } from "./client.js"; import { withBearer } from "./credentials.js"; -interface Principal { - user_id: number; - authentication_type: string; - principal_id: string; - client_id?: string; - grant_id?: number; -} +type Principal = McpPrincipalResponse; type PrincipalResolution = | { kind: "ok"; principal: Principal } diff --git a/scripts/check-api-compatibility.sh b/scripts/check-api-compatibility.sh new file mode 100755 index 00000000..b749d780 --- /dev/null +++ b/scripts/check-api-compatibility.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +exec python3 "$(dirname "$0")/contracts/compatibility.py" "$@" diff --git a/scripts/contracts/bundle.py b/scripts/contracts/bundle.py new file mode 100644 index 00000000..0414620e --- /dev/null +++ b/scripts/contracts/bundle.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Export the exact checked-out component's contract for retention with its image. + +This records evidence; deployment verification still rejects unverified operations. +Never use this to manufacture contracts for older releases from current sources. +""" +import argparse +import json +import subprocess +import sys + +from compatibility import ROOT, ContractError, digest, load, policy, version +from check import check + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--component", choices=["backend", "frontend", "mcp"], required=True) + parser.add_argument("--version", required=True) + parser.add_argument("--variant", choices=["server", "cloud"]) + parser.add_argument("--output", required=True) + args = parser.parse_args() + version(args.version) + errors, gaps = check() + if errors: + raise ContractError("; ".join(errors)) + if args.component == "backend" and not args.variant: + raise ContractError("backend bundle requires --variant") + sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip() + # Verify tracked contract inputs match the commit whose identity is exported. + paths = list(load(ROOT / "docs/developer/api-contract-source-review.json")) + ["docs/developer/openapi.yaml", "docs/developer/api-contract-source-review.json"] + tracked = subprocess.run(["git", "ls-files", "--error-unmatch", "--", *paths], cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if tracked.returncode: + raise ContractError("commit all audited source files before exporting a source-bound bundle") + result = subprocess.run(["git", "diff", "--quiet", "HEAD", "--", *paths], cwd=ROOT) + if result.returncode: + raise ContractError("commit audited contract inputs before exporting a source-bound bundle") + contract = load(ROOT / "docs/developer/openapi.yaml") + # Until consumer call coverage is complete, use the entire backend surface. + # This is conservative and documented; never silently omit dependencies. + bundle = {"format": "marreq.contract.v1", "component": args.component, "version": args.version, "source_sha": sha, "contract": contract} + if args.component == "backend": + bundle.update(variant=args.variant, frontend_compatibility=load(ROOT / "marreq-core/frontend_compatibility.json")) + else: + directory = "mcp-server" if args.component == "mcp" else "frontend" + bundle["compatibility"] = load(ROOT / directory / "compatibility.json") + policy(bundle["compatibility"]) + from pathlib import Path + Path(args.output).write_text(json.dumps(bundle, indent=2) + "\n") + print(json.dumps({"file": args.output, "sha256": digest(args.output), "version": args.version, "source_sha": sha})) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except (ContractError, OSError, ValueError) as error: + print(str(error), file=sys.stderr) + sys.exit(1) diff --git a/scripts/contracts/check.py b/scripts/contracts/check.py new file mode 100644 index 00000000..8b98ceeb --- /dev/null +++ b/scripts/contracts/check.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Validate OpenAPI, route coverage, compatibility metadata, and review evidence.""" +import argparse +import hashlib +import json +import sys + +from openapi_spec_validator import validate +from compatibility import ROOT, ContractError, load, policy, version, resolve +from generate import generated +from inventory import source_routes + + +def source_evidence(): + files = set() + for directory in ("marreq-core/src", "marreq-server/src", "marreq-cloud/src", "frontend/src/api", "mcp-server/src"): + files.update(file for file in (ROOT / directory).rglob("*") if file.suffix in (".ts", ".rs") and "__tests__" not in file.parts and not file.name.endswith(".test.ts") and file.name != "generated-contract.ts") + # Direct frontend calls outside the centralized API modules are still + # dependencies, including binary downloads/imports and OAuth navigation. + for file in (ROOT / "frontend/src").rglob("*"): + if file.suffix in (".ts", ".tsx") and "__tests__" not in file.parts: + content = file.read_text() + if "/api/" in content or "fetch(" in content: + files.add(file) + files.update(ROOT / name for name in ("marreq-core/src/app.rs", "marreq-core/src/build_info.rs", "marreq-core/src/api/mod.rs", "marreq-cloud/src/routes.rs", "marreq-cloud/src/deployment.rs", "marreq-server/src/deployment.rs", "marreq-server/src/routes.rs", "marreq-server/src/main.rs", "marreq-cloud/src/main.rs", "frontend/compatibility.json", "marreq-core/frontend_compatibility.json", "mcp-server/compatibility.json")) + return {str(file.relative_to(ROOT)): hashlib.sha256(file.read_bytes()).hexdigest() for file in sorted(files) if file.is_file()} + + +def check(require_complete=False): + spec = load(ROOT / "docs/developer/openapi.yaml") + resolve(spec, spec) + validate(spec) + for component in ("frontend", "mcp-server"): + policy(load(ROOT / component / "compatibility.json")) + backend_range = load(ROOT / "marreq-core/frontend_compatibility.json") + if set(backend_range) != {"min_version", "max_version"} or version(backend_range["min_version"]) > version(backend_range["max_version"]): + raise ContractError("invalid backend frontend compatibility range") + errors = [] + source = {(r["path"], r["method"]): r for r in source_routes()} + documented = {(p, m): op for p, item in spec["paths"].items() for m, op in item.items()} + for key in source.keys() - documented.keys(): + errors.append(f"undocumented backend route: {key}") + for key in documented.keys() - source.keys(): + errors.append(f"documented route missing in backend: {key}") + for key in source.keys() & documented.keys(): + variants = [source[key]["variant"]] if source[key]["variant"] != "shared" else ["server", "cloud"] + if documented[key].get("x-marreq-variants") != variants: + errors.append(f"deployment variant mismatch: {key}") + if documented[key].get("x-marreq-guards") != source[key]["guards"]: + errors.append(f"authentication guard mismatch: {key}") + if documented[key].get("x-marreq-handler") != source[key]["handler"]: + errors.append(f"handler mismatch: {key}") + for path, content in generated(spec).items(): + if not path.exists() or path.read_text() != content: + errors.append(f"generated artifact drift: {path.relative_to(ROOT)}; run scripts/contracts/generate.py") + lock = ROOT / "docs/developer/api-contract-source-review.json" + if not lock.exists() or load(lock) != source_evidence(): + errors.append("API source changed since contract review. Audit wire behavior, update OpenAPI/tests, then run check.py --record-source-review. This acknowledgement is not a substitute for schema or integration tests.") + gaps = [f"{m.upper()} {p}" for (p, m), op in documented.items() if op.get("x-marreq-unverified")] + if require_complete and spec.get("x-marreq-contract-complete") is not True: + errors.append("producer contract is explicitly incomplete; finish the P0 audit before publication") + if require_complete and gaps: + errors.extend(f"unverified wire contract: {gap}" for gap in gaps) + return errors, gaps + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--record-source-review", action="store_true", help="Explicitly acknowledge source audit; review resulting diff") + parser.add_argument("--require-complete", action="store_true", help="Release safety: fail on inventory-only operations") + args = parser.parse_args() + if args.record_source_review: + (ROOT / "docs/developer/api-contract-source-review.json").write_text(json.dumps(source_evidence(), indent=2) + "\n") + errors, gaps = check(args.require_complete) + print(json.dumps({"valid": not errors, "unverified_operations": len(gaps), "errors": errors}, indent=2)) + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/contracts/check_baseline.py b/scripts/contracts/check_baseline.py new file mode 100644 index 00000000..4c34a673 --- /dev/null +++ b/scripts/contracts/check_baseline.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Reject breaking changes to audited operations in the PR base contract.""" +import copy +import json +from pathlib import Path +import sys + +from compatibility import ROOT, compare, load + + +def check(previous, proposed): + if "x-marreq-contract-complete" not in previous: + # The pre-bootstrap document is explicitly partial and has a different + # /api server base. It is not evidence for a historical consumer release. + return {"compatible": None, "bootstrap": True, "errors": [], "reason": "Previous OpenAPI predates verified contract inventory; no historical compatibility established."} + audited = copy.deepcopy(previous) + audited["paths"] = {path: {method: op for method, op in item.items() if not op.get("x-marreq-unverified")} for path, item in previous["paths"].items()} + audited["paths"] = {path: item for path, item in audited["paths"].items() if item} + errors = sorted(set(error for variant in ("server", "cloud") for error in compare(audited, proposed, variant))) + return {"compatible": not errors, "errors": errors} + + +if __name__ == "__main__": + result = check(load(Path(sys.argv[1])), load(ROOT / "docs/developer/openapi.yaml")) + print(json.dumps(result, indent=2)) + sys.exit(1 if result["errors"] else 0) diff --git a/scripts/contracts/compatibility.py b/scripts/contracts/compatibility.py new file mode 100644 index 00000000..ef89be12 --- /dev/null +++ b/scripts/contracts/compatibility.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +"""Fail-closed, offline compatibility checks for immutable component bundles. + +The deployment plan is an input, never inferred from package versions or main. +Only local files are read; artifact downloading/authentication belongs to CI/CD. +""" +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +from pathlib import Path +import re +import sys + +import yaml +from openapi_spec_validator import validate as validate_spec + +ROOT = Path(__file__).resolve().parents[2] +METHODS = {"get", "post", "put", "patch", "delete", "head", "options"} + + +class ContractError(ValueError): + pass + + +def load(path): + with Path(path).open() as stream: + return yaml.safe_load(stream) + + +def version(value): + # No coercion, wildcards, prereleases, or lexicographic comparison. Release + # candidates need their own explicitly verified deployment plan. + if not isinstance(value, str) or not re.fullmatch(r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)", value): + raise ContractError(f"expected stable SemVer X.Y.Z, got {value!r}") + return tuple(map(int, value.split("."))) + + +def policy(value): + if not isinstance(value, dict) or set(value) - {"requires_backend_min", "requires_backend_max"}: + raise ContractError("invalid compatibility metadata fields") + if "requires_backend_min" not in value: + raise ContractError("compatibility metadata requires requires_backend_min") + low = version(value["requires_backend_min"]) + high = version(value["requires_backend_max"]) if "requires_backend_max" in value else None + if high is not None and low > high: + raise ContractError("compatibility minimum exceeds maximum") + return low, high + + +def resolve(node, document, seen=()): + """Expand local refs; reject external refs and recursive schemas conservatively.""" + if isinstance(node, list): + return [resolve(item, document, seen) for item in node] + if not isinstance(node, dict): + return node + if "$ref" in node: + ref = node["$ref"] + if not ref.startswith("#/") or ref in seen: + raise ContractError(f"unsupported external or recursive reference: {ref}") + target = document + for key in ref[2:].split("/"): + target = target[key.replace("~1", "/").replace("~0", "~")] + target = copy.deepcopy(target) + target.update({key: value for key, value in node.items() if key != "$ref"}) + return resolve(target, document, (*seen, ref)) + return {key: resolve(value, document, seen) for key, value in node.items()} + + +def subset(source, target, location): + """Prove every source value is accepted by target, or fail closed. + + Requests: released consumer -> proposed producer. + Responses: proposed producer -> released consumer. + Unsupported changed constraints cannot accidentally establish compatibility. + """ + if source == target or target is True or target == {} or source is False: + return [] + if target is False or source is True: + return [f"{location}: schema acceptance changed"] + if not isinstance(source, dict) or not isinstance(target, dict): + return [f"{location}: unsupported schema change"] + ignored = {"title", "description", "examples", "example", "deprecated", "default", "readOnly", "writeOnly", "$schema", "x-rust-model"} + supported = {"type", "enum", "const", "properties", "required", "items", "additionalProperties", "minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"} + unknown = (set(source) | set(target)) - supported - ignored + errors = [] + for key in sorted(unknown): + if source.get(key) != target.get(key): + errors.append(f"{location}: changed {key} needs explicit compatibility evidence") + def types(schema): + value = schema.get("type", []) + return set(value if isinstance(value, list) else [value]) + a, b = types(source), types(target) + if b and (not a or any(t not in b and not (t == "integer" and "number" in b) for t in a)): + errors.append(f"{location}: type {sorted(a)} is not accepted by {sorted(b)}") + for key in ("enum", "const"): + if key in target: + candidates = source.get("enum", [source["const"]] if "const" in source else None) + accepted = target[key] if key == "enum" else [target[key]] + if candidates is None or any(item not in accepted for item in candidates): + errors.append(f"{location}: incompatible {key}") + for key in ("minimum", "minLength", "minItems", "minProperties"): + if key in target and (key not in source or source[key] < target[key]): + errors.append(f"{location}: tightened {key}") + for key in ("maximum", "maxLength", "maxItems", "maxProperties"): + if key in target and (key not in source or source[key] > target[key]): + errors.append(f"{location}: tightened {key}") + for key in sorted(set(target.get("required", [])) - set(source.get("required", []))): + errors.append(f"{location}.{key}: required property is not guaranteed") + source_props = source.get("properties", {}) + target_props = target.get("properties", {}) + source_extra = source.get("additionalProperties", True) + target_extra = target.get("additionalProperties", True) + for key in sorted(set(source_props) | set(target_props)): + left = source_props.get(key, source_extra) + right = target_props.get(key, target_extra) + if left is not False: + errors.extend(subset(left, right, f"{location}.{key}")) + errors.extend([] if source_extra is False else subset(source_extra, target_extra, f"{location}.*")) + if "items" in source or "items" in target: + errors.extend(subset(source.get("items", True), target.get("items", True), f"{location}[]")) + return errors + + +def operations(spec, variant): + result = {} + for path, item in spec["paths"].items(): + for method in METHODS & item.keys(): + op = item[method] + if variant in op.get("x-marreq-variants", ["server", "cloud"]): + result[(path, method)] = {**op, "parameters": item.get("parameters", []) + op.get("parameters", [])} + return result + + +def compare(consumer, backend, variant): + consumer_resolved = resolve(consumer, consumer) + backend_resolved = resolve(backend, backend) + validate_spec(consumer) + validate_spec(backend) + required = operations(consumer_resolved, variant) + offered = operations(backend_resolved, variant) + errors = [] + for key, old in required.items(): + path, method = key + at = f"{method.upper()} {path}" + if key not in offered: + errors.append(f"{at}: endpoint missing in {variant} backend") + continue + new = offered[key] + for op in (old, new): + if op.get("x-marreq-unverified"): + errors.append(f"{at}: contract has unverified behavior; compatibility cannot be established") + old_security = old.get("security", consumer.get("security", [])) + new_security = new.get("security", backend.get("security", [])) + if old_security != new_security: + errors.append(f"{at}: authentication/scopes changed") + if consumer.get("components", {}).get("securitySchemes") != backend.get("components", {}).get("securitySchemes"): + errors.append(f"{at}: security scheme definitions changed") + for name in ("x-marreq-policy", "x-marreq-csrf", "x-marreq-bearer-scopes"): + if old.get(name) != new.get(name): + errors.append(f"{at}: {name} changed") + old_params = {(p["in"], p["name"]): p for p in old.get("parameters", [])} + new_params = {(p["in"], p["name"]): p for p in new.get("parameters", [])} + for p, param in new_params.items(): + if p not in old_params: + if param.get("required"): + errors.append(f"{at}: new required {p} parameter") + else: + if param.get("required") and not old_params[p].get("required"): + errors.append(f"{at}: {p} parameter became required") + errors.extend(subset(old_params[p].get("schema", {}), param.get("schema", {}), f"{at} parameter {p}")) + if set(old_params) - set(new_params): + errors.append(f"{at}: accepted parameters removed") + old_body, new_body = old.get("requestBody", {}), new.get("requestBody", {}) + if new_body.get("required") and not old_body.get("required"): + errors.append(f"{at}: request body became required") + for media, content in old_body.get("content", {}).items(): + if media not in new_body.get("content", {}): + errors.append(f"{at}: request media {media} removed") + else: + errors.extend(subset(content.get("schema", {}), new_body["content"][media].get("schema", {}), f"{at} request")) + old_responses, new_responses = old["responses"], new["responses"] + # Even error status changes matter: clients branch on 401/403/404/409. + if old_responses.keys() != new_responses.keys(): + errors.append(f"{at}: response status codes changed") + for status in old_responses.keys() & new_responses.keys(): + previous = old_responses[status] + proposed = new_responses[status] + if previous.get("headers", {}) != proposed.get("headers", {}): + errors.append(f"{at} {status}: response headers changed") + old_content, new_content = previous.get("content", {}), proposed.get("content", {}) + if old_content.keys() != new_content.keys(): + errors.append(f"{at} {status}: response media changed") + for media in old_content.keys() & new_content.keys(): + errors.extend(subset(new_content[media].get("schema", {}), old_content[media].get("schema", {}), f"{at} response {status}")) + return sorted(set(errors)) + + +def digest(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def read_bundle(descriptor, component, directory): + required = {"file", "sha256", "version", "source_sha"} + if not isinstance(descriptor, dict) or set(descriptor) != required: + raise ContractError(f"{component}: descriptor requires exactly {sorted(required)}") + version(descriptor["version"]) + if not re.fullmatch(r"[a-f0-9]{40}", descriptor["source_sha"]): + raise ContractError(f"{component}: immutable source SHA required") + relative = Path(descriptor["file"]) + if relative.is_absolute() or ".." in relative.parts: + raise ContractError(f"{component}: bundle file must stay inside the plan directory") + path = directory / relative + if not path.resolve().is_relative_to(directory.resolve()): + raise ContractError(f"{component}: bundle symlink escapes the plan directory") + if not path.is_file(): + raise ContractError(f"{component}: missing historical artifact {path}; never substitute current main") + if not re.fullmatch(r"[a-f0-9]{64}", descriptor["sha256"]) or digest(path) != descriptor["sha256"]: + raise ContractError(f"{component}: bundle SHA-256 mismatch") + bundle = load(path) + if bundle.get("format") != "marreq.contract.v1" or bundle.get("component") != component: + raise ContractError(f"{component}: wrong bundle format/component") + for key in ("version", "source_sha"): + if bundle.get(key) != descriptor[key]: + raise ContractError(f"{component}: bundle {key} disagrees with deployment plan") + resolve(bundle["contract"], bundle["contract"]) + validate_spec(bundle["contract"]) + return bundle + + +def verify_manifest(path): + manifest = load(path) + if not isinstance(manifest, dict) or set(manifest) != {"format", "variant", "backend", "frontend", "mcp"}: + raise ContractError("deployment plan requires format, variant, backend, frontend and mcp") + if manifest["format"] != "marreq.deployment-contract.v1" or manifest["variant"] not in ("server", "cloud"): + raise ContractError("invalid deployment plan format/variant") + bundles = {name: read_bundle(manifest[name], name, Path(path).parent) for name in ("backend", "frontend", "mcp")} + backend = bundles["backend"] + if backend.get("variant") != manifest["variant"]: + raise ContractError("backend artifact deployment variant mismatch") + errors = [] + backend_version = version(backend["version"]) + frontend_range = backend.get("frontend_compatibility", {}) + if set(frontend_range) != {"min_version", "max_version"}: + raise ContractError("backend frontend compatibility metadata is incomplete") + lo, hi = version(frontend_range["min_version"]), version(frontend_range["max_version"]) + if lo > hi: + raise ContractError("backend frontend compatibility range is inverted") + if not lo <= version(bundles["frontend"]["version"]) <= hi: + errors.append("frontend: outside backend's supported consumer range") + for name in ("frontend", "mcp"): + consumer = bundles[name] + low, high = policy(consumer.get("compatibility")) + if backend_version < low or (high is not None and backend_version > high): + errors.append(f"{name}: backend {backend['version']} is outside declared range") + errors.extend(f"{name}: {error}" for error in compare(consumer["contract"], backend["contract"], manifest["variant"])) + return {"compatible": not errors, "variant": manifest["variant"], "versions": {name: bundle["version"] for name, bundle in bundles.items()}, "errors": errors} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True, type=Path) + parser.add_argument("--candidate-component", choices=["backend", "frontend", "mcp"]) + parser.add_argument("--candidate-version") + parser.add_argument("--candidate-source-sha") + parser.add_argument("--candidate-bundle", type=Path) + args = parser.parse_args() + try: + candidate = (args.candidate_component, args.candidate_version, args.candidate_source_sha) + if any(candidate): + if not all(candidate): + raise ContractError("candidate check requires component, version and source SHA") + manifest = load(args.manifest) + descriptor = manifest[args.candidate_component] + if descriptor["version"] != args.candidate_version or descriptor["source_sha"] != args.candidate_source_sha: + raise ContractError("deployment plan does not reference the exact tagged candidate; refusing to check a different artifact") + if args.candidate_bundle: + if not all(candidate): + raise ContractError("candidate bundle requires candidate identity") + if digest(args.candidate_bundle) != descriptor["sha256"]: + raise ContractError("candidate contract is not the source-exported bundle for this tag") + result = verify_manifest(args.manifest) + except (ValueError, OSError, KeyError, TypeError) as error: + result = {"compatible": False, "errors": [str(error)]} + except Exception as error: # validators also have their own exception types + result = {"compatible": False, "errors": [f"invalid contract: {error}"]} + print(json.dumps(result, indent=2)) + return 0 if result["compatible"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/contracts/generate.py b/scripts/contracts/generate.py new file mode 100644 index 00000000..b9037409 --- /dev/null +++ b/scripts/contracts/generate.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Reproduce TypeScript wire types and compiled-Rocket route expectations.""" +import argparse +import json +import sys + +from compatibility import ROOT, load + + +def ts(node): + if node is True or node == {}: + return "unknown" + if node is False: + return "never" + if "$ref" in node: + return node["$ref"].split("/")[-1] + for key in ("oneOf", "anyOf", "allOf"): + if key in node: + join = " & " if key == "allOf" else " | " + return "(" + join.join(ts(item) for item in node[key]) + ")" + if "const" in node: + return json.dumps(node["const"]) + if "enum" in node: + return " | ".join(json.dumps(item) for item in node["enum"]) + kind = node.get("type") + if isinstance(kind, list): + return " | ".join(ts({**node, "type": value}) for value in kind) + if kind == "null": + return "null" + if kind in ("number", "integer"): + return "number" + if kind in ("string", "boolean"): + return kind + if kind == "array": + return f"Array<{ts(node.get('items', {}))}>" + if kind == "object" or "properties" in node: + lines = [] + for name, value in node.get("properties", {}).items(): + optional = "" if name in node.get("required", []) else "?" + lines.append(f" {json.dumps(name)}{optional}: {ts(value)};") + extra = node.get("additionalProperties", True) + if isinstance(extra, dict): + lines.append(f" [key: string]: {ts(extra)};") + elif extra: + lines.append(" [key: string]: unknown;") + return "{\n" + "\n".join(lines) + "\n}" + if node.get("x-marreq-unverified"): + return "unknown" + raise ValueError(f"unsupported TypeScript schema: {node}") + + +def generated(spec): + header = "// Generated by scripts/contracts/generate.py from docs/developer/openapi.yaml.\n// Do not edit; this file carries wire types, not a second contract.\n\n" + types = header + "\n\n".join(f"export type {name} = {ts(schema)};" for name, schema in sorted(spec["components"]["schemas"].items())) + "\n" + routes = [] + for path, item in spec["paths"].items(): + for method, operation in item.items(): + routes.append({"method": method.upper(), "path": path, "variants": operation.get("x-marreq-variants", ["server", "cloud"])}) + return { + ROOT / "frontend/src/api/generated-contract.ts": types, + ROOT / "mcp-server/src/generated-contract.ts": types, + ROOT / "docs/developer/api-contract-routes.json": json.dumps(routes, indent=2) + "\n", + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + stale = [] + for path, content in generated(load(ROOT / "docs/developer/openapi.yaml")).items(): + if args.check: + if not path.exists() or path.read_text() != content: + stale.append(str(path.relative_to(ROOT))) + else: + path.write_text(content) + if stale: + print("Generated contract drift; run python3 scripts/contracts/generate.py:\n" + "\n".join(stale), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/contracts/inventory.py b/scripts/contracts/inventory.py new file mode 100644 index 00000000..9376ed63 --- /dev/null +++ b/scripts/contracts/inventory.py @@ -0,0 +1,26 @@ +"""Source audit helpers, corroborated by Rocket's compiled route inventory. + +This extracts route signatures, not Rust syntax or inferred serialization schemas. +The checked-in OpenAPI remains the authoritative wire contract. +""" +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parents[2] + + +def source_routes(root=ROOT): + routes = [] + for directory, variant in (("marreq-core/src/api", "shared"), ("marreq-cloud/src/api", "cloud"), ("marreq-server/src/api", "server")): + for file in sorted((root / directory).glob("*.rs")): + text = file.read_text() + # Rocket route attributes always precede a free handler function. + pattern = r'#\[(get|post|put|patch|delete)\(\s*"([^"]+)"(.*?)\)\]\s*pub (?:async )?fn (\w+)\s*\((.*?)\)\s*(?:->\s*(.*?))?\s*\{' + for match in re.finditer(pattern, text, re.S): + method, uri, attributes, name, arguments, returns = match.groups() + base = "" if file.stem == "oauth" else "/api" + path, _, query = uri.partition("?") + path = base + re.sub(r"<([\w]+)(?:\.\.)?>", r"{\1}", path) + guards = re.findall(r"\b(?:ApiUser(?:OrBearer)?|SessionUser|OptionalSessionUser|AdminOnly|Project[A-Z][A-Za-z]*|ProjectsRead|StdioMcpAuditAuth|McpAuditAuth|McpPrincipalAuth|InternalMcpAudit)\b", arguments) + routes.append({"path": path, "method": method, "handler": f"{file.stem}::{name}", "source": str(file.relative_to(root)), "variant": variant, "arguments": arguments.strip(), "returns": (returns or "()").strip(), "query": query, "guards": guards, "data": re.findall(r'data\s*=\s*"<([^>]+)>"', attributes)}) + return routes diff --git a/scripts/contracts/requirements.txt b/scripts/contracts/requirements.txt new file mode 100644 index 00000000..d21b1402 --- /dev/null +++ b/scripts/contracts/requirements.txt @@ -0,0 +1,3 @@ +PyYAML==6.0.3 +jsonschema==4.26.0 +openapi-spec-validator==0.7.2 diff --git a/scripts/contracts/smoke-consumers.mjs b/scripts/contracts/smoke-consumers.mjs new file mode 100644 index 00000000..abbb0821 --- /dev/null +++ b/scripts/contracts/smoke-consumers.mjs @@ -0,0 +1,49 @@ +// Real HTTP producer/consumer smoke test. Run only through test-backend.sh: +// it provides an isolated fixture DB and loopback-only Rocket process. +import assert from 'node:assert/strict'; +import { appendFileSync } from 'node:fs'; +import { MarreqClient } from '../../mcp-server/dist/client.js'; +import { fetchJson } from '../../frontend/src/api/transport.ts'; + +const base = process.env.MARREQ_CONTRACT_BACKEND_URL; +assert.match(base ?? '', /^http:\/\/127\.0\.0\.1:\d+$/); +assert.ok(process.env.MARREQ_CONTRACT_TRANSCRIPT); +const networkFetch = globalThis.fetch; +globalThis.fetch = async (input, init = {}) => { + const url = new URL(input, base); + assert.equal(url.origin, base, 'contract smoke must stay on the isolated backend'); + const response = await networkFetch(url, init); + const text = await response.clone().text(); + let body = null; + try { body = JSON.parse(text); } catch { /* Empty guard and 204 bodies are valid. */ } + // Authorization headers/cookies are never retained in the transcript. + appendFileSync(process.env.MARREQ_CONTRACT_TRANSCRIPT, JSON.stringify({ + method: init.method ?? 'GET', path: url.pathname + url.search, + status: response.status, + request: typeof init.body === 'string' ? JSON.parse(init.body) : null, + response: body, + }) + '\n'); + return response; +}; + +// Execute the actual frontend transport against the running Rocket binary. +const build = await fetchJson('/api/meta/build'); +assert.equal(build.deployment_mode, 'server'); +const capabilities = await fetchJson('/api/meta/deployment'); +assert.equal(capabilities.allows_self_registration, false); + +// These delegated fixture credentials exist only in the ephemeral DB. +const context = { baseUrl: base, apiToken: 'uc-write', projectId: 1, mode: 'draft_write', traceWrite: false }; +const writer = new MarreqClient(context); +const reader = new MarreqClient({ ...context, apiToken: 'uc-read' }); +assert.ok(Array.isArray(await reader.listUseCases())); +const created = await writer.createUseCase({ title: 'Network contract use case', primary_actor: 'Tester' }, 'network-contract-create-key'); +assert.equal(typeof created.id, 'number'); +const document = { schema: 'marreq.use-case.v1', title: 'Network contract use case', main: [{ id: 's_net01', text: 'Exercise the real client against Rocket' }] }; +const saved = await writer.putUseCaseDraft(created.id, created.draft.revision, document); +assert.equal(saved.document.main[0].id, 's_net01'); +const detail = await reader.getUseCase(created.id); +assert.equal(detail.id, created.id); +await assert.rejects(() => reader.createUseCase({ title: 'Denied write' }, 'network-contract-denied-key'), (error) => error.status === 403); +await assert.rejects(() => reader.withProject(2).listUseCases(), (error) => error.status === 403 || /403/.test(error.message)); +console.log('Real frontend transport and MCP HTTP read/write/scope/project-boundary checks passed.'); diff --git a/scripts/contracts/test-backend.sh b/scripts/contracts/test-backend.sh new file mode 100755 index 00000000..4a1c66d4 --- /dev/null +++ b/scripts/contracts/test-backend.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Creates a disposable DB; ignores DATABASE_URL and .env. No production access. +set -euo pipefail +cd "$(dirname "$0")/../.." +work=$(mktemp -d) +container="marreq-contract-$RANDOM-$$" +backend_pid= +cleanup() { if [[ -n "$backend_pid" ]]; then kill "$backend_pid" 2>/dev/null || true; wait "$backend_pid" 2>/dev/null || true; fi; docker rm -f "$container" >/dev/null 2>&1 || true; rm -rf "$work"; } +trap cleanup EXIT +# No bind mounts, persistent volumes or remotely supplied database URLs. +docker run --detach --rm --name "$container" \ + --tmpfs /var/lib/postgresql/data \ + -e POSTGRES_DB=marreq_contract_test -e POSTGRES_USER=contract_test \ + -e POSTGRES_PASSWORD=disposable-contract-test \ + -p 127.0.0.1::5432 pgvector/pgvector:pg17@sha256:ac08538c6f8b9904c33c8224c5e5706dbe760aca29db1d096972b4052c22a75d >/dev/null +ready=false +for attempt in {1..60}; do + if docker exec "$container" pg_isready -U contract_test -d marreq_contract_test >/dev/null 2>&1; then ready=true; break; fi + sleep 1 +done +if [[ "$ready" != true ]]; then echo "isolated contract DB did not become ready" >&2; exit 1; fi +for migration in marreq-core/migrations/*/up.sql; do + docker exec -i "$container" psql -v ON_ERROR_STOP=1 -U contract_test -d marreq_contract_test < "$migration" > "$work/migration.log" 2>&1 || { cat "$work/migration.log"; exit 1; } +done +port=$(docker port "$container" 5432/tcp) +port=${port##*:} +export MARREQ_TEST_DATABASE_URL="postgres://contract_test:disposable-contract-test@127.0.0.1:$port/marreq_contract_test" +export DATABASE_URL="$MARREQ_TEST_DATABASE_URL" +export MARREQ_CONTRACT_TRANSCRIPT="$work/exchanges.jsonl" +cargo test --locked -p marreq-core --no-default-features --test delegated_oauth_mcp_tools_postgres_integration --no-run --message-format=json > "$work/test-build.jsonl" +test_executable=$(python3 -c 'import json,sys; print(next(r["executable"] for r in map(json.loads,open(sys.argv[1])) if r.get("executable") and r["target"]["name"]=="delegated_oauth_mcp_tools_postgres_integration"))' "$work/test-build.jsonl") +# A clean working directory and environment prevent dotenv/SMTP/semantic-search +# configuration inherited from a developer machine from entering test processes. +(cd "$work" && env -i PATH="$PATH" LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" \ + MARREQ_TEST_DATABASE_URL="$MARREQ_TEST_DATABASE_URL" \ + MARREQ_CONTRACT_TRANSCRIPT="$MARREQ_CONTRACT_TRANSCRIPT" \ + MARREQ_UI_MODE=api_only "$test_executable" --nocapture) +# Run the actual frontend transport and MCP HTTP client against a live socket. +# All database and service configuration is pinned to this ephemeral test stack. +npm run build --prefix mcp-server +cargo build --locked -p marreq-server --message-format=json > "$work/server-build.jsonl" +server_executable=$(python3 -c 'import json,sys; print(next(r["executable"] for r in map(json.loads,open(sys.argv[1])) if r.get("executable") and r["target"]["name"]=="marreq-server"))' "$work/server-build.jsonl") +backend_port=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()') +export ROCKET_ADDRESS=127.0.0.1 ROCKET_PORT="$backend_port" +export ROCKET_DATABASES="{my_db={url=\"$DATABASE_URL\",pool_size=2}}" +export MARREQ_PUBLIC_BASE_URL=http://localhost:8080 MARREQ_MCP_PUBLIC_URL=http://localhost:8080/mcp +export MARREQ_MCP_AUDIT_SECRET=integration-test-audit-secret-32-bytes +export MARREQ_AUTO_SEED=false MARREQ_UI_MODE=api_only +export MARREQ_CONTRACT_BACKEND_URL="http://127.0.0.1:$backend_port" +(cd "$work" && exec env -i PATH="$PATH" LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" \ + DATABASE_URL="$DATABASE_URL" ROCKET_ADDRESS=127.0.0.1 ROCKET_PORT="$backend_port" \ + ROCKET_DATABASES="$ROCKET_DATABASES" MARREQ_PUBLIC_BASE_URL="$MARREQ_PUBLIC_BASE_URL" \ + MARREQ_MCP_PUBLIC_URL="$MARREQ_MCP_PUBLIC_URL" MARREQ_MCP_AUDIT_SECRET="$MARREQ_MCP_AUDIT_SECRET" \ + MARREQ_AUTO_SEED=false MARREQ_UI_MODE=api_only "$server_executable") > "$work/backend.log" 2>&1 & +backend_pid=$! +ready=false +for attempt in {1..60}; do + if ! kill -0 "$backend_pid" 2>/dev/null; then cat "$work/backend.log"; exit 1; fi + if curl --silent --fail "$MARREQ_CONTRACT_BACKEND_URL/api/meta/health" > /dev/null; then ready=true; break; fi + sleep 1 +done +if [[ "$ready" != true ]]; then cat "$work/backend.log"; exit 1; fi +frontend/node_modules/.bin/rolldown scripts/contracts/smoke-consumers.mjs --platform node --format esm --file "$work/smoke.mjs" +node "$work/smoke.mjs" +# Fails if the test skipped, produced no transcript, or omitted a critical case. +python3 scripts/contracts/verify_transcript.py "$MARREQ_CONTRACT_TRANSCRIPT" diff --git a/scripts/contracts/test_compatibility.py b/scripts/contracts/test_compatibility.py new file mode 100644 index 00000000..823c9d34 --- /dev/null +++ b/scripts/contracts/test_compatibility.py @@ -0,0 +1,227 @@ +import copy +import hashlib +import json +from pathlib import Path +import subprocess +import tempfile +import unittest + +from compatibility import ContractError, ROOT, compare, policy, subset, verify_manifest + + +def spec(): + obj = {"type": "object", "properties": {"id": {"type": "integer"}, "state": {"type": "string", "enum": ["draft", "approved"]}}, "required": ["id", "state"], "additionalProperties": True} + return {"openapi": "3.1.0", "info": {"title": "fixture", "version": "1.0.0"}, "paths": {"/api/things/{id}": {"post": {"security": [{"bearerAuth": []}], "parameters": [{"in": "path", "name": "id", "required": True, "schema": {"type": "integer"}}], "requestBody": {"required": True, "content": {"application/json": {"schema": {"type": "object", "required": ["title"], "properties": {"title": {"type": "string"}}, "additionalProperties": False}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": obj}}}, "401": {"description": "Unauthorized"}}}}}, "components": {"securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer"}}}} + + +def operation(document): + return document["paths"]["/api/things/{id}"]["post"] + + +def response(document): + return operation(document)["responses"]["200"]["content"]["application/json"]["schema"] + + +def request(document): + return operation(document)["requestBody"]["content"]["application/json"]["schema"] + + +class CompatibilityTests(unittest.TestCase): + def setUp(self): + self.old = spec() + self.new = copy.deepcopy(self.old) + + def check(self): + return compare(self.old, self.new, "server") + + def test_valid_openapi(self): + self.assertEqual(self.check(), []) + + def test_removed_route(self): + self.new["paths"] = {} + self.assertIn("endpoint missing", self.check()[0]) + + def test_incompatible_request_type(self): + request(self.new)["properties"]["title"] = {"type": "integer"} + self.assertTrue(self.check()) + + def test_incompatible_response_type(self): + response(self.new)["properties"]["id"] = {"type": "string"} + self.assertTrue(self.check()) + + def test_removed_required_response(self): + response(self.new)["required"].remove("id") + self.assertTrue(self.check()) + + def test_new_required_request(self): + request(self.new)["properties"]["reason"] = {"type": "string"} + request(self.new)["required"].append("reason") + self.assertTrue(self.check()) + + def test_optional_request_addition(self): + request(self.new)["properties"]["reason"] = {"type": "string"} + self.assertEqual(self.check(), []) + + def test_response_addition(self): + response(self.new)["properties"]["extra"] = {"type": "boolean"} + response(self.new)["required"].append("extra") + self.assertEqual(self.check(), []) + + def test_response_enum_extension_breaks_exhaustive_consumers(self): + response(self.new)["properties"]["state"]["enum"].append("reviewed") + self.assertTrue(self.check()) + + def test_request_enum_restriction(self): + request(self.old)["properties"]["title"]["enum"] = ["a", "b"] + request(self.new)["properties"]["title"]["enum"] = ["a"] + self.assertTrue(self.check()) + + def test_oauth_scope_mismatch(self): + operation(self.new)["x-marreq-bearer-scopes"] = ["things:write"] + self.assertTrue(self.check()) + + def test_authentication_mismatch(self): + operation(self.new)["security"] = [] + self.assertTrue(self.check()) + + def test_status_code_mismatch(self): + operation(self.new)["responses"]["201"] = operation(self.new)["responses"].pop("200") + self.assertTrue(self.check()) + + def test_variants(self): + operation(self.old)["x-marreq-variants"] = ["cloud"] + self.new["paths"] = {} + self.assertEqual(self.check(), []) + self.assertTrue(compare(self.old, self.new, "cloud")) + + def test_unverified_contract_fails_even_when_identical(self): + operation(self.old)["x-marreq-unverified"] = True + self.assertTrue(compare(self.old, self.old, "cloud")) + + def test_unsupported_constraint_changes_fail_closed(self): + self.assertTrue(subset({"type": "string"}, {"type": "string", "pattern": "a+"}, "request")) + + def test_nullable_response_breaks_nonnullable_consumer(self): + self.assertTrue(subset({"type": ["string", "null"]}, {"type": "string"}, "response")) + + def test_metadata_invalid_or_incomplete(self): + for value in ({}, None, {"requires_backend_min": "1.x"}, {"requires_backend_min": "2.0.0", "requires_backend_max": "1.0.0"}, {"requires_backend_min": "1.0.0", "typo": "x"}): + with self.subTest(value=value), self.assertRaises(ContractError): + policy(value) + + def deployment(self, directory, incompatible=False, backend_version="1.1.0"): + plan = {"format": "marreq.deployment-contract.v1", "variant": "server"} + for component, ver in (("backend", backend_version), ("frontend", "1.0.0"), ("mcp", "1.2.0")): + contract = spec() + if incompatible and component == "backend": + response(contract)["required"].remove("id") + bundle = {"format": "marreq.contract.v1", "component": component, "version": ver, "source_sha": "a" * 40, "contract": contract} + if component == "backend": + bundle.update(variant="server", frontend_compatibility={"min_version": "1.0.0", "max_version": "2.0.0"}) + else: + bundle["compatibility"] = {"requires_backend_min": "1.0.0", "requires_backend_max": "1.9.0"} + data = json.dumps(bundle).encode() + (directory / f"{component}.json").write_bytes(data) + plan[component] = {"file": f"{component}.json", "sha256": hashlib.sha256(data).hexdigest(), "version": ver, "source_sha": "a" * 40} + path = directory / "plan.json" + path.write_text(json.dumps(plan)) + return path + + def test_unchanged_frontend_and_mcp_with_new_compatible_backend(self): + with tempfile.TemporaryDirectory() as temp: + result = verify_manifest(self.deployment(Path(temp))) + self.assertTrue(result["compatible"]) + self.assertNotEqual(result["versions"]["frontend"], result["versions"]["backend"]) + self.assertNotEqual(result["versions"]["mcp"], result["versions"]["backend"]) + + def test_unchanged_consumers_with_incompatible_backend(self): + with tempfile.TemporaryDirectory() as temp: + result = verify_manifest(self.deployment(Path(temp), incompatible=True)) + self.assertFalse(result["compatible"]) + self.assertTrue(any(e.startswith("frontend:") for e in result["errors"])) + self.assertTrue(any(e.startswith("mcp:") for e in result["errors"])) + + def test_versions_fail_independently_of_contracts(self): + with tempfile.TemporaryDirectory() as temp: + result = verify_manifest(self.deployment(Path(temp), backend_version="2.0.0")) + self.assertFalse(result["compatible"]) + + def test_missing_historical_artifact(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp)) + (Path(temp) / "mcp.json").unlink() + with self.assertRaisesRegex(ContractError, "missing historical artifact"): + verify_manifest(path) + + def test_tampered_artifact(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp)) + with (Path(temp) / "frontend.json").open("a") as stream: + stream.write(" ") + with self.assertRaisesRegex(ContractError, "SHA-256 mismatch"): + verify_manifest(path) + + def test_descriptor_version_cannot_substitute_another_artifact(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp)) + plan = json.loads(path.read_text()) + plan["frontend"]["version"] = "1.0.1" + path.write_text(json.dumps(plan)) + with self.assertRaisesRegex(ContractError, "version disagrees"): + verify_manifest(path) + + def test_bundle_path_cannot_escape_plan_directory(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp)) + plan = json.loads(path.read_text()) + plan["mcp"]["file"] = "../mcp.json" + path.write_text(json.dumps(plan)) + with self.assertRaisesRegex(ContractError, "inside the plan directory"): + verify_manifest(path) + + def test_backend_variant_cannot_be_substituted(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp)) + plan = json.loads(path.read_text()) + plan["variant"] = "cloud" + path.write_text(json.dumps(plan)) + with self.assertRaisesRegex(ContractError, "variant mismatch"): + verify_manifest(path) + + def test_candidate_plan_must_reference_tagged_source(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp)) + import sys + result = subprocess.run([sys.executable, str(ROOT / "scripts/contracts/compatibility.py"), "--manifest", str(path), "--candidate-component", "backend", "--candidate-version", "1.1.0", "--candidate-source-sha", "b" * 40], capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + self.assertIn("exact tagged candidate", json.loads(result.stdout)["errors"][0]) + + def test_invalid_openapi_does_not_pass_compatibility(self): + self.new.pop("info") + with self.assertRaises(Exception): + self.check() + + def test_external_refs_are_rejected_before_any_validator_network_access(self): + response(self.new)["properties"]["id"] = {"$ref": "https://example.test/schema.json"} + with self.assertRaisesRegex(ContractError, "external or recursive"): + self.check() + + def test_release_gate_zero_for_compatible_independent_artifacts(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp)) + import sys + result = subprocess.run([sys.executable, str(ROOT / "scripts/contracts/compatibility.py"), "--manifest", str(path)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0) + self.assertTrue(json.loads(result.stdout)["compatible"]) + + def test_release_gate_nonzero_on_real_violation(self): + with tempfile.TemporaryDirectory() as temp: + path = self.deployment(Path(temp), incompatible=True) + import sys + result = subprocess.run([sys.executable, str(ROOT / "scripts/contracts/compatibility.py"), "--manifest", str(path)], capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + self.assertFalse(json.loads(result.stdout)["compatible"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/contracts/test_contract_tooling.py b/scripts/contracts/test_contract_tooling.py new file mode 100644 index 00000000..78ef3d3f --- /dev/null +++ b/scripts/contracts/test_contract_tooling.py @@ -0,0 +1,70 @@ +import copy +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from compatibility import ContractError, ROOT, load +from check import check +from check_baseline import check as check_baseline +from generate import generated +from test_compatibility import spec, response +from verify_transcript import verify + + +class ProducerTests(unittest.TestCase): + def test_openapi_has_all_source_routes_and_generated_artifacts(self): + # Review lock is checked separately and cannot stand in for route/schema checks. + errors, gaps = check() + self.assertEqual(errors, []) + self.assertTrue(gaps) # Be honest about the bootstrap's incomplete evidence. + + def test_documented_route_missing_from_actual_source_is_rejected(self): + from inventory import source_routes + routes = source_routes() + with patch("check.source_routes", return_value=routes[1:]): + errors, _ = check() + self.assertTrue(any("documented route missing in backend" in error for error in errors)) + + def test_cloud_route_accidentally_enabled_on_server_is_rejected(self): + from inventory import source_routes + routes = copy.deepcopy(source_routes()) + next(route for route in routes if route["variant"] == "cloud")["variant"] = "shared" + with patch("check.source_routes", return_value=routes): + errors, _ = check() + self.assertTrue(any("deployment variant mismatch" in error for error in errors)) + + def test_source_change_without_contract_review_is_rejected(self): + with patch("check.source_evidence", return_value={"changed.rs": "f" * 64}): + errors, _ = check() + self.assertTrue(any("API source changed" in error for error in errors)) + + def test_release_completeness_cannot_pass_inventory_only_entries(self): + errors, gaps = check(require_complete=True) + self.assertTrue(gaps) + self.assertTrue(any("unverified wire contract" in error for error in errors)) + + def test_audited_baseline_rejects_required_response_removal(self): + old, new = spec(), spec() + old["x-marreq-contract-complete"] = False + response(new)["required"].remove("id") + self.assertTrue(check_baseline(old, new)["errors"]) + + def test_generated_types_come_from_the_contract(self): + document = load(ROOT / "docs/developer/openapi.yaml") + result = generated(document) + document["components"]["schemas"]["BuildInfo"]["properties"]["new_required"] = {"type": "integer"} + document["components"]["schemas"]["BuildInfo"]["required"].append("new_required") + self.assertNotEqual(result, generated(document)) + + def test_empty_integration_transcript_is_not_a_passing_backend_test(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "exchanges.jsonl" + path.write_text("") + with self.assertRaisesRegex(ContractError, "lacks critical cases"): + verify(path) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/contracts/verify_transcript.py b/scripts/contracts/verify_transcript.py new file mode 100644 index 00000000..c513cb5b --- /dev/null +++ b/scripts/contracts/verify_transcript.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Validate real Rocket responses and requests recorded by the DB integration test. + +No credentials are recorded. Inventory-only operations are counted separately. +""" +import argparse +import copy +import tempfile +from pathlib import Path +import json +import re +import sys + +from jsonschema import Draft202012Validator, ValidationError +from compatibility import ROOT, ContractError, load, operations, resolve + + +def verify(path): + spec = load(ROOT / "docs/developer/openapi.yaml") + declared = operations(resolve(spec, spec), "server") + records = [json.loads(line) for line in path.read_text().splitlines() if line.strip()] + checked, inventory_only, seen = 0, 0, set() + for record in records: + matches = [(p, op) for (p, m), op in declared.items() if m.upper() == record["method"] and re.fullmatch(re.sub(r"\{[^}]+\}", "[^/]+", re.escape(p).replace(r"\{", "{").replace(r"\}", "}")), record["path"].split("?")[0])] + if matches: + specificity = min(p.count("{") for p, _ in matches) + matches = [(p, op) for p, op in matches if p.count("{") == specificity] + if len(matches) != 1: + raise ContractError(f"ambiguous or missing operation for {record['method']} {record['path']}") + path_template, op = matches[0] + if op.get("x-marreq-unverified"): + inventory_only += 1 + continue + status = str(record["status"]) + response = op["responses"].get(status, op["responses"].get("default")) + if response is None: + raise ContractError(f"{path_template}: undocumented actual status {status}") + body = record["response"] + schema = response.get("content", {}).get("application/json", {}).get("schema") + if schema is not None: + Draft202012Validator(schema).validate(body) + if record.get("request") is not None and status.startswith("2"): + request_schema = op.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema") + if request_schema is None: + raise ContractError(f"{path_template}: undocumented JSON request") + Draft202012Validator(request_schema).validate(record["request"]) + checked += 1 + seen.add((record["method"], path_template, status)) + required = {("GET", "/api/meta/build", "200"), ("GET", "/api/meta/deployment", "200"), ("GET", "/api/projects/{project_id}/use-cases", "200"), ("POST", "/api/projects/{project_id}/use-cases", "201"), ("PUT", "/api/projects/{project_id}/use-cases/{id}/draft", "200"), ("GET", "/api/projects/{project_id}/use-cases", "403")} + missing = required - seen + if missing: + raise ContractError(f"integration transcript lacks critical cases: {sorted(missing)}") + return {"validated_exchanges": checked, "inventory_only_exchanges": inventory_only} + + +def negative_controls(path): + """Demonstrate rejection using corrupted copies of actual backend traffic.""" + records = [json.loads(line) for line in path.read_text().splitlines() if line.strip()] + cases = [] + for label in ("removed guaranteed response property", "incompatible response type", "incompatible MCP request"): + mutated = copy.deepcopy(records) + if label == "incompatible MCP request": + entry = next(r for r in mutated if r["method"] == "POST" and r["status"] == 201 and r["path"].endswith("/use-cases")) + entry["request"]["title"] = 123 + else: + entry = next(r for r in mutated if r["path"] == "/api/meta/build") + if label == "removed guaranteed response property": + del entry["response"]["backend_version"] + else: + entry["response"]["backend_version"] = 123 + with tempfile.TemporaryDirectory() as directory: + candidate = Path(directory) / "negative.jsonl" + candidate.write_text("\n".join(json.dumps(r) for r in mutated) + "\n") + try: + verify(candidate) + except ValidationError: + cases.append(label) + else: + raise ContractError(f"negative control was silently accepted: {label}") + return cases + + +if __name__ == "__main__": + from pathlib import Path + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("transcript", type=Path) + args = parser.parse_args() + result = verify(args.transcript) + result["rejected_negative_controls"] = negative_controls(args.transcript) + print(json.dumps(result, indent=2)) From 839b92e0e77782c3211a361be026131baf0f98b7 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 27 Oct 2026 20:21:46 +0100 Subject: [PATCH 2/2] wip --- .github/workflows/api-contracts.yml | 56 - .github/workflows/marreq-ci.yml | 29 +- .github/workflows/release-artifacts.yml | 23 +- .gitignore | 4 - Cargo.lock | 493 ++- Cargo.toml | 2 +- docker/Dockerfile | 2 - docker/frontend/Dockerfile | 2 - docs/developer/api-contract-routes.json | 1958 ----------- .../developer/api-contract-source-review.json | 378 -- docs/developer/http-api-contract.md | 274 +- docs/developer/mcp-setup.md | 21 +- docs/developer/openapi.yaml | 3033 +++-------------- docs/developer/use-cases-api.md | 19 +- docs/releases.md | 167 +- frontend/src/api/__tests__/catalog.test.ts | 4 +- frontend/src/api/catalog.ts | 4 +- frontend/src/api/generated-contract.ts | 934 ++--- frontend/src/api/types.ts | 21 +- frontend/src/pages/SystemLogsPage.tsx | 4 +- .../__tests__/ReportBuilderPage.test.tsx | 1 + marreq-cloud/Cargo.toml | 2 +- marreq-cloud/tests/api_contract_inventory.rs | 73 +- ...ed_oauth_mcp_tools_postgres_integration.rs | 7 + mcp-server/Dockerfile | 1 - mcp-server/dist/client.js | 104 +- mcp-server/dist/gap-tools.js | 276 ++ mcp-server/dist/generated-contract.js | 1 + mcp-server/dist/index.js | 98 +- mcp-server/dist/pagination.js | 49 + mcp-server/dist/prompts.js | 52 + mcp-server/dist/resources.js | 49 + mcp-server/package-lock.json | 66 +- mcp-server/src/client.ts | 13 +- mcp-server/src/generated-contract.ts | 888 +++-- mcp-server/src/remote.ts | 4 +- package-lock.json | 258 +- package.json | 1 + scripts/check-api-compatibility.sh | 2 +- .../__pycache__/check.cpython-314.pyc | Bin 0 -> 8966 bytes .../check_baseline.cpython-314.pyc | Bin 0 -> 2385 bytes .../__pycache__/compatibility.cpython-314.pyc | Bin 0 -> 23840 bytes .../__pycache__/generate.cpython-314.pyc | Bin 0 -> 6439 bytes .../__pycache__/inventory.cpython-314.pyc | Bin 0 -> 2736 bytes .../test_compatibility.cpython-314.pyc | Bin 0 -> 24708 bytes .../test_contract_tooling.cpython-314.pyc | Bin 0 -> 7690 bytes .../verify_transcript.cpython-314.pyc | Bin 0 -> 8050 bytes scripts/contracts/bundle.py | 59 - scripts/contracts/check.py | 80 - scripts/contracts/check_baseline.py | 26 - scripts/contracts/compatibility.py | 294 -- scripts/contracts/generate.py | 84 - scripts/contracts/inventory.py | 26 - scripts/contracts/requirements.txt | 3 - scripts/contracts/test_compatibility.py | 227 -- scripts/contracts/test_contract_tooling.py | 70 - scripts/contracts/verify_transcript.py | 90 - scripts/generate-api-types.mjs | 26 + scripts/install-oasdiff.sh | 15 + ...-consumers.mjs => smoke-api-consumers.mjs} | 43 +- .../test-backend.sh => test-api-contracts.sh} | 52 +- tools/marreq-contracts/Cargo.toml | 20 + tools/marreq-contracts/src/artifact.rs | 326 ++ tools/marreq-contracts/src/contract.rs | 387 +++ tools/marreq-contracts/src/coverage.rs | 714 ++++ tools/marreq-contracts/src/lib.rs | 4 + tools/marreq-contracts/src/main.rs | 204 ++ tools/marreq-contracts/src/traffic.rs | 103 + tools/marreq-contracts/tests/compatibility.rs | 459 +++ 69 files changed, 5300 insertions(+), 7385 deletions(-) delete mode 100644 .github/workflows/api-contracts.yml delete mode 100644 docs/developer/api-contract-routes.json delete mode 100644 docs/developer/api-contract-source-review.json create mode 100644 mcp-server/dist/gap-tools.js create mode 100644 mcp-server/dist/generated-contract.js create mode 100644 mcp-server/dist/pagination.js create mode 100644 mcp-server/dist/prompts.js create mode 100644 mcp-server/dist/resources.js create mode 100644 scripts/contracts/__pycache__/check.cpython-314.pyc create mode 100644 scripts/contracts/__pycache__/check_baseline.cpython-314.pyc create mode 100644 scripts/contracts/__pycache__/compatibility.cpython-314.pyc create mode 100644 scripts/contracts/__pycache__/generate.cpython-314.pyc create mode 100644 scripts/contracts/__pycache__/inventory.cpython-314.pyc create mode 100644 scripts/contracts/__pycache__/test_compatibility.cpython-314.pyc create mode 100644 scripts/contracts/__pycache__/test_contract_tooling.cpython-314.pyc create mode 100644 scripts/contracts/__pycache__/verify_transcript.cpython-314.pyc delete mode 100644 scripts/contracts/bundle.py delete mode 100644 scripts/contracts/check.py delete mode 100644 scripts/contracts/check_baseline.py delete mode 100644 scripts/contracts/compatibility.py delete mode 100644 scripts/contracts/generate.py delete mode 100644 scripts/contracts/inventory.py delete mode 100644 scripts/contracts/requirements.txt delete mode 100644 scripts/contracts/test_compatibility.py delete mode 100644 scripts/contracts/test_contract_tooling.py delete mode 100644 scripts/contracts/verify_transcript.py create mode 100644 scripts/generate-api-types.mjs create mode 100755 scripts/install-oasdiff.sh rename scripts/{contracts/smoke-consumers.mjs => smoke-api-consumers.mjs} (51%) rename scripts/{contracts/test-backend.sh => test-api-contracts.sh} (54%) create mode 100644 tools/marreq-contracts/Cargo.toml create mode 100644 tools/marreq-contracts/src/artifact.rs create mode 100644 tools/marreq-contracts/src/contract.rs create mode 100644 tools/marreq-contracts/src/coverage.rs create mode 100644 tools/marreq-contracts/src/lib.rs create mode 100644 tools/marreq-contracts/src/main.rs create mode 100644 tools/marreq-contracts/src/traffic.rs create mode 100644 tools/marreq-contracts/tests/compatibility.rs diff --git a/.github/workflows/api-contracts.yml b/.github/workflows/api-contracts.yml deleted file mode 100644 index da7ff175..00000000 --- a/.github/workflows/api-contracts.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: API contracts - -on: - workflow_call: - -permissions: - contents: read - -jobs: - contracts: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - cache: pip - cache-dependency-path: scripts/contracts/requirements.txt - - name: Install contract validation tooling - run: python -m pip install -r scripts/contracts/requirements.txt - - name: Validate OpenAPI, source review, metadata and generated consumers - run: python scripts/contracts/check.py - - name: Exercise compatible and breaking historical combinations - run: python -m unittest discover -s scripts/contracts -v - - name: Check audited contracts against PR base - if: github.event_name == 'pull_request' - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - git show "$BASE_SHA:docs/developer/openapi.yaml" > /tmp/base-openapi.yaml - python scripts/contracts/check_baseline.py /tmp/base-openapi.yaml - - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 - with: - toolchain: stable - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - shared-key: api-contracts - - name: Install PostgreSQL link dependency - run: sudo apt-get update && sudo apt-get install -y libpq-dev - - name: Verify actual compiled Server and Cloud route mounts - run: cargo test --locked -p marreq-cloud --test api_contract_inventory - - uses: actions/setup-node@v7 - with: - node-version: '22' - - name: Typecheck and test consumers of generated wire types - run: | - npm ci --prefix frontend - npm ci --prefix mcp-server - npm run build --prefix frontend - npm run build --prefix mcp-server - npm test --prefix frontend - npm test --prefix mcp-server - - name: Verify real Rocket and HTTP consumers with isolated migrated PostgreSQL - run: scripts/contracts/test-backend.sh diff --git a/.github/workflows/marreq-ci.yml b/.github/workflows/marreq-ci.yml index b0b478f7..00e27265 100644 --- a/.github/workflows/marreq-ci.yml +++ b/.github/workflows/marreq-ci.yml @@ -15,7 +15,34 @@ concurrency: jobs: api-contracts: - uses: ./.github/workflows/api-contracts.yml + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 + with: + toolchain: stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - uses: actions/setup-node@v7 + with: + node-version: '22' + cache: npm + - name: Install pinned standards tooling + run: scripts/install-oasdiff.sh + - name: Test native compatibility semantics and CLI failures + run: cargo test --locked -p marreq-contracts + - name: Install TypeScript tooling and consumers + run: | + npm ci + npm ci --prefix frontend + npm ci --prefix mcp-server + - name: Check generated consumer schemas + run: node scripts/generate-api-types.mjs --check + - name: Verify compiled Server/Cloud operations + run: cargo test --locked -p marreq-cloud --test api_contract_inventory + - name: Exercise isolated real Server/Cloud and HTTP consumers + run: scripts/test-api-contracts.sh + - name: Require verified contracts for every current consumer operation + run: cargo run --locked -p marreq-contracts -- validate lint: runs-on: ubuntu-latest diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index cefd10bc..c9bff04c 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -12,20 +12,21 @@ permissions: packages: write jobs: - contract-tests: - uses: ./.github/workflows/api-contracts.yml - compatibility: - needs: contract-tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 with: - python-version: '3.12' - - run: python -m pip install -r scripts/contracts/requirements.txt - - name: Require complete producer contracts before publication - run: python scripts/contracts/check.py --require-complete + toolchain: stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - name: Install pinned OpenAPI standards engine + run: scripts/install-oasdiff.sh + - name: Test contract semantics and validate producer document + run: | + cargo test --locked -p marreq-contracts + cargo run --locked -p marreq-contracts -- validate --document-only + cargo test --locked -p marreq-cloud --test api_contract_inventory - name: Require immutable deployment-plan revision id: plan env: @@ -52,9 +53,9 @@ jobs: *) echo "unsupported component tag" >&2; exit 1 ;; esac if [[ "$component" == backend ]]; then - python scripts/contracts/bundle.py --component backend --variant server --version "$version" --output /tmp/candidate-contract.json + cargo run --locked -p marreq-contracts -- bundle --component backend --variant server --version "$version" --output /tmp/candidate-contract.json else - python scripts/contracts/bundle.py --component "$component" --version "$version" --output /tmp/candidate-contract.json + cargo run --locked -p marreq-contracts -- bundle --component "$component" --version "$version" --output /tmp/candidate-contract.json fi scripts/check-api-compatibility.sh \ --manifest .release-contracts/deployment.json \ diff --git a/.gitignore b/.gitignore index 479399d5..7c21b673 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,3 @@ _bmad-output/ .cursorignore AGENTS.md - -# Python contract tooling -__pycache__/ -*.pyc diff --git a/Cargo.lock b/Cargo.lock index 9130bfbb..b6141e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,20 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -84,6 +98,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "allsorts-azul" version = "0.17.2" @@ -116,6 +136,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -356,7 +426,7 @@ checksum = "591eba69b7c085632e22ca03606f0bbea68d53bbe26c8962e2d549af20c6a561" dependencies = [ "paste", "roman-numerals-rs", - "strum", + "strum 0.27.2", "unic-langid", "unicode-normalization", "unscanny", @@ -443,6 +513,12 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "brotli-decompressor" version = "5.0.3" @@ -484,6 +560,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" version = "1.25.2" @@ -673,6 +755,46 @@ dependencies = [ "serde_path_to_error", ] +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + [[package]] name = "cmake" version = "0.1.58" @@ -730,6 +852,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.8" @@ -962,9 +1090,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "fe3e9d21d39e6f39ff46e03d5b621424f88ee23d8a1672c6bfc9ccc7f2da4f6a" dependencies = [ "cmov", ] @@ -1065,6 +1193,12 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "data-url" version = "0.3.2" @@ -1371,6 +1505,9 @@ name = "email_address" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" @@ -1439,7 +1576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1462,6 +1599,17 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fancy-regex" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301f5bf187b3c295fce6468d3875037a0bccc5f6b151c63cac2f85babf21912" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fast-float2" version = "0.2.4" @@ -1555,6 +1703,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1623,6 +1782,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e246562084dde8ebbcc943b261c406ce4f68e5032ec28029a251a47d6a295500" +dependencies = [ + "num", + "num-bigint", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1736,6 +1905,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -1745,7 +1928,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -1879,6 +2062,8 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -2619,9 +2804,15 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -2761,6 +2952,60 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42a11b47492e6a2cc76e9c7bcc8077776cb31dac444e0795d7f61bcc6441c08" +dependencies = [ + "ahash", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex 0.19.2", + "fraction", + "getrandom 0.3.4", + "itoa", + "jsonschema-regex", + "jsonschema-value", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "serde", + "serde_json", + "strum 0.28.0", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "107aeb6ff14b8deb5610096a8ce5c554ec89806b521147eaeb3cae05b07edb7a" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "jsonschema-value" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e281d6b177bb4834f2ffd6f92aa0443cffe29c996b251c8d94eb6f456186feba" +dependencies = [ + "ahash", + "bytecount", + "fraction", + "getrandom 0.3.4", + "memchr", + "num-cmp", + "num-traits", + "serde_json", + "zmij", +] + [[package]] name = "kamadak-exif" version = "0.6.1" @@ -2992,9 +3237,9 @@ dependencies = [ "chrono", "diesel", "lettre", + "marreq-contracts", "marreq-core", "rand 0.10.3", - "regex", "rocket", "serde", "serde_json", @@ -3004,6 +3249,24 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "marreq-contracts" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "jsonschema", + "semver", + "serde", + "serde_json", + "serde_yaml_ng", + "sha2 0.11.1", + "tempfile", + "tree-sitter", + "tree-sitter-typescript", + "walkdir", +] + [[package]] name = "marreq-core" version = "0.1.0" @@ -3092,6 +3355,12 @@ dependencies = [ "libc", ] +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + [[package]] name = "mime" version = "0.3.17" @@ -3227,6 +3496,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -3253,6 +3536,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -3278,6 +3576,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3333,6 +3642,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -3446,6 +3761,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "p256" version = "0.13.2" @@ -3970,7 +4291,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.5", "thiserror 2.0.21", "tokio", "tracing", @@ -4009,9 +4330,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.5", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4029,6 +4350,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -4167,6 +4494,23 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "referencing" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42818036d3b37cda6bd4c494543158a36e431cb717015ef7c93acc6ba214d3b" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.17.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.13.1" @@ -4536,7 +4880,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4594,7 +4938,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4750,6 +5094,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -4797,6 +5145,8 @@ version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ + "foldhash", + "indexmap 2.14.2", "itoa", "memchr", "serde", @@ -4891,6 +5241,19 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap 2.14.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha2" version = "0.10.9" @@ -5107,6 +5470,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "strict-num" version = "0.1.1" @@ -5139,7 +5508,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -5154,6 +5532,18 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subsetter" version = "0.2.6" @@ -5231,7 +5621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" dependencies = [ "bincode", - "fancy-regex", + "fancy-regex 0.16.2", "flate2", "fnv", "once_cell", @@ -5299,7 +5689,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5432,9 +5822,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.54.0" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -5649,6 +6039,36 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree-sitter" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0af592be68c579aa78a16846bd19422978c3c52e438523d45ff5d1bff1f9d4a" + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -6247,6 +6667,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6312,6 +6748,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -6337,6 +6779,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -6511,7 +6962,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6747,6 +7198,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "write-fonts" version = "0.48.1" diff --git a/Cargo.toml b/Cargo.toml index ee7c0dd6..82233590 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # Virtual workspace: three-crate split — marreq-core (lib), marreq-server (bin), marreq-cloud (bin). [workspace] resolver = "2" -members = ["marreq-core", "marreq-server", "marreq-cloud"] +members = ["marreq-core", "marreq-server", "marreq-cloud", "tools/marreq-contracts"] [workspace.package] edition = "2024" diff --git a/docker/Dockerfile b/docker/Dockerfile index 303f7ff3..0de03a8e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -85,8 +85,6 @@ COPY ${MARREQ_BIN}/Rocket.toml ./Rocket.toml # diesel CLI uses /app/diesel.toml + /app/migrations (see docker-entrypoint.sh) COPY marreq-core/diesel.toml ./diesel.toml -COPY docs/developer/openapi.yaml /app/contracts/openapi.yaml -COPY marreq-core/frontend_compatibility.json /app/contracts/frontend_compatibility.json # Attachment storage (MARREQ_ATTACHMENTS_DIR). Created here so a new named # volume mounted on it inherits the marreq owner. diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index a644685a..868d4453 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -28,8 +28,6 @@ ENV MARREQ_UMAMI_WEBSITE_ID= ENV MARREQ_UMAMI_HOST=umami ENV MARREQ_UMAMI_PORT=3000 COPY --from=build /build/frontend/dist /usr/share/nginx/html -COPY frontend/compatibility.json /usr/share/nginx/html/contracts/compatibility.json -COPY docs/developer/openapi.yaml /usr/share/nginx/html/contracts/openapi.yaml COPY docker/frontend/nginx.conf.template /etc/nginx/templates/default.conf.template COPY --chmod=755 docker/frontend/40-marreq-umami.sh /docker-entrypoint.d/40-marreq-umami.sh EXPOSE 80 diff --git a/docs/developer/api-contract-routes.json b/docs/developer/api-contract-routes.json deleted file mode 100644 index fdfce3d3..00000000 --- a/docs/developer/api-contract-routes.json +++ /dev/null @@ -1,1958 +0,0 @@ -[ - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{id}/activity", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications/{id}/activity", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/applicability", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/applicability", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/applicability/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/applicability/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/applicability/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/attachments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/attachments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/attachments/{attachment_id}/download", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/attachments/{attachment_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/storage", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/storage/quota", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/attachments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/attachments/{attachment_id}/download", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/auth/providers", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/auth/external/{provider}/start", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/auth/external/{provider}/link", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/auth/external/{provider}/callback", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/auth/csrf", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/auth/login", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/auth/identities", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/auth/identities/{identity_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/auth/logout", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/auth/change-password", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/auth/me", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/auth/me", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/admin/backup/info", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/admin/backup", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/baselines", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/requirements", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/traceability", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/verifications", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/diff", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/cache/stats", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/cache/clear", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/cache/cleanup", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/cache/performance", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/cache/recommendations", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/cache/reset-counters", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/cache/health", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/catalog", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/categories", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/categories", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/categories/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/categories/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/categories/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{requirement_id}/comments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/requirements/{requirement_id}/comments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/requirements/{requirement_id}/comments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/requirements/{requirement_id}/comments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/custom_fields", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/custom_fields", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/custom_fields/{field_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/custom_fields/{field_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/custom_fields/{field_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/dashboard", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/dsm", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/requirements.xlsx", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/verifications.xlsx", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/matrix.xlsx", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/dsm.xlsx", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/matrix-links.xlsx", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/requirements.pdf", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/report.pdf", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/requirements.reqif", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/requirements.reqifz", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/baselines/{filename}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/bundle.json", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/exports/bundle.zip", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/groups", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/groups", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/groups/creatable", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/groups/{group_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PATCH", - "path": "/api/groups/{group_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/groups/{group_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/groups/{group_id}/projects", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/groups/{group_id}/members", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/groups/{group_id}/members/{user_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/groups/{group_id}/members/{user_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/home/attention", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/search", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/imports/excel/preview", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/imports/excel", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/imports/reqif", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/imports/bundle", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/admin/logs", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/admin/logs/stats", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/admin/logs/export.json", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/admin/logs/cleanup", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/matrix", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/matrix", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications/{verification_id}/matrix", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/verifications/{verification_id}/matrix", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/mcp/principal", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/mcp/audit", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/mcp/internal/audit", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/me/permissions", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/members", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/members/{user_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/members/{user_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/reviewers", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/reviewers", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/meta/health", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/meta/deployment", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/meta/build", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/notifications", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/notifications/unread-count", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PATCH", - "path": "/api/notifications/{id}/read", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/notifications/read-all", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/notifications/preferences", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/notifications/preferences/{project_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/notifications/preferences/{project_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/.well-known/oauth-authorization-server", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/.well-known/oauth-protected-resource", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/.well-known/oauth-protected-resource/mcp", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/oauth/register", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/oauth/authorize", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/oauth/authorize", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/oauth/token", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/oauth/grants", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/oauth/grants/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PATCH", - "path": "/api/projects/{project_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/archive", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/unarchive", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/project-from-path/{slug}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/reports/types", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/report_templates", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/report_templates", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/report_templates/{template_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PATCH", - "path": "/api/projects/{project_id}/report_templates/{template_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/report_templates/{template_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/reports/{file}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/requirements/{req_id}/versions/{v1}/diff/{v2}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{req_id}/versions/{v1}/diff/{v2}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/requirements/{req_id}/diff/current", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/requirement-version-links", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirement-version-links", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/requirement-version-links/{link_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirement-version-links/link-types", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/requirements", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/requirements", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/requirements", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/requirements/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/requirements/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PATCH", - "path": "/api/requirements/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PATCH", - "path": "/api/projects/{project_id}/requirements/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/requirements/{id}/versions", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{id}/versions", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/requirements/{req_id}/versions/{version_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{req_id}/versions/{version_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/requirements/{id}/impacted_tests", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{id}/impacted_tests", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/requirements/{req_id}/versions/{version_id}/approval", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/requirements/{req_id}/versions/{version_id}/approval", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/saved_views", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/saved_views", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/saved_views/{view_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PATCH", - "path": "/api/projects/{project_id}/saved_views/{view_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/saved_views/{view_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/semantic_search", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/requirements/ask", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/requirements/reindex", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/index_status", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/semantic_search/status", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/status", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/status", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/status/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/status/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/status/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{id}/trace_up", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{id}/trace_down", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/coverage_report", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/traceability/clear_suspect", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/traceability/clear_suspect", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/use-cases", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/next-code", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/use-cases/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}/draft", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/use-cases/{id}/draft", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/use-cases/{id}/draft", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/use-cases/{id}/draft/operations", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/use-cases/{id}/draft/publish", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}/versions", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}/versions/{version_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/use-cases/{id}/versions/{version_id}/approval", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}/links", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/use-cases/{id}/links", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/use-cases/{id}/links/{link_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/use-cases/{id}/links/{link_id}/clear_suspect", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{requirement_id}/use-cases", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications/{verification_id}/use-cases", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}/draft/diff", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}/versions/{a}/diff/{b}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/use-cases/{id}/comments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/use-cases/{id}/comments", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/use-cases", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/use-cases/{id}/diff/current", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/users", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/users", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/users/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/users/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/users/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/users/{id}/password", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications/{verification_id}/control", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/verifications/{verification_id}/control", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/requirements/{requirement_id}/close_out", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/close_out", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/requirements/{requirement_id}/compliance", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications/{id}/snapshots", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications/{id}/snapshots/{v1}/diff/{v2}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/baselines/{baseline_id}/verifications/{id}/diff/current", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verification-methods", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/verification-methods", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/verification-methods/{method_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/projects/{project_id}/verification-methods/{method_id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/verification-status", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/verification-status", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/verification-status/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/verification-status/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/verification-status/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/verifications", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/verifications", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/verifications", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/projects/{project_id}/verifications/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "PUT", - "path": "/api/projects/{project_id}/verifications/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/verifications/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "DELETE", - "path": "/api/verifications/{id}", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/verifications/{id}/field", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/projects/{project_id}/verifications/{id}/field", - "variants": [ - "server", - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/auth/register", - "variants": [ - "cloud" - ] - }, - { - "method": "GET", - "path": "/api/auth/verify-email", - "variants": [ - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/auth/forgot-password", - "variants": [ - "cloud" - ] - }, - { - "method": "POST", - "path": "/api/auth/reset-password", - "variants": [ - "cloud" - ] - } -] diff --git a/docs/developer/api-contract-source-review.json b/docs/developer/api-contract-source-review.json deleted file mode 100644 index 7f587699..00000000 --- a/docs/developer/api-contract-source-review.json +++ /dev/null @@ -1,378 +0,0 @@ -{ - "frontend/compatibility.json": "0d9bcff76abe3a44b59c5bbcd3b73acd29266394e25c16b7c4b069bec28b9472", - "frontend/src/api/attachments.ts": "3ab5a6039eea83e9a84c1198e686802edbac7812821eadad84050da3a0b40089", - "frontend/src/api/auth.ts": "8b43e8187605037285550f751588ab4ed3b36b7813babd0e7ea397e9cb893f0c", - "frontend/src/api/baselines.ts": "2048ad67ce7d5bb56d6762e13344423e599b7dc71b287b26ae9dd0c07c2be022", - "frontend/src/api/catalog.ts": "9b80cbd031a50fedc09abc09f9784506582d68715ee92b56b4066faa1ab9abcf", - "frontend/src/api/client.ts": "ab4787005a7a4139996d857c4bf06414d8884d4462e3702452f9c1cdcf24df66", - "frontend/src/api/dashboard.ts": "2de1b33077d0de43ff695c222b90870bbda4916595332f80249796aac2f93c1c", - "frontend/src/api/exports.ts": "8dc5817e40727ced7ffc9b9f485b92bbf716764f22c3a48d0925a07a5cc48a19", - "frontend/src/api/groups.ts": "3e58295865acecb4f0fa18be7a5c490d7a011a4eab1bd771f2e6b7cfaa07fb2f", - "frontend/src/api/home.ts": "2454252b15757c234a781b6d14559d10f73ec0a468defc77c0407fa8877137ff", - "frontend/src/api/imports.ts": "72a998ce604f6b23158786b5992cf7add1ab2be532100be4b22ed9e77e6cd4d3", - "frontend/src/api/logs.ts": "9822d3be5e25aeb397fc6993740b9eeeaebca68262619e13e35f7c5d9ec95b44", - "frontend/src/api/meta.ts": "e4441b4ea3a89e65589e2d837b1f4a4e404061d3c8182cfc6b7839fe40c46e9d", - "frontend/src/api/notifications.ts": "35f6cdb54f1f1ebb229368d1a667ef1a3038600cf5f11b0614b5cc83a8e1ce89", - "frontend/src/api/projects.ts": "6bdf2aab505c5c422450fb6d375307d3fc6e01de4a5430f898ec84253a3fff40", - "frontend/src/api/publicAuth.ts": "207d53b750d9065911365f2a96c4d3a4a110983123b264e19b96da3ea0fd74ed", - "frontend/src/api/reports.ts": "1fc085f17031c28a011288dc6d85c5a87bc723b5007132f1769b886f41dbe560", - "frontend/src/api/requirements.ts": "9935a13bcc618a5b95be7d4a0783456d9f5fd9d32c3391964cbb2d3731471d78", - "frontend/src/api/savedViews.ts": "95a7dc49f2b7b217fc63d1032afc064b9a6aa852e513bf468a0f983073197760", - "frontend/src/api/traceability.ts": "7dcdab37cd865af634081bc8df0b05dab225e4df124f59fa8c9b526f2f99b22b", - "frontend/src/api/transport.ts": "25db6fdcde2a1c5c65e08dfce1a92f21e04b7b4b9a8ec2f613bbc0d2b63bf59a", - "frontend/src/api/types.ts": "9b4b6b7b71730b6ea0d214dbe087c264b864df91930f78ba88a7ed77be361489", - "frontend/src/api/useCases.ts": "1867c3aba24d4883fa243299067fff54abfcdb77195f771d4cd6bfef24c7832f", - "frontend/src/api/users.ts": "7155f0bc8f6768753d6a766ca25145259e0fd59c1321870f2967821e4c6151c6", - "frontend/src/api/verificationControl.ts": "f372bfaad0f15c7b112798ba54298776cb8993a9bfb41abde4dd3490974e5ad2", - "frontend/src/api/verifications.ts": "09315bcd631942a15aa01cf6d35e951bdf09c38f045572a50d86fade5c6b4042", - "frontend/src/components/AttachmentsPanel.tsx": "864c91baffb45c90703a7d6f5f4e7dd7245887c684e24b4931470dba39e42195", - "frontend/src/components/CommentBody.tsx": "ca3be1bd63cdac8761218a24e35445d33877e8d201ba56a4c99a7e82a11da5fe", - "frontend/src/components/CompatibilityBanner.tsx": "802755e5d6078469f55bc2fae084046d262709f1895e771e5f705a1efa766dac", - "frontend/src/components/DeleteProjectSection.tsx": "bed2e4b648e36d1a5fbf8a109a14279b87bb26d6ffeef445a971558c8d04734d", - "frontend/src/components/HierarchyGraph.tsx": "ea11fbe1e1eb949f23bc5913b9b182dabe796d9e0d8a79adb1a02d7474e7c7e1", - "frontend/src/components/MentionTextarea.tsx": "228f4f7b836b129a5a9d1e223b6c90f1d015fafee9c34d9626b61d569acc8446", - "frontend/src/components/NotificationPanel.tsx": "d787fad22b8bce38598991541da458f8b4c263131e07227e7d51df412f2b5c9b", - "frontend/src/components/ProjectCreateForm.tsx": "4432de404b310f026d9ad59eeca998b9b5502fccdf985f23a7afee95b560a0d3", - "frontend/src/components/ProjectGeneralSettings.tsx": "151ca068c4911be56f42ad14fb12530b95ac5c353939e432f2f5c18e54750358", - "frontend/src/components/ProjectStorageSettings.tsx": "0b77e7fbeb87ca2fdf29c4b316d87499c0505b38af6f7ab743f57ee1d2d7b60e", - "frontend/src/components/RequirementCloseOutPanel.tsx": "c9dab4f0c3ed9a4477414faaf869adecbc934ed39c216542c2c8a75ad90e45a6", - "frontend/src/components/RequirementCommentComposer.tsx": "ebe202796ba001fee0d63b492236a358ec8ef7d80fea330f5c2f342a2742b531", - "frontend/src/components/RequirementMatrixPicker.tsx": "814990316c34b18594959134663b4b1ffc27f0f3b12acf1c4d7a39db746d6400", - "frontend/src/components/RequirementReviewCard.tsx": "2cb83ac3cd3cc78a407c40dbcd56a127aa4426d6eaecb70a02a336331c2a7118", - "frontend/src/components/RequirementVersionDiffDialog.tsx": "80a6c1aa4b30da3693ccfb9614b05fad9a57fab40cbb9b652308b3235345bbb7", - "frontend/src/components/RequirementsTable.tsx": "c88067b10d2debecb6a1e887d1c5eff8be40a08f70191d69d1bcfe8590a377a0", - "frontend/src/components/SavedViewsToolbar.tsx": "b0dec9ba27a8055cb07bfd79d1c54f512432d1de67e314312125e8292425cfad", - "frontend/src/components/TraceabilityGraph.tsx": "6c74f0abc03db92a55c8b1d670185ae69fbbc3f5999d9cee409ac6c5c044de23", - "frontend/src/components/VerificationControlPanel.tsx": "994b105d7a29e3d8fb9bbe9d0dd78f97377f416415e00ba12a90a64027e94550", - "frontend/src/components/VerificationStatusMetrics.tsx": "0cf513fadec90402790797faf3a158b7c03c696c621fe0c11a3dac9c1fb21428", - "frontend/src/components/VerificationVersionDiffDialog.tsx": "532ff63107b952943f516cccd08e4bb2547b101db8b61c6cfb8814a7dcaa074f", - "frontend/src/components/VerificationsTable.tsx": "7712820673fea66961898d70ab98f60b46e823fec0370db209bce54ca7119398", - "frontend/src/components/dsm/DsmGrid.tsx": "417d102c6f81f220f026dfac83181262688d45cefab60a24ead6c4fd6104a4c4", - "frontend/src/components/dsm/DsmSidePanel.tsx": "795b7d6b208b8ac7bc65c473deaf58f2e19ff40a2d5496b35e82da481c00c186", - "frontend/src/components/dsm/DsmView.tsx": "f478d1cebd6d1ff7cad418eb9953c6dd7619b412ca33219176a1fb69eb02ab1d", - "frontend/src/components/matrix/MatrixGrid.tsx": "2fff2d35fb634a7f5e1e2f08a8e63e75b12ae91e0fdddc6e8abba263d40cd759", - "frontend/src/components/matrix/MatrixSidePanel.tsx": "e12b080999f75d1e5b93f4f0ad77c22582807a06e41807ed86cd78c7540327ee", - "frontend/src/components/matrix/MatrixView.tsx": "8b173ad6c53f9082e8a4c28db5d6d65cdb26faf947d07426b28eaebfe3dcaf0d", - "frontend/src/components/reports/DocumentSettingsForm.tsx": "5e5bda5e256db2cb0df6f6745a0f56059cfdb9df8cda31d668103cc3a6919d1e", - "frontend/src/components/reports/ReportDocumentsCard.tsx": "ea4599e550fc798fd6c55b62788f01f503c5b0482834e7e2d4128bfc0caec4b3", - "frontend/src/components/reports/SectionList.tsx": "7adb1c33b62f3ef05e0db15af981219799f1e993ebf4da5c73d3aa1f81907bc4", - "frontend/src/components/start/StartSearch.tsx": "0cba50200262e52bda2fbf22e7c57803e1d2db0c2faa60eb5587d8af4af2a167", - "frontend/src/components/useCases/BaselineUseCasesSection.tsx": "b4d20fb9a89f5c730379aab6c3e4417133b38f700f39e79a4e64c7cad202a822", - "frontend/src/components/useCases/BranchCard.tsx": "2c16c584c566340617833503e180d2be175e7415761d8455b9457061834fd4f2", - "frontend/src/components/useCases/LinkChip.tsx": "0ca3ea3f0c024c85eec9d5d6dd2ca4525f4d06b5b74d021b2a72222295590ca6", - "frontend/src/components/useCases/LinkDialog.tsx": "0db09520c092df0410e9e65231e94e4005daa64c41fd56b7ba180c758e3fa782", - "frontend/src/components/useCases/RelatedArtifactsPanel.tsx": "03fbafa70a47258bbe715c9d2005bf26a6bf677b6d3926bb98f6c9582661cfdb", - "frontend/src/components/useCases/RequirementPeek.tsx": "09d3b1de5afa65ab1ba3e3add96fe3cd9ca12aef944c5ac29adffa6a91a351d6", - "frontend/src/components/useCases/UseCaseDiscussion.tsx": "73f54e3c66cc786226fce62764c7221d34895dcb46fd004d60dd0901a7954544", - "frontend/src/components/useCases/UseCaseDocumentView.tsx": "46957ce3466f1a0d4ac45112c1229a2c68fd428fbe515d1681faad3fced4b405", - "frontend/src/components/useCases/UseCaseFlowView.tsx": "ffb2b6acfdebc2e4fdd5df4b6f1ba974d07aa45556b57e4124d505257dedfe22", - "frontend/src/components/useCases/UseCaseReviewView.tsx": "f642c462715da9f95cbe0f0791ebdbe77bab97eabd791ba41abce87048dbf583", - "frontend/src/components/useCases/UsedInUseCasesCard.tsx": "cd65c36495771faac75eb6104cc5cc35c50c22c42247d3c8db7d6208bdd2a427", - "frontend/src/components/versionDiffUi.tsx": "288799d997ee305283411d9637910a4a9eafa6d57d6416f3ad7b14b1f7915a72", - "frontend/src/context/DashboardContext.tsx": "fc5b37e22e5c758062ade4f23474186d06fbfab39830ddd6caf4b56340762a81", - "frontend/src/hooks/useBuildInfo.ts": "444733da6fffd5734a5511370d1ed3253ea6d05b2425d91bbba0aae4fed12b77", - "frontend/src/hooks/useNotifications.ts": "ff1cc4fbabddfe4f91c1dd98231dd082c5eb812249020d577ac8962abb383124", - "frontend/src/pages/AccountPage.tsx": "2c6b72c059ed8585e92a7bfb3cbd6ec01066801d27a00fe1ced770671cfaebec", - "frontend/src/pages/AdminPage.tsx": "7cac5d4dc76bafc498831509c34c8870f774bb75cc9a816d63e48f3fab2d1a98", - "frontend/src/pages/BackupPage.tsx": "aa5912d7f6e4b76f7759b790951dfd4fb5b0748261ca928488cb590465abe8cf", - "frontend/src/pages/BaselineDetailPage.tsx": "590af60312aa864a7bda51be8f85e04c2af90edcd3a1e1bead7e74c497ba53a2", - "frontend/src/pages/BaselinesPage.tsx": "efec445a6c5f8b8062b0817cd49673519a6da4e401d353d81b802dcd95b80fd9", - "frontend/src/pages/ChangePasswordPage.tsx": "0ff12abd9c144cfc2fa1528cdc3b4076b00b686bba48c7ddf567ee6a0b4846ad", - "frontend/src/pages/CreateRequirementPage.tsx": "b7b87777c9438ed186c9bdca1f7533e4b8d68520efa3560c6edf13f64cb75cef", - "frontend/src/pages/CreateVerificationPage.tsx": "c638e2d1e8be63854f0acd4b167aa575fa85b18119ef10b2684d0dbd3f2c204a", - "frontend/src/pages/DashboardPage.tsx": "5ec394dbe016542d4b85a031aa589aa360e9ab2e336e6eab6389fb763c65e806", - "frontend/src/pages/EditRequirementPage.tsx": "acd9281739516a859e32111db4bb5a224e529ad6b3f77e77d6a5790b036278b4", - "frontend/src/pages/EditVerificationPage.tsx": "f9f7a455dd564269e3d02be7e7f156aaa27a37c9b41e0520c42f389891d145d4", - "frontend/src/pages/ForgotPasswordPage.tsx": "c8beb4f6ab9869c01f6fc3f08f2b624c6f2828e41c9433d285e0b2827dacde45", - "frontend/src/pages/ImportPage.tsx": "3d64cecd82bb0ac326202940401155aeaee3e24da2b6fa1f2f9f6d557db8cb07", - "frontend/src/pages/LogAnalyticsPage.tsx": "b5e389677fe998fdd60e0daca5290de48edc17904cd07876c51ca317d4ae3e28", - "frontend/src/pages/LoginPage.tsx": "574ca76f3b7d20021aa66ca786fd7a46b088e340710dc50f658b0ff0c14a99be", - "frontend/src/pages/ProjectBundleImportPage.tsx": "4162d83cad0c9633ba392bf065edbfd93acb9d99ae64d9ddb4506828f2a2d525", - "frontend/src/pages/ProjectCreatePage.tsx": "17e145d53aebdbde58753a27b093c5b7a70cb109ca507d6a4f2c504fa0743317", - "frontend/src/pages/ProjectLayout.tsx": "3bfc38ff8fe368dc8ff25ab05f5f6acb5ec37a33aabfb4e10d5ba8e849a9322f", - "frontend/src/pages/RegisterPage.tsx": "1e9e057dde4e9faf2c39e2b5a1f8aa4ea8f6c01ee802006464c78bc2f5e40f88", - "frontend/src/pages/ReportBuilderPage.tsx": "8f5d61e7801a1a2deb1b5e51108fec0380bd4d83dc6cc839820d818696d50f3d", - "frontend/src/pages/ReportsPage.tsx": "1b37d6a6b5ec8b8a5fe116f7f8fc070a95524f36b1b5373a9aa76e7fc1abaf94", - "frontend/src/pages/ResetPasswordPage.tsx": "bf60116c75c4178d98cb52a25e4c74fce8cfe7b01f3868357a055f95262ddf36", - "frontend/src/pages/StartPage.tsx": "92e1ab484cf0c151118aef27ab0278f9f9a57a141e7230f2572a493830591205", - "frontend/src/pages/SystemLogsPage.tsx": "cd505938abd22298c9eff02316c2e1d12fc58aeaecd0ba07c2d2fb8640a4f537", - "frontend/src/pages/UseCasesPage.tsx": "8b7bd84008cdd1005f7177912e23306951fb58f04ca070f217a230fcb2e6c80e", - "frontend/src/pages/VerifyEmailPage.tsx": "d12d0483bef2b7f1242b70cd49de89671ec39e04e557e5183788cf3b0e85a0b6", - "frontend/src/pages/ViewRequirementPage.tsx": "32cd5e22c24aeae559c87f06a7f5daf7bced700fb1cbbbfd489c98e84739b2aa", - "frontend/src/pages/ViewUseCasePage.tsx": "1cf82735d28145d518dc3d7b9f4970c6a9afe735707000c511a2fee85e3c65af", - "frontend/src/pages/ViewVerificationPage.tsx": "333753fc8f69e2c69579097191e1118fdcc2b51309c0c915e394775aa7560a2c", - "frontend/src/pages/account/ProfileSection.tsx": "4853064f8b07d51cf97c2437b021fec43c5e7bc21ae54f1979242021cfda9d0c", - "frontend/src/pages/admin/AdminLayout.tsx": "fcd781952701cf7b14a34077669d15e0daad07aae8f7318c9f5d61969c617849", - "frontend/src/pages/admin/SetPasswordDialog.tsx": "66dec75ff74ae92c46ff6539786caccdd7075b5182b85ff022772dc2a5493729", - "frontend/src/pages/admin/UserFormDialog.tsx": "7b90758cf2349f92173da92a31a250aa0cb7146e4888dadf862cff611b5b9571", - "frontend/src/pages/catalog/CatalogApplicabilityPage.tsx": "3e0322240e4da2a13854fa061207987dd97f65d6efb016102a6335a6977ff804", - "frontend/src/pages/catalog/CatalogCategoriesPage.tsx": "c4819f09441e4d435f6b2970ccfde47bba93aa9a8bc27c2f6741962d8e8e2176", - "frontend/src/pages/catalog/CatalogCustomFieldsPage.tsx": "48009a19ac670cc45c79894fa425eb9c6b1e9e7162dec18fb7a9ac1f59c3e390", - "frontend/src/pages/catalog/CatalogRequirementStatusesPage.tsx": "2e49517a487c93a32bb493ef5455073da81000b8378cb2a7a80638f95185d4c3", - "frontend/src/pages/catalog/CatalogVerificationMethodsPage.tsx": "5034a088d804caf187109a9709cea94e1773166b6e55b48ae9a2c1c83c2f3fe4", - "frontend/src/pages/catalog/CatalogVerificationStatusesPage.tsx": "8a24da86a3a6d98d0dafa4f1ddd201dda014880ef420502ae61f2c02796cc231", - "frontend/src/pages/groups/GroupCreatePage.tsx": "ef599da27c8b439dd77899b5b6c7e9378be83af68114da5cf321c3d5f7f6edfa", - "frontend/src/pages/groups/GroupEditPage.tsx": "16c192ed8e7c060f5e32a701c2e93ca4cd03e0431e381c13c999c127996ea995", - "frontend/src/pages/groups/GroupMembersPage.tsx": "29a3f2c44b8c374b8569e49dd341e8e403cfb0125e40124409f469ffe9ef4aca", - "frontend/src/pages/groups/GroupViewPage.tsx": "ab42dc3302004af815427e247da6c66ed9485d3190b56690eb8687a84d72884c", - "frontend/src/pages/groups/GroupsListPage.tsx": "8ade08bf0e4815ee866e4bbbe1de1a52592de1cba57659e52c69a9b44f6d4530", - "frontend/src/pages/settings/MembersSettingsPage.tsx": "8a9089ac90817ad47a4306b12081e7b262142082c9a545f864f45700629e44b2", - "frontend/src/pages/settings/NotificationSettingsPage.tsx": "45a06bcbc7c68580e11a1dfc10d92a71fa07d63e8636c2f70db0d2e9fa37db4f", - "frontend/src/pages/settings/ProjectSettingsLayout.tsx": "59eaadac194a13684ffb2285cf2f6d9bcef3d781e9433eb3a6ac439c5b188383", - "frontend/src/pages/settings/settingsContext.ts": "43047b5fda62fa4131d7928842ddaf6e7600c90b684abdc38b8c71ee918765f3", - "frontend/src/statusAuthorDefaults.ts": "fbcbdbfa7e1402ce194a0796a665bb8fc7b22da209cdda630b6e19798c67306b", - "frontend/src/test/reportFixtures.ts": "126979fb70781d2c29175510b5162c37eb62442ea66a2f60243821aca68fce60", - "frontend/src/utils/dsm.ts": "f9248410826e11d2eba0acbadac7877675e91acf7d9e17607409c8db4e561c52", - "frontend/src/utils/duplicateRequirement.ts": "dd9ae9e091d773f2dbe4edd75afbc548e8d90f71c85dfda9be974073703ee4bb", - "frontend/src/utils/mentions.ts": "ac1f4b8a4ffc2086f945315e40b7001d03c48ffa42a93875d3f09ea06cb110d2", - "frontend/src/utils/parseUser.ts": "528441744ab75019bb683f3cd2e5d142abe425e63d4407592c1cbcdd37957a7d", - "frontend/src/utils/projectUrl.ts": "146123b4a0a460339350dc7ab96dde0649623c7a67cff68997fe005a6e4cbf2f", - "frontend/src/utils/reportDefinition.ts": "f39b5528889f7bd0fbd5668e5cc8eabc296db77cae433526dccb036df913167b", - "frontend/src/utils/savedViewDefinition.ts": "a1d1251f7d60ea03c4b3a8a55f75722e936bdf30dd6f370b836fa42fba3a0f7c", - "frontend/src/utils/useCaseDocument.ts": "a51556ac25f9e5bd3c4c92328a86738bacf2c45a0a42608dd858821f3c492de4", - "marreq-cloud/src/api/auth_public.rs": "b080c11b42d95b02a21b6f49226bd1c1c9f72a6eec4f2056f6a7f29ef4def378", - "marreq-cloud/src/api/mod.rs": "0a570340b80f76ced32d7f30c057c16d95dad39ef433180911f7a8225da18919", - "marreq-cloud/src/deployment.rs": "6833286dc0ceb2933e95a1bb884a5174548bd801b645b97ee05d0d7ca042bd66", - "marreq-cloud/src/fairings/cloud_admin_bootstrap.rs": "d9cad7b7e1c71c071696fee5bbabd387ca438e741a821e577b26fa0a8c65ea01", - "marreq-cloud/src/fairings/mod.rs": "6c14ec5bd25e8835c4b19cbdb1674a3540ad7cf106dcd43c136e9ddc263db272", - "marreq-cloud/src/lib.rs": "8f873d14051b1b1c177bb9b66eb81b9430f970073219d8cff8deb7834c2cf4fc", - "marreq-cloud/src/main.rs": "45f07e434e587a3c8ec95fec94b127d43fcbb395ae9da6351ea4115ba07dcdcf", - "marreq-cloud/src/routes.rs": "36ba1db8aa241bbd96757d341bf2058e2bf650019242ee2a09ee90d03b833905", - "marreq-cloud/src/services/mod.rs": "4607f9a7c5c8651472b8016b6ac8f68ca3431939ee4c3f9d0e944422f7348a25", - "marreq-cloud/src/services/registration_service.rs": "433de11216e66c659fca1572d2c9326c7f27b58076a6c65bbc92d4bb876eef41", - "marreq-core/frontend_compatibility.json": "3e569883e23b5411ca13570743096c893cf876362060007d0a17a8ee5623d2a7", - "marreq-core/src/api/activity.rs": "94a3fd9c6aa73ac989935925f108d35db55b148cebbc6dfdcde62b8872d1809e", - "marreq-core/src/api/applicability.rs": "aa88a53ccdeda77df4d83f7bffde00668bf6152ef1e7b598ca9179ecb472b081", - "marreq-core/src/api/attachments.rs": "a7f600d5e6293038ee668ca673ed093be06714e1191a06b640bb99cd11f56124", - "marreq-core/src/api/auth.rs": "42c42fe249ee62a7e5bbceb05030bb63f50806ba4c8b9ca36ceaa49b5e372ba1", - "marreq-core/src/api/backup.rs": "a364be4c85072f311d0a8e0930ec42dae25182ac035cbe4ee9576e2d1d74cbe6", - "marreq-core/src/api/baselines.rs": "53bee136e032cb6f167d639c06d9feb7f230378a88a5fee6de5819611b3c239c", - "marreq-core/src/api/cache.rs": "63410e53b7e2c01758bf565af66981dbd5bd046fd222d5ea9c6b5e4a6f571c75", - "marreq-core/src/api/catalog.rs": "8a7017fff0a5faefe8d124aacc0ed3f1a68c5289398969391bec3d40c9cb53ae", - "marreq-core/src/api/categories.rs": "af9ebb58df5c50e7d1a590006400ce2f5113d846b481cc5e32b6a1a0d0f070b5", - "marreq-core/src/api/comments.rs": "4d5429530e90a4f9fb4cf5b7c224c7744a5c89b4dd14d6c5aa11c4b4c09e161c", - "marreq-core/src/api/custom_fields.rs": "7138b0fee7f5c2a938c277413f7bd8289c4ffaf9ecf70635f9eecdd4d3c5b48b", - "marreq-core/src/api/dashboard.rs": "d262e1de00a6b180951ac80891c6ed2574a5b96f939b2cee4bf21c4a80952665", - "marreq-core/src/api/dsm.rs": "609ccf610c64d69438c28fdce7623cf81ff68a1f91f0f7f9755ad0985d81c474", - "marreq-core/src/api/error.rs": "f8703993e7a5c266b2cb7b946246a5cf285a46ddda9d4f976e8a1d0e4f6c3d0e", - "marreq-core/src/api/exports.rs": "ed842ab4cff67fd556e779bf65744e4bf1d42e8bd3513a10be4cd3a1731ae40b", - "marreq-core/src/api/groups.rs": "3d8f31614abdc70bb2f9dddb08d1b8464556d5a04fccffd16c337692d91a60ad", - "marreq-core/src/api/guards.rs": "ba11edb5858a725a36e1988a7ea76d1ac0642945bb6c3f8406eee31d00e7deda", - "marreq-core/src/api/home.rs": "3b8f325d7f1096734378702bf42c72e8c7adb0793b7c77ce51478b8f6a18dfbd", - "marreq-core/src/api/idempotency.rs": "7586819b8488daed5c7fb51495c70c9369a8cb069d1f9aed76cdb0ebc12e6f8a", - "marreq-core/src/api/imports.rs": "d76d3923cd34f58f02b55a600c56e2491b4b14c8269bad23c9ddd137343e518b", - "marreq-core/src/api/logs.rs": "126db136b1bee0f78227cf7be98fd330309700ecf6981acd4fd1dfeac74bfc2a", - "marreq-core/src/api/matrix.rs": "951a83ff3feaef5b94b902b8101779873d5075eda609946376a7668338d02f22", - "marreq-core/src/api/mcp.rs": "01451ffb92308c53634718d763433e60b1714c42f2f02a33d3e1a7c46338a0cd", - "marreq-core/src/api/members.rs": "398c1bd74615b611c6fd71cb3d54b2c26c077b90d0db5b52ea8197fe1fb99a83", - "marreq-core/src/api/meta.rs": "13ff923c9635214132f8e5ac6be236fa9e0dc0eddca7ea826993655c4ce0fb2f", - "marreq-core/src/api/mod.rs": "a8b106368e1fed4766fc75c8135f5ec63e867452e18afa096afabd66012d1a2c", - "marreq-core/src/api/notifications.rs": "ca1f959e06e5f8e8d8e55cb4d83342006378be4c778ac92cf67e1c885d9b0a74", - "marreq-core/src/api/oauth.rs": "ce1a150b3c3b3ef2118de60767f95ac502000c29ae94520b5bbf1d14202eb801", - "marreq-core/src/api/prelude.rs": "3aca18c037023d5076101771d0c32045fb8cd2dda3d8410bdda9f2b6241ef9f2", - "marreq-core/src/api/projects.rs": "e607a66955222dad5ad6fb29daf8c881fc730d12fbc4c391bdebf7664d97ca2d", - "marreq-core/src/api/projects_session.rs": "06a1689822c8956ff4570931cdc056a198e0bfeae85b6e171ea11546d8c3c840", - "marreq-core/src/api/reports.rs": "09c31e4c57508f6aec4b042d86e87765690f82ef64a5c6b0fde233fc2a190e10", - "marreq-core/src/api/requirement_diff.rs": "39a662d096001564033e2069c67265b797d5b7c5eee45bfe26cdeee1c04cf637", - "marreq-core/src/api/requirement_version_links.rs": "a2790428840fb0c99a874f26ea524b6649ab3760b5218b8bf6ea20c0d6510848", - "marreq-core/src/api/requirements.rs": "bf2317f5637bedd763f6a37a9b015fce0e064c58315530a0f6430724606df93a", - "marreq-core/src/api/saved_views.rs": "ed3a13e3d95d56b1a6a2f41e123a7dc6944ab1b534845c3c8309de6c9f111d6f", - "marreq-core/src/api/semantic_search.rs": "6e4da1230a89137f3339fab09750ed9b8b773a3eb2722680909b32fd69a92ccf", - "marreq-core/src/api/status.rs": "4c1ed40c20c74cf541677f8573b60af5afd4eb32decda9a4d9d10b69082c6299", - "marreq-core/src/api/traceability.rs": "b45e4e0ab57e9fb8da0d4bb58e32945b4cb8cf9d2f175d548502360f411807e4", - "marreq-core/src/api/use_cases.rs": "3355a964773df4500bb173bac7b1d78ce9e2110e48c6e628bfa3f6f249b1e766", - "marreq-core/src/api/users.rs": "98bc2ed2ce937ef745399178f78b59517ec10a8677497ce4f7c8a5c6374bbe66", - "marreq-core/src/api/verification_control.rs": "5f49241dc2ceae386381c3c5a088e75e7e67d5c5140ef8cbd0540c021e83882c", - "marreq-core/src/api/verification_diff.rs": "3c1957bddba35bf6a3c88e91e02353584aaf52dd8fbb853ccdef812fd2fd2fc7", - "marreq-core/src/api/verification_methods.rs": "092ce0a633249acfb9f387ab859bf832512bdbb68277c906084e8929c11ba58e", - "marreq-core/src/api/verification_status.rs": "91fe8437410198511618bbc7638480be7c0e6dfa1c63ef67ea45733dc070b746", - "marreq-core/src/api/verifications.rs": "77e5d01401363a2d0a26664b9015ca836261a108aba631d9d2a6b5468b285f71", - "marreq-core/src/app.rs": "f506f1951a8db3660348f69630a7e77fbe5109a0365f0a26fb49fc0049b50cbd", - "marreq-core/src/auth/config.rs": "fe55c128b1c8886451a45d5b0863840005de6afda5f16b77573512885e33fbde", - "marreq-core/src/auth/csrf.rs": "a988dc55f7cdbc390072fcdd91fda6f43233023c85d05a04c036152f95ad9b0a", - "marreq-core/src/auth/delegated.rs": "61a549874674294a037394625ac81b05844f88627b5531cabf812bf15174c3dc", - "marreq-core/src/auth/errors.rs": "acb72478d2c6bd08f59efeed13d01d484c46a7024dd791c65910948abd7f022d", - "marreq-core/src/auth/external.rs": "bf0c4a33b3b849de41a31c7761f9478eed37f55e6e320fa5640a6b050f0b9bf5", - "marreq-core/src/auth/guards/admin.rs": "bdc1c1654926cb9b5425897371c87b402393d2d5c1fae29cc8fc37f40f3d1df1", - "marreq-core/src/auth/guards/api.rs": "ffdebb6107730290213f249fceea8a5796089339ae5a8d8a61014d074917150c", - "marreq-core/src/auth/guards/bearer.rs": "138e9c96e04372e8f2c3f33c298146bfbe128bf53ddd94e147a27856b41ee7b7", - "marreq-core/src/auth/guards/group.rs": "2042fe1d4af6350d0eb8a42e23ca5a35452cc8e9bd80535cae13154f0cc78f48", - "marreq-core/src/auth/guards/html.rs": "21529d3a0d23ac112f57d8cf7be81bde9824a42a9d44ad48bd91a0a2798b2389", - "marreq-core/src/auth/guards/mod.rs": "09e01eb7522a013007167447d0102fd7173d8588e1803fbc406748eeb5dda7c8", - "marreq-core/src/auth/guards/project.rs": "3a03e405327e02f35f1b69348a03b0ae1762d0e19f24c9f46acd0bb69da341d0", - "marreq-core/src/auth/guards/route_params.rs": "e076f960e7316ab45e33d78419296dc56ffa1020dcce95711f9865bbc4620fd1", - "marreq-core/src/auth/guards/session.rs": "b290b71d9d88f340f955b205cefc9c726728ff887b4014b803573fb173c39563", - "marreq-core/src/auth/login.rs": "48b258c2467fa4b7b82f07d9fba566be8efad302fb92ec445580ee6cc3705326", - "marreq-core/src/auth/logout.rs": "04e469e64e7bde1c21e5aca119f2ca166e28c747976b7ba494550275b65e6d03", - "marreq-core/src/auth/mod.rs": "ba9b96813124d3c439033a0fb5ff4815d48f0383bc05de9878bbfca8cf92938b", - "marreq-core/src/auth/password.rs": "1babc63db0d5adbccbc5aa94d378ec1b5a9ea962a1a40b642f308a44f75c441d", - "marreq-core/src/auth/password_policy.rs": "038967fae772a36137fc236b4e618776ce7a21ce53b80e20c1b5746919b6b9f0", - "marreq-core/src/auth/provider.rs": "cf7dd4fb37f566af283bd876cc0ac492b5c4d1e275e70c8f3b170e682ce8ebb2", - "marreq-core/src/auth/rate_limiter/config.rs": "9c225c383fd2fdb444199103a3dfc8ad892ae387c073162badfe64dd1df7713c", - "marreq-core/src/auth/rate_limiter/mod.rs": "f57a3c416486907adf7340d86058352046b553a1fc5a8de5b9d4e2d45913e8e3", - "marreq-core/src/auth/rate_limiter/postgres.rs": "4324ccc8fa90bfaf1e79bd44907f758bc2e07d72a4d34839b71afa7ade845428", - "marreq-core/src/auth/rate_limiter/store.rs": "7b8e861c56f62331d05996b7dd74d8f0e970fb1ebfadfc136e3a4d5e9220c0fd", - "marreq-core/src/auth/session.rs": "5c1df56bf8fc67ef2782d9e2781074ee6a366add00f997ca4e67fea5d06485b2", - "marreq-core/src/auth/session_config.rs": "732703ffde7f9f651b51c47ff544089edfd4fdda6fcd11ddef54e37968c2f605", - "marreq-core/src/auth/transaction_cookie.rs": "de91b4949f98267b232a3b3152460053ab16f3315e8821bb7186e28deaa4b069", - "marreq-core/src/authorization.rs": "4facd135c6f2b3c9c16249d9ab63fe4cf9d1fed8afe2190fb9d403d671f7d44d", - "marreq-core/src/backup.rs": "7e0223d93afbfd52dc0395e023c4be7156cb2aff193d27ec00557731754b19fb", - "marreq-core/src/build_info.rs": "3f602ae76288c24acc8efd5e445a299ca3c671c4b69b1c2695adc68c184c142b", - "marreq-core/src/config.rs": "44b3bfd89821e6965da447dde30045b181afbdd51e6540e980ce0912d6d40621", - "marreq-core/src/cors.rs": "31aa625f8ed94d43eb41e4d29c2ce35a30b61256b0388abf53dc30d580fc23a3", - "marreq-core/src/db_types.rs": "080883843e7ae0facaf671951d3985fbf8614f956dbb24db4c3f1624cfb206af", - "marreq-core/src/deployment/mod.rs": "7ca21f6bb56fc06009f4193c4509eb4b44622825ba33287a002e0a2f6f9a3873", - "marreq-core/src/deployment/test_support.rs": "3678a6ea0d4b989b14e630c3a9283b9a88b949e4b256e74b428df076308659bf", - "marreq-core/src/diff/mod.rs": "de6112f9c9731040d1af1afb2637863ae16c814105caebef2ba16bb9efdde7f7", - "marreq-core/src/fairings/cache_control.rs": "3a27895c3f0e2f5c9d41f922ac786ed59848ba5911434212c784daf270f1516e", - "marreq-core/src/fairings/csrf.rs": "be695fea17b384d081b2aa3dafa2fc0934c11b33cee48ef2a16139cb8a396d9f", - "marreq-core/src/fairings/mod.rs": "bb30f3b994c05075094bd4d2b91e96145a9f4daff1fcf0dc2b06206029854cdb", - "marreq-core/src/fairings/oauth_challenge.rs": "d09b75d9b7a82bdf232700dc4f8eb66a30a5a524031558da32f5c3f4b22859e4", - "marreq-core/src/fairings/request_log.rs": "0c1e067a3977e5c9d7ab77f58f188724d5d8414ac362e10a61822c8dd4ccf839", - "marreq-core/src/fairings/security_headers.rs": "f97b003cfb9a73d42aee51c4e2dcf94064b779f638a424be917a95c4ef89deed", - "marreq-core/src/fairings/semantic_index.rs": "498f6e3a73719bc92b3818b855a837da300380dd8c868ae9562c401069fa3abd", - "marreq-core/src/generators/excel.rs": "6a1ff8ca77c14227e5e93b086c5cc051601d1b47305dfc0f781055915a8ae57b", - "marreq-core/src/generators/mod.rs": "c1ccb1039a1282c40f4813cd5cd1ea598449f539e303784f7ac27754fb7c42b8", - "marreq-core/src/generators/reports.rs": "97cd2c6d58572e65f18dd20cd3bf68a193b610173d962e37781e2f4a04550958", - "marreq-core/src/generators/tests.rs": "d75f80eb06e806fe094672c361284267128c5400e9ad986f7f30fadf8a09c1f5", - "marreq-core/src/helper_functions/decorators.rs": "a1e66680f74295c681054eeb2b00dfdf859c6fc41fbe77e1fba6d9d3b70b1f32", - "marreq-core/src/helper_functions/filters.rs": "cba543dae55a85d7e45c304483856c3adfc7be988d6268f7bc63e1d6e5300976", - "marreq-core/src/helper_functions/mod.rs": "4c8fa19cde6104e8df42dbc0e5d4393d3ee1af7b2c83777d2add317f880991bf", - "marreq-core/src/helper_functions/reports.rs": "2b7cbd806ac46dfed4a836fa62cc6ac3b14bdbf8f8e6bd7019e1b1b96562154f", - "marreq-core/src/helper_functions/utils.rs": "ca5c701668ca6859527d185cbcb5574b528ada66e6bde154f1200eeaff6b7bc0", - "marreq-core/src/importers/excel.rs": "6fdea2824c9650724056618c4d17f5e27b347f6169be2365e33df4e32c4d71ec", - "marreq-core/src/importers/mod.rs": "c737520bbde0e3ae1d3f7e8b2f66bad2a9508f1da01116f8c190d2ab3c8543e8", - "marreq-core/src/importers/project_bundle.rs": "538e47b3b288ac938c7f6063e8058524b80f7cbdef16ba4ac8a648ffaabc93fb", - "marreq-core/src/importers/tests.rs": "e0f5331e87fd77c7f468199e75f4d2437ba5726bc8c975a957be734ec9399098", - "marreq-core/src/lib.rs": "1455ce36de9647931b03fd18c1f4a1c82a6df41e2df40948805996fbc4a7da6e", - "marreq-core/src/logger.rs": "b6a29960712e7fa9ad9ebfa33c041daab6e5a80d350615237903921e17491e85", - "marreq-core/src/models/decorators.rs": "40bb2f62a4037fbaf9f23aef4400606f4640e5f37471a4a8923e0dc46f7bf3b5", - "marreq-core/src/models/entities.rs": "1eabe06f2e169a5300cef45f85e897a9611233b1fdc0ac743a9bbbee705ac5fe", - "marreq-core/src/models/forms.rs": "7dae5b25d0cda8f5d943df5e546cd3e12fe691872052d475dd8f023ed6402c20", - "marreq-core/src/models/mod.rs": "649f0f88778ac1a4145ab00a07d685277cc365ce4be75801b1f462ad1ef3cc1c", - "marreq-core/src/models/semantic_search.rs": "3221de526e270bcfac49df625d129d93df0849137c201ecd58b0b0afcc9d8370", - "marreq-core/src/models/tests.rs": "66760a34dd08e200871c6bcddf0cd3572cebfea7ea3c3a11e5d7ff5b85c9641b", - "marreq-core/src/namespaces.rs": "0b7c8e3d7c437e8ee50daaaa81d578d21f20543496801bea7032fb9a4b8d12c0", - "marreq-core/src/permissions.rs": "aba57beb9f1ae6583e3c79c07530085328befd1b2cba7a2d925cead7eca10a3d", - "marreq-core/src/reports/data.rs": "2a3611d90d886b15dd8fae7f054cddd79aa42898d7a8855d5c0c34ad1533f3f5", - "marreq-core/src/reports/definition.rs": "8131924beb38d768c09b2e8c64c535de325a215eee0a1104e1b055da5c1a550c", - "marreq-core/src/reports/mod.rs": "dd04e70a3dd01e41561d0cc986655d08b1c70707dac2100803a4e4d64f5c8671", - "marreq-core/src/reports/model.rs": "8bd9b9d2b37f50d6e5058b507734d50c5685b98f8620cbfaaa418316d5eb916c", - "marreq-core/src/reports/odt.rs": "1990b981599d0c99bb296877e8806c4635098497903a50b60cc8d58ef98d4bfd", - "marreq-core/src/reports/pdf.rs": "05dd88da08ca9bd9dbceafd09c1c4a978505701e9b60cbe3c99ca83a41ae0b46", - "marreq-core/src/reports/sections.rs": "edc74cc6432a9ae0b0fece0f3fe2590173cd50f491e691e2161a30a1f5b266c4", - "marreq-core/src/repository/cache/keys.rs": "4480d4a36550e69a86162d029b9fbd18cf0f1ef7d2a9ad9ec4fb7fef0dba63f5", - "marreq-core/src/repository/cache/mod.rs": "70280d28b9fe0df543ba951fac7249ad498b49c206536f2dda32ca076df98349", - "marreq-core/src/repository/cache/repository.rs": "06eaec21cd1b7aaf219b39790076fbf400e07d0172bbf4ea10cfc0b84742d60d", - "marreq-core/src/repository/cache/stats.rs": "6f7c20f039b29f6060a1d76c3e8f11aece1828406a347dbdc6f303d06c8182f9", - "marreq-core/src/repository/cache_middleware.rs": "700959d0e2161082f1e2df92b1fd7309f8869fcb05679fd22b12c8e76f504269", - "marreq-core/src/repository/diesel_repo/attachments.rs": "995a5bf18dd274109d235de35a9d9655cc5aaa0dbaaf734aff6e47ec3054f6e8", - "marreq-core/src/repository/diesel_repo/baselines.rs": "1b636f5785c91ae8801608da4af8b5de2991f38edfd447f3e3285810035ddb75", - "marreq-core/src/repository/diesel_repo/comments.rs": "d2fe0edc50e95a4975a804ac68576bfee6166d91ae6d393b736a690a77f1162d", - "marreq-core/src/repository/diesel_repo/custom_fields.rs": "2c79f7da0dd3329712dc526d9f275ae3b5529b60ce2c5a38b3823c341aaaf7f9", - "marreq-core/src/repository/diesel_repo/groups.rs": "dcf3ecbbf2366a732fe72fc722afac9b4584772b58dbbaf7dc3903add4da75e6", - "marreq-core/src/repository/diesel_repo/idempotency.rs": "b8245707ed7c35683d461cc14ee8e8952bb654b63d9f87da2959ce2b371f9959", - "marreq-core/src/repository/diesel_repo/identities.rs": "29d8cba985744fb801f643585cd81f2d6e762bd60d2a748857dcdbed45d95769", - "marreq-core/src/repository/diesel_repo/logs.rs": "828817d0a2163df7b0d22176eadea4757731dd7ae4b6e88549b6fc68b0c848f2", - "marreq-core/src/repository/diesel_repo/lookups.rs": "35bfeba0d637674d912b97c72100ef58b09cf419eec25c368d7065a3354f491a", - "marreq-core/src/repository/diesel_repo/matrix.rs": "e7a75f4d2dfe71183ff0938c101130403343ecb21283f6c490d1543ab46d04e1", - "marreq-core/src/repository/diesel_repo/mod.rs": "3bef7d6ac3c6a21da409b8a9eec67d819f7ecf81ec340383f997148a11c76082", - "marreq-core/src/repository/diesel_repo/notifications.rs": "c49f845b11f9fa64e23a637a682919140133c761d2a9f049ebbfe0d3bc24b618", - "marreq-core/src/repository/diesel_repo/oauth.rs": "7c616112874226213fa2ddaf66cde5012d5c1398c279403ceb3e7ef506c99eba", - "marreq-core/src/repository/diesel_repo/projects.rs": "5f1a9a0081a7c6674e74e9bfe35cb3d0114e434bf97944453b41eeb1958fb1a0", - "marreq-core/src/repository/diesel_repo/report_templates.rs": "3d4542ced77375a9dc9e7066f288338d3dc042fcec5193c2b5181c140d470f64", - "marreq-core/src/repository/diesel_repo/requirement_links.rs": "6775b67d323497e444149a3c45c8ccec11d87a602add7d001ecec4963e88239b", - "marreq-core/src/repository/diesel_repo/requirements.rs": "24a0e00ddc48062949761b431dd46741ed8d261a0f228df2727c99708a85b75d", - "marreq-core/src/repository/diesel_repo/saved_views.rs": "6051322dcc26e2f6c194243460f3f292d12893035deb844b098a48cee980b9ca", - "marreq-core/src/repository/diesel_repo/use_cases.rs": "fcec4e43d0d65a467daa6abc5fcce97582ca71fc78c2f0ef4461b70094c3bfa1", - "marreq-core/src/repository/diesel_repo/users.rs": "883cf4fe2a69a882fbf71b37ddbed93191cb20910c2fc5d75d8c2aed7d92197d", - "marreq-core/src/repository/diesel_repo/verification_control.rs": "29c63e62ab0a445eaf11e28756a301d1ede358005eeaf12fc95769e4e5911f90", - "marreq-core/src/repository/diesel_repo/verifications.rs": "4493d652fd759c63e6220e8b2ef17a08e547283180da82aba189b03f9a9caa9c", - "marreq-core/src/repository/diesel_repo/workspaces.rs": "4ebe8759cc12391bc6b0b7679f1bfba462518c467aaf5576f50e777d338d7622", - "marreq-core/src/repository/diesel_repo_mock.rs": "2af62bd62ae078aafff93e01e7be20096e5b86eec28e034f0b81f2673e348e37", - "marreq-core/src/repository/errors.rs": "9cf994d8f428567cd8f0ca5206ba0b363e555e4dfb018561968e2cc7081dce3c", - "marreq-core/src/repository/mod.rs": "13f6e6696619adef56e4b541399be903837c7b6a31df20f7d552055fe1694dde", - "marreq-core/src/repository/tests.rs": "fe1dd6ba26d7ebf588d4fe16b33d030303af5875d3e2017d30154125bfe84413", - "marreq-core/src/reqif/archive.rs": "0bb0e69455933ae03303ddcd53c50573aad11f176e603bdd32e1db28cb573d36", - "marreq-core/src/reqif/export.rs": "66cf5c01957bf4eba237b59c0cfb7863e9ae35d0648f3bd87a1668263a0234cd", - "marreq-core/src/reqif/import.rs": "0dbbdfac7dfe711840d1e83a38bec5b74334ef89da844928f9b0cc6207d6666e", - "marreq-core/src/reqif/mapping.rs": "1cb83d2f18e1c6161c05680d22ef16dfa055531519043247ff2685e8ea122189", - "marreq-core/src/reqif/mod.rs": "48a98c711cb01e94f40dd29a7b7f9abcf02d42e601f436e4961247c3aa4bba1a", - "marreq-core/src/reqif/schema.rs": "fc5601a2d63f35312efa437e19b14dfa197ab0bb0b4c38e731b52c0683ad0ff3", - "marreq-core/src/reqif/vendor_import_tests.rs": "bf74ae53531b3a6ad1f725e26be528b6f3332a00127c3e3edf924a3843bbaec4", - "marreq-core/src/rich_text.rs": "ff4e2c4f60e15f52e933fb6226cecf193f4bac9c47d45b1a0e2158d28751936e", - "marreq-core/src/routes/api_info.rs": "1994ccb506523391848cc2b5202bffffff5362c9f3e3937c833a4d07944f7467", - "marreq-core/src/routes/catchers.rs": "ed18bab7d2bc2bcfd36ca96a49fe1071acb08747d09cf010b01d53dc3db5e65e", - "marreq-core/src/routes/helpers.rs": "396527d619b65b32880b59d5f75b195372d2396164536d6b8224bbb78103a3ea", - "marreq-core/src/routes/mod.rs": "c25096024e4b6fd5c63ec7e0b658a34bb3faabad2b6a5c08825b37af1909c2a2", - "marreq-core/src/saved_view_definition.rs": "a222e3e907f7ed5dacc759961eba299389eae4bd79387b8370772b6a526da4c3", - "marreq-core/src/schema.rs": "4a8a5e2aa8a44ad974e1cb0737ebf609f80f0c729c103ec5476adb039f98f536", - "marreq-core/src/services/applicability_service.rs": "da755f23a963182b284686c1d4987356449e96764780646b33b80e804cf7e719", - "marreq-core/src/services/attachment_service.rs": "ba06b3dde76eca6730fe17db100b335d03daac762f953efffe8969af274b7338", - "marreq-core/src/services/base_service.rs": "e649209878f6920588beebca68188171e423bbadec50a5e8a3ead9d16ce0ad1c", - "marreq-core/src/services/baseline_service.rs": "726dbce5864593256f856cdbcdf4a3ce37faf627642cbb5968fb82438a767a07", - "marreq-core/src/services/cache_service.rs": "8a94395ef50b6b369cd935f3614e6aacdb24a4ad1e1237581214122afa9acb7a", - "marreq-core/src/services/category_service.rs": "9c5c14bfe03c398b2dde0d22aee7567c6dc5ef325c66d8598be7e46a6441b35c", - "marreq-core/src/services/comment_service.rs": "b7ead06f324840fea8c0ab35812b45c9a954d5a72c44fee340331c86b647db02", - "marreq-core/src/services/config_authorization_tests.rs": "b1ec6c9fbdf28769258f8d23c8aaaf1e4dbba62621c97e5b037b03ce1642d4ce", - "marreq-core/src/services/custom_field_service.rs": "7778d7e2a95560e608efe5ca4dd33353cf73d30a125f7eb7a679b38a24d295ec", - "marreq-core/src/services/decorated_requirement_service.rs": "e7a04e3beccbe151b1d936b53cbbd2df2d9a0405f649d6d02e868d6e842815b3", - "marreq-core/src/services/decorated_test_service.rs": "37c82e2843540b78367f7337988d347cedb03b25e7005edcb0b45972c07d4bd6", - "marreq-core/src/services/dsm_service.rs": "9409bdb79b7269305a04381cf77d6d8329de8fec54d033766d6cb344504331ed", - "marreq-core/src/services/email_sender.rs": "1a556590e3b034839fde6adc634dd563ca93a1642af070c5646b148690cf5a73", - "marreq-core/src/services/external_auth_service.rs": "1a03d40e81701a094f16b1010c1d90af5246d565f8ce35b5ecabb654a81b2dfe", - "marreq-core/src/services/group_service.rs": "bdb286fead51cde5d91a669f6f3aca77a1cabc86ef1389e287b3d193cebd3e90", - "marreq-core/src/services/log_service.rs": "b63aee03c8fde42e9143c2f4f19e7eb5df37a1299380829d093c0e44dcde5e5c", - "marreq-core/src/services/matrix_service.rs": "57ace88167fccefe005e4dabb852a44d4c333107308208f0bec6e7bfc82cd1bf", - "marreq-core/src/services/mentions.rs": "37338f744e659c1424409c3375e71fdc8df51c44a2ee67815f0b293383198483", - "marreq-core/src/services/mod.rs": "419b0ffb22da94c32ae983a7a1fd9cfe83cf8c6066d995d2afbebdab378f2af9", - "marreq-core/src/services/notification_service.rs": "959bf356384407505d9c13158072aea84767d32834b807c681667db6ab3bd575", - "marreq-core/src/services/project_service.rs": "cb0ba576c9e4a33700e99a07c2d57d9ed26f4be54f2c700a2c55380e402c8a6d", - "marreq-core/src/services/reqif_service.rs": "1c4134dbaeacb09b259507159528a593acaf36a0a182ec7ba6aa4aefbec92f24", - "marreq-core/src/services/reqifz_service.rs": "f838756002594afbedbc94346e1c143394b2e63c2fd1d071f3de49db972b2e31", - "marreq-core/src/services/requirement_analytics_service.rs": "1388c3f6b506802ff4fb9255e5047ad66b66701b2645750d460c647e287544f8", - "marreq-core/src/services/requirement_diff_service.rs": "33cd9c92855de59df501e8343b508e316275c03852703012967bfe74b53ea2d9", - "marreq-core/src/services/requirement_service.rs": "3e8d9dc89253780582981b3086094e66dfb69aeb329575ad810a68e32e57e084", - "marreq-core/src/services/saved_view_service.rs": "0c8376fdd9fc2d6eb402382a3338cac73f0507e1fb1ba1c64b305ec9a4bb778f", - "marreq-core/src/services/semantic_search/config.rs": "5203461fa16be63887069fb6ebf3ed0833096ee6823f050ffa73d93ca4300198", - "marreq-core/src/services/semantic_search/document_builder.rs": "81f6f8d224d5595eb6e57bc3604ecfc338bb634fca682cb5bc43e7d48332d52f", - "marreq-core/src/services/semantic_search/embedding_provider.rs": "3f8e5d3e66bf78ae3216cadc76ab4e4c7480226830abba4a2e722268fbd765a4", - "marreq-core/src/services/semantic_search/indexing_service.rs": "8ea2d41ab9a64a8958e78163ab76d1a960220a1f5786077f1c75cc6c69d62a08", - "marreq-core/src/services/semantic_search/llm_provider.rs": "b7318a4e6e8ae2e794a51a2d644d88cd6a6674b1de4fca7098d83cf85ffe4fa3", - "marreq-core/src/services/semantic_search/mod.rs": "2cf968e31b72125552698ad1beebd8648b2cc628410c3aff0046303470fd6200", - "marreq-core/src/services/semantic_search/search_service.rs": "3603273c894ddb0d2f2980ab8b6290f70664c3d313011ba0a082ea0f2a393c54", - "marreq-core/src/services/status_service.rs": "b85e510fc9deffa37c51106d4d56d033aba5f8499cd7a53fc94452708e081b23", - "marreq-core/src/services/tests.rs": "244cb904f099f380754efab68ed8632a350ed5c27b333670e7d057705004bcaa", - "marreq-core/src/services/use_case_service.rs": "0e1e3f42e63b5990389bc62ee9765dc3bd71d619908897d5ad036a52f85b60b4", - "marreq-core/src/services/user_provisioning_service.rs": "ed4a1d192b3a40e66f689f80730985cb9acb6e88bc6cb6307c875103257a42d9", - "marreq-core/src/services/user_service.rs": "855663cc391eda53582ca2bf8160bdbe5dfe6e803e257a5b377c7d2b6d2acb2d", - "marreq-core/src/services/verification_control_service.rs": "b2015f9051fdcbfabfec9330a890ba303feb55c252df6143c49a0cde4310fee7", - "marreq-core/src/services/verification_diff_service.rs": "63f1bdb62d3e1092bf45a5907fe3a393707e832f390bdc839dc7aae4c1804308", - "marreq-core/src/services/verification_method_service.rs": "8491d252d76cbf3f20fa20c2850e45d962ca2a55715247a8b25b781ae03d4a66", - "marreq-core/src/services/verification_service.rs": "75eb4a790eb92dd866e91037d6d4f8ccb3db80b61958014d5d2a3751e4ec10f2", - "marreq-core/src/status_enums.rs": "0b3794e640c934f10584780bb703cbd8dd54bf1fd218841c70e5975ee85e20a3", - "marreq-core/src/storage/content_type.rs": "e324695785b60c63fc14a76d53d4ab5a1891fb623ea6485502f92cb892f6c423", - "marreq-core/src/storage/local.rs": "112afb565a8eff8007ee499baaf5751a96dec78df5805b23d40effc8ce099801", - "marreq-core/src/storage/mod.rs": "79653093398c728e8a2be8270e0ccadcc14bf3c8ac8dbce62d0fe344c42de66b", - "marreq-core/src/use_case_document/diff.rs": "9059f723d9f14704f80e1901be32483d0c3de552dd61f8a45d5dcc12e5032dd9", - "marreq-core/src/use_case_document/mod.rs": "d27cff5c38cfc5a88a47b3e8615f7bf2ce19b11345b603dbd6e46fff125e72e7", - "marreq-core/src/use_case_document/operations.rs": "78ab1e3750a7ace36991660e23bc5fc9e05e88de2452d90ecbd4d7fa8095595c", - "marreq-core/src/validation/error.rs": "e80f3f225c3300a9e49f78c2a07ebbe66fb0ce7237e1010f8f51341380f0805d", - "marreq-core/src/validation/mod.rs": "41643b71b016e8a1d07384979ec9cd54c5ed860ab9a8a78326485251638f3cd0", - "marreq-core/src/validation/tests.rs": "589456901adce6f94a66db2b3ee33ea01f1db596a9be51680abdeef8b05fed1b", - "marreq-server/src/api/mod.rs": "5707e1cbc3c10b9e33e62bc7b3b244a7a565024a1c7147562634c0da7b1d3331", - "marreq-server/src/deployment.rs": "840d3a3324402d64a2448d888ea2f569656f2fc07c8d38dfc75b862b0e2714d9", - "marreq-server/src/main.rs": "b7c82e45b16873d46e9a4c178e1311d62b2d7a90a15d4e0d39a6848fe3c859b6", - "marreq-server/src/routes.rs": "348dbf2079a01db04f81209371c33c01e20ddab4ed4f1c8b34108e172c360058", - "mcp-server/compatibility.json": "0d9bcff76abe3a44b59c5bbcd3b73acd29266394e25c16b7c4b069bec28b9472", - "mcp-server/src/check-duplicates.ts": "ccdf3f47bb2d562a316b4d957cae8786f00c4255d1100e5f8bfb08202ae15d75", - "mcp-server/src/client.ts": "6011376ae4200afd83a8110d5c997df604791b3d058726b977e0270100c80306", - "mcp-server/src/context.ts": "7a0666e3f92ce56f55442425dbc1c917f1670665bd8c30fc363036d2479b09f4", - "mcp-server/src/credentials.ts": "e7697f096ccc60de4709e9b94c8bb0091c23fb090234ae4c2a36fc5f1011639c", - "mcp-server/src/gap-tools.ts": "c0453c6dac1ed6c6beb2460c47849ed5b6bf70e790b4ffc7b4fdc3f35b19f6a8", - "mcp-server/src/index.ts": "8ae7b012c087c4d522e688d1119b555d252ad2199553e8829b4445a73f46c272", - "mcp-server/src/pagination.ts": "64d0dcdec7dc6ba268f44745e281da0a2a52839804d328261be336686f8f5a5d", - "mcp-server/src/prompts.ts": "e80f45676aa1e29db25b1c637366a99c04bd0b7dd2aa402d050c000ea93ca698", - "mcp-server/src/remote.ts": "db6a0bee20bc73d3c6635d5364dc5a98e06a0b30aae5aa07482a664cef4d328b", - "mcp-server/src/resources.ts": "0c68424f0676391ecdf66df5ae8d78f026e4b82d8683e6fe0759ab33d6720fd6" -} diff --git a/docs/developer/http-api-contract.md b/docs/developer/http-api-contract.md index 4bb86309..9a62f0bc 100644 --- a/docs/developer/http-api-contract.md +++ b/docs/developer/http-api-contract.md @@ -213,179 +213,101 @@ A minimal **OpenAPI 3** sketch for auth and session project listing lives in [`o See [`../../docker/README.md`](../../docker/README.md). -## P0 contract-hardening audit and current coverage (#423) - -**This is an incomplete bootstrap, not completion of the P0 prerequisite.** The -OpenAPI inventory lists all 245 compiled HTTP API operations: 241 on Server and -245 on Cloud, including origin-root OAuth routes. Currently 39 operations have -explicit wire schemas; 206 are marked `x-marreq-unverified` and are inventory -entries only. The release completeness gate deliberately rejects them. A passing -OpenAPI validator or PR check does **not** establish full API compatibility. - -Architecture assessment: - -| Boundary | Implementation and observations | -| --- | --- | -| Shared backend | `marreq-core/src/app.rs::declared_mounts` composes policy-classified API and root OAuth routes. Rocket guards enforce sessions, Bearer scopes, membership and permissions. Diesel models are not automatically wire schemas: flattened DTOs, computed JSON, omitted fields and custom responders matter. | -| Server | `marreq-server/src/routes.rs` currently adds no endpoints. User creation/admin promotion and database backup are shared endpoints with deployment-mode restrictions, not absent Server/Cloud routes. | -| Cloud | Four additional public account endpoints: register, verify-email, forgot-password and reset-password. Their presence differs from Server; shared capabilities also differ at runtime. | -| Frontend | `frontend/src/api/*.ts`, plus direct fetches in pages/components. Session credentials and CSRF are essential; binary downloads, multipart imports and redirects cannot be modeled as ordinary JSON. Only metadata/provider discovery and selected use-case response models have migrated to generated types so far. | -| MCP | `mcp-server/src/client.ts` wraps project-scoped REST calls; `index.ts` defines tools, `remote.ts` owns MCP HTTP sessions and delegated credentials. Principal identity has nullable `client_id`/`grant_id`, even though the previous TypeScript definitions described optional strings/numbers. The Node MCP transport and `/healthz` are outside the Rust HTTP API. | -| Authentication | `/api/home/attention` now accepts `ProjectsRead` scoped credentials; the older session-only description above is stale. `/api/search` remains session-only. `/oauth/*` and `/.well-known/*` are mounted at the origin root. Guard/fairing responses can be empty or framework-generated; consumers must not assume every error is an `ApiError` JSON object. | -| Release | Existing component tags remain the publishing triggers. Independent artifact versions are allowed. Current `main` is never substituted for an unavailable historical contract. No deployment or global release migration is implemented here. | - -The explicitly modeled surface includes public build/deployment/CSRF/provider -metadata, current user, MCP principal, the four Cloud account routes, three OAuth -discovery routes, and all 25 use-case operations (including draft operations, -publishing, versions, approvals, comments, links, baseline content and diffs). -Use-case document requests distinguish omitted defaults from response properties -that Serde always emits. Unmodeled inventory entries do not promise fabricated -response bodies/status codes. Origin-root UI/HTML and internal CSRF-denial routes -are excluded from the API inventory comparison. - -### Source of truth and change workflow - -`openapi.yaml` is the wire contract. This retains Rocket/Diesel and introduces no -backend runtime dependency. `api-contract-routes.json` and both -`generated-contract.ts` files are reproducible derivatives, not independently -maintained contracts. The source-review digest records which producer/consumer -sources were audited. It detects changes requiring review, but **does not prove -serialization correctness** and is not a replacement for integration tests. - -When changing an endpoint: - -1. Inspect the actual Rocket mount, guards, Serde serialization/default/flatten/ - skip behavior, response status, and both consumers. Update OpenAPI accordingly. -2. Add or extend producer exchanges and consumer tests. Remove - `x-marreq-unverified` only after auditing the complete operation, not merely - because its route exists. Preserve unknown error-body handling. -3. Regenerate derived artifacts and explicitly record the source review: - - ```sh - python3 scripts/contracts/generate.py - python3 scripts/contracts/check.py --record-source-review - python3 scripts/contracts/check.py - python3 -m unittest discover -s scripts/contracts -v - ``` - -4. Run the compiled route check and isolated real-backend checks: - - ```sh - cargo test --locked -p marreq-cloud --test api_contract_inventory - npm ci --prefix frontend - npm ci --prefix mcp-server - scripts/contracts/test-backend.sh - ``` - -Install Python tooling with `python3 -m pip install -r -scripts/contracts/requirements.txt` in a virtual environment. Backend builds need -`libpq-dev`; Docker and Node 22 are needed for the isolated smoke check. The script -creates a new loopback-only PostgreSQL container with tmpfs storage, applies -migrations, seeds synthetic OAuth credentials through the integration test, and -starts a real `marreq-server` process. It exercises the actual frontend transport -and built MCP client over HTTP. It records requests/responses, never Authorization -headers or cookies. It ignores existing database URLs when creating its test DB; -it never connects to a production database. Cleanup runs on exit. Missing/skipped -critical producer exchanges fail transcript validation. - -### Remaining risks before P0 can be accepted - -- Audit the remaining **206** operations: authentication protocol/session flows, - projects/groups, requirements/verifications, traceability, matrices, baselines, - catalogs, administration, search, imports, exports, reports and attachments. - Presence in the inventory is not schema coverage. -- Complete per-consumer operation selection and generated request/response type - adoption, including direct frontend fetches. Bundles currently conservatively - require the entire backend surface; this can reject unrelated changes. -- Add exhaustive consumer request checks and live Cloud/session/OAuth protocol - coverage. Current executable producer/consumer coverage is representative, - not exhaustive, and no historical release is retrospectively certified. -- Retain verified bundles for real released artifact versions and bootstrap an - accurate deployment plan. Versions in package manifests alone are insufficient. -- Close existing test/security failures before making this a release-ready change. - -Until these are resolved, leave the PR in draft and keep publication blocked by -`check.py --require-complete`. Do not remove that gate to make a release green. - -### Implementation validation record (2026-10-27) - -All npm installs used the committed lockfiles (`npm ci` at root, in `frontend`, -and in `mcp-server`). Rust commands on the development host used temporary -`LIBRARY_PATH`/`LD_LIBRARY_PATH` settings pointing to an extracted `libpq` package; -no system/production database configuration was changed. - -| Exact command | Result | -| --- | --- | -| `python3 scripts/contracts/check.py` | PASS; 245 inventoried operations, 39 modeled operations, 206 explicit gaps | -| `python3 scripts/contracts/generate.py --check` | PASS; generated types and compiled-route expectations reproducible | -| `python3 -m unittest discover -s scripts/contracts -v` | PASS; 39 positive/negative tests, including historical plans and process exit codes | -| `python3 scripts/contracts/check.py --require-complete` | EXPECTED FAIL (exit 1); incomplete producer contract plus 206 unverified operations | -| `python3 scripts/contracts/compatibility.py --manifest /tmp/marreq-nonexistent-plan.json` | EXPECTED FAIL (exit 1); structured missing-evidence error | -| `cargo test --locked -p marreq-cloud --test api_contract_inventory` | PASS; compiled Server and Cloud routes match derived expectations | -| `scripts/contracts/test-backend.sh` | PASS; disposable migrated PostgreSQL, production Rocket handlers, real Server HTTP process and actual frontend/MCP clients; 15 validated exchanges, 33 explicitly inventory-only exchanges, three corrupted-traffic negative controls rejected | -| `cargo test --workspace` | PASS; 2,061 passed, one ignored. Default workspace feature unification enables test helpers; DB-specific tests without explicit test DB may skip internally. The separate isolated production-path run above is required evidence. | -| `cargo fmt --all -- --check` | PASS | -| `cargo clippy --workspace --all-targets -- -D warnings` | PASS | -| `npm run build --prefix frontend` | PASS; existing large-chunk warning | -| `npm run build --prefix mcp-server` | PASS | -| `npm test --prefix mcp-server` | PASS; 51 tests | -| `npm run test:coverage --prefix frontend` | PASS; 108 suites, 704 tests, 70.36% line coverage. An earlier ordinary full run had one report-builder failure; its isolated rerun and full coverage rerun passed. | -| `npx vitest run marreq-core/tests/js` | PASS; 411 tests, seven skipped | -| `npx --no-install stylelint 'frontend/static/**/*.css' --config .stylelintrc.json` | PASS | -| `npm run check:unused-css` | PASS (exit 0); existing warning reports 379 unused selectors | -| `npm audit --prefix frontend --omit=dev --audit-level=high` | PASS; no production vulnerabilities reported | -| `npm audit --prefix mcp-server --omit=dev --audit-level=high` | FAIL; existing lockfile reports one high and three moderate advisories | -| `cargo deny check advisories` | FAIL; existing locked dependencies trigger RUSTSEC-2026-0384, RUSTSEC-2026-0385 and RUSTSEC-2026-0387, plus yanked-crate warnings | -| `bash -n scripts/check-api-compatibility.sh scripts/contracts/test-backend.sh` | PASS | -| `git diff --check` | PASS | - -Dependency/security failures are reported, not suppressed or allowlisted. Their -remediation remains necessary before release readiness. The source-review digest -is deliberately conservative: backend service/model/guard changes and direct -frontend calls outside API modules require renewed review as well. This does not -replace the remaining serialization/consumer verification work. - -Changed files in this draft implementation (including generated artifacts): - -- `.github/workflows/api-contracts.yml` -- `.github/workflows/marreq-ci.yml` -- `.github/workflows/release-artifacts.yml` -- `.gitignore` -- `Cargo.lock` -- `docker/Dockerfile` -- `docker/frontend/Dockerfile` -- `docs/developer/api-contract-routes.json` -- `docs/developer/api-contract-source-review.json` -- `docs/developer/http-api-contract.md` -- `docs/developer/mcp-setup.md` -- `docs/developer/openapi.yaml` -- `docs/developer/use-cases-api.md` -- `docs/releases.md` -- `frontend/src/api/generated-contract.ts` -- `frontend/src/api/types.ts` -- `frontend/src/hooks/__tests__/useBuildInfo.test.ts` -- `frontend/src/pages/__tests__/AccountPage.test.tsx` -- `frontend/src/pages/__tests__/AdminPage.test.tsx` -- `frontend/src/pages/__tests__/HelpPage.test.tsx` -- `frontend/src/pages/__tests__/LoginPage.test.tsx` -- `marreq-cloud/Cargo.toml` -- `marreq-cloud/tests/api_contract_inventory.rs` -- `marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs` -- `mcp-server/Dockerfile` -- `mcp-server/compatibility.json` -- `mcp-server/src/client.ts` -- `mcp-server/src/generated-contract.ts` -- `mcp-server/src/remote.ts` -- `scripts/check-api-compatibility.sh` -- `scripts/contracts/bundle.py` -- `scripts/contracts/check.py` -- `scripts/contracts/check_baseline.py` -- `scripts/contracts/compatibility.py` -- `scripts/contracts/generate.py` -- `scripts/contracts/inventory.py` -- `scripts/contracts/requirements.txt` -- `scripts/contracts/smoke-consumers.mjs` -- `scripts/contracts/test-backend.sh` -- `scripts/contracts/test_compatibility.py` -- `scripts/contracts/test_contract_tooling.py` -- `scripts/contracts/verify_transcript.py` +## Consumer-aware contract verification (#423 / #424) + +`docs/developer/openapi.yaml` is the single wire contract. Only operations used by +frontend or MCP belong in release compatibility checks. A route inventory is not +serialization evidence: `x-marreq-unverified` means the operation still needs an +audit and cannot be used in a releasable consumer bundle. Unconsumed backend +endpoints are outside this contract; adding/changing them does not itself block +an existing consumer. + +**The refactor remains draft.** The earlier 245-operation global completeness gate +was removed, but most remaining gaps are genuinely consumed APIs. Completing their +wire schemas, binding more producer DTO serialization to those schemas, and +retaining actual historical artifact evidence are still release blockers. A +passing document validator alone is not a compatibility guarantee. + +### Architecture and rationale + +- `tools/marreq-contracts` is a workspace Rust CLI/library. Serde/serde_yaml_ng parse + data, `semver` validates versions, `sha2` pins bundle content, and `jsonschema` + validates actual request/response bodies offline. +- OpenAPI validation and breaking-change analysis use checksum-pinned **oasdiff + 1.34.0**, not a custom JSON Schema subset algorithm. See the upstream + [breaking-change semantics](https://github.com/oasdiff/oasdiff/blob/main/docs/BREAKING-CHANGES.md). + Request acceptance and response guarantees are compared in their respective + directions. Unknown advanced schema semantics fail for the affected selected + schema; they never silently certify a combination. +- Consumer calls are parsed with the established TypeScript/TSX **tree-sitter** + grammar. The analyzer follows finite URL helpers, local constants, templates, + branch alternatives and explicit HTTP methods. It includes awaited generic + calls, direct fetches, shared transports, multipart/download wrappers, and API + browser navigation. Unknown paths/methods fail; there is no source regex parser, + source-review checksum or separately maintained full route list. +- The small protocol dependency list covers browser/OAuth callbacks and discovery + which are not ordinary `fetch` expressions. Its IDs must exist in OpenAPI. All + other dependencies are discovered from consumer source. The only dynamic href + exclusion is `StatementText`'s sanitized user-content link: user-authored + navigation is not an application HTTP client operation. It does not exclude + fetches or fixed API links in that component. +- Compiled Rocket mount tests check every declared consumer operation against + Server/Cloud registration. Extra backend routes are permitted. Four public + account routes are Cloud-only; shared deployment capabilities remain runtime + behavior rather than invented Server/Cloud route differences. +- Standard `openapi-typescript` emits only schemas reachable by each consumer. + Committed declarations keep Rust/global OpenAPI out of frontend/MCP Docker + builds. Each consumer owns its minimal build input; the duplicate global type + snapshots were replaced by independently selected declarations. + +The old Python comparator, regex inventory parser, source-review hashes, +generated route JSON and separate reusable workflow were removed. The unrelated +existing requirements-compliance Python script is retained. + +### Developer commands + +Install the pinned upstream engine once (`scripts/install-oasdiff.sh`; CI does +this automatically), add its destination to PATH, or set `OASDIFF` to that binary. +The installer currently handles Linux x86_64; other platforms may install the same +upstream version themselves. No contract command downloads refs or schemas. + +```sh +cargo test --locked -p marreq-contracts +cargo run --locked -p marreq-contracts -- validate +cargo run --locked -p marreq-contracts -- coverage --component frontend +cargo run --locked -p marreq-contracts -- coverage --component mcp +node scripts/generate-api-types.mjs +node scripts/generate-api-types.mjs --check +cargo test --locked -p marreq-cloud --test api_contract_inventory +scripts/test-api-contracts.sh +``` + +`validate` checks document/policy validity and requires a verified schema for +**every discovered consumer dependency**. `--document-only` checks the document +without certifying consumer coverage; it cannot create a consumer release bundle. +`select --allow-unverified` is used only for development type generation, never +for bundle export or compatibility verification. + +When adding/changing an HTTP call, update the actual Rocket implementation and +OpenAPI, then regenerate declarations and extend existing consumer/producer tests. +A new unknown call, unresolved dynamic call, or newly used unverified operation +fails validation. Remove `x-marreq-unverified` only after auditing request defaults, +response nullability/required fields, errors/statuses, media, guards and scopes. +Do not infer a schema from one response or from a TypeScript assertion. + +The isolated smoke test runs migrated tmpfs PostgreSQL plus real Server and Cloud +binaries, exercising the actual frontend transport/session flow and MCP HTTP +client. Test processes use a clean directory/environment, loopback addresses and +synthetic credentials; existing `.env`/database/SMTP configuration is excluded. +Transcripts omit headers/cookies and redact login credentials. Default workspace +unit tests can unify `test-helpers`; the separate production-path run is essential. +It remains representative evidence, not certification of all unmodeled APIs. + +### Scope limits + +The CLI reports exact operation and call-site counts. Keep the PR draft while any +required operation is unverified, a producer behavior lacks adequate contract +protection, or a required check fails. Hand-maintained schemas still need broader +Rust DTO/serialization verification; compiled route presence alone is insufficient. +The remaining upstream Rust advisory is tracked in +[#425](https://github.com/MarreqSW/Marreq/issues/425); the security gate is unchanged. diff --git a/docs/developer/mcp-setup.md b/docs/developer/mcp-setup.md index 1cc120ba..7b85eae6 100644 --- a/docs/developer/mcp-setup.md +++ b/docs/developer/mcp-setup.md @@ -360,14 +360,13 @@ Reference: shared route list in `marreq-core/src/api/mod.rs` (plus deployment-sp - **Connection refused**: Ensure Marreq is running and `MARREQ_BASE_URL` is correct (e.g. `http://localhost:8000`). - **Tool not found**: Confirm `MARREQ_MODE` and `MARREQ_TRACE_WRITE` — extended and trace tools are only registered when those settings enable them. -## Backend compatibility metadata - -`mcp-server/compatibility.json` records the build-time supported backend range, -independently of the MCP component's version. It is retained in the MCP image. -CI validates its shape and release verification combines it with the exact -released MCP contract bundle; ranges alone do not establish compatibility. -See [release contract verification](../releases.md#api-compatibility-gate-and-retained-artifact-contracts-423-p0). -The current bootstrap still has unverified API operations and intentionally cannot -certify an independently deployed production combination. Existing mocked MCP -unit tests remain; `scripts/contracts/test-backend.sh` additionally executes the -actual client against a real loopback Rocket server and an isolated database. +## Artifact compatibility + +`mcp-server/compatibility.json` declares its backend range independently of MCP's +version and remains in the MCP image. The complete global OpenAPI is not copied +into that image or the public nginx directory. Consumer-specific immutable bundles +are retained alongside release artifacts and verified by the Rust +`marreq-contracts` CLI; see [release verification](../releases.md#exact-artifact-api-compatibility-423). +A missing historical bundle or an unknown operation fails safely. Backend ranges +alone are not compatibility evidence. The existing mocked tool tests remain, +with actual HTTP/session/OAuth smoke checks in `scripts/test-api-contracts.sh`. diff --git a/docs/developer/openapi.yaml b/docs/developer/openapi.yaml index 932e8797..6f23216b 100644 --- a/docs/developer/openapi.yaml +++ b/docs/developer/openapi.yaml @@ -1,20 +1,13 @@ openapi: 3.1.0 -info: {title: Marreq HTTP API, version: 0.1.0, description: Rocket route inventory plus explicitly audited wire - schemas. Operations marked x-marreq-unverified are NOT complete contracts and cause release verification to - fail closed. Root OAuth paths are distinct from /api. The Node MCP transport is not a Rust REST endpoint. See - http-api-contract.md for scope and remaining work.} +info: {title: Marreq HTTP API, version: 0.1.0, description: Consumer-facing HTTP contracts. Unconsumed backend routes + are outside compatibility enforcement. Required operations marked x-marreq-unverified fail verification; no + missing schemas are inferred from inventory entries.} servers: - {url: /} paths: /api/projects/{project_id}/requirements/{id}/activity: get: operationId: activity_requirement_activity_by_project - tags: [activity] - summary: activity / requirement activity by project - x-marreq-variants: [server, cloud] - x-marreq-handler: activity::requirement_activity_by_project - x-marreq-source: marreq-core/src/api/activity.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -27,20 +20,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/verifications/{id}/activity: get: operationId: activity_verification_activity_by_project - tags: [activity] - summary: activity / verification activity by project - x-marreq-variants: [server, cloud] - x-marreq-handler: activity::verification_activity_by_project - x-marreq-source: marreq-core/src/api/activity.rs - x-marreq-guards: [ProjectVerificationsRead] parameters: - in: path name: project_id @@ -53,73 +39,32 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/applicability: get: operationId: applicability_list - tags: [applicability] - summary: applicability / list - x-marreq-variants: [server, cloud] - x-marreq-handler: applicability::list - x-marreq-source: marreq-core/src/api/applicability.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] post: operationId: applicability_create - tags: [applicability] - summary: applicability / create - x-marreq-variants: [server, cloud] - x-marreq-handler: applicability::create - x-marreq-source: marreq-core/src/api/applicability.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/applicability/{id}: - get: - operationId: applicability_get - tags: [applicability] - summary: applicability / get - x-marreq-variants: [server, cloud] - x-marreq-handler: applicability::get - x-marreq-source: marreq-core/src/api/applicability.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] put: operationId: applicability_update - tags: [applicability] - summary: applicability / update - x-marreq-variants: [server, cloud] - x-marreq-handler: applicability::update - x-marreq-source: marreq-core/src/api/applicability.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -128,18 +73,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] delete: operationId: applicability_delete - tags: [applicability] - summary: applicability / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: applicability::delete - x-marreq-source: marreq-core/src/api/applicability.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -148,19 +86,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/attachments: get: operationId: attachments_list - tags: [attachments] - summary: attachments / list - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::list - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [ProjectRequirementsAndVerificationsRead] parameters: - in: path name: project_id @@ -169,19 +100,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: attachments_upload - tags: [attachments] - summary: attachments / upload - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::upload - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -190,46 +114,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/attachments/{attachment_id}/download: get: operationId: attachments_download - tags: [attachments] - summary: attachments / download - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::download - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [ProjectAccessOrBearer] - parameters: - - in: path - name: project_id - required: true - schema: {type: integer} - - in: path - name: attachment_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - - bearerAuth: [] - /api/projects/{project_id}/attachments/{attachment_id}: - delete: - operationId: attachments_delete - tags: [attachments] - summary: attachments / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::delete - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -242,20 +133,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/storage: get: operationId: attachments_get_storage - tags: [attachments] - summary: attachments / get storage - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::get_storage - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -264,20 +148,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/storage/quota: put: operationId: attachments_set_quota - tags: [attachments] - summary: attachments / set quota - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::set_quota - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [AdminOnly] parameters: - in: path name: project_id @@ -286,19 +163,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/baselines/{baseline_id}/attachments: get: operationId: attachments_list_for_baseline - tags: [attachments] - summary: attachments / list for baseline - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::list_for_baseline - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -311,20 +181,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/{baseline_id}/attachments/{attachment_id}/download: get: operationId: attachments_download_for_baseline - tags: [attachments] - summary: attachments / download for baseline - x-marreq-variants: [server, cloud] - x-marreq-handler: attachments::download_for_baseline - x-marreq-source: marreq-core/src/api/attachments.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -341,20 +204,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/auth/providers: get: operationId: auth_auth_providers - tags: [auth] - summary: auth / auth providers - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_providers - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [] security: [] parameters: [] responses: @@ -366,12 +222,6 @@ paths: /api/auth/external/{provider}/start: get: operationId: auth_auth_external_start - tags: [auth] - summary: auth / auth external start - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_external_start - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [] parameters: - in: path name: provider @@ -380,39 +230,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: [] - /api/auth/external/{provider}/link: - post: - operationId: auth_auth_external_link_start - tags: [auth] - summary: auth / auth external link start - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_external_link_start - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [OptionalSessionUser] - parameters: - - in: path - name: provider - required: true - schema: {type: string} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/auth/external/{provider}/callback: get: operationId: auth_auth_external_callback - tags: [auth] - summary: auth / auth external callback - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_external_callback - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [OptionalSessionUser] parameters: - in: path name: provider @@ -421,128 +243,21 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: [] - /api/auth/csrf: - get: - operationId: auth_auth_csrf - tags: [auth] - summary: auth / auth csrf - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_csrf - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [] - security: [] - parameters: [] - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - csrf_token: &id005 {type: string} - required: [csrf_token] - additionalProperties: true - /api/auth/login: - post: - operationId: auth_auth_login - tags: [auth] - summary: auth / auth login - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_login - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: [] /api/auth/identities: get: operationId: auth_auth_identities - tags: [auth] - summary: auth / auth identities - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_identities - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [OptionalSessionUser] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/auth/identities/{identity_id}: - delete: - operationId: auth_auth_identity_delete - tags: [auth] - summary: auth / auth identity delete - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_identity_delete - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [OptionalSessionUser] - parameters: - - in: path - name: identity_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/auth/logout: - post: - operationId: auth_auth_logout - tags: [auth] - summary: auth / auth logout - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_logout - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/auth/change-password: - post: - operationId: auth_auth_change_password - tags: [auth] - summary: auth / auth change password - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_change_password - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [OptionalSessionUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/auth/me: get: operationId: auth_auth_me - tags: [auth] - summary: auth / auth me - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_me - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [OptionalSessionUser] security: - cookieAuth: [] parameters: [] @@ -561,63 +276,36 @@ paths: schema: {$ref: '#/components/schemas/ApiError'} put: operationId: auth_auth_update_me - tags: [auth] - summary: auth / auth update me - x-marreq-variants: [server, cloud] - x-marreq-handler: auth::auth_update_me - x-marreq-source: marreq-core/src/api/auth.rs - x-marreq-guards: [OptionalSessionUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/admin/backup/info: get: operationId: backup_info - tags: [backup] - summary: backup / info - x-marreq-variants: [server, cloud] - x-marreq-handler: backup::info - x-marreq-source: marreq-core/src/api/backup.rs - x-marreq-guards: [AdminOnly] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/admin/backup: post: operationId: backup_create - tags: [backup] - summary: backup / create - x-marreq-variants: [server, cloud] - x-marreq-handler: backup::create - x-marreq-source: marreq-core/src/api/backup.rs - x-marreq-guards: [AdminOnly] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/baselines: get: operationId: baselines_list - tags: [baselines] - summary: baselines / list - x-marreq-variants: [server, cloud] - x-marreq-handler: baselines::list - x-marreq-source: marreq-core/src/api/baselines.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -626,19 +314,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: baselines_create - tags: [baselines] - summary: baselines / create - x-marreq-variants: [server, cloud] - x-marreq-handler: baselines::create - x-marreq-source: marreq-core/src/api/baselines.rs - x-marreq-guards: [ProjectBaselinesWrite] parameters: - in: path name: project_id @@ -647,20 +328,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/{baseline_id}: get: operationId: baselines_get - tags: [baselines] - summary: baselines / get - x-marreq-variants: [server, cloud] - x-marreq-handler: baselines::get - x-marreq-source: marreq-core/src/api/baselines.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -673,20 +347,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/{baseline_id}/requirements: get: operationId: baselines_get_requirements - tags: [baselines] - summary: baselines / get requirements - x-marreq-variants: [server, cloud] - x-marreq-handler: baselines::get_requirements - x-marreq-source: marreq-core/src/api/baselines.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -699,20 +366,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/{baseline_id}/traceability: get: operationId: baselines_get_traceability - tags: [baselines] - summary: baselines / get traceability - x-marreq-variants: [server, cloud] - x-marreq-handler: baselines::get_traceability - x-marreq-source: marreq-core/src/api/baselines.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -725,20 +385,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/{baseline_id}/verifications: get: operationId: baselines_get_verifications - tags: [baselines] - summary: baselines / get verifications - x-marreq-variants: [server, cloud] - x-marreq-handler: baselines::get_verifications - x-marreq-source: marreq-core/src/api/baselines.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -751,20 +404,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/diff: get: operationId: baselines_diff_baselines - tags: [baselines] - summary: baselines / diff baselines - x-marreq-variants: [server, cloud] - x-marreq-handler: baselines::diff_baselines - x-marreq-source: marreq-core/src/api/baselines.rs - x-marreq-guards: [ProjectBaselinesRead] parameters: - in: path name: project_id @@ -773,138 +419,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/cache/stats: - get: - operationId: cache_stats - tags: [cache] - summary: cache / stats - x-marreq-variants: [server, cloud] - x-marreq-handler: cache::stats - x-marreq-source: marreq-core/src/api/cache.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/cache/clear: - post: - operationId: cache_clear - tags: [cache] - summary: cache / clear - x-marreq-variants: [server, cloud] - x-marreq-handler: cache::clear - x-marreq-source: marreq-core/src/api/cache.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/cache/cleanup: - post: - operationId: cache_cleanup - tags: [cache] - summary: cache / cleanup - x-marreq-variants: [server, cloud] - x-marreq-handler: cache::cleanup - x-marreq-source: marreq-core/src/api/cache.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/cache/performance: - get: - operationId: cache_performance - tags: [cache] - summary: cache / performance - x-marreq-variants: [server, cloud] - x-marreq-handler: cache::performance - x-marreq-source: marreq-core/src/api/cache.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/cache/recommendations: - get: - operationId: cache_recommendations - tags: [cache] - summary: cache / recommendations - x-marreq-variants: [server, cloud] - x-marreq-handler: cache::recommendations - x-marreq-source: marreq-core/src/api/cache.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/cache/reset-counters: - post: - operationId: cache_reset_counters - tags: [cache] - summary: cache / reset counters - x-marreq-variants: [server, cloud] - x-marreq-handler: cache::reset_counters - x-marreq-source: marreq-core/src/api/cache.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/cache/health: - get: - operationId: cache_health - tags: [cache] - summary: cache / health - x-marreq-variants: [server, cloud] - x-marreq-handler: cache::health - x-marreq-source: marreq-core/src/api/cache.rs - x-marreq-guards: [] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: [] /api/projects/{project_id}/catalog: get: operationId: catalog_get - tags: [catalog] - summary: catalog / get - x-marreq-variants: [server, cloud] - x-marreq-handler: catalog::get - x-marreq-source: marreq-core/src/api/catalog.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -913,36 +434,22 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/categories: get: operationId: categories_list - tags: [categories] - summary: categories / list - x-marreq-variants: [server, cloud] - x-marreq-handler: categories::list - x-marreq-source: marreq-core/src/api/categories.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] post: operationId: categories_create - tags: [categories] - summary: categories / create - x-marreq-variants: [server, cloud] - x-marreq-handler: categories::create - x-marreq-source: marreq-core/src/api/categories.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} @@ -952,34 +459,8 @@ paths: security: - cookieAuth: [] /api/categories/{id}: - get: - operationId: categories_get - tags: [categories] - summary: categories / get - x-marreq-variants: [server, cloud] - x-marreq-handler: categories::get - x-marreq-source: marreq-core/src/api/categories.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] put: operationId: categories_update - tags: [categories] - summary: categories / update - x-marreq-variants: [server, cloud] - x-marreq-handler: categories::update - x-marreq-source: marreq-core/src/api/categories.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -988,18 +469,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] delete: operationId: categories_delete - tags: [categories] - summary: categories / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: categories::delete - x-marreq-source: marreq-core/src/api/categories.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -1008,19 +482,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/requirements/{requirement_id}/comments: get: operationId: comments_list_by_project - tags: [comments] - summary: comments / list by project - x-marreq-variants: [server, cloud] - x-marreq-handler: comments::list_by_project - x-marreq-source: marreq-core/src/api/comments.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -1033,19 +500,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: comments_create_by_project - tags: [comments] - summary: comments / create by project - x-marreq-variants: [server, cloud] - x-marreq-handler: comments::create_by_project - x-marreq-source: marreq-core/src/api/comments.rs - x-marreq-guards: [ProjectRequirementsWrite] parameters: - in: path name: project_id @@ -1058,20 +518,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/requirements/{requirement_id}/comments: get: operationId: comments_list - tags: [comments] - summary: comments / list - x-marreq-variants: [server, cloud] - x-marreq-handler: comments::list - x-marreq-source: marreq-core/src/api/comments.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: requirement_id @@ -1080,18 +533,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] post: operationId: comments_create - tags: [comments] - summary: comments / create - x-marreq-variants: [server, cloud] - x-marreq-handler: comments::create - x-marreq-source: marreq-core/src/api/comments.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: requirement_id @@ -1100,19 +546,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/custom_fields: get: operationId: custom_fields_list_by_project - tags: [custom_fields] - summary: custom fields / list by project - x-marreq-variants: [server, cloud] - x-marreq-handler: custom_fields::list_by_project - x-marreq-source: marreq-core/src/api/custom_fields.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1121,19 +560,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: custom_fields_create - tags: [custom_fields] - summary: custom fields / create - x-marreq-variants: [server, cloud] - x-marreq-handler: custom_fields::create - x-marreq-source: marreq-core/src/api/custom_fields.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1148,39 +580,8 @@ paths: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/custom_fields/{field_id}: - get: - operationId: custom_fields_get - tags: [custom_fields] - summary: custom fields / get - x-marreq-variants: [server, cloud] - x-marreq-handler: custom_fields::get - x-marreq-source: marreq-core/src/api/custom_fields.rs - x-marreq-guards: [ProjectAccessOrBearer] - parameters: - - in: path - name: project_id - required: true - schema: {type: integer} - - in: path - name: field_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - - bearerAuth: [] put: operationId: custom_fields_update - tags: [custom_fields] - summary: custom fields / update - x-marreq-variants: [server, cloud] - x-marreq-handler: custom_fields::update - x-marreq-source: marreq-core/src/api/custom_fields.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1193,19 +594,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: custom_fields_delete - tags: [custom_fields] - summary: custom fields / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: custom_fields::delete - x-marreq-source: marreq-core/src/api/custom_fields.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1218,37 +612,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/dashboard: - get: - operationId: dashboard_dashboard_json - tags: [dashboard] - summary: dashboard / dashboard json - x-marreq-variants: [server, cloud] - x-marreq-handler: dashboard::dashboard_json - x-marreq-source: marreq-core/src/api/dashboard.rs - x-marreq-guards: [OptionalSessionUser] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/dsm: get: operationId: dsm_get_dsm - tags: [dsm] - summary: dsm / get dsm - x-marreq-variants: [server, cloud] - x-marreq-handler: dsm::get_dsm - x-marreq-source: marreq-core/src/api/dsm.rs - x-marreq-guards: [ProjectRequirementsAndTraceabilityRead] parameters: - in: path name: project_id @@ -1257,20 +627,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/requirements.xlsx: get: operationId: exports_export_requirements_xlsx - tags: [exports] - summary: exports / export requirements xlsx - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_requirements_xlsx - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1279,20 +642,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/verifications.xlsx: get: operationId: exports_export_verifications_xlsx - tags: [exports] - summary: exports / export verifications xlsx - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_verifications_xlsx - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1301,20 +657,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/matrix.xlsx: get: operationId: exports_export_matrix_xlsx - tags: [exports] - summary: exports / export matrix xlsx - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_matrix_xlsx - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1323,20 +672,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/dsm.xlsx: get: operationId: exports_export_dsm_xlsx - tags: [exports] - summary: exports / export dsm xlsx - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_dsm_xlsx - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1345,20 +687,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/matrix-links.xlsx: get: operationId: exports_export_matrix_links_xlsx - tags: [exports] - summary: exports / export matrix links xlsx - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_matrix_links_xlsx - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1367,20 +702,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/requirements.pdf: get: operationId: exports_export_requirements_pdf - tags: [exports] - summary: exports / export requirements pdf - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_requirements_pdf - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1389,20 +717,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/report.pdf: get: operationId: exports_export_report_pdf - tags: [exports] - summary: exports / export report pdf - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_report_pdf - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1411,20 +732,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/requirements.reqif: get: operationId: exports_export_requirements_reqif - tags: [exports] - summary: exports / export requirements reqif - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_requirements_reqif - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1433,20 +747,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/requirements.reqifz: get: operationId: exports_export_requirements_reqifz - tags: [exports] - summary: exports / export requirements reqifz - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_requirements_reqifz - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1455,20 +762,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/baselines/{filename}: get: operationId: exports_export_baseline_reqif - tags: [exports] - summary: exports / export baseline reqif - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_baseline_reqif - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1481,20 +781,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/bundle.json: get: operationId: exports_export_project_bundle - tags: [exports] - summary: exports / export project bundle - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_project_bundle - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1503,20 +796,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/exports/bundle.zip: get: operationId: exports_export_project_bundle_zip - tags: [exports] - summary: exports / export project bundle zip - x-marreq-variants: [server, cloud] - x-marreq-handler: exports::export_project_bundle_zip - x-marreq-source: marreq-core/src/api/exports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1525,73 +811,45 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/groups: get: operationId: groups_list - tags: [groups] - summary: groups / list - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::list - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: groups_create - tags: [groups] - summary: groups / create - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::create - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/groups/creatable: get: operationId: groups_list_creatable - tags: [groups] - summary: groups / list creatable - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::list_creatable - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/groups/{group_id}: get: operationId: groups_get - tags: [groups] - summary: groups / get - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::get - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: - in: path name: group_id @@ -1600,19 +858,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] patch: operationId: groups_update - tags: [groups] - summary: groups / update - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::update - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: - in: path name: group_id @@ -1621,19 +872,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: groups_delete - tags: [groups] - summary: groups / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::delete - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: - in: path name: group_id @@ -1642,20 +886,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/groups/{group_id}/projects: get: operationId: groups_list_projects - tags: [groups] - summary: groups / list projects - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::list_projects - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: - in: path name: group_id @@ -1664,20 +901,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/groups/{group_id}/members: get: operationId: groups_list_members - tags: [groups] - summary: groups / list members - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::list_members - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: - in: path name: group_id @@ -1686,20 +916,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/groups/{group_id}/members/{user_id}: put: operationId: groups_set_member_role - tags: [groups] - summary: groups / set member role - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::set_member_role - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: - in: path name: group_id @@ -1712,19 +935,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: groups_remove_member - tags: [groups] - summary: groups / remove member - x-marreq-variants: [server, cloud] - x-marreq-handler: groups::remove_member - x-marreq-source: marreq-core/src/api/groups.rs - x-marreq-guards: [ApiUserOrBearer] parameters: - in: path name: group_id @@ -1737,55 +953,34 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/home/attention: get: operationId: home_attention - tags: [home] - summary: home / attention - x-marreq-variants: [server, cloud] - x-marreq-handler: home::attention - x-marreq-source: marreq-core/src/api/home.rs - x-marreq-guards: [ProjectsRead] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/search: get: operationId: home_search - tags: [home] - summary: home / search - x-marreq-variants: [server, cloud] - x-marreq-handler: home::search - x-marreq-source: marreq-core/src/api/home.rs - x-marreq-guards: [SessionUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/imports/excel/preview: post: operationId: imports_preview_excel - tags: [imports] - summary: imports / preview excel - x-marreq-variants: [server, cloud] - x-marreq-handler: imports::preview_excel - x-marreq-source: marreq-core/src/api/imports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1794,20 +989,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/imports/excel: post: operationId: imports_commit_excel - tags: [imports] - summary: imports / commit excel - x-marreq-variants: [server, cloud] - x-marreq-handler: imports::commit_excel - x-marreq-source: marreq-core/src/api/imports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1816,20 +1004,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/imports/reqif: post: operationId: imports_commit_reqif - tags: [imports] - summary: imports / commit reqif - x-marreq-variants: [server, cloud] - x-marreq-handler: imports::commit_reqif - x-marreq-source: marreq-core/src/api/imports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -1838,123 +1019,64 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/imports/bundle: post: operationId: imports_import_project_bundle - tags: [imports] - summary: imports / import project bundle - x-marreq-variants: [server, cloud] - x-marreq-handler: imports::import_project_bundle - x-marreq-source: marreq-core/src/api/imports.rs - x-marreq-guards: [ApiUserOrBearer] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/admin/logs: get: operationId: logs_list - tags: [logs] - summary: logs / list - x-marreq-variants: [server, cloud] - x-marreq-handler: logs::list - x-marreq-source: marreq-core/src/api/logs.rs - x-marreq-guards: [AdminOnly] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/admin/logs/stats: get: operationId: logs_stats - tags: [logs] - summary: logs / stats - x-marreq-variants: [server, cloud] - x-marreq-handler: logs::stats - x-marreq-source: marreq-core/src/api/logs.rs - x-marreq-guards: [AdminOnly] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/admin/logs/export.json: get: operationId: logs_export_json - tags: [logs] - summary: logs / export json - x-marreq-variants: [server, cloud] - x-marreq-handler: logs::export_json - x-marreq-source: marreq-core/src/api/logs.rs - x-marreq-guards: [AdminOnly] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/admin/logs/cleanup: post: operationId: logs_cleanup - tags: [logs] - summary: logs / cleanup - x-marreq-variants: [server, cloud] - x-marreq-handler: logs::cleanup - x-marreq-source: marreq-core/src/api/logs.rs - x-marreq-guards: [AdminOnly] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/matrix: - get: - operationId: matrix_list - tags: [matrix] - summary: matrix / list - x-marreq-variants: [server, cloud] - x-marreq-handler: matrix::list - x-marreq-source: marreq-core/src/api/matrix.rs - x-marreq-guards: [ApiUser] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/matrix: get: operationId: matrix_list_by_project - tags: [matrix] - summary: matrix / list by project - x-marreq-variants: [server, cloud] - x-marreq-handler: matrix::list_by_project - x-marreq-source: marreq-core/src/api/matrix.rs - x-marreq-guards: [ProjectTraceabilityRead] parameters: - in: path name: project_id @@ -1963,20 +1085,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/verifications/{verification_id}/matrix: get: operationId: matrix_get_verification_matrix - tags: [matrix] - summary: matrix / get verification matrix - x-marreq-variants: [server, cloud] - x-marreq-handler: matrix::get_verification_matrix - x-marreq-source: marreq-core/src/api/matrix.rs - x-marreq-guards: [ProjectTraceabilityRead] parameters: - in: path name: project_id @@ -1989,19 +1104,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] put: operationId: matrix_put_verification_matrix - tags: [matrix] - summary: matrix / put verification matrix - x-marreq-variants: [server, cloud] - x-marreq-handler: matrix::put_verification_matrix - x-marreq-source: marreq-core/src/api/matrix.rs - x-marreq-guards: [ProjectTraceabilityWrite] parameters: - in: path name: project_id @@ -2014,20 +1122,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/mcp/principal: get: operationId: mcp_principal - tags: [mcp] - summary: mcp / principal - x-marreq-variants: [server, cloud] - x-marreq-handler: mcp::principal - x-marreq-source: marreq-core/src/api/mcp.rs - x-marreq-guards: [McpPrincipalAuth] security: - cookieAuth: [] - bearerAuth: [] @@ -2045,47 +1146,27 @@ paths: /api/mcp/audit: post: operationId: mcp_audit - tags: [mcp] - summary: mcp / audit - x-marreq-variants: [server, cloud] - x-marreq-handler: mcp::audit - x-marreq-source: marreq-core/src/api/mcp.rs - x-marreq-guards: [StdioMcpAuditAuth] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - bearerAuth: [] x-marreq-authentication-note: Personal API tokens only; delegated OAuth and sessions rejected. /api/mcp/internal/audit: post: operationId: mcp_internal_audit - tags: [mcp] - summary: mcp / internal audit - x-marreq-variants: [server, cloud] - x-marreq-handler: mcp::internal_audit - x-marreq-source: marreq-core/src/api/mcp.rs - x-marreq-guards: [InternalMcpAudit, McpAuditAuth] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - mcpServiceAuth: [] /api/projects/{project_id}/me/permissions: get: operationId: members_get_my_permissions - tags: [members] - summary: members / get my permissions - x-marreq-variants: [server, cloud] - x-marreq-handler: members::get_my_permissions - x-marreq-source: marreq-core/src/api/members.rs - x-marreq-guards: [ProjectProjectsRead] parameters: - in: path name: project_id @@ -2094,20 +1175,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/members: get: operationId: members_list_members - tags: [members] - summary: members / list members - x-marreq-variants: [server, cloud] - x-marreq-handler: members::list_members - x-marreq-source: marreq-core/src/api/members.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2116,20 +1190,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/members/{user_id}: put: operationId: members_set_member_role - tags: [members] - summary: members / set member role - x-marreq-variants: [server, cloud] - x-marreq-handler: members::set_member_role - x-marreq-source: marreq-core/src/api/members.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2142,19 +1209,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: members_remove_member - tags: [members] - summary: members / remove member - x-marreq-variants: [server, cloud] - x-marreq-handler: members::remove_member - x-marreq-source: marreq-core/src/api/members.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2167,20 +1227,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/reviewers: get: operationId: members_list_project_reviewers - tags: [members] - summary: members / list project reviewers - x-marreq-variants: [server, cloud] - x-marreq-handler: members::list_project_reviewers - x-marreq-source: marreq-core/src/api/members.rs - x-marreq-guards: [ProjectProjectsRead] parameters: - in: path name: project_id @@ -2189,19 +1242,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] put: operationId: members_put_project_reviewers - tags: [members] - summary: members / put project reviewers - x-marreq-variants: [server, cloud] - x-marreq-handler: members::put_project_reviewers - x-marreq-source: marreq-core/src/api/members.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2210,60 +1256,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/: - get: - operationId: meta_api_root - tags: [meta] - summary: meta / api root - x-marreq-variants: [server, cloud] - x-marreq-handler: meta::api_root - x-marreq-source: marreq-core/src/api/meta.rs - x-marreq-guards: [] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/meta/health: - get: - operationId: meta_health - tags: [meta] - summary: meta / health - x-marreq-variants: [server, cloud] - x-marreq-handler: meta::health - x-marreq-source: marreq-core/src/api/meta.rs - x-marreq-guards: [] - security: [] - parameters: [] - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - status: {const: ok, type: string} - service: {const: marreq, type: string} - required: [status, service] - additionalProperties: true /api/meta/deployment: get: operationId: meta_deployment_info - tags: [meta] - summary: meta / deployment info - x-marreq-variants: [server, cloud] - x-marreq-handler: meta::deployment_info - x-marreq-source: marreq-core/src/api/meta.rs - x-marreq-guards: [] security: [] parameters: [] responses: @@ -2275,12 +1274,6 @@ paths: /api/meta/build: get: operationId: meta_build_info - tags: [meta] - summary: meta / build info - x-marreq-variants: [server, cloud] - x-marreq-handler: meta::build_info - x-marreq-source: marreq-core/src/api/meta.rs - x-marreq-guards: [] security: [] parameters: [] responses: @@ -2292,46 +1285,16 @@ paths: /api/notifications: get: operationId: notifications_list - tags: [notifications] - summary: notifications / list - x-marreq-variants: [server, cloud] - x-marreq-handler: notifications::list - x-marreq-source: marreq-core/src/api/notifications.rs - x-marreq-guards: [ApiUser] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/notifications/unread-count: - get: - operationId: notifications_unread_count - tags: [notifications] - summary: notifications / unread count - x-marreq-variants: [server, cloud] - x-marreq-handler: notifications::unread_count - x-marreq-source: marreq-core/src/api/notifications.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/notifications/{id}/read: patch: operationId: notifications_mark_read - tags: [notifications] - summary: notifications / mark read - x-marreq-variants: [server, cloud] - x-marreq-handler: notifications::mark_read - x-marreq-source: marreq-core/src/api/notifications.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -2340,53 +1303,32 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/notifications/read-all: post: operationId: notifications_mark_all_read - tags: [notifications] - summary: notifications / mark all read - x-marreq-variants: [server, cloud] - x-marreq-handler: notifications::mark_all_read - x-marreq-source: marreq-core/src/api/notifications.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/notifications/preferences: get: operationId: notifications_get_preferences - tags: [notifications] - summary: notifications / get preferences - x-marreq-variants: [server, cloud] - x-marreq-handler: notifications::get_preferences - x-marreq-source: marreq-core/src/api/notifications.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/notifications/preferences/{project_id}: put: operationId: notifications_set_preference - tags: [notifications] - summary: notifications / set preference - x-marreq-variants: [server, cloud] - x-marreq-handler: notifications::set_preference - x-marreq-source: marreq-core/src/api/notifications.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: project_id @@ -2395,18 +1337,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] delete: operationId: notifications_delete_preference - tags: [notifications] - summary: notifications / delete preference - x-marreq-variants: [server, cloud] - x-marreq-handler: notifications::delete_preference - x-marreq-source: marreq-core/src/api/notifications.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: project_id @@ -2415,19 +1350,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /.well-known/oauth-authorization-server: get: operationId: oauth_authorization_server_metadata - tags: [oauth] - summary: oauth / authorization server metadata - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::authorization_server_metadata - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [] parameters: [] responses: '200': @@ -2463,47 +1391,9 @@ paths: additionalProperties: true description: See implementation and http-api-contract.md for validation rules. security: [] - /.well-known/oauth-protected-resource: - get: - operationId: oauth_protected_resource_metadata - tags: [oauth] - summary: oauth / protected resource metadata - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::protected_resource_metadata - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [] - parameters: [] - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - resource: *id001 - authorization_servers: - type: array - items: *id001 - scopes_supported: - type: array - items: *id001 - bearer_methods_supported: - type: array - items: *id001 - required: [resource, authorization_servers, scopes_supported, bearer_methods_supported] - additionalProperties: true - description: See implementation and http-api-contract.md for validation rules. - security: [] /.well-known/oauth-protected-resource/mcp: get: operationId: oauth_protected_resource_metadata_mcp - tags: [oauth] - summary: oauth / protected resource metadata mcp - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::protected_resource_metadata_mcp - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [] parameters: [] responses: '200': @@ -2530,149 +1420,63 @@ paths: /oauth/register: post: operationId: oauth_register - tags: [oauth] - summary: oauth / register - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::register - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: [] /oauth/authorize: get: operationId: oauth_authorize_page - tags: [oauth] - summary: oauth / authorize page - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::authorize_page - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [OptionalSessionUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: [] post: operationId: oauth_authorize_decision - tags: [oauth] - summary: oauth / authorize decision - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::authorize_decision - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [SessionUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /oauth/token: post: operationId: oauth_token - tags: [oauth] - summary: oauth / token - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::token - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: [] - /api/oauth/grants: - get: - operationId: oauth_grants - tags: [oauth] - summary: oauth / grants - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::grants - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [SessionUser] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/oauth/grants/{id}: - delete: - operationId: oauth_revoke_grant - tags: [oauth] - summary: oauth / revoke grant - x-marreq-variants: [server, cloud] - x-marreq-handler: oauth::revoke_grant - x-marreq-source: marreq-core/src/api/oauth.rs - x-marreq-guards: [SessionUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects: post: operationId: projects_create - tags: [projects] - summary: projects / create - x-marreq-variants: [server, cloud] - x-marreq-handler: projects::create - x-marreq-source: marreq-core/src/api/projects.rs - x-marreq-guards: [ApiUserOrBearer] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] get: operationId: projects_session_list_for_session - tags: [projects_session] - summary: projects session / list for session - x-marreq-variants: [server, cloud] - x-marreq-handler: projects_session::list_for_session - x-marreq-source: marreq-core/src/api/projects_session.rs - x-marreq-guards: [ProjectsRead] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}: patch: operationId: projects_update - tags: [projects] - summary: projects / update - x-marreq-variants: [server, cloud] - x-marreq-handler: projects::update - x-marreq-source: marreq-core/src/api/projects.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2681,19 +1485,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: projects_delete - tags: [projects] - summary: projects / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: projects::delete - x-marreq-source: marreq-core/src/api/projects.rs - x-marreq-guards: [ProjectAccess] parameters: - in: path name: project_id @@ -2702,19 +1499,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/archive: post: operationId: projects_archive - tags: [projects] - summary: projects / archive - x-marreq-variants: [server, cloud] - x-marreq-handler: projects::archive - x-marreq-source: marreq-core/src/api/projects.rs - x-marreq-guards: [ProjectAccess] parameters: - in: path name: project_id @@ -2723,19 +1513,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/unarchive: post: operationId: projects_unarchive - tags: [projects] - summary: projects / unarchive - x-marreq-variants: [server, cloud] - x-marreq-handler: projects::unarchive - x-marreq-source: marreq-core/src/api/projects.rs - x-marreq-guards: [ProjectAccess] parameters: - in: path name: project_id @@ -2744,19 +1527,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/project-from-path/{slug}: get: operationId: projects_session_project_from_path - tags: [projects_session] - summary: projects session / project from path - x-marreq-variants: [server, cloud] - x-marreq-handler: projects_session::project_from_path - x-marreq-source: marreq-core/src/api/projects_session.rs - x-marreq-guards: [OptionalSessionUser] parameters: - in: path name: slug @@ -2765,19 +1541,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/reports/types: get: operationId: reports_list_types - tags: [reports] - summary: reports / list types - x-marreq-variants: [server, cloud] - x-marreq-handler: reports::list_types - x-marreq-source: marreq-core/src/api/reports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2786,20 +1555,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/report_templates: get: operationId: reports_list_templates - tags: [reports] - summary: reports / list templates - x-marreq-variants: [server, cloud] - x-marreq-handler: reports::list_templates - x-marreq-source: marreq-core/src/api/reports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2808,19 +1570,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: reports_create_template - tags: [reports] - summary: reports / create template - x-marreq-variants: [server, cloud] - x-marreq-handler: reports::create_template - x-marreq-source: marreq-core/src/api/reports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2829,20 +1584,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/report_templates/{template_id}: - get: - operationId: reports_get_template - tags: [reports] - summary: reports / get template - x-marreq-variants: [server, cloud] - x-marreq-handler: reports::get_template - x-marreq-source: marreq-core/src/api/reports.rs - x-marreq-guards: [ProjectAccessOrBearer] + patch: + operationId: reports_update_template parameters: - in: path name: project_id @@ -2855,44 +1603,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - - bearerAuth: [] - patch: - operationId: reports_update_template - tags: [reports] - summary: reports / update template - x-marreq-variants: [server, cloud] - x-marreq-handler: reports::update_template - x-marreq-source: marreq-core/src/api/reports.rs - x-marreq-guards: [ProjectAccessOrBearer] - parameters: - - in: path - name: project_id - required: true - schema: {type: integer} - - in: path - name: template_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: reports_delete_template - tags: [reports] - summary: reports / delete template - x-marreq-variants: [server, cloud] - x-marreq-handler: reports::delete_template - x-marreq-source: marreq-core/src/api/reports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2905,20 +1621,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/reports/{file}: post: operationId: reports_generate - tags: [reports] - summary: reports / generate - x-marreq-variants: [server, cloud] - x-marreq-handler: reports::generate - x-marreq-source: marreq-core/src/api/reports.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -2931,49 +1640,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/requirements/{req_id}/versions/{v1}/diff/{v2}: - get: - operationId: requirement_diff_diff_versions - tags: [requirement_diff] - summary: requirement diff / diff versions - x-marreq-variants: [server, cloud] - x-marreq-handler: requirement_diff::diff_versions - x-marreq-source: marreq-core/src/api/requirement_diff.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: req_id - required: true - schema: {type: integer} - - in: path - name: v1 - required: true - schema: {type: integer} - - in: path - name: v2 - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/requirements/{req_id}/versions/{v1}/diff/{v2}: get: operationId: requirement_diff_diff_versions_by_project - tags: [requirement_diff] - summary: requirement diff / diff versions by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirement_diff::diff_versions_by_project - x-marreq-source: marreq-core/src/api/requirement_diff.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -2994,20 +1667,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/{baseline_id}/requirements/{req_id}/diff/current: get: operationId: requirement_diff_diff_baseline_vs_current - tags: [requirement_diff] - summary: requirement diff / diff baseline vs current - x-marreq-variants: [server, cloud] - x-marreq-handler: requirement_diff::diff_baseline_vs_current - x-marreq-source: marreq-core/src/api/requirement_diff.rs - x-marreq-guards: [ProjectRequirementsAndBaselinesRead] parameters: - in: path name: project_id @@ -3024,20 +1690,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirement-version-links: post: operationId: requirement_version_links_create - tags: [requirement_version_links] - summary: requirement version links / create - x-marreq-variants: [server, cloud] - x-marreq-handler: requirement_version_links::create - x-marreq-source: marreq-core/src/api/requirement_version_links.rs - x-marreq-guards: [ProjectTraceabilityWrite] parameters: - in: path name: project_id @@ -3046,19 +1705,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] get: operationId: requirement_version_links_list - tags: [requirement_version_links] - summary: requirement version links / list - x-marreq-variants: [server, cloud] - x-marreq-handler: requirement_version_links::list - x-marreq-source: marreq-core/src/api/requirement_version_links.rs - x-marreq-guards: [ProjectTraceabilityRead] parameters: - in: path name: project_id @@ -3067,20 +1719,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirement-version-links/{link_id}: delete: operationId: requirement_version_links_delete - tags: [requirement_version_links] - summary: requirement version links / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: requirement_version_links::delete - x-marreq-source: marreq-core/src/api/requirement_version_links.rs - x-marreq-guards: [ProjectTraceabilityWrite] parameters: - in: path name: project_id @@ -3093,20 +1738,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirement-version-links/link-types: get: operationId: requirement_version_links_link_types - tags: [requirement_version_links] - summary: requirement version links / link types - x-marreq-variants: [server, cloud] - x-marreq-handler: requirement_version_links::link_types - x-marreq-source: marreq-core/src/api/requirement_version_links.rs - x-marreq-guards: [ProjectTraceabilityRead] parameters: - in: path name: project_id @@ -3115,53 +1753,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/requirements: - get: - operationId: requirements_list - tags: [requirements] - summary: requirements / list - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::list - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - post: - operationId: requirements_create - tags: [requirements] - summary: requirements / create - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::create - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/requirements: get: operationId: requirements_list_by_project - tags: [requirements] - summary: requirements / list by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::list_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -3170,19 +1768,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: requirements_create_by_project - tags: [requirements] - summary: requirements / create by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::create_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsWrite] parameters: - in: path name: project_id @@ -3191,60 +1782,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/requirements/{id}: - get: - operationId: requirements_get - tags: [requirements] - summary: requirements / get - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::get - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] delete: operationId: requirements_delete - tags: [requirements] - summary: requirements / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::delete - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - patch: - operationId: requirements_patch_requirement - tags: [requirements] - summary: requirements / patch requirement - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::patch_requirement - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -3253,19 +1797,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/requirements/{id}: get: operationId: requirements_get_by_project - tags: [requirements] - summary: requirements / get by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::get_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -3278,19 +1815,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] patch: operationId: requirements_patch_by_project - tags: [requirements] - summary: requirements / patch by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::patch_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsWrite] parameters: - in: path name: project_id @@ -3303,41 +1833,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/requirements/{id}/versions: - get: - operationId: requirements_list_versions - tags: [requirements] - summary: requirements / list versions - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::list_versions - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/requirements/{id}/versions: get: operationId: requirements_list_versions_by_project - tags: [requirements] - summary: requirements / list versions by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::list_versions_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -3350,45 +1852,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/requirements/{req_id}/versions/{version_id}: - get: - operationId: requirements_get_version - tags: [requirements] - summary: requirements / get version - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::get_version - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: req_id - required: true - schema: {type: integer} - - in: path - name: version_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/requirements/{req_id}/versions/{version_id}: get: operationId: requirements_get_version_by_project - tags: [requirements] - summary: requirements / get version by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::get_version_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -3405,41 +1875,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/requirements/{id}/impacted_tests: - get: - operationId: requirements_get_impacted_tests - tags: [requirements] - summary: requirements / get impacted tests - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::get_impacted_tests - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/requirements/{id}/impacted_tests: get: operationId: requirements_get_impacted_tests_by_project - tags: [requirements] - summary: requirements / get impacted tests by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::get_impacted_tests_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsAndVerificationsRead] parameters: - in: path name: project_id @@ -3452,45 +1894,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/requirements/{req_id}/versions/{version_id}/approval: - put: - operationId: requirements_set_version_approval - tags: [requirements] - summary: requirements / set version approval - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::set_version_approval - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: req_id - required: true - schema: {type: integer} - - in: path - name: version_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/requirements/{req_id}/versions/{version_id}/approval: put: operationId: requirements_set_version_approval_by_project - tags: [requirements] - summary: requirements / set version approval by project - x-marreq-variants: [server, cloud] - x-marreq-handler: requirements::set_version_approval_by_project - x-marreq-source: marreq-core/src/api/requirements.rs - x-marreq-guards: [ProjectRequirementsApprove] parameters: - in: path name: project_id @@ -3507,20 +1917,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/saved_views: get: operationId: saved_views_list - tags: [saved_views] - summary: saved views / list - x-marreq-variants: [server, cloud] - x-marreq-handler: saved_views::list - x-marreq-source: marreq-core/src/api/saved_views.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -3529,19 +1932,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: saved_views_create - tags: [saved_views] - summary: saved views / create - x-marreq-variants: [server, cloud] - x-marreq-handler: saved_views::create - x-marreq-source: marreq-core/src/api/saved_views.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -3550,20 +1946,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/saved_views/{view_id}: get: operationId: saved_views_get - tags: [saved_views] - summary: saved views / get - x-marreq-variants: [server, cloud] - x-marreq-handler: saved_views::get - x-marreq-source: marreq-core/src/api/saved_views.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -3576,19 +1965,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] patch: operationId: saved_views_update - tags: [saved_views] - summary: saved views / update - x-marreq-variants: [server, cloud] - x-marreq-handler: saved_views::update - x-marreq-source: marreq-core/src/api/saved_views.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -3601,19 +1983,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: saved_views_delete - tags: [saved_views] - summary: saved views / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: saved_views::delete - x-marreq-source: marreq-core/src/api/saved_views.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -3626,20 +2001,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirements/semantic_search: get: operationId: semantic_search_semantic_search - tags: [semantic_search] - summary: semantic search / semantic search - x-marreq-variants: [server, cloud] - x-marreq-handler: semantic_search::semantic_search - x-marreq-source: marreq-core/src/api/semantic_search.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -3648,20 +2016,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirements/ask: post: operationId: semantic_search_ask - tags: [semantic_search] - summary: semantic search / ask - x-marreq-variants: [server, cloud] - x-marreq-handler: semantic_search::ask - x-marreq-source: marreq-core/src/api/semantic_search.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -3670,62 +2031,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] - /api/projects/{project_id}/requirements/reindex: - post: - operationId: semantic_search_reindex - tags: [semantic_search] - summary: semantic search / reindex - x-marreq-variants: [server, cloud] - x-marreq-handler: semantic_search::reindex - x-marreq-source: marreq-core/src/api/semantic_search.rs - x-marreq-guards: [AdminOnly, ProjectAccess] - parameters: - - in: path - name: project_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/projects/{project_id}/requirements/index_status: get: operationId: semantic_search_index_status - tags: [semantic_search] - summary: semantic search / index status - x-marreq-variants: [server, cloud] - x-marreq-handler: semantic_search::index_status - x-marreq-source: marreq-core/src/api/semantic_search.rs - x-marreq-guards: [ProjectAccess] - parameters: - - in: path - name: project_id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - /api/projects/{project_id}/requirements/semantic_search/status: - get: - operationId: semantic_search_search_status - tags: [semantic_search] - summary: semantic search / search status - x-marreq-variants: [server, cloud] - x-marreq-handler: semantic_search::search_status - x-marreq-source: marreq-core/src/api/semantic_search.rs - x-marreq-guards: [ProjectAccess] parameters: - in: path name: project_id @@ -3734,72 +2046,31 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/status: get: operationId: status_list_requirement_statuses - tags: [status] - summary: status / list requirement statuses - x-marreq-variants: [server, cloud] - x-marreq-handler: status::list_requirement_statuses - x-marreq-source: marreq-core/src/api/status.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] post: operationId: status_create_requirement_status - tags: [status] - summary: status / create requirement status - x-marreq-variants: [server, cloud] - x-marreq-handler: status::create_requirement_status - x-marreq-source: marreq-core/src/api/status.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/status/{id}: - get: - operationId: status_get_requirement_status - tags: [status] - summary: status / get requirement status - x-marreq-variants: [server, cloud] - x-marreq-handler: status::get_requirement_status - x-marreq-source: marreq-core/src/api/status.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] put: operationId: status_update_requirement_status - tags: [status] - summary: status / update requirement status - x-marreq-variants: [server, cloud] - x-marreq-handler: status::update_requirement_status - x-marreq-source: marreq-core/src/api/status.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -3808,18 +2079,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] delete: operationId: status_delete_requirement_status - tags: [status] - summary: status / delete requirement status - x-marreq-variants: [server, cloud] - x-marreq-handler: status::delete_requirement_status - x-marreq-source: marreq-core/src/api/status.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -3828,19 +2092,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/requirements/{id}/trace_up: get: operationId: traceability_trace_up - tags: [traceability] - summary: traceability / trace up - x-marreq-variants: [server, cloud] - x-marreq-handler: traceability::trace_up - x-marreq-source: marreq-core/src/api/traceability.rs - x-marreq-guards: [ProjectRequirementsAndTraceabilityRead] parameters: - in: path name: project_id @@ -3853,20 +2110,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirements/{id}/trace_down: get: operationId: traceability_trace_down - tags: [traceability] - summary: traceability / trace down - x-marreq-variants: [server, cloud] - x-marreq-handler: traceability::trace_down - x-marreq-source: marreq-core/src/api/traceability.rs - x-marreq-guards: [ProjectRequirementsVerificationsAndTraceabilityRead] parameters: - in: path name: project_id @@ -3879,20 +2129,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/coverage_report: get: operationId: traceability_coverage_report - tags: [traceability] - summary: traceability / coverage report - x-marreq-variants: [server, cloud] - x-marreq-handler: traceability::coverage_report - x-marreq-source: marreq-core/src/api/traceability.rs - x-marreq-guards: [ProjectTraceabilityRead] parameters: - in: path name: project_id @@ -3901,37 +2144,23 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/traceability/clear_suspect: post: operationId: traceability_clear_suspect - tags: [traceability] - summary: traceability / clear suspect - x-marreq-variants: [server, cloud] - x-marreq-handler: traceability::clear_suspect - x-marreq-source: marreq-core/src/api/traceability.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/traceability/clear_suspect: post: operationId: traceability_clear_suspect_by_project - tags: [traceability] - summary: traceability / clear suspect by project - x-marreq-variants: [server, cloud] - x-marreq-handler: traceability::clear_suspect_by_project - x-marreq-source: marreq-core/src/api/traceability.rs - x-marreq-guards: [ProjectTraceabilityWrite] parameters: - in: path name: project_id @@ -3940,20 +2169,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/use-cases: get: operationId: use_cases_list - tags: [use_cases] - summary: use cases / list - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::list - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -4042,12 +2264,6 @@ paths: semantics. post: operationId: use_cases_create - tags: [use_cases] - summary: use cases / create - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::create - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] parameters: - in: path name: project_id @@ -4081,8 +2297,7 @@ paths: - cookieAuth: [] - bearerAuth: [] x-marreq-bearer-scopes: ['use_cases:write'] - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf requestBody: required: true content: @@ -4104,12 +2319,6 @@ paths: /api/projects/{project_id}/use-cases/next-code: get: operationId: use_cases_next_code - tags: [use_cases] - summary: use cases / next code - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::next_code - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -4141,12 +2350,6 @@ paths: /api/projects/{project_id}/use-cases/{id}: get: operationId: use_cases_get - tags: [use_cases] - summary: use cases / get - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::get - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -4188,12 +2391,6 @@ paths: semantics. delete: operationId: use_cases_delete - tags: [use_cases] - summary: use cases / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::delete - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] security: - cookieAuth: [] - bearerAuth: [] @@ -4223,21 +2420,14 @@ paths: '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} '500': {$ref: '#/components/responses/ApiError'} - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf x-marreq-bearer-scopes: ['use_cases:write'] description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict semantics. /api/projects/{project_id}/use-cases/{id}/draft: - get: - operationId: use_cases_get_draft - tags: [use_cases] - summary: use cases / get draft - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::get_draft - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] + put: + operationId: use_cases_put_draft security: - cookieAuth: [] - bearerAuth: [] @@ -4250,12 +2440,17 @@ paths: name: id required: true schema: {type: integer} - responses: + - in: header + name: X-CSRF-Token + required: false + schema: {type: string} + description: Required for session mutations; Bearer requests use scoped authentication instead. + responses: '200': description: Success content: application/json: - schema: {$ref: '#/components/schemas/use_cases_DraftView_response'} + schema: {$ref: '#/components/schemas/use_cases_DraftChangeView_response'} '400': {$ref: '#/components/responses/ApiError'} '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} @@ -4266,18 +2461,18 @@ paths: '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} '500': {$ref: '#/components/responses/ApiError'} - x-marreq-bearer-scopes: ['use_cases:read'] + x-marreq-csrf: session-csrf + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_PutDraftRequest_request'} + x-marreq-bearer-scopes: ['use_cases:write'] description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict semantics. - put: - operationId: use_cases_put_draft - tags: [use_cases] - summary: use cases / put draft - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::put_draft - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] + delete: + operationId: use_cases_discard_draft security: - cookieAuth: [] - bearerAuth: [] @@ -4296,11 +2491,7 @@ paths: schema: {type: string} description: Required for session mutations; Bearer requests use scoped authentication instead. responses: - '200': - description: Success - content: - application/json: - schema: {$ref: '#/components/schemas/use_cases_DraftChangeView_response'} + '204': {description: Deleted} '400': {$ref: '#/components/responses/ApiError'} '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} @@ -4311,25 +2502,13 @@ paths: '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} '500': {$ref: '#/components/responses/ApiError'} - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/use_cases_PutDraftRequest_request'} + x-marreq-csrf: session-csrf x-marreq-bearer-scopes: ['use_cases:write'] description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict semantics. - delete: - operationId: use_cases_discard_draft - tags: [use_cases] - summary: use cases / discard draft - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::discard_draft - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] + get: + operationId: use_cases_get_draft security: - cookieAuth: [] - bearerAuth: [] @@ -4342,13 +2521,12 @@ paths: name: id required: true schema: {type: integer} - - in: header - name: X-CSRF-Token - required: false - schema: {type: string} - description: Required for session mutations; Bearer requests use scoped authentication instead. responses: - '204': {description: Deleted} + '200': + description: Success + content: + application/json: + schema: {$ref: '#/components/schemas/use_cases_DraftView_response'} '400': {$ref: '#/components/responses/ApiError'} '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} @@ -4359,21 +2537,13 @@ paths: '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} '500': {$ref: '#/components/responses/ApiError'} - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback - x-marreq-bearer-scopes: ['use_cases:write'] + x-marreq-bearer-scopes: ['use_cases:read'] description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict semantics. /api/projects/{project_id}/use-cases/{id}/draft/operations: post: operationId: use_cases_draft_operations - tags: [use_cases] - summary: use cases / draft operations - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::draft_operations - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] parameters: - in: path name: project_id @@ -4418,17 +2588,10 @@ paths: items: {$ref: '#/components/schemas/UseCaseOperation'} additionalProperties: true required: [operations] - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf /api/projects/{project_id}/use-cases/{id}/draft/publish: post: operationId: use_cases_publish - tags: [use_cases] - summary: use cases / publish - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::publish - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] parameters: - in: path name: project_id @@ -4477,17 +2640,10 @@ paths: - {type: 'null'} additionalProperties: true required: [base_revision] - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf /api/projects/{project_id}/use-cases/{id}/versions: get: operationId: use_cases_list_versions - tags: [use_cases] - summary: use cases / list versions - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::list_versions - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -4522,60 +2678,9 @@ paths: description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict semantics. - /api/projects/{project_id}/use-cases/{id}/versions/{version_id}: - get: - operationId: use_cases_get_version - tags: [use_cases] - summary: use cases / get version - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::get_version - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] - security: - - cookieAuth: [] - - bearerAuth: [] - parameters: - - in: path - name: project_id - required: true - schema: {type: integer} - - in: path - name: id - required: true - schema: {type: integer} - - in: path - name: version_id - required: true - schema: {type: integer} - responses: - '200': - description: Success - content: - application/json: - schema: {$ref: '#/components/schemas/use_cases_VersionView_response'} - '400': {$ref: '#/components/responses/ApiError'} - '401': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty - or framework generated; do not require an ApiError body.'} - '403': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty - or framework generated; do not require an ApiError body.'} - '404': {$ref: '#/components/responses/ApiError'} - '409': {$ref: '#/components/responses/ApiError'} - '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty - or framework generated; do not require an ApiError body.'} - '500': {$ref: '#/components/responses/ApiError'} - x-marreq-bearer-scopes: ['use_cases:read'] - description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review - permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict - semantics. /api/projects/{project_id}/use-cases/{id}/versions/{version_id}/approval: put: operationId: use_cases_set_version_approval - tags: [use_cases] - summary: use cases / set version approval - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::set_version_approval - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesApprove] security: - cookieAuth: [] - bearerAuth: [] @@ -4613,8 +2718,7 @@ paths: '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} '500': {$ref: '#/components/responses/ApiError'} - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf requestBody: required: true content: @@ -4627,12 +2731,6 @@ paths: /api/projects/{project_id}/use-cases/{id}/links: get: operationId: use_cases_list_links - tags: [use_cases] - summary: use cases / list links - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::list_links - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -4669,12 +2767,6 @@ paths: semantics. post: operationId: use_cases_create_link - tags: [use_cases] - summary: use cases / create link - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::create_link - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] parameters: - in: path name: project_id @@ -4727,17 +2819,10 @@ paths: - *id002 - {type: 'null'} additionalProperties: true - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf /api/projects/{project_id}/use-cases/{id}/links/{link_id}: delete: operationId: use_cases_delete_link - tags: [use_cases] - summary: use cases / delete link - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::delete_link - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] security: - cookieAuth: [] - bearerAuth: [] @@ -4771,8 +2856,7 @@ paths: '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} '500': {$ref: '#/components/responses/ApiError'} - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf x-marreq-bearer-scopes: ['use_cases:write'] description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict @@ -4780,12 +2864,6 @@ paths: /api/projects/{project_id}/use-cases/{id}/links/{link_id}/clear_suspect: post: operationId: use_cases_clear_link_suspect - tags: [use_cases] - summary: use cases / clear link suspect - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::clear_link_suspect - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] security: - cookieAuth: [] - bearerAuth: [] @@ -4823,8 +2901,7 @@ paths: '422': {description: 'Authentication, authorization, CSRF or Rocket validation failure. Body may be empty or framework generated; do not require an ApiError body.'} '500': {$ref: '#/components/responses/ApiError'} - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf x-marreq-bearer-scopes: ['use_cases:write'] description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict @@ -4832,12 +2909,6 @@ paths: /api/projects/{project_id}/requirements/{requirement_id}/use-cases: get: operationId: use_cases_requirement_use_cases - tags: [use_cases] - summary: use cases / requirement use cases - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::requirement_use_cases - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -4875,12 +2946,6 @@ paths: /api/projects/{project_id}/verifications/{verification_id}/use-cases: get: operationId: use_cases_verification_use_cases - tags: [use_cases] - summary: use cases / verification use cases - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::verification_use_cases - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -4918,12 +2983,6 @@ paths: /api/projects/{project_id}/use-cases/{id}/draft/diff: get: operationId: use_cases_draft_diff - tags: [use_cases] - summary: use cases / draft diff - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::draft_diff - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] parameters: - in: path name: project_id @@ -4959,12 +3018,6 @@ paths: /api/projects/{project_id}/use-cases/{id}/versions/{a}/diff/{b}: get: operationId: use_cases_version_diff - tags: [use_cases] - summary: use cases / version diff - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::version_diff - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] parameters: - in: path name: project_id @@ -5008,12 +3061,6 @@ paths: /api/projects/{project_id}/use-cases/{id}/comments: get: operationId: use_cases_list_comments - tags: [use_cases] - summary: use cases / list comments - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::list_comments - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -5064,12 +3111,6 @@ paths: semantics. post: operationId: use_cases_create_comment - tags: [use_cases] - summary: use cases / create comment - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::create_comment - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesWrite] parameters: - in: path name: project_id @@ -5122,17 +3163,10 @@ paths: - {type: 'null'} additionalProperties: true required: [body] - x-marreq-csrf: session mutations require matching private csrf cookie and X-CSRF-Token; login/logout allow - trusted Origin/Referer fallback + x-marreq-csrf: session-csrf /api/projects/{project_id}/baselines/{baseline_id}/use-cases: get: operationId: use_cases_baseline_use_cases - tags: [use_cases] - summary: use cases / baseline use cases - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::baseline_use_cases - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] security: - cookieAuth: [] - bearerAuth: [] @@ -5167,113 +3201,9 @@ paths: description: Session or scoped Bearer authentication; project membership and ViewRequirements/EditRequirements/review permissions apply. Archived projects reject mutations. See use-cases-api.md for validation and revision-conflict semantics. - /api/projects/{project_id}/baselines/{baseline_id}/use-cases/{id}/diff/current: - get: - operationId: use_cases_baseline_use_case_diff - tags: [use_cases] - summary: use cases / baseline use case diff - x-marreq-variants: [server, cloud] - x-marreq-handler: use_cases::baseline_use_case_diff - x-marreq-source: marreq-core/src/api/use_cases.rs - x-marreq-guards: [ProjectUseCasesRead] - parameters: - - in: path - name: project_id - required: true - schema: {type: integer} - - in: path - name: baseline_id - required: true - schema: {type: integer} - - in: path - name: id - required: true - schema: {type: integer} - - in: query - name: include_unchanged - required: false - schema: {type: boolean} - responses: - '200': - description: Success - content: - application/json: - schema: {$ref: '#/components/schemas/UseCaseDiff'} - '400': {$ref: '#/components/responses/ApiError'} - '401': {description: Guard/fairing/validation failure; body may be empty or framework generated} - '403': {description: Guard/fairing/validation failure; body may be empty or framework generated} - '404': {$ref: '#/components/responses/ApiError'} - '409': {$ref: '#/components/responses/ApiError'} - '422': {description: Guard/fairing/validation failure; body may be empty or framework generated} - '500': {$ref: '#/components/responses/ApiError'} - description: Use-case service contract; see use-cases-api.md for revision conflicts, document validation, - idempotency, publishing, links and review permissions. - security: - - cookieAuth: [] - - bearerAuth: [] - x-marreq-bearer-scopes: ['use_cases:read'] - /api/users: - get: - operationId: users_list - tags: [users] - summary: users / list - x-marreq-variants: [server, cloud] - x-marreq-handler: users::list - x-marreq-source: marreq-core/src/api/users.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] - post: - operationId: users_create - tags: [users] - summary: users / create - x-marreq-variants: [server, cloud] - x-marreq-handler: users::create - x-marreq-source: marreq-core/src/api/users.rs - x-marreq-guards: [AdminOnly] - parameters: [] - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] /api/users/{id}: - get: - operationId: users_get - tags: [users] - summary: users / get - x-marreq-variants: [server, cloud] - x-marreq-handler: users::get - x-marreq-source: marreq-core/src/api/users.rs - x-marreq-guards: [AdminOnly] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] put: operationId: users_update - tags: [users] - summary: users / update - x-marreq-variants: [server, cloud] - x-marreq-handler: users::update - x-marreq-source: marreq-core/src/api/users.rs - x-marreq-guards: [AdminOnly] parameters: - in: path name: id @@ -5282,18 +3212,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] delete: operationId: users_delete - tags: [users] - summary: users / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: users::delete - x-marreq-source: marreq-core/src/api/users.rs - x-marreq-guards: [AdminOnly] parameters: - in: path name: id @@ -5302,19 +3225,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/users/{id}/password: put: operationId: users_set_password - tags: [users] - summary: users / set password - x-marreq-variants: [server, cloud] - x-marreq-handler: users::set_password - x-marreq-source: marreq-core/src/api/users.rs - x-marreq-guards: [AdminOnly] parameters: - in: path name: id @@ -5323,19 +3239,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/projects/{project_id}/verifications/{verification_id}/control: get: operationId: verification_control_get_verification_control - tags: [verification_control] - summary: verification control / get verification control - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_control::get_verification_control - x-marreq-source: marreq-core/src/api/verification_control.rs - x-marreq-guards: [ProjectVerificationsRead] parameters: - in: path name: project_id @@ -5348,19 +3257,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] put: operationId: verification_control_put_verification_control - tags: [verification_control] - summary: verification control / put verification control - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_control::put_verification_control - x-marreq-source: marreq-core/src/api/verification_control.rs - x-marreq-guards: [ProjectVerificationsWrite] parameters: - in: path name: project_id @@ -5373,20 +3275,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirements/{requirement_id}/close_out: get: operationId: verification_control_get_requirement_close_out - tags: [verification_control] - summary: verification control / get requirement close out - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_control::get_requirement_close_out - x-marreq-source: marreq-core/src/api/verification_control.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -5399,20 +3294,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/close_out: get: operationId: verification_control_get_project_close_out - tags: [verification_control] - summary: verification control / get project close out - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_control::get_project_close_out - x-marreq-source: marreq-core/src/api/verification_control.rs - x-marreq-guards: [ProjectRequirementsRead] parameters: - in: path name: project_id @@ -5421,20 +3309,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/requirements/{requirement_id}/compliance: put: operationId: verification_control_put_requirement_compliance - tags: [verification_control] - summary: verification control / put requirement compliance - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_control::put_requirement_compliance - x-marreq-source: marreq-core/src/api/verification_control.rs - x-marreq-guards: [ProjectRequirementsWrite] parameters: - in: path name: project_id @@ -5447,20 +3328,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/verifications/{id}/snapshots: get: operationId: verification_diff_list_snapshots_by_project - tags: [verification_diff] - summary: verification diff / list snapshots by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_diff::list_snapshots_by_project - x-marreq-source: marreq-core/src/api/verification_diff.rs - x-marreq-guards: [ProjectVerificationsRead] parameters: - in: path name: project_id @@ -5473,20 +3347,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/verifications/{id}/snapshots/{v1}/diff/{v2}: get: operationId: verification_diff_diff_snapshots_by_project - tags: [verification_diff] - summary: verification diff / diff snapshots by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_diff::diff_snapshots_by_project - x-marreq-source: marreq-core/src/api/verification_diff.rs - x-marreq-guards: [ProjectVerificationsRead] parameters: - in: path name: project_id @@ -5507,20 +3374,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/baselines/{baseline_id}/verifications/{id}/diff/current: get: operationId: verification_diff_diff_baseline_vs_current - tags: [verification_diff] - summary: verification diff / diff baseline vs current - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_diff::diff_baseline_vs_current - x-marreq-source: marreq-core/src/api/verification_diff.rs - x-marreq-guards: [ProjectVerificationsAndBaselinesRead] parameters: - in: path name: project_id @@ -5537,20 +3397,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/verification-methods: get: operationId: verification_methods_list_by_project - tags: [verification_methods] - summary: verification methods / list by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_methods::list_by_project - x-marreq-source: marreq-core/src/api/verification_methods.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -5559,19 +3412,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: verification_methods_create_by_project - tags: [verification_methods] - summary: verification methods / create by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_methods::create_by_project - x-marreq-source: marreq-core/src/api/verification_methods.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -5588,12 +3434,6 @@ paths: /api/projects/{project_id}/verification-methods/{method_id}: put: operationId: verification_methods_update_by_project - tags: [verification_methods] - summary: verification methods / update by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_methods::update_by_project - x-marreq-source: marreq-core/src/api/verification_methods.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -5606,19 +3446,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] delete: operationId: verification_methods_delete_by_project - tags: [verification_methods] - summary: verification methods / delete by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_methods::delete_by_project - x-marreq-source: marreq-core/src/api/verification_methods.rs - x-marreq-guards: [ProjectAccessOrBearer] parameters: - in: path name: project_id @@ -5631,73 +3464,32 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/verification-status: get: operationId: verification_status_list_verification_statuses - tags: [verification_status] - summary: verification status / list verification statuses - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_status::list_verification_statuses - x-marreq-source: marreq-core/src/api/verification_status.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] post: operationId: verification_status_create_verification_status - tags: [verification_status] - summary: verification status / create verification status - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_status::create_verification_status - x-marreq-source: marreq-core/src/api/verification_status.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/verification-status/{id}: - get: - operationId: verification_status_get_verification_status - tags: [verification_status] - summary: verification status / get verification status - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_status::get_verification_status - x-marreq-source: marreq-core/src/api/verification_status.rs - x-marreq-guards: [ApiUser] - parameters: - - in: path - name: id - required: true - schema: {type: integer} - responses: - default: {description: Wire behavior has not yet been verified.} - x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. - security: - - cookieAuth: [] put: operationId: verification_status_update_verification_status - tags: [verification_status] - summary: verification status / update verification status - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_status::update_verification_status - x-marreq-source: marreq-core/src/api/verification_status.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -5706,18 +3498,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] delete: operationId: verification_status_delete_verification_status - tags: [verification_status] - summary: verification status / delete verification status - x-marreq-variants: [server, cloud] - x-marreq-handler: verification_status::delete_verification_status - x-marreq-source: marreq-core/src/api/verification_status.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -5726,35 +3511,21 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] /api/verifications: get: operationId: verifications_list - tags: [verifications] - summary: verifications / list - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::list - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] post: operationId: verifications_create - tags: [verifications] - summary: verifications / create - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::create - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ApiUser] parameters: [] responses: default: {description: Wire behavior has not yet been verified.} @@ -5766,12 +3537,6 @@ paths: /api/projects/{project_id}/verifications: get: operationId: verifications_list_by_project - tags: [verifications] - summary: verifications / list by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::list_by_project - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ProjectVerificationsRead] parameters: - in: path name: project_id @@ -5780,19 +3545,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] post: operationId: verifications_create_by_project - tags: [verifications] - summary: verifications / create by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::create_by_project - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ProjectVerificationsWrite] parameters: - in: path name: project_id @@ -5801,20 +3559,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/projects/{project_id}/verifications/{id}: get: operationId: verifications_get_by_project - tags: [verifications] - summary: verifications / get by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::get_by_project - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ProjectVerificationsRead] parameters: - in: path name: project_id @@ -5827,19 +3578,12 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] put: operationId: verifications_update_by_project - tags: [verifications] - summary: verifications / update by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::update_by_project - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ProjectVerificationsWrite] parameters: - in: path name: project_id @@ -5852,20 +3596,13 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - bearerAuth: [] /api/verifications/{id}: get: operationId: verifications_get - tags: [verifications] - summary: verifications / get - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::get - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -5874,18 +3611,11 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] delete: operationId: verifications_delete - tags: [verifications] - summary: verifications / delete - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::delete - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ApiUser] parameters: - in: path name: id @@ -5894,20 +3624,17 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - /api/verifications/{id}/field: + /api/projects/{project_id}/verifications/{id}/field: post: - operationId: verifications_update_field - tags: [verifications] - summary: verifications / update field - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::update_field - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ApiUser] + operationId: verifications_update_field_by_project parameters: + - in: path + name: project_id + required: true + schema: {type: integer} - in: path name: id required: true @@ -5915,26 +3642,32 @@ paths: responses: default: {description: Wire behavior has not yet been verified.} x-marreq-unverified: true - description: Route inventory only. Request/response serialization, status codes and authorization require - audit before compatibility can be established. + description: Wire contract pending audit; required consumers fail verification. security: - cookieAuth: [] - /api/projects/{project_id}/verifications/{id}/field: - post: - operationId: verifications_update_field_by_project - tags: [verifications] - summary: verifications / update field by project - x-marreq-variants: [server, cloud] - x-marreq-handler: verifications::update_field_by_project - x-marreq-source: marreq-core/src/api/verifications.rs - x-marreq-guards: [ProjectVerificationsWrite] + - bearerAuth: [] + /api/auth/csrf: + get: + operationId: auth_auth_csrf + security: [] + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + csrf_token: {type: string} + required: [csrf_token] + additionalProperties: true + /api/auth/identities/{identity_id}: + delete: + operationId: auth_auth_identity_delete parameters: - in: path - name: project_id - required: true - schema: {type: integer} - - in: path - name: id + name: identity_id required: true schema: {type: integer} responses: @@ -5944,16 +3677,10 @@ paths: audit before compatibility can be established. security: - cookieAuth: [] - - bearerAuth: [] /api/auth/register: post: operationId: auth_public_auth_register - tags: [auth_public] - summary: auth public / auth register x-marreq-variants: [cloud] - x-marreq-handler: auth_public::auth_register - x-marreq-source: marreq-cloud/src/api/auth_public.rs - x-marreq-guards: [] parameters: [] responses: '201': @@ -5988,26 +3715,111 @@ paths: schema: type: object properties: - username: *id001 - name: *id001 - email: *id001 - password: *id001 + username: &id005 {type: string} + name: *id005 + email: *id005 + password: *id005 required: [username, name, email, password] additionalProperties: true + /api/auth/login: + post: + operationId: auth_auth_login + parameters: [] + responses: + '200': + description: Authenticated session established + content: + application/json: + schema: + type: object + properties: + status: {type: string, const: ok} + user: {$ref: '#/components/schemas/entities_User_response'} + required: [status, user] + '400': {$ref: '#/components/responses/ApiError'} + '403': {description: CSRF failure; body may be framework generated} + '410': {$ref: '#/components/responses/ApiError'} + '422': {description: Rocket JSON validation failure} + '500': {$ref: '#/components/responses/ApiError'} + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + x-marreq-csrf: auth-origin-fallback + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [username, password] + properties: + username: {type: string} + password: {type: string} + /api/users: + post: + operationId: users_create + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + get: + operationId: users_list + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/projects/{project_id}/attachments/{attachment_id}: + delete: + operationId: attachments_delete + parameters: + - in: path + name: project_id + required: true + schema: {type: integer} + - in: path + name: attachment_id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + - bearerAuth: [] + /api/auth/external/{provider}/link: + post: + operationId: auth_auth_external_link_start + parameters: + - in: path + name: provider + required: true + schema: {type: string} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] /api/auth/verify-email: get: operationId: auth_public_auth_verify_email - tags: [auth_public] - summary: auth public / auth verify email x-marreq-variants: [cloud] - x-marreq-handler: auth_public::auth_verify_email - x-marreq-source: marreq-cloud/src/api/auth_public.rs - x-marreq-guards: [] parameters: - in: query name: token required: true - schema: *id001 + schema: *id005 responses: '200': description: Success @@ -6024,15 +3836,36 @@ paths: description: Cloud-only public account flow. Registration returns 201 for creation or 200 for an existing email, preventing enumeration. Forgot-password always reports success. CSRF fairing rules still apply. security: [] + /api/auth/logout: + post: + operationId: auth_auth_logout + parameters: [] + responses: + '200': + description: Session and CSRF cookies cleared + content: + application/json: + schema: + type: object + properties: + status: {type: string, const: ok} + required: [status] + '403': {description: CSRF failure; body may be framework generated} + '500': {$ref: '#/components/responses/ApiError'} + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: [] + x-marreq-csrf: auth-origin-fallback + requestBody: + required: false + description: Optional ignored body; the SPA sends an empty JSON object. + content: + application/json: + schema: {} /api/auth/forgot-password: post: operationId: auth_public_auth_forgot_password - tags: [auth_public] - summary: auth public / auth forgot password x-marreq-variants: [cloud] - x-marreq-handler: auth_public::auth_forgot_password - x-marreq-source: marreq-cloud/src/api/auth_public.rs - x-marreq-guards: [] parameters: [] responses: '200': @@ -6057,18 +3890,72 @@ paths: schema: type: object properties: - email: *id001 + email: *id005 required: [email] additionalProperties: true + /api/notifications/unread-count: + get: + operationId: notifications_unread_count + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/auth/change-password: + post: + operationId: auth_auth_change_password + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/oauth/grants: + get: + operationId: oauth_grants + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/oauth/grants/{id}: + delete: + operationId: oauth_revoke_grant + parameters: + - in: path + name: id + required: true + schema: {type: integer} + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] + /api/dashboard: + get: + operationId: dashboard_dashboard_json + parameters: [] + responses: + default: {description: Wire behavior has not yet been verified.} + x-marreq-unverified: true + description: Route inventory only. Request/response serialization, status codes and authorization require + audit before compatibility can be established. + security: + - cookieAuth: [] /api/auth/reset-password: post: operationId: auth_public_auth_reset_password - tags: [auth_public] - summary: auth public / auth reset password x-marreq-variants: [cloud] - x-marreq-handler: auth_public::auth_reset_password - x-marreq-source: marreq-cloud/src/api/auth_public.rs - x-marreq-guards: [] parameters: [] responses: '200': @@ -6093,8 +3980,8 @@ paths: schema: type: object properties: - token: *id001 - new_password: *id001 + token: *id005 + new_password: *id005 required: [token, new_password] additionalProperties: true components: @@ -6115,7 +4002,6 @@ components: items: {$ref: '#/components/schemas/external_PublicAuthProvider_response'} additionalProperties: true required: [password_enabled, external] - x-rust-model: marreq-core/src/auth/external.rs::ProviderDiscovery external_PublicAuthProvider_response: type: object properties: @@ -6123,7 +4009,6 @@ components: display_name: {type: string} additionalProperties: true required: [id, display_name] - x-rust-model: marreq-core/src/auth/external.rs::PublicAuthProvider entities_User_response: type: object properties: @@ -6137,7 +4022,6 @@ components: email_verified: {type: boolean} additionalProperties: true required: [id, username, name, email, creation_date, last_login, is_admin, email_verified] - x-rust-model: marreq-core/src/models/entities.rs::User use_cases_UseCaseRow_response: type: object properties: @@ -6166,14 +4050,12 @@ components: additionalProperties: true required: [id, reference_code, title, primary_actor, step_count, alternative_count, exception_count, approval_state, current_version_number, has_draft, link_count, suspect_count, updated_at] - x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseRow use_cases_NextCode_response: type: object properties: reference_code: {type: string} additionalProperties: true required: [reference_code] - x-rust-model: marreq-core/src/api/use_cases.rs::NextCode use_cases_UseCaseDetail_response: type: object properties: @@ -6197,7 +4079,6 @@ components: - {type: 'null'} additionalProperties: true required: [id, project_id, reference_code, title, created_by, created_at, updated_at, current_version, draft] - x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseDetail use_cases_VersionView_response: type: object properties: @@ -6245,7 +4126,6 @@ components: additionalProperties: true required: [id, version_number, approval_state, title, primary_actor, change_note, created_by, created_by_name, created_at, reviewed_by, reviewed_at, approved_by, approved_at, document, labels] - x-rust-model: marreq-core/src/api/use_cases.rs::VersionView use_cases_VersionSummary_response: type: object properties: @@ -6289,7 +4169,6 @@ components: additionalProperties: true required: [id, version_number, approval_state, title, primary_actor, change_note, created_by, created_by_name, created_at, reviewed_by, reviewed_at, approved_by, approved_at] - x-rust-model: marreq-core/src/api/use_cases.rs::VersionSummary use_cases_DraftSummary_response: type: object properties: @@ -6309,7 +4188,6 @@ components: updated_at: {type: string} additionalProperties: true required: [revision, base_version_id, updated_by, updated_by_name, updated_at] - x-rust-model: marreq-core/src/api/use_cases.rs::DraftSummary use_cases_DraftView_response: type: object properties: @@ -6334,7 +4212,6 @@ components: additionalProperties: {type: string} additionalProperties: true required: [use_case_id, revision, base_version_id, updated_by, updated_by_name, updated_at, document, labels] - x-rust-model: marreq-core/src/api/use_cases.rs::DraftView use_cases_PutDraftRequest_request: type: object properties: @@ -6345,7 +4222,6 @@ components: document: {$ref: '#/components/schemas/UseCaseDocument'} additionalProperties: true required: [document] - x-rust-model: marreq-core/src/api/use_cases.rs::PutDraftRequest use_cases_DraftChangeView_response: type: object properties: @@ -6374,14 +4250,12 @@ components: additionalProperties: true required: [use_case_id, revision, base_version_id, updated_by, updated_by_name, updated_at, document, labels, effects] - x-rust-model: marreq-core/src/api/use_cases.rs::DraftChangeView use_cases_SetApprovalRequest_request: type: object properties: state: {type: string} additionalProperties: true required: [state] - x-rust-model: marreq-core/src/api/use_cases.rs::SetApprovalRequest use_cases_LinkView_response: type: object properties: @@ -6437,7 +4311,6 @@ components: required: [id, element_id, element_label, target_type, requirement_id, verification_id, target_reference_code, target_title, rationale, suspect, suspect_at, suspect_reason, linked_requirement_version_id, current_requirement_version_id, created_by, created_at] - x-rust-model: marreq-core/src/api/use_cases.rs::LinkView use_cases_ClearSuspectResponse_response: type: object properties: @@ -6445,7 +4318,6 @@ components: cleared: {type: boolean} additionalProperties: true required: [status, cleared] - x-rust-model: marreq-core/src/api/use_cases.rs::ClearSuspectResponse use_cases_UseCaseRef_response: type: object properties: @@ -6464,7 +4336,6 @@ components: suspect: {type: boolean} additionalProperties: true required: [use_case_id, reference_code, title, link_id, element_id, element_label, suspect] - x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseRef use_cases_UseCaseCommentView_response: type: object properties: @@ -6489,7 +4360,6 @@ components: additionalProperties: true required: [id, use_case_id, use_case_version_id, element_id, element_label, author_id, author_name, body, created_at] - x-rust-model: marreq-core/src/api/use_cases.rs::UseCaseCommentView use_cases_BaselineUseCaseView_response: type: object properties: @@ -6515,7 +4385,6 @@ components: additionalProperties: true required: [use_case_id, reference_code, title, primary_actor, version_id, version_number, approval_state, current_version_id, current_version_number, link_count] - x-rust-model: marreq-core/src/api/use_cases.rs::BaselineUseCaseView ApiError: type: object required: [status, error, message] @@ -6527,16 +4396,16 @@ components: BuildInfo: type: object properties: - backend_version: *id005 - backend_git_sha: *id005 + backend_version: &id006 {type: string} + backend_git_sha: *id006 deployment_mode: type: string enum: [server, cloud] frontend_compatibility: type: object properties: - min_version: *id005 - max_version: *id005 + min_version: *id006 + max_version: *id006 required: [min_version, max_version] additionalProperties: true required: [backend_version, backend_git_sha, deployment_mode, frontend_compatibility] @@ -6544,8 +4413,8 @@ components: UseCaseElement: type: object properties: - id: &id006 {type: string} - text: *id006 + id: &id007 {type: string} + text: *id007 additionalProperties: false required: [id, text] UseCaseBranchEnd: @@ -6553,7 +4422,7 @@ components: - type: object properties: type: {const: rejoin, type: string} - step: *id006 + step: *id007 additionalProperties: false required: [type, step] - type: object @@ -6568,17 +4437,17 @@ components: type: object properties: schema: {type: string, const: marreq.use-case.v1} - title: *id006 + title: *id007 primary_actor: anyOf: - - *id006 + - *id007 - {type: 'null'} secondary_actors: type: array - items: *id006 + items: *id007 summary: anyOf: - - *id006 + - *id007 - {type: 'null'} preconditions: type: array @@ -6597,12 +4466,12 @@ components: UseCaseBranch: type: object properties: - id: *id006 + id: *id007 kind: type: string enum: [alternative, exception] - title: *id006 - from_step: *id006 + title: *id007 + from_step: *id007 end: {$ref: '#/components/schemas/UseCaseBranchEnd'} steps: type: array @@ -6614,28 +4483,28 @@ components: - type: object properties: type: {type: string, const: relabelled} - id: *id006 - from: *id006 - to: *id006 + id: *id007 + from: *id007 + to: *id007 additionalProperties: true required: [type, id, from, to] - type: object properties: type: {type: string, const: removed} - id: *id006 + id: *id007 additionalProperties: true required: [type, id] - type: object properties: type: {type: string, const: rebranched} - id: *id006 - from_step: *id006 + id: *id007 + from_step: *id007 additionalProperties: true required: [type, id, from_step] - type: object properties: type: {type: string, const: created} - id: *id006 + id: *id007 additionalProperties: true required: [type, id] DeploymentInfo: @@ -6644,12 +4513,12 @@ components: mode: type: string enum: [server, cloud] - allows_self_registration: &id007 {type: boolean} - requires_email_verification: *id007 - allows_admin_promotion: *id007 - assigns_personal_workspace: *id007 - allows_self_administered_user_creation: *id007 - allows_database_backup: *id007 + allows_self_registration: &id008 {type: boolean} + requires_email_verification: *id008 + allows_admin_promotion: *id008 + assigns_personal_workspace: *id008 + allows_self_administered_user_creation: *id008 + allows_database_backup: *id008 required: [mode, allows_self_registration, requires_email_verification, allows_admin_promotion, assigns_personal_workspace, allows_self_administered_user_creation, allows_database_backup] additionalProperties: true @@ -6669,22 +4538,21 @@ components: - {type: 'null'} additionalProperties: true required: [user_id, authentication_type, principal_id, client_id, grant_id] - x-rust-model: marreq-core/src/api/mcp.rs::McpPrincipalResponse UseCaseDocumentResponse: type: object properties: schema: {type: string, const: marreq.use-case.v1} - title: &id008 {type: string} + title: &id009 {type: string} primary_actor: anyOf: - - *id008 + - *id009 - {type: 'null'} secondary_actors: type: array - items: *id008 + items: *id009 summary: anyOf: - - *id008 + - *id009 - {type: 'null'} preconditions: type: array @@ -6703,12 +4571,12 @@ components: UseCaseBranchResponse: type: object properties: - id: &id009 {type: string} + id: &id010 {type: string} kind: type: string enum: [alternative, exception] - title: *id009 - from_step: *id009 + title: *id010 + from_step: *id010 end: {$ref: '#/components/schemas/UseCaseBranchEnd'} steps: type: array @@ -6954,13 +4822,13 @@ components: additionalProperties: type: object properties: - old: &id010 + old: &id011 oneOf: - *id002 - {type: 'null'} - type: array items: *id002 - new: *id010 + new: *id011 additionalProperties: true required: [old, new] elements: @@ -6984,6 +4852,5 @@ components: content: application/json: schema: {$ref: '#/components/schemas/ApiError'} -x-marreq-contract-complete: false security: - cookieAuth: [] diff --git a/docs/developer/use-cases-api.md b/docs/developer/use-cases-api.md index 6340723c..3920ac80 100644 --- a/docs/developer/use-cases-api.md +++ b/docs/developer/use-cases-api.md @@ -167,13 +167,12 @@ Comments are immutable. `@username` mentions of project members send a `mentione - **Project bundle.** `bundle.json` has an optional `use_cases` section (see the HTTP API contract). - **MCP.** `list_use_cases`, `get_use_case`, `get_use_case_diff` and `list_use_case_links` (`use_cases:read`); `create_use_case`, `edit_use_case_draft`, `publish_use_case_draft` and `link_use_case_element` (`use_cases:write`, `MARREQ_MODE=draft_write`). Approval is not exposed to agents. See [mcp-setup.md](mcp-setup.md). -## Machine-readable contract - -The use-case routes and wire schemas are described in `openapi.yaml`, including -request defaults, response nullability, draft-operation tagged unions, version -publishing, links, comments and diffs. Derived TypeScript models are generated by -`scripts/contracts/generate.py`; do not hand-edit `generated-contract.ts`. -The isolated backend contract smoke test covers representative list/create/draft -operations, delegated scope rejection and project boundaries. Full API contract -coverage and broader cross-version evidence remain tracked in the P0 audit in -[http-api-contract.md](http-api-contract.md#p0-contract-hardening-audit-and-current-coverage-423). +## Executable contract checks + +Use-case request/response definitions are in `openapi.yaml`; consumer declarations +are generated with standard `openapi-typescript` through +`node scripts/generate-api-types.mjs`. Native Rust tests and +`scripts/test-api-contracts.sh` check representative real read/write, session, +Bearer-scope and project-boundary behavior. See the +[contract workflow](http-api-contract.md#consumer-aware-contract-verification-423--424) +for scope limits and the required schema-update procedure. diff --git a/docs/releases.md b/docs/releases.md index 645e74e7..e749832b 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -82,130 +82,75 @@ Images carry OCI label `org.opencontainers.image.version`. When `MARREQ_VERSION` is unset or empty, each artifact falls back to its manifest version. A leading `v` is stripped so the value stays plain `X.Y.Z` for the compatibility check. Local images: `make docker-server MARREQ_VERSION=0.1.3`. -## API compatibility gate and retained artifact contracts (#423 P0) - -The P0 bootstrap is **not release-ready**: 206 HTTP operations still lack audited -wire contracts. `API contracts` CI validates the document, generated artifacts, -source-review evidence, both compiled route inventories, negative historical -fixtures and representative real producer/consumer exchanges. Release publication -also requires `scripts/contracts/check.py --require-complete`, which currently -fails intentionally. This must remain a draft until the coverage gaps described in -[the HTTP contract audit](developer/http-api-contract.md#p0-contract-hardening-audit-and-current-coverage-423) -are closed. - -### Compatibility policy - -Component SemVer describes each artifact independently. Equal versions are never -required. A consumer declares `requires_backend_min` and optionally an inclusive -`requires_backend_max`; frontend and MCP metadata are validated independently. -The backend's existing frontend acceptance range is also checked. Stable -`X.Y.Z` versions are supported; prerelease and wildcard ranges fail closed. -Omitting an upper bound is appropriate only with executable retained-contract -checks; an upper bound is a temporary tested-support ceiling, not proof that -all versions inside it work. Existing runtime `CompatibilityBanner` warnings are -preserved for manually assembled deployments. - -A producer must continue accepting every previously valid consumer request and -returning responses accepted by that released consumer. Additive optional request -fields and additive response fields are compatible when the consumer accepts -unknown properties. Removing an endpoint or guaranteed response property, -changing a field type/nullability, adding a required request field, restricting a -request enum, extending a response enum used by exhaustive consumers, changing -status/media/header behavior or changing authentication/scopes is potentially -breaking. Breaking changes require a new API-compatible deployment combination -and a component SemVer breaking release; while 0.x, treat minor changes with the -same explicit care rather than assuming all 0.x releases interoperate. Unknown -changed schema constraints/unions are rejected conservatively for manual review. - -Previously released consumers retain their own contracts and policy. A newer -backend must satisfy those old contracts, not regenerated contracts from current -`main`. During rolling upgrades, check each backend/consumer combination that -can coexist, including old and new replicas; upgrade consumers first only when -the old backend also passes. No gate establishes database migration safety; -that remains a separate P0 workstream in #423. - -### Export and retain an exact component contract - -After committing audited contract inputs on the component's source commit: +## Exact artifact API compatibility (#423) + +The Rust CLI checks each consumer's own retained contract, never current-main +substitutes. A bundle contains only its selected operations and reachable schemas, +component/version/source SHA and compatibility policy. Backend bundles contain +verified producer operations for one variant; unknown unconsumed routes are not +invented. Consumer export fails on any required unknown/unverified dependency. + +Export from the artifact's clean source commit: ```sh -python3 scripts/contracts/bundle.py --component backend --variant server \ - --version 0.2.4 --output backend-0.2.4.json -python3 scripts/contracts/bundle.py --component frontend \ - --version 0.2.1 --output frontend-0.2.1.json -python3 scripts/contracts/bundle.py --component mcp \ - --version 0.3.0 --output mcp-0.3.0.json +cargo run --locked -p marreq-contracts -- bundle --component backend --variant server --version 0.2.4 --output backend.json +cargo run --locked -p marreq-contracts -- bundle --component frontend --version 0.2.1 --output frontend.json +cargo run --locked -p marreq-contracts -- bundle --component mcp --version 0.3.0 --output mcp.json ``` -Run each command on **that artifact's own source commit**, not all on current -`main`. The exporter refuses dirty audited inputs and records the exact source -SHA. Retain the JSON next to the versioned image/release with its SHA-256. Images -also carry their build-time OpenAPI and compatibility metadata. The exporter can -record incomplete bootstrap evidence for review, but the gate cannot certify it. -Old releases without retained evidence require a real audit on their source -commit; missing evidence never falls back to current sources. +Each command must run on **that component version's actual source**, not all on +main. Retain the output beside its release artifact. `marreq.contract.v2` requires +consumer-specific evidence; the old whole-backend bootstrap bundle is not +retrospectively certified. Never fabricate contracts for already published images. -### Verify the resulting deployment, including unchanged artifacts - -Prepare a `deployment.json` with exactly these top-level fields: +A `marreq.deployment-contract.v2` plan names `variant` (`server`/`cloud`) and +`backend`, `frontend`, `mcp`. Each descriptor contains exactly `file`, `sha256`, +`version`, `source_sha`. Files must remain inside the plan directory. SHA-256 pins +**contract bytes, not Docker image provenance**; a trusted orchestrator must bind +the bundle/source to the actual image digest. This prerequisite does not implement +that future #423 release/provenance/CD architecture. ```json { - "format": "marreq.deployment-contract.v1", + "format": "marreq.deployment-contract.v2", "variant": "server", - "backend": { - "file": "backend-0.2.4.json", - "sha256": "<64 lowercase hex characters>", - "version": "0.2.4", - "source_sha": "<40 lowercase hex characters>" - }, - "frontend": { - "file": "frontend-0.2.1.json", - "sha256": "<64 lowercase hex characters>", - "version": "0.2.1", - "source_sha": "<40 lowercase hex characters>" - }, - "mcp": { - "file": "mcp-0.3.0.json", - "sha256": "<64 lowercase hex characters>", - "version": "0.3.0", - "source_sha": "<40 lowercase hex characters>" - } + "backend": {"file": "backend.json", "sha256": "<64 hex>", "version": "0.2.4", "source_sha": "<40 hex>"}, + "frontend": {"file": "frontend.json", "sha256": "<64 hex>", "version": "0.2.1", "source_sha": "<40 hex>"}, + "mcp": {"file": "mcp.json", "sha256": "<64 hex>", "version": "0.3.0", "source_sha": "<40 hex>"} } ``` -Place referenced bundles inside the plan directory. Verify offline: - ```sh +cargo run --locked -p marreq-contracts -- verify --manifest deployment.json +# Equivalent thin wrapper: scripts/check-api-compatibility.sh --manifest deployment.json ``` -Exit 0 means all checked declarations/contracts are compatible. Exit 1 means -incompatibility, incomplete evidence, invalid metadata, wrong deployment variant, -a missing artifact, or a digest/source/version mismatch. Output is structured JSON -with component-specific diagnostics. The comparator checks request acceptance in -the consumer-to-producer direction and response acceptance in the reverse -direction. This command does not download artifacts, execute plans, send tokens, -publish images or deploy anything. SHA-256 pins content; trusted provenance for -retrieving artifacts remains the release/deployment orchestrator's responsibility. - -### Current tag workflow and future global releases - -`release-artifacts.yml` retains the existing component tag triggers. Every image -publisher depends on contract tests and the mandatory deployment gate. Once P0 -coverage is complete, prepare a separate immutable revision in this repository -containing `deployment.json` and all referenced bundles; set the repository variable -`MARREQ_RELEASE_CONTRACTS_SHA` to that full commit SHA. This revision is prepared -**after** the candidate source commit exists, avoiding self-referential source -hashes in a manifest committed with its own candidate. The workflow checks out -that exact plan revision, exports the candidate contract from the tagged source, -and checks its digest, version and source SHA against the plan. Unchanged -frontend/MCP/backend bundles are never replaced with the tag checkout's versions. -The candidate contract is retained as a workflow artifact before image publishing. -No default production version or fabricated historical manifest is provided. - -The future global-release/selective-deployment pipeline should retrieve trusted -retained bundles, construct every resulting deployment plan, and invoke this -same command plus the reusable `api-contracts.yml` workflow. Global version -registry/tag migration, image digest provenance, actual CD, database migrations, -rollback and VPS configuration remain in #423. Nothing here deploys to the VPS. +For this backend-only update, retain the actual frontend 0.2.1 and MCP 0.3.0 +bundles. The candidate 0.2.4 must satisfy their request/response contracts and each +consumer's backend range. Equal component versions are not required. The backend's +existing frontend acceptance range is also enforced. An unrelated backend route +outside both consumer bundles can change without failing their compatibility. + +Optional request fields and additional response fields are compatible where the +old contract accepts them. Removed guaranteed response properties, new required +request properties, narrowed request enums, widened closed response enums, +incompatible types/authentication, or missing required operations fail. Per-operation +Bearer scopes and stable CSRF policy IDs are checked separately; unused global +security schemes and exact global status/header set equality do not couple APIs. +The pinned standards engine decides ordinary OpenAPI changes, not homegrown rules. +Missing evidence, altered hashes, mismatched source/version, invalid policy, +unsupported semantics and uncertain required operations exit nonzero with JSON +errors. Version ranges supplement executable contracts and never prove them alone. +During rolling upgrades, check every old/new combination that may coexist. + +Current component-tag publishing is retained. A publication job checks out an +immutable plan revision from `MARREQ_RELEASE_CONTRACTS_SHA`, exports the exact tagged +candidate, and verifies its version, source and bundle hash against the plan before +any image publisher runs. The release gate uses the **actual artifact combination**; +it does not require the latest checkout's unchanged consumers. The PR gate instead +checks all calls in current source. No gate requires coverage of unconsumed routes. +No global release migration or production deployment is performed here. + +The PR remains draft until required consumer schema gaps and security failures are +resolved; a document-only check is not a merge/release-readiness assertion. diff --git a/frontend/src/api/__tests__/catalog.test.ts b/frontend/src/api/__tests__/catalog.test.ts index 13efb309..bcac0bec 100644 --- a/frontend/src/api/__tests__/catalog.test.ts +++ b/frontend/src/api/__tests__/catalog.test.ts @@ -157,9 +157,9 @@ describe('catalog API', () => { { name: 'listVerificationMethods', - call: () => listVerificationMethods(), + call: () => listVerificationMethods(5), method: 'GET', - url: '/api/verification-methods', + url: '/api/projects/5/verification-methods', }, { name: 'createVerificationMethod scopes the body to the project', diff --git a/frontend/src/api/catalog.ts b/frontend/src/api/catalog.ts index 8fb00c17..d84a3a45 100644 --- a/frontend/src/api/catalog.ts +++ b/frontend/src/api/catalog.ts @@ -248,8 +248,8 @@ export async function deleteCustomField( }); } -export async function listVerificationMethods(): Promise { - return fetchJson('/api/verification-methods'); +export async function listVerificationMethods(projectId: number): Promise { + return fetchJson(`/api/projects/${projectId}/verification-methods`); } export async function createVerificationMethod( diff --git a/frontend/src/api/generated-contract.ts b/frontend/src/api/generated-contract.ts index 5e16cda6..13b8e14f 100644 --- a/frontend/src/api/generated-contract.ts +++ b/frontend/src/api/generated-contract.ts @@ -1,450 +1,484 @@ -// Generated by scripts/contracts/generate.py from docs/developer/openapi.yaml. -// Do not edit; this file carries wire types, not a second contract. - -export type ApiError = { - "status": number; - "error": string; - "message": string; - [key: string]: unknown; -}; - -export type BuildInfo = { - "backend_version": string; - "backend_git_sha": string; - "deployment_mode": "server" | "cloud"; - "frontend_compatibility": { - "min_version": string; - "max_version": string; - [key: string]: unknown; -}; - [key: string]: unknown; -}; - -export type DeploymentInfo = { - "mode": "server" | "cloud"; - "allows_self_registration": boolean; - "requires_email_verification": boolean; - "allows_admin_promotion": boolean; - "assigns_personal_workspace": boolean; - "allows_self_administered_user_creation": boolean; - "allows_database_backup": boolean; - [key: string]: unknown; -}; - -export type McpPrincipalResponse = { - "user_id": number; - "authentication_type": string; - "principal_id": string; - "client_id": (string | null); - "grant_id": (number | null); - [key: string]: unknown; -}; - -export type UseCaseBranch = { - "id": string; - "kind": "alternative" | "exception"; - "title": string; - "from_step": string; - "end": UseCaseBranchEnd; - "steps"?: Array; -}; - -export type UseCaseBranchEnd = ({ - "type": "rejoin"; - "step": string; -} | { - "type": "end"; - "outcome": "success" | "failure"; -}); - -export type UseCaseBranchResponse = { - "id": string; - "kind": "alternative" | "exception"; - "title": string; - "from_step": string; - "end": UseCaseBranchEnd; - "steps": Array; -}; - -export type UseCaseDependents = ("cascade" | { - "rebranch_to": string; - [key: string]: unknown; -}); - -export type UseCaseDiff = { - "from": (UseCaseDiffEnd | null); - "to": UseCaseDiffEnd; - "summary": { - "added": number; - "removed": number; - "edited": number; - "moved": number; - "rebranched": number; - [key: string]: unknown; -}; - "header": { - [key: string]: { - "old": (string | null | Array); - "new": (string | null | Array); - [key: string]: unknown; -}; -}; - "elements": Array; - [key: string]: unknown; -}; - -export type UseCaseDiffEnd = { - "version_id": (number | null); - "version_number": (number | null); - "draft_revision": (number | null); - [key: string]: unknown; -}; - -export type UseCaseDiffSide = { - "list": "preconditions" | "main" | "postconditions" | "branch" | "branches"; - "branch_id"?: string; - "label": (string | null); - "text": string; - "from_step"?: string; - "end"?: UseCaseBranchEnd; - "kind"?: "alternative" | "exception"; - [key: string]: unknown; -}; - -export type UseCaseDocument = { - "schema"?: "marreq.use-case.v1"; - "title": string; - "primary_actor"?: (string | null); - "secondary_actors"?: Array; - "summary"?: (string | null); - "preconditions"?: Array; - "main"?: Array; - "branches"?: Array; - "postconditions"?: Array; -}; - -export type UseCaseDocumentResponse = { - "schema": "marreq.use-case.v1"; - "title": string; - "primary_actor": (string | null); - "secondary_actors": Array; - "summary": (string | null); - "preconditions": Array; - "main": Array; - "branches": Array; - "postconditions": Array; -}; - -export type UseCaseEffect = ({ - "type": "relabelled"; - "id": string; - "from": string; - "to": string; - [key: string]: unknown; -} | { - "type": "removed"; - "id": string; - [key: string]: unknown; -} | { - "type": "rebranched"; - "id": string; - "from_step": string; - [key: string]: unknown; -} | { - "type": "created"; - "id": string; - [key: string]: unknown; -}); - -export type UseCaseElement = { - "id": string; - "text": string; -}; - -export type UseCaseElementDiff = { - "id": string; - "kind": "precondition" | "postcondition" | "step" | "branch"; - "changes": Array<"added" | "removed" | "edited" | "moved" | "rebranched">; - "old": (UseCaseDiffSide | null); - "new": (UseCaseDiffSide | null); - "text_diff"?: Array<{ - "op": "equal" | "delete" | "insert"; - "text": string; - [key: string]: unknown; -}>; - [key: string]: unknown; -}; - -export type UseCaseListRef = ("preconditions" | "main" | "postconditions" | { - "branch": string; - [key: string]: unknown; -}); - -export type UseCaseNewBranch = { - "id"?: (string | null); - "kind": "alternative" | "exception"; - "title": string; - "from_step": string; - "end": UseCaseBranchEnd; - "steps"?: Array; -}; - -export type UseCaseNewElement = { - "id"?: (string | null); - "text": string; -}; - -export type UseCaseOperation = ({ - "op": "set_header"; - "title"?: (string | null); - "primary_actor"?: (string | null); - "secondary_actors"?: (Array | null); - "summary"?: (string | null); -} | { - "op": "add_element"; - "list": UseCaseListRef; - "after"?: (string | null); - "element": UseCaseNewElement; -} | { - "op": "update_text"; - "id": string; - "text": string; -} | { - "op": "move_element"; - "id": string; - "list"?: (UseCaseListRef | null); - "after"?: (string | null); -} | { - "op": "remove_element"; - "id": string; - "dependents"?: (UseCaseDependents | null); -} | { - "op": "add_branch"; - "branch": UseCaseNewBranch; -} | { - "op": "update_branch"; - "id": string; - "title"?: (string | null); - "kind"?: ("alternative" | "exception" | null); - "from_step"?: (string | null); - "end"?: (UseCaseBranchEnd | null); -} | { - "op": "remove_branch"; - "id": string; -}); - -export type UseCasePublishResponse = { - "version": use_cases_VersionView_response; - "removed_links": Array; - [key: string]: unknown; -}; - -export type entities_User_response = { - "id": number; - "username": string; - "name": string; - "email": string; - "creation_date": string; - "last_login": string; - "is_admin": boolean; - "email_verified": boolean; - [key: string]: unknown; -}; - -export type external_ProviderDiscovery_response = { - "password_enabled": boolean; - "external": Array; - [key: string]: unknown; -}; - -export type external_PublicAuthProvider_response = { - "id": string; - "display_name": string; - [key: string]: unknown; -}; - -export type use_cases_BaselineUseCaseView_response = { - "use_case_id": number; - "reference_code": string; - "title": string; - "primary_actor": (string | null); - "version_id": number; - "version_number": number; - "approval_state": string; - "current_version_id": (number | null); - "current_version_number": (number | null); - "link_count": number; - [key: string]: unknown; -}; - -export type use_cases_ClearSuspectResponse_response = { - "status": string; - "cleared": boolean; - [key: string]: unknown; -}; - -export type use_cases_DraftChangeView_response = { - "use_case_id": number; - "revision": number; - "base_version_id": (number | null); - "updated_by": (number | null); - "updated_by_name": (string | null); - "updated_at": string; - "document": UseCaseDocumentResponse; - "labels": { - [key: string]: string; -}; - "effects": Array; - [key: string]: unknown; -}; - -export type use_cases_DraftSummary_response = { - "revision": number; - "base_version_id": (number | null); - "updated_by": (number | null); - "updated_by_name": (string | null); - "updated_at": string; - [key: string]: unknown; -}; - -export type use_cases_DraftView_response = { - "use_case_id": number; - "revision": number; - "base_version_id": (number | null); - "updated_by": (number | null); - "updated_by_name": (string | null); - "updated_at": string; - "document": UseCaseDocumentResponse; - "labels": { - [key: string]: string; -}; - [key: string]: unknown; -}; - -export type use_cases_LinkView_response = { - "id": number; - "element_id": (string | null); - "element_label": (string | null); - "target_type": "requirement" | "verification"; - "requirement_id": (number | null); - "verification_id": (number | null); - "target_reference_code": string; - "target_title": string; - "rationale": (string | null); - "suspect": boolean; - "suspect_at": (string | null); - "suspect_reason": (string | null); - "linked_requirement_version_id": (number | null); - "current_requirement_version_id": (number | null); - "created_by": (number | null); - "created_at": string; - [key: string]: unknown; -}; - -export type use_cases_NextCode_response = { - "reference_code": string; - [key: string]: unknown; -}; - -export type use_cases_PutDraftRequest_request = { - "base_revision"?: (number | null); - "document": UseCaseDocument; - [key: string]: unknown; -}; - -export type use_cases_SetApprovalRequest_request = { - "state": string; - [key: string]: unknown; -}; - -export type use_cases_UseCaseCommentView_response = { - "id": number; - "use_case_id": number; - "use_case_version_id": (number | null); - "element_id": (string | null); - "element_label": (string | null); - "author_id": number; - "author_name": string; - "body": string; - "created_at": string; - [key: string]: unknown; -}; - -export type use_cases_UseCaseDetail_response = { - "id": number; - "project_id": number; - "reference_code": string; - "title": string; - "created_by": (number | null); - "created_at": string; - "updated_at": string; - "current_version": (use_cases_VersionView_response | null); - "draft": (use_cases_DraftSummary_response | null); - [key: string]: unknown; -}; - -export type use_cases_UseCaseRef_response = { - "use_case_id": number; - "reference_code": string; - "title": string; - "link_id": number; - "element_id": (string | null); - "element_label": (string | null); - "suspect": boolean; - [key: string]: unknown; -}; - -export type use_cases_UseCaseRow_response = { - "id": number; - "reference_code": string; - "title": string; - "primary_actor": (string | null); - "step_count": number; - "alternative_count": number; - "exception_count": number; - "approval_state": (string | null); - "current_version_number": (number | null); - "has_draft": boolean; - "link_count": number; - "suspect_count": number; - "updated_at": string; - [key: string]: unknown; -}; - -export type use_cases_VersionSummary_response = { - "id": number; - "version_number": number; - "approval_state": string; - "title": string; - "primary_actor": (string | null); - "change_note": (string | null); - "created_by": (number | null); - "created_by_name": (string | null); - "created_at": string; - "reviewed_by": (number | null); - "reviewed_at": (string | null); - "approved_by": (number | null); - "approved_at": (string | null); - [key: string]: unknown; -}; - -export type use_cases_VersionView_response = { - "id": number; - "version_number": number; - "approval_state": string; - "title": string; - "primary_actor": (string | null); - "change_note": (string | null); - "created_by": (number | null); - "created_by_name": (string | null); - "created_at": string; - "reviewed_by": (number | null); - "reviewed_at": (string | null); - "approved_by": (number | null); - "approved_at": (string | null); - "document": UseCaseDocumentResponse; - "labels": { - [key: string]: string; -}; - [key: string]: unknown; -}; +// Generated by scripts/generate-api-types.mjs; do not edit. +export interface components { + schemas: { + ApiError: { + error: string; + message: string; + status: number; + } & { + [key: string]: unknown; + }; + BuildInfo: { + backend_git_sha: string; + backend_version: string; + /** @enum {string} */ + deployment_mode: "server" | "cloud"; + frontend_compatibility: { + max_version: string; + min_version: string; + } & { + [key: string]: unknown; + }; + } & { + [key: string]: unknown; + }; + DeploymentInfo: { + allows_admin_promotion: boolean; + allows_database_backup: boolean; + allows_self_administered_user_creation: boolean; + allows_self_registration: boolean; + assigns_personal_workspace: boolean; + /** @enum {string} */ + mode: "server" | "cloud"; + requires_email_verification: boolean; + } & { + [key: string]: unknown; + }; + UseCaseBranch: { + end: components["schemas"]["UseCaseBranchEnd"]; + from_step: string; + id: string; + /** @enum {string} */ + kind: "alternative" | "exception"; + steps?: components["schemas"]["UseCaseElement"][]; + title: string; + }; + UseCaseBranchEnd: { + step: string; + /** @constant */ + type: "rejoin"; + } | { + /** @enum {string} */ + outcome: "success" | "failure"; + /** @constant */ + type: "end"; + }; + UseCaseBranchResponse: { + end: components["schemas"]["UseCaseBranchEnd"]; + from_step: string; + id: string; + /** @enum {string} */ + kind: "alternative" | "exception"; + steps: components["schemas"]["UseCaseElement"][]; + title: string; + }; + UseCaseDependents: "cascade" | ({ + rebranch_to: string; + } & { + [key: string]: unknown; + }); + UseCaseDiff: { + elements: components["schemas"]["UseCaseElementDiff"][]; + from: components["schemas"]["UseCaseDiffEnd"] | null; + header: { + [key: string]: { + new: string | null | string[]; + old: string | null | string[]; + } & { + [key: string]: unknown; + }; + }; + summary: { + added: number; + edited: number; + moved: number; + rebranched: number; + removed: number; + } & { + [key: string]: unknown; + }; + to: components["schemas"]["UseCaseDiffEnd"]; + } & { + [key: string]: unknown; + }; + UseCaseDiffEnd: { + draft_revision: number | null; + version_id: number | null; + version_number: number | null; + } & { + [key: string]: unknown; + }; + UseCaseDiffSide: { + branch_id?: string; + end?: components["schemas"]["UseCaseBranchEnd"]; + from_step?: string; + /** @enum {string} */ + kind?: "alternative" | "exception"; + label: string | null; + /** @enum {string} */ + list: "preconditions" | "main" | "postconditions" | "branch" | "branches"; + text: string; + } & { + [key: string]: unknown; + }; + UseCaseDocument: { + branches?: components["schemas"]["UseCaseBranch"][]; + main?: components["schemas"]["UseCaseElement"][]; + postconditions?: components["schemas"]["UseCaseElement"][]; + preconditions?: components["schemas"]["UseCaseElement"][]; + primary_actor?: string | null; + /** @constant */ + schema?: "marreq.use-case.v1"; + secondary_actors?: string[]; + summary?: string | null; + title: string; + }; + UseCaseDocumentResponse: { + branches: components["schemas"]["UseCaseBranchResponse"][]; + main: components["schemas"]["UseCaseElement"][]; + postconditions: components["schemas"]["UseCaseElement"][]; + preconditions: components["schemas"]["UseCaseElement"][]; + primary_actor: string | null; + /** @constant */ + schema: "marreq.use-case.v1"; + secondary_actors: string[]; + summary: string | null; + title: string; + }; + UseCaseEffect: ({ + from: string; + id: string; + to: string; + /** @constant */ + type: "relabelled"; + } & { + [key: string]: unknown; + }) | ({ + id: string; + /** @constant */ + type: "removed"; + } & { + [key: string]: unknown; + }) | ({ + from_step: string; + id: string; + /** @constant */ + type: "rebranched"; + } & { + [key: string]: unknown; + }) | ({ + id: string; + /** @constant */ + type: "created"; + } & { + [key: string]: unknown; + }); + UseCaseElement: { + id: string; + text: string; + }; + UseCaseElementDiff: { + changes: ("added" | "removed" | "edited" | "moved" | "rebranched")[]; + id: string; + /** @enum {string} */ + kind: "precondition" | "postcondition" | "step" | "branch"; + new: components["schemas"]["UseCaseDiffSide"] | null; + old: components["schemas"]["UseCaseDiffSide"] | null; + text_diff?: ({ + /** @enum {string} */ + op: "equal" | "delete" | "insert"; + text: string; + } & { + [key: string]: unknown; + })[]; + } & { + [key: string]: unknown; + }; + UseCaseListRef: ("preconditions" | "main" | "postconditions") | ({ + branch: string; + } & { + [key: string]: unknown; + }); + UseCaseNewBranch: { + end: components["schemas"]["UseCaseBranchEnd"]; + from_step: string; + id?: string | null; + /** @enum {string} */ + kind: "alternative" | "exception"; + steps?: components["schemas"]["UseCaseNewElement"][]; + title: string; + }; + UseCaseNewElement: { + id?: string | null; + text: string; + }; + UseCaseOperation: { + /** @constant */ + op: "set_header"; + primary_actor?: string | null; + secondary_actors?: string[] | null; + summary?: string | null; + title?: string | null; + } | { + after?: string | null; + element: components["schemas"]["UseCaseNewElement"]; + list: components["schemas"]["UseCaseListRef"]; + /** @constant */ + op: "add_element"; + } | { + id: string; + /** @constant */ + op: "update_text"; + text: string; + } | { + after?: string | null; + id: string; + list?: components["schemas"]["UseCaseListRef"] | null; + /** @constant */ + op: "move_element"; + } | { + dependents?: components["schemas"]["UseCaseDependents"] | null; + id: string; + /** @constant */ + op: "remove_element"; + } | { + branch: components["schemas"]["UseCaseNewBranch"]; + /** @constant */ + op: "add_branch"; + } | { + end?: components["schemas"]["UseCaseBranchEnd"] | null; + from_step?: string | null; + id: string; + kind?: ("alternative" | "exception") | null; + /** @constant */ + op: "update_branch"; + title?: string | null; + } | { + id: string; + /** @constant */ + op: "remove_branch"; + }; + UseCasePublishResponse: { + removed_links: components["schemas"]["use_cases_LinkView_response"][]; + version: components["schemas"]["use_cases_VersionView_response"]; + } & { + [key: string]: unknown; + }; + entities_User_response: { + creation_date: string; + email: string; + email_verified: boolean; + id: number; + is_admin: boolean; + last_login: string; + name: string; + username: string; + } & { + [key: string]: unknown; + }; + external_ProviderDiscovery_response: { + external: components["schemas"]["external_PublicAuthProvider_response"][]; + password_enabled: boolean; + } & { + [key: string]: unknown; + }; + external_PublicAuthProvider_response: { + display_name: string; + id: string; + } & { + [key: string]: unknown; + }; + use_cases_BaselineUseCaseView_response: { + approval_state: string; + current_version_id: number | null; + current_version_number: number | null; + link_count: number; + primary_actor: string | null; + reference_code: string; + title: string; + use_case_id: number; + version_id: number; + version_number: number; + } & { + [key: string]: unknown; + }; + use_cases_ClearSuspectResponse_response: { + cleared: boolean; + status: string; + } & { + [key: string]: unknown; + }; + use_cases_DraftChangeView_response: { + base_version_id: number | null; + document: components["schemas"]["UseCaseDocumentResponse"]; + effects: components["schemas"]["UseCaseEffect"][]; + labels: { + [key: string]: string; + }; + revision: number; + updated_at: string; + updated_by: number | null; + updated_by_name: string | null; + use_case_id: number; + } & { + [key: string]: unknown; + }; + use_cases_DraftSummary_response: { + base_version_id: number | null; + revision: number; + updated_at: string; + updated_by: number | null; + updated_by_name: string | null; + } & { + [key: string]: unknown; + }; + use_cases_DraftView_response: { + base_version_id: number | null; + document: components["schemas"]["UseCaseDocumentResponse"]; + labels: { + [key: string]: string; + }; + revision: number; + updated_at: string; + updated_by: number | null; + updated_by_name: string | null; + use_case_id: number; + } & { + [key: string]: unknown; + }; + use_cases_LinkView_response: { + created_at: string; + created_by: number | null; + current_requirement_version_id: number | null; + element_id: string | null; + element_label: string | null; + id: number; + linked_requirement_version_id: number | null; + rationale: string | null; + requirement_id: number | null; + suspect: boolean; + suspect_at: string | null; + suspect_reason: string | null; + target_reference_code: string; + target_title: string; + /** @enum {string} */ + target_type: "requirement" | "verification"; + verification_id: number | null; + } & { + [key: string]: unknown; + }; + use_cases_NextCode_response: { + reference_code: string; + } & { + [key: string]: unknown; + }; + use_cases_PutDraftRequest_request: { + base_revision?: number | null; + document: components["schemas"]["UseCaseDocument"]; + } & { + [key: string]: unknown; + }; + use_cases_SetApprovalRequest_request: { + state: string; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseCommentView_response: { + author_id: number; + author_name: string; + body: string; + created_at: string; + element_id: string | null; + element_label: string | null; + id: number; + use_case_id: number; + use_case_version_id: number | null; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseDetail_response: { + created_at: string; + created_by: number | null; + current_version: components["schemas"]["use_cases_VersionView_response"] | null; + draft: components["schemas"]["use_cases_DraftSummary_response"] | null; + id: number; + project_id: number; + reference_code: string; + title: string; + updated_at: string; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseRef_response: { + element_id: string | null; + element_label: string | null; + link_id: number; + reference_code: string; + suspect: boolean; + title: string; + use_case_id: number; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseRow_response: { + alternative_count: number; + approval_state: string | null; + current_version_number: number | null; + exception_count: number; + has_draft: boolean; + id: number; + link_count: number; + primary_actor: string | null; + reference_code: string; + step_count: number; + suspect_count: number; + title: string; + updated_at: string; + } & { + [key: string]: unknown; + }; + use_cases_VersionSummary_response: { + approval_state: string; + approved_at: string | null; + approved_by: number | null; + change_note: string | null; + created_at: string; + created_by: number | null; + created_by_name: string | null; + id: number; + primary_actor: string | null; + reviewed_at: string | null; + reviewed_by: number | null; + title: string; + version_number: number; + } & { + [key: string]: unknown; + }; + use_cases_VersionView_response: { + approval_state: string; + approved_at: string | null; + approved_by: number | null; + change_note: string | null; + created_at: string; + created_by: number | null; + created_by_name: string | null; + document: components["schemas"]["UseCaseDocumentResponse"]; + id: number; + labels: { + [key: string]: string; + }; + primary_actor: string | null; + reviewed_at: string | null; + reviewed_by: number | null; + title: string; + version_number: number; + } & { + [key: string]: unknown; + }; + }; + responses: { + /** @description API error; use cases may include code and structured details. Guard/fairing errors can have an empty body. */ + ApiError: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiError"]; + }; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index be9cc5c0..32c8be4f 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -1,3 +1,4 @@ +import type { components as ApiComponents } from './generated-contract'; /** API shapes aligned with marreq-core JSON (snake_case). */ export interface RequirementStatus { @@ -187,9 +188,9 @@ export interface DashboardPayload { csrf_token: string; } -export type DeploymentInfo = import('./generated-contract').DeploymentInfo; +export type DeploymentInfo = ApiComponents['schemas']['DeploymentInfo']; -export type AuthProviderDiscovery = import('./generated-contract').external_ProviderDiscovery_response; +export type AuthProviderDiscovery = ApiComponents['schemas']['external_ProviderDiscovery_response']; export interface ConnectedIdentity { id: number; @@ -217,7 +218,7 @@ export interface FrontendCompatibility { } /** Response from `GET /api/meta/build` (issue #213). */ -export type BuildInfo = import('./generated-contract').BuildInfo; +export type BuildInfo = ApiComponents['schemas']['BuildInfo']; export type RegistrationBody = { username: string; @@ -972,16 +973,16 @@ export interface UseCaseDocument { /** Display labels by element id: `3`, `A3`, `A3.1`, `E4`, `P1`, `Q1`. */ export type UseCaseLabels = Record; -export type UseCaseRow = import('./generated-contract').use_cases_UseCaseRow_response; +export type UseCaseRow = ApiComponents['schemas']['use_cases_UseCaseRow_response']; -export type UseCaseVersionSummary = import('./generated-contract').use_cases_VersionSummary_response; +export type UseCaseVersionSummary = ApiComponents['schemas']['use_cases_VersionSummary_response']; export interface UseCaseVersion extends UseCaseVersionSummary { document: UseCaseDocument; labels: UseCaseLabels; } -export type UseCaseDraftSummary = import('./generated-contract').use_cases_DraftSummary_response; +export type UseCaseDraftSummary = ApiComponents['schemas']['use_cases_DraftSummary_response']; export interface UseCaseDraft extends UseCaseDraftSummary { use_case_id: number; @@ -1043,10 +1044,10 @@ export type UseCaseOperation = | { op: 'remove_branch'; id: string }; /** A link from a use case, or one of its elements, to a requirement or verification (issue #400). */ -export type UseCaseLink = import('./generated-contract').use_cases_LinkView_response; +export type UseCaseLink = ApiComponents['schemas']['use_cases_LinkView_response']; /** A use case that links to a requirement or verification. */ -export type UseCaseRef = import('./generated-contract').use_cases_UseCaseRef_response; +export type UseCaseRef = ApiComponents['schemas']['use_cases_UseCaseRef_response']; /** Structural diff of two use case documents (issue #401). */ export type UseCaseChange = 'added' | 'removed' | 'edited' | 'moved' | 'rebranched'; @@ -1084,7 +1085,7 @@ export interface UseCaseDiff { elements: UseCaseElementDiff[]; } -export type UseCaseComment = import('./generated-contract').use_cases_UseCaseCommentView_response; +export type UseCaseComment = ApiComponents['schemas']['use_cases_UseCaseCommentView_response']; /** A use case version a baseline recorded (issue #402). */ -export type BaselineUseCase = import('./generated-contract').use_cases_BaselineUseCaseView_response; +export type BaselineUseCase = ApiComponents['schemas']['use_cases_BaselineUseCaseView_response']; diff --git a/frontend/src/pages/SystemLogsPage.tsx b/frontend/src/pages/SystemLogsPage.tsx index 82f3f7c7..16a8ee12 100644 --- a/frontend/src/pages/SystemLogsPage.tsx +++ b/frontend/src/pages/SystemLogsPage.tsx @@ -114,7 +114,7 @@ export default function SystemLogsPage() { listVerificationStatuses().catch(() => []), listCategories().catch(() => []), listApplicability().catch(() => []), - listVerificationMethods().catch(() => []), + Promise.all((dashboard?.projects ?? []).map((p) => listVerificationMethods(p.id).catch(() => []))).then((rows) => rows.flat()), ]); setItems(res.items); setTotal(res.total); @@ -131,7 +131,7 @@ export default function SystemLogsPage() { setLoading(false); } }, - [filterParams], + [filterParams, dashboard?.projects], ); useEffect(() => { diff --git a/frontend/src/pages/__tests__/ReportBuilderPage.test.tsx b/frontend/src/pages/__tests__/ReportBuilderPage.test.tsx index 9cc91639..191dde0a 100644 --- a/frontend/src/pages/__tests__/ReportBuilderPage.test.tsx +++ b/frontend/src/pages/__tests__/ReportBuilderPage.test.tsx @@ -159,6 +159,7 @@ describe('ReportBuilderPage', () => { await waitFor(() => expect(screen.getByTestId('where')).toHaveTextContent('?type=vcd')); await userEvent.click(screen.getByRole('checkbox', { name: 'Include Contents' })); + await screen.findByText('Unsaved changes.'); vi.mocked(window.confirm).mockReturnValue(false); await userEvent.selectOptions(screen.getByRole('combobox', { name: 'Report' }), 'coverage'); expect(screen.getByTestId('where')).toHaveTextContent('?type=vcd'); diff --git a/marreq-cloud/Cargo.toml b/marreq-cloud/Cargo.toml index 114fba30..861acbb7 100644 --- a/marreq-cloud/Cargo.toml +++ b/marreq-cloud/Cargo.toml @@ -31,7 +31,7 @@ anyhow = { workspace = true } sha2 = { workspace = true } [dev-dependencies] -regex = { workspace = true } +marreq-contracts = { path = "../tools/marreq-contracts" } marreq-core = { path = "../marreq-core", features = ["test-helpers"] } tokio = { workspace = true, features = ["rt"] } serde_json = { workspace = true } diff --git a/marreq-cloud/tests/api_contract_inventory.rs b/marreq-cloud/tests/api_contract_inventory.rs index a0d6e8c5..d1dbac2a 100644 --- a/marreq-cloud/tests/api_contract_inventory.rs +++ b/marreq-cloud/tests/api_contract_inventory.rs @@ -1,15 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -//! Compare the compiled Rocket mount inventory with the OpenAPI-derived artifact. -//! No database or test-double routing: declared_mounts is also used by production. +//! Consumer contracts must correspond to compiled production Rocket mounts. +//! Additional, unconsumed backend routes deliberately do not fail this check. use std::collections::BTreeSet; #[test] -fn compiled_server_and_cloud_routes_match_openapi_inventory() { - let expected: Vec = serde_json::from_str(include_str!( - "../../docs/developer/api-contract-routes.json" - )) - .expect("generated route inventory"); - let parameter = regex::Regex::new(r"<([\w]+)(?:\.\.)?>").unwrap(); +fn consumed_contract_routes_exist_in_server_and_cloud() { + let spec = marreq_contracts::contract::load( + &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../docs/developer/openapi.yaml"), + ) + .unwrap(); for (variant, extra) in [ ("server", Vec::new()), ("cloud", marreq_cloud::routes::routes()), @@ -18,39 +17,35 @@ fn compiled_server_and_cloud_routes_match_openapi_inventory() { let actual: BTreeSet<_> = [("/api", mounts.api), ("", mounts.root)] .into_iter() .flat_map(|(base, routes)| { - let parameter = ¶meter; - routes.into_iter().filter_map(move |(_, route)| { - let path = route.uri.path().to_owned(); - // Origin root UI and internal CSRF denial routes are not - // HTTP consumer APIs. OAuth routes under root are included. - if base.is_empty() - && !(path.starts_with("/oauth/") - || path.starts_with("/.well-known/") - || path.starts_with("/api/")) - { - return None; - } - let normalized = parameter.replace_all(&path, "{$1}"); - Some((route.method.to_string(), format!("{base}{normalized}"))) + routes.into_iter().map(move |(_, route)| { + ( + route.method.to_string().to_lowercase(), + format!( + "{base}{}", + route.uri.path().replace('<', "{").replace('>', "}") + ), + ) }) }) .collect(); - let documented: BTreeSet<_> = expected - .iter() - .filter(|op| { - op["variants"] - .as_array() - .unwrap() - .iter() - .any(|v| v == variant) - }) - .map(|op| { - ( - op["method"].as_str().unwrap().to_owned(), - op["path"].as_str().unwrap().to_owned(), - ) - }) - .collect(); - assert_eq!(actual, documented, "{variant} route contract drift"); + let documented = marreq_contracts::contract::operations(&spec, Some(variant)).unwrap(); + for key in documented.keys() { + assert!( + actual.contains(&(key.method.clone(), key.path.clone())), + "{variant}: missing compiled {key}" + ); + } + for (key, op) in marreq_contracts::contract::operations(&spec, None).unwrap() { + if op + .get("x-marreq-variants") + .and_then(serde_json::Value::as_array) + .is_some_and(|a| !a.iter().any(|x| x == variant)) + { + assert!( + !actual.contains(&(key.method, key.path)), + "deployment-specific endpoint leaked into {variant}" + ); + } + } } } diff --git a/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs b/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs index 946cb674..6c6d6bb7 100644 --- a/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs +++ b/marreq-core/tests/delegated_oauth_mcp_tools_postgres_integration.rs @@ -169,6 +169,13 @@ async fn delegated_scopes_gate_the_routes_wrapped_by_mcp_tools() { SELECT setval(pg_get_serial_sequence('requirement_version_links','id'), 500);", ) .expect("seed fixtures"); + // Isolated browser-session fixture, also used by the real HTTP smoke test. + let password_hash = + marreq_core::auth::password::hash_password("Contract-only-fixture-58!z").unwrap(); + diesel::sql_query("UPDATE users SET password_hash=$1, email_verified=true WHERE id=1") + .bind::(password_hash) + .execute(&mut conn) + .unwrap(); for (raw, scopes) in [ ("projects", vec!["projects:read"]), diff --git a/mcp-server/Dockerfile b/mcp-server/Dockerfile index 1cafae39..3ff8b9bf 100644 --- a/mcp-server/Dockerfile +++ b/mcp-server/Dockerfile @@ -14,7 +14,6 @@ COPY --from=build --chown=node:node /app/node_modules ./node_modules COPY --from=build --chown=node:node /app/dist ./dist COPY --chown=node:node mcp-server/MARREQ_SKILL.md ./MARREQ_SKILL.md COPY --chown=node:node mcp-server/compatibility.json ./compatibility.json -COPY docs/developer/openapi.yaml /app/contracts/openapi.yaml USER node EXPOSE 3000 HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \ diff --git a/mcp-server/dist/client.js b/mcp-server/dist/client.js index 8ff7f956..35f319ba 100644 --- a/mcp-server/dist/client.js +++ b/mcp-server/dist/client.js @@ -47,12 +47,16 @@ export class MarreqClient { async getRequirement(id) { return this.request(`/api/projects/${this.ctx.projectId}/requirements/${id}`); } - async listRequirements(approvalState, hasTests) { + async listRequirements(approvalState, hasTests, extra = {}) { const params = new URLSearchParams(); if (approvalState != null) params.set("approval_state", approvalState); if (hasTests != null) params.set("has_tests", String(hasTests)); + for (const [key, value] of Object.entries(extra)) { + if (value != null && value !== "") + params.set(key, String(value)); + } const q = params.toString(); return this.request(`/api/projects/${this.ctx.projectId}/requirements${q ? `?${q}` : ""}`); } @@ -273,6 +277,104 @@ export class MarreqClient { body: JSON.stringify(payload), }); } + // ----- Tool gaps (issue #416) ----- + project(path) { + return `/api/projects/${this.ctx.projectId}${path}`; + } + withQuery(path, query) { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (value != null && value !== "") + params.set(key, String(value)); + } + const q = params.toString(); + return q ? `${path}?${q}` : path; + } + async listRequirementLinkTypes() { + return this.request(this.project("/requirement-version-links/link-types")); + } + async listRequirementLinks(query = {}) { + return this.request(this.withQuery(this.project("/requirement-version-links"), query)); + } + async createRequirementLink(payload, idempotencyKey) { + return this.request(this.project("/requirement-version-links"), { + method: "POST", + headers: { "Idempotency-Key": idempotencyKey }, + body: JSON.stringify(payload), + }); + } + async deleteRequirementLink(linkId) { + return this.request(this.project(`/requirement-version-links/${linkId}`), { method: "DELETE" }); + } + async deleteUseCaseLink(useCaseId, linkId) { + return this.request(this.useCases(`/${useCaseId}/links/${linkId}`), { method: "DELETE" }); + } + async clearUseCaseLinkSuspect(useCaseId, linkId) { + return this.request(this.useCases(`/${useCaseId}/links/${linkId}/clear_suspect`), { method: "POST" }); + } + async listLinkedUseCases(kind, id) { + return this.request(this.project(`/${kind}s/${id}/use-cases`)); + } + async askRequirements(payload) { + return this.request(this.project("/requirements/ask"), { method: "POST", body: JSON.stringify(payload) }); + } + async listReviewers() { + return this.request(this.project("/reviewers")); + } + async getMyPermissions() { + return this.request(this.project("/me/permissions")); + } + async listSavedViews() { + return this.request(this.project("/saved_views")); + } + async listAttachments(entityType, entityId) { + return this.request(this.withQuery(this.project("/attachments"), { entity_type: entityType, entity_id: entityId })); + } + /** What needs the user's attention across projects (not project-scoped). */ + async getAttention() { + return this.request("/api/home/attention"); + } + async listUseCaseVersions(useCaseId) { + return this.request(this.useCases(`/${useCaseId}/versions`)); + } + async listUseCaseComments(useCaseId, query = {}) { + return this.request(this.withQuery(this.useCases(`/${useCaseId}/comments`), query)); + } + async createUseCaseComment(useCaseId, payload, idempotencyKey) { + return this.request(this.useCases(`/${useCaseId}/comments`), { + method: "POST", + headers: { "Idempotency-Key": idempotencyKey }, + body: JSON.stringify(payload), + }); + } + async discardUseCaseDraft(useCaseId) { + return this.request(this.useCases(`/${useCaseId}/draft`), { method: "DELETE" }); + } + /** Project close-out, or one requirement's when requirementId is given. */ + async getCloseOut(requirementId) { + return this.request(this.project(requirementId != null ? `/requirements/${requirementId}/close_out` : "/close_out")); + } + async getImpactedTests(requirementId) { + return this.request(this.project(`/requirements/${requirementId}/impacted_tests`)); + } + async listVerificationSnapshots(verificationId) { + return this.request(this.project(`/verifications/${verificationId}/snapshots`)); + } + async compareVerificationSnapshots(verificationId, v1, v2) { + return this.request(this.project(`/verifications/${verificationId}/snapshots/${v1}/diff/${v2}`)); + } + async setVerificationField(verificationId, field, value) { + return this.request(this.project(`/verifications/${verificationId}/field`), { + method: "POST", + body: JSON.stringify({ field, value }), + }); + } + async getBaselineContents(baselineId, kind) { + return this.request(this.project(`/baselines/${baselineId}/${kind === "use_cases" ? "use-cases" : "verifications"}`)); + } + async diffBaselineVerificationVsCurrent(baselineId, verificationId) { + return this.request(this.project(`/baselines/${baselineId}/verifications/${verificationId}/diff/current`)); + } get projectId() { return this.ctx.projectId; } diff --git a/mcp-server/dist/gap-tools.js b/mcp-server/dist/gap-tools.js new file mode 100644 index 00000000..7ca6712a --- /dev/null +++ b/mcp-server/dist/gap-tools.js @@ -0,0 +1,276 @@ +/** + * MCP tools added for issue #416 (tool gaps). Each one wraps one project-scoped + * backend route; none is a generic REST passthrough. Modes follow the PDR: + * core = every mode, RX = read_extended and up, DW = draft_write, TW = the + * MARREQ_TRACE_WRITE flag (same gate as put_verification_matrix). + */ +import { randomUUID } from "node:crypto"; +import { z } from "zod"; +import { contextAllowsReadExtended } from "./context.js"; +import { paginate, paginationFields } from "./pagination.js"; +const id = (what) => z.number().int().positive().describe(what); +export function registerGapTools(deps) { + const { server, ctx, client, forProject, projectField, operationKey, withAudit, jsonContent } = deps; + const add = (spec) => { + const shape = { + ...(spec.global ? {} : projectField), + ...spec.shape, + ...(spec.paged ? paginationFields : {}), + }; + server.registerTool(spec.name, { + description: spec.description, + inputSchema: z.object(shape), + ...(spec.destructive ? { annotations: { destructiveHint: true } } : {}), + }, async (args) => { + const toolClient = spec.global ? client : forProject(args?.project_id); + const { idempotency_key: _key, ...audited } = args ?? {}; + const summary = spec.summary ? spec.summary(args ?? {}) : audited; + const out = await withAudit(toolClient, spec.name, JSON.stringify(summary), Boolean(spec.write), () => spec.run(toolClient, args ?? {})); + return { content: [jsonContent(spec.paged ? paginate(out, args ?? {}, 50) : out)] }; + }); + }; + // ----- Core: every mode ----- + add({ + name: "list_linked_use_cases", + description: "Use cases linked to a requirement or to a verification, with the link ids and suspect flags. Give exactly one of requirement_id or verification_id.", + shape: { + requirement_id: id("Requirement id").optional(), + verification_id: id("Verification id").optional(), + }, + paged: true, + run: (c, a) => { + if ((a.requirement_id == null) === (a.verification_id == null)) { + throw new Error("Give exactly one of requirement_id or verification_id"); + } + return a.requirement_id != null + ? c.listLinkedUseCases("requirement", a.requirement_id) + : c.listLinkedUseCases("verification", a.verification_id); + }, + }); + add({ + name: "ask_requirements", + description: "Answer a question from the project's requirements (retrieval plus an LLM call, so it costs time and money; prefer semantic_search_requirements when you only need matches). Fails with a clear error when answer generation is disabled on the server.", + shape: { + query: z.string().min(1), + k: z.number().int().min(1).max(20).optional().describe("Requirements used as context (default 10, max 20)."), + status_filter: id("Only requirements with this status id").optional(), + category_filter: id("Only requirements in this category id").optional(), + applicability_filter: id("Only requirements with this applicability id").optional(), + verification_filter: id("Only requirements with this verification method id").optional(), + }, + summary: (a) => ({ query_length: String(a.query ?? "").length, k: a.k }), + run: (c, a) => { + const { project_id: _p, ...payload } = a; + return c.askRequirements(payload); + }, + }); + add({ + name: "list_use_case_versions", + description: "Version history of a use case: version ids and numbers, approval states and change notes. Read a version with get_use_case(version_id).", + shape: { use_case_id: id("Use case id") }, + paged: true, + run: (c, a) => c.listUseCaseVersions(a.use_case_id), + }); + add({ + name: "get_baseline_contents", + description: "What a baseline recorded besides requirements: its use cases (kind=use_cases) or its verifications (kind=verifications).", + shape: { baseline_id: id("Baseline id"), kind: z.enum(["use_cases", "verifications"]) }, + paged: true, + run: (c, a) => c.getBaselineContents(a.baseline_id, a.kind), + }); + // ----- RX: read_extended and up ----- + if (contextAllowsReadExtended(ctx)) { + add({ + name: "list_requirement_link_types", + description: "Link types allowed between requirement versions (for link_requirements).", + shape: {}, + run: (c) => c.listRequirementLinkTypes(), + }); + add({ + name: "list_requirement_links", + description: "Requirement version links of the project, optionally filtered by source version, target version or link type. Use the link id with unlink_requirements.", + shape: { + source_version_id: id("Source requirement version id").optional(), + target_version_id: id("Target requirement version id").optional(), + link_type: z.string().min(1).optional(), + }, + paged: true, + run: (c, a) => c.listRequirementLinks({ + source_version_id: a.source_version_id, + target_version_id: a.target_version_id, + link_type: a.link_type, + }), + }); + add({ + name: "list_reviewers", + description: "User ids designated as reviewers of the project.", + shape: {}, + run: (c) => c.listReviewers(), + }); + add({ + name: "get_my_permissions", + description: "The authenticated user's effective permissions in the project. Check it before set_approval or other writes that may be refused.", + shape: {}, + run: (c) => c.getMyPermissions(), + }); + add({ + name: "list_saved_views", + description: "Saved requirement views visible to the user. Pass a view id as view_id to list_requirements.", + shape: {}, + paged: true, + run: (c) => c.listSavedViews(), + }); + add({ + name: "list_attachments", + description: "Attachments of one requirement or verification (metadata only; files are not downloaded). Over OAuth it needs both requirements:read and verifications:read.", + shape: { entity_type: z.enum(["requirement", "verification"]), entity_id: id("Requirement or verification id") }, + paged: true, + run: (c, a) => c.listAttachments(a.entity_type, a.entity_id), + }); + add({ + name: "get_attention", + description: "What needs the user's attention across their projects: approvals waiting, drafts to review, suspect links, notifications. Not tied to one project.", + shape: {}, + global: true, + run: (c) => c.getAttention(), + }); + add({ + name: "list_use_case_comments", + description: "Comments on a use case, optionally for one version or one step/condition (element_id).", + shape: { + use_case_id: id("Use case id"), + version_id: id("Only comments on this version").optional(), + element_id: z.string().min(1).optional(), + }, + paged: true, + run: (c, a) => c.listUseCaseComments(a.use_case_id, { version_id: a.version_id, element_id: a.element_id }), + }); + add({ + name: "get_close_out", + description: "Close-out status of the project, or of one requirement when requirement_id is given: what lacks a passing verification.", + shape: { requirement_id: id("Requirement id").optional() }, + run: (c, a) => c.getCloseOut(a.requirement_id), + }); + add({ + name: "impacted_tests", + description: "Verifications linked to a requirement that are marked suspect because the requirement changed. Run it after patch_requirement.", + shape: { requirement_id: id("Requirement id") }, + paged: true, + run: (c, a) => c.getImpactedTests(a.requirement_id), + }); + add({ + name: "list_verification_snapshots", + description: "Chronological snapshots of a verification, rebuilt from the audit log. Compare two with compare_verification_snapshots.", + shape: { verification_id: id("Verification id") }, + paged: true, + run: (c, a) => c.listVerificationSnapshots(a.verification_id), + }); + add({ + name: "compare_verification_snapshots", + description: "Structured diff between two snapshots of a verification (v1 = older, v2 = newer snapshot id).", + shape: { verification_id: id("Verification id"), v1: id("Older snapshot id"), v2: id("Newer snapshot id") }, + run: (c, a) => c.compareVerificationSnapshots(a.verification_id, a.v1, a.v2), + }); + add({ + name: "diff_baseline_verification_vs_current", + description: "Diff of a verification as a baseline recorded it against the verification now.", + shape: { baseline_id: id("Baseline id"), verification_id: id("Verification id") }, + run: (c, a) => c.diffBaselineVerificationVsCurrent(a.baseline_id, a.verification_id), + }); + } + // ----- DW: draft_write ----- + if (ctx.mode === "draft_write") { + add({ + name: "unlink_use_case_element", + description: "Delete one use case link (a requirement or verification linked to the use case or one of its steps). Get link ids from list_use_case_links. Deletes only the link, never the use case, requirement or verification.", + shape: { use_case_id: id("Use case id"), link_id: id("Link id") }, + write: true, + destructive: true, + run: (c, a) => c.deleteUseCaseLink(a.use_case_id, a.link_id), + }); + add({ + name: "clear_use_case_link_suspect", + description: "Clear the suspect flag of a use case link after checking the linked requirement still fits the use case.", + shape: { use_case_id: id("Use case id"), link_id: id("Link id") }, + write: true, + run: (c, a) => c.clearUseCaseLinkSuspect(a.use_case_id, a.link_id), + }); + add({ + name: "create_use_case_comment", + description: "Comment on a use case, on one version (use_case_version_id) or one step/condition (element_id). @username mentions of project members notify them. Retrying with the same idempotency_key does not post twice.", + shape: { + idempotency_key: operationKey, + use_case_id: id("Use case id"), + body: z.string().min(1), + use_case_version_id: id("Version the comment is about").optional(), + element_id: z.string().min(1).optional(), + }, + write: true, + summary: (a) => ({ + use_case_id: a.use_case_id, + body_len: String(a.body ?? "").length, + use_case_version_id: a.use_case_version_id, + element_id: a.element_id, + }), + run: (c, a) => c.createUseCaseComment(a.use_case_id, { body: a.body, use_case_version_id: a.use_case_version_id ?? null, element_id: a.element_id ?? null }, a.idempotency_key ?? randomUUID()), + }); + add({ + name: "discard_use_case_draft", + description: "Discard the shared draft of a use case (unpublished edits are lost; published versions stay). Read the draft first with get_use_case_diff to be sure it is stale.", + shape: { use_case_id: id("Use case id") }, + write: true, + destructive: true, + run: (c, a) => c.discardUseCaseDraft(a.use_case_id), + }); + add({ + name: "set_verification_field", + description: "Change one field of a verification without resending the rest. Fields: name, description, source, status_id, reference_code, parent_id, verification_method_id, author_id, reviewer_id. Status changes keep reviewer enforcement.", + shape: { + verification_id: id("Verification id"), + field: z.enum([ + "name", + "description", + "source", + "status_id", + "reference_code", + "parent_id", + "verification_method_id", + "author_id", + "reviewer_id", + ]), + value: z.string().describe("New value; ids as digits. parent_id: empty or 0 clears it."), + }, + write: true, + run: (c, a) => c.setVerificationField(a.verification_id, a.field, a.value), + }); + } + // ----- TW: MARREQ_TRACE_WRITE ----- + if (ctx.traceWrite) { + add({ + name: "link_requirements", + description: "Link two requirement versions (source derives from / refines / satisfies / depends on / relates to target; see list_requirement_link_types). Both versions must be in the project; cycles are refused. Retrying with the same idempotency_key returns the same link.", + shape: { + idempotency_key: operationKey, + source_version_id: id("Source requirement version id"), + target_version_id: id("Target requirement version id"), + link_type: z.string().min(1), + rationale: z.string().optional(), + }, + write: true, + run: (c, a) => c.createRequirementLink({ + source_version_id: a.source_version_id, + target_version_id: a.target_version_id, + link_type: a.link_type, + rationale: a.rationale ?? null, + }, a.idempotency_key ?? randomUUID()), + }); + add({ + name: "unlink_requirements", + description: "Delete one requirement version link by id (from list_requirement_links). Deletes only the link; baselines taken earlier keep what they recorded.", + shape: { link_id: id("Link id") }, + write: true, + destructive: true, + run: (c, a) => c.deleteRequirementLink(a.link_id), + }); + } +} diff --git a/mcp-server/dist/generated-contract.js b/mcp-server/dist/generated-contract.js new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/mcp-server/dist/generated-contract.js @@ -0,0 +1 @@ +export {}; diff --git a/mcp-server/dist/index.js b/mcp-server/dist/index.js index f19cf540..4e1efafb 100644 --- a/mcp-server/dist/index.js +++ b/mcp-server/dist/index.js @@ -7,6 +7,10 @@ import { z } from "zod"; import { contextAllowsReadExtended, loadContext } from "./context.js"; import { MarreqAuthenticationError, MarreqClient } from "./client.js"; import { loadTransportConfig, startRemoteServer } from "./remote.js"; +import { registerGapTools } from "./gap-tools.js"; +import { paginate, paginationFields } from "./pagination.js"; +import { registerResources } from "./resources.js"; +import { registerPrompts } from "./prompts.js"; /** Requirement statements and verification descriptions use "Marreq statement Markdown" (marreq-core/src/rich_text.rs). */ const DESCRIPTION_FORMAT = "Verification description in Marreq statement Markdown: paragraphs separated by a blank line, '- ' bulleted and '1. ' numbered lists, **bold**, *italic*, `code`, [label](https://...) links (http/https/mailto only). Max 2000 bytes including markup."; const STATEMENT_FORMAT = "Requirement statement in Marreq statement Markdown: paragraphs separated by a blank line, '- ' bulleted and '1. ' numbered lists, **bold**, *italic*, `code`, [label](https://...) links (http/https/mailto only). Max 2000 bytes including markup."; @@ -129,6 +133,31 @@ export function createMarreqServer(ctx) { edit_use_case_draft: ["use_cases:write"], publish_use_case_draft: ["use_cases:write"], link_use_case_element: ["use_cases:write"], + // Tool gaps (issue #416) + list_requirement_link_types: ["traceability:read"], + list_requirement_links: ["traceability:read"], + link_requirements: ["traceability:write"], + unlink_requirements: ["traceability:write"], + unlink_use_case_element: ["use_cases:write"], + clear_use_case_link_suspect: ["use_cases:write"], + list_linked_use_cases: ["use_cases:read"], + ask_requirements: ["requirements:read"], + list_reviewers: ["projects:read"], + get_my_permissions: ["projects:read"], + list_saved_views: ["requirements:read"], + list_attachments: ["requirements:read", "verifications:read"], + get_attention: ["projects:read"], + list_use_case_versions: ["use_cases:read"], + list_use_case_comments: ["use_cases:read"], + create_use_case_comment: ["use_cases:write"], + discard_use_case_draft: ["use_cases:write"], + get_close_out: ["requirements:read"], + impacted_tests: ["requirements:read", "verifications:read"], + list_verification_snapshots: ["verifications:read"], + compare_verification_snapshots: ["verifications:read"], + set_verification_field: ["verifications:write"], + get_baseline_contents: ["baselines:read"], + diff_baseline_verification_vs_current: ["verifications:read", "baselines:read"], }; const originalRegisterTool = server.registerTool.bind(server); const unsafeRegisterTool = originalRegisterTool; @@ -142,6 +171,8 @@ export function createMarreqServer(ctx) { "create_verification", "create_use_case", "publish_use_case_draft", + "link_requirements", + "create_use_case_comment", ].includes(name); const annotations = { readOnlyHint: !mutating, @@ -172,11 +203,11 @@ export function createMarreqServer(ctx) { }); server.registerTool("list_projects", { description: "List only projects accessible to the authenticated Marreq user.", - inputSchema: z.object({}), + inputSchema: z.object({ ...paginationFields }), annotations: { readOnlyHint: true }, - }, async () => { + }, async (args) => { const out = await withAudit(client, "list_projects", "{}", false, () => client.listProjects()); - return { content: [jsonContent(out)] }; + return { content: [jsonContent(paginate(out, args ?? {}))] }; }); server.registerTool("get_requirement", { description: "Get a requirement by id (project-scoped, with trace summary)", @@ -188,21 +219,35 @@ export function createMarreqServer(ctx) { return { content: [jsonContent(out)] }; }); server.registerTool("list_requirements", { - description: "List requirements in the project with optional filters (approval_state, has_tests)", + description: "List requirements in the project with optional filters (approval_state, has_tests, status_id, category_id, free-text q, a saved view_id from list_saved_views) and sorting (sort_column, sort_dir). Pass limit (and cursor from next_cursor) to page; without them the whole list is returned.", inputSchema: z.object({ ...projectField, filter: z .object({ approval_state: z.enum(["draft", "reviewed", "approved"]).optional(), has_tests: z.boolean().optional(), + status_id: z.number().int().positive().optional(), + category_id: z.number().int().positive().optional(), + q: z.string().optional().describe("Text to find in code, title or statement"), + view_id: z.number().int().positive().optional().describe("Saved view id (list_saved_views)"), }) .optional(), + sort_column: z.string().optional(), + sort_dir: z.enum(["asc", "desc"]).optional(), + ...paginationFields, }), }, async (args) => { const f = args?.filter; const toolClient = forProject(args?.project_id); - const out = await withAudit(toolClient, "list_requirements", JSON.stringify(args ?? {}), false, () => toolClient.listRequirements(f?.approval_state, f?.has_tests)); - return { content: [jsonContent(out)] }; + const out = await withAudit(toolClient, "list_requirements", JSON.stringify(args ?? {}), false, () => toolClient.listRequirements(f?.approval_state, f?.has_tests, { + status_id: f?.status_id, + category_id: f?.category_id, + q: f?.q, + view_id: f?.view_id, + sort_column: args?.sort_column, + sort_dir: args?.sort_dir, + })); + return { content: [jsonContent(paginate(out, args ?? {}))] }; }); server.registerTool("get_versions", { description: "Get version history for a requirement", @@ -311,11 +356,12 @@ export function createMarreqServer(ctx) { q: z.string().optional().describe("Filter on code, title or actor"), approval_state: z.enum(["draft", "reviewed", "approved", "unpublished"]).optional(), has_suspect: z.boolean().optional().describe("Only use cases with suspect links"), + ...paginationFields, }), - }, async ({ project_id, q, approval_state, has_suspect }) => { + }, async ({ project_id, q, approval_state, has_suspect, limit, cursor }) => { const toolClient = forProject(project_id); const out = await withAudit(toolClient, "list_use_cases", JSON.stringify({ q, approval_state, has_suspect }), false, () => toolClient.listUseCases({ q, approval_state, has_suspect })); - return { content: [jsonContent(out)] }; + return { content: [jsonContent(paginate(out, { limit, cursor }))] }; }); server.registerTool("get_use_case", { description: "Get a use case: its current version (or version_id) as a marreq.use-case.v1 document with computed labels (1, 2, A3, A3.1, E4), and the shared draft (document, labels, revision) when one exists. Element ids such as s_… and b_… are stable; labels are not.", @@ -347,21 +393,21 @@ export function createMarreqServer(ctx) { }); server.registerTool("list_use_case_links", { description: "Requirements and verifications linked to a use case or to one of its steps, with suspect flags (a linked requirement changed since the link was made or cleared).", - inputSchema: z.object({ ...projectField, use_case_id: z.number().int().positive() }), - }, async ({ project_id, use_case_id }) => { + inputSchema: z.object({ ...projectField, use_case_id: z.number().int().positive(), ...paginationFields }), + }, async ({ project_id, use_case_id, limit, cursor }) => { const toolClient = forProject(project_id); const out = await withAudit(toolClient, "list_use_case_links", JSON.stringify({ use_case_id }), false, () => toolClient.listUseCaseLinks(use_case_id)); - return { content: [jsonContent(out)] }; + return { content: [jsonContent(paginate(out, { limit, cursor }))] }; }); // read_extended / draft_write: extra read tools (catalog, verifications, audit, matrix read, …) if (contextAllowsReadExtended(ctx)) { server.registerTool("list_verifications", { description: "List verifications (tests) in the project. Requires MARREQ_MODE=read_extended or draft_write.", - inputSchema: z.object({ ...projectField }), - }, async ({ project_id }) => { + inputSchema: z.object({ ...projectField, ...paginationFields }), + }, async ({ project_id, limit, cursor }) => { const toolClient = forProject(project_id); const out = await withAudit(toolClient, "list_verifications", "{}", false, () => toolClient.listVerificationsByProject()); - return { content: [jsonContent(out)] }; + return { content: [jsonContent(paginate(out, { limit, cursor }))] }; }); server.registerTool("get_verification", { description: "Get one verification by id. The record must belong to MARREQ_PROJECT_ID. Requires read_extended or draft_write mode.", @@ -381,11 +427,11 @@ export function createMarreqServer(ctx) { }); server.registerTool("list_baselines", { description: "List baselines for the project (metadata only). Use get_baseline for full snapshot.", - inputSchema: z.object({ ...projectField }), - }, async ({ project_id }) => { + inputSchema: z.object({ ...projectField, ...paginationFields }), + }, async ({ project_id, limit, cursor }) => { const toolClient = forProject(project_id); const out = await withAudit(toolClient, "list_baselines", "{}", false, () => toolClient.listBaselinesByProject()); - return { content: [jsonContent(out)] }; + return { content: [jsonContent(paginate(out, { limit, cursor }))] }; }); server.registerTool("get_requirement_activity", { description: "Audit log entries for a requirement (create/update history with field summaries).", @@ -411,12 +457,13 @@ export function createMarreqServer(ctx) { ...projectField, requirement_id: z.string(), requirement_version_id: z.number().optional(), + ...paginationFields, }), - }, async ({ project_id, requirement_id, requirement_version_id }) => { + }, async ({ project_id, requirement_id, requirement_version_id, limit, cursor }) => { const id = parseInt(requirement_id, 10); const toolClient = forProject(project_id); const out = await withAudit(toolClient, "list_requirement_comments", JSON.stringify({ requirement_id, requirement_version_id }), false, () => toolClient.listRequirementComments(id, requirement_version_id ?? null)); - return { content: [jsonContent(out)] }; + return { content: [jsonContent(paginate(out, { limit, cursor }))] }; }); server.registerTool("get_verification_matrix", { description: "Requirement ids linked to a verification in the traceability matrix (read).", @@ -531,6 +578,7 @@ export function createMarreqServer(ctx) { reviewer_id: z.number().optional(), category_id: z.number().optional(), applicability_id: z.number().optional(), + justification: z.string().optional().describe("Why the requirement exists; an empty string clears it."), custom_fields: z .array(z.object({ field_id: z.number(), value: z.string() })) .optional(), @@ -708,6 +756,18 @@ export function createMarreqServer(ctx) { return { content: [jsonContent(out)] }; }); } + registerGapTools({ + server: server, + ctx, + client, + forProject, + projectField, + operationKey, + withAudit, + jsonContent, + }); + registerResources(server, { ctx, client, withAudit }); + registerPrompts(server); return server; } async function main() { diff --git a/mcp-server/dist/pagination.js b/mcp-server/dist/pagination.js new file mode 100644 index 00000000..3cf3ef59 --- /dev/null +++ b/mcp-server/dist/pagination.js @@ -0,0 +1,49 @@ +import { z } from "zod"; +export const MAX_PAGE_SIZE = 200; +export const DEFAULT_PAGE_SIZE = 50; +/** Optional `limit` and opaque `cursor` inputs shared by list tools. */ +export const paginationFields = { + limit: z + .number() + .int() + .min(1) + .max(MAX_PAGE_SIZE) + .optional() + .describe(`Page size (max ${MAX_PAGE_SIZE}). Pass next_cursor from the previous page as cursor to continue.`), + cursor: z.string().optional().describe("Opaque cursor from a previous page's next_cursor."), +}; +function encodeCursor(offset) { + return Buffer.from(JSON.stringify({ o: offset }), "utf8").toString("base64url"); +} +function decodeCursor(cursor) { + try { + const { o } = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")); + if (typeof o === "number" && Number.isInteger(o) && o >= 0) + return o; + } + catch { + // fall through to the error below + } + throw new Error("Invalid cursor: pass the next_cursor value of a previous page unchanged"); +} +/** + * Slices a list response in the MCP server (the backend still returns the whole + * list). Tools that predate paging keep their full output until the caller sends + * `limit` or `cursor`; new list tools pass `defaultLimit` to page from the start. + * Anything that is not an array is returned as is. + */ +export function paginate(data, args, defaultLimit) { + if (!Array.isArray(data)) + return data; + if (args.limit == null && args.cursor == null && defaultLimit == null) + return data; + const limit = Math.min(args.limit ?? defaultLimit ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE); + const offset = args.cursor != null ? decodeCursor(args.cursor) : 0; + const end = offset + limit; + const page = { + items: data.slice(offset, end), + total: data.length, + next_cursor: end < data.length ? encodeCursor(end) : null, + }; + return page; +} diff --git a/mcp-server/dist/prompts.js b/mcp-server/dist/prompts.js new file mode 100644 index 00000000..8b6752a0 --- /dev/null +++ b/mcp-server/dist/prompts.js @@ -0,0 +1,52 @@ +/** + * MCP prompts (issue #416). Each prompt names the tools it expects, so a client + * can plan the calls. They only describe work; no prompt writes anything. + */ +import { z } from "zod"; +const projectArg = z.string().regex(/^\d+$/).describe("Project id from list_projects"); +const requirementArg = z.string().regex(/^\d+$/).describe("Requirement id"); +function user(text) { + return { messages: [{ role: "user", content: { type: "text", text } }] }; +} +export const PROMPT_NAMES = [ + "review_requirement", + "derive_children", + "coverage_gap_analysis", + "use_case_from_requirement", +]; +export function registerPrompts(server) { + server.registerPrompt("review_requirement", { + title: "Review a requirement", + description: "Check one requirement for ambiguity, testability, duplicates, traceability and suspect links.", + argsSchema: { project_id: projectArg, requirement_id: requirementArg }, + }, ({ project_id, requirement_id }) => user(`Review requirement ${requirement_id} in project ${project_id}; do not change anything.\n` + + `Tools: get_requirement, get_versions, trace_up, trace_down, get_close_out (with requirement_id), ` + + `list_linked_use_cases, semantic_search_requirements.\n` + + `Report: is it atomic, unambiguous and testable; does a similar requirement already exist; does it have a parent ` + + `and a passing verification; are any links suspect. Give findings first, then proposed edits for me to approve.`)); + server.registerPrompt("derive_children", { + title: "Derive child requirements", + description: "Propose atomic child requirements for a parent, without creating them.", + argsSchema: { project_id: projectArg, requirement_id: requirementArg }, + }, ({ project_id, requirement_id }) => user(`Propose child requirements for requirement ${requirement_id} in project ${project_id}.\n` + + `Tools: get_requirement, list_project_catalog, semantic_search_requirements, list_requirement_link_types.\n` + + `Write each child as one atomic, testable statement, check it against existing requirements for overlap, ` + + `and say which link type ties it to the parent. List them for my approval; create them with ` + + `create_requirement (parent_links) only after I say so.`)); + server.registerPrompt("coverage_gap_analysis", { + title: "Coverage gap analysis", + description: "Find requirements without verification and tests made stale by changes.", + argsSchema: { project_id: projectArg }, + }, ({ project_id }) => user(`Analyse verification coverage in project ${project_id}.\n` + + `Tools: coverage_report, get_close_out, list_requirements (has_tests=false), impacted_tests, get_verification_matrix.\n` + + `Rank the gaps by approval state and importance, list suspect tests, and suggest which verification would close ` + + `each gap. Do not link or create anything until I choose.`)); + server.registerPrompt("use_case_from_requirement", { + title: "Use case from a requirement", + description: "Draft a use case that exercises a requirement, and link it.", + argsSchema: { project_id: projectArg, requirement_id: requirementArg }, + }, ({ project_id, requirement_id }) => user(`Draft a use case for requirement ${requirement_id} in project ${project_id}.\n` + + `Tools: get_requirement, list_use_cases, create_use_case, edit_use_case_draft, link_use_case_element.\n` + + `Check list_use_cases for an existing one first. Outline the actor, preconditions, main steps and alternatives, ` + + `show me the outline, then create the draft and link it to the requirement. Publish only if I ask.`)); +} diff --git a/mcp-server/dist/resources.js b/mcp-server/dist/resources.js new file mode 100644 index 00000000..7b41c7fd --- /dev/null +++ b/mcp-server/dist/resources.js @@ -0,0 +1,49 @@ +/** + * MCP resources (issue #416): requirements, use cases and the catalog as + * addressable documents, and the workflow guide. Each read reuses the client + * method and backend scope of the matching tool. + */ +import { readFile } from "node:fs/promises"; +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +const JSON_TYPE = "application/json"; +function one(value) { + return Array.isArray(value) ? value[0] ?? "" : value ?? ""; +} +function positiveInt(raw, what) { + const n = Number(raw); + if (!Number.isInteger(n) || n <= 0) + throw new Error(`Invalid ${what} in resource URI: ${raw}`); + return n; +} +async function guideText() { + try { + return await readFile(new URL("../MARREQ_SKILL.md", import.meta.url), "utf8"); + } + catch { + return "The Marreq workflow guide (MARREQ_SKILL.md) is not available in this deployment."; + } +} +export function registerResources(server, { client, withAudit }) { + const json = (uri, data) => ({ + contents: [{ uri: uri.href, mimeType: JSON_TYPE, text: JSON.stringify(data, null, 2) }], + }); + // The project in the URI is passed through; the backend still checks that the + // caller has the scope and project permission. + const read = async (uri, vars, name, fn) => { + const projectClient = client.withProject(positiveInt(one(vars.project_id), "project id")); + const out = await withAudit(projectClient, `resource:${name}`, JSON.stringify({ uri: uri.href }), false, () => fn(projectClient)); + return json(uri, out); + }; + server.registerResource("requirement", new ResourceTemplate("marreq://projects/{project_id}/requirements/{id}", { list: undefined }), { title: "Requirement", description: "One requirement with its trace summary.", mimeType: JSON_TYPE }, (uri, vars) => read(uri, vars, "requirement", (c) => c.getRequirement(positiveInt(one(vars.id), "requirement id")))); + server.registerResource("use-case", new ResourceTemplate("marreq://projects/{project_id}/use-cases/{id}", { list: undefined }), { title: "Use case", description: "The current version of one use case.", mimeType: JSON_TYPE }, (uri, vars) => read(uri, vars, "use-case", (c) => c.getUseCase(positiveInt(one(vars.id), "use case id")))); + server.registerResource("catalog", new ResourceTemplate("marreq://projects/{project_id}/catalog", { list: undefined }), { + title: "Project catalog", + description: "Statuses, categories, applicability, verification methods and users of the project.", + mimeType: JSON_TYPE, + }, (uri, vars) => read(uri, vars, "catalog", (c) => c.listProjectCatalog())); + server.registerResource("guide", "marreq://guide", { + title: "Marreq workflow guide", + description: "How to review, create and modify requirements and use cases with the Marreq tools.", + mimeType: "text/markdown", + }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: "text/markdown", text: await guideText() }] })); +} diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index 28094b84..3c59f108 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -228,9 +228,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -249,9 +246,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -270,9 +264,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -291,9 +282,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -312,9 +300,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -333,9 +318,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -954,17 +936,17 @@ } }, "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", "dependencies": { "bytes": "^3.1.2", - "content-type": "^2.0.0", + "content-type": "^2.1.0", "debug": "^4.4.3", "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "^0.7.3", "on-finished": "^2.4.1", - "qs": "^6.15.2", + "qs": "^6.16.1", "raw-body": "^3.0.2", "type-is": "^2.1.0" }, @@ -1326,9 +1308,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "funding": [ { "type": "github", @@ -1515,9 +1497,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -1535,9 +1517,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", "engines": { "node": ">= 12" } @@ -1727,9 +1709,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1752,9 +1731,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1777,9 +1753,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1802,9 +1775,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2110,9 +2080,9 @@ } }, "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.0.tgz", + "integrity": "sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==", "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" diff --git a/mcp-server/src/client.ts b/mcp-server/src/client.ts index 6e083172..cb3b831d 100644 --- a/mcp-server/src/client.ts +++ b/mcp-server/src/client.ts @@ -1,10 +1,9 @@ -import type { - McpPrincipalResponse, - use_cases_UseCaseRow_response as UseCaseRow, - use_cases_UseCaseDetail_response as UseCaseDetail, - use_cases_DraftView_response as UseCaseDraft, - use_cases_DraftChangeView_response as UseCaseDraftChange, -} from "./generated-contract.js"; +import type { components } from "./generated-contract.js"; +type McpPrincipalResponse = components["schemas"]["McpPrincipalResponse"]; +type UseCaseRow = components["schemas"]["use_cases_UseCaseRow_response"]; +type UseCaseDetail = components["schemas"]["use_cases_UseCaseDetail_response"]; +type UseCaseDraft = components["schemas"]["use_cases_DraftView_response"]; +type UseCaseDraftChange = components["schemas"]["use_cases_DraftChangeView_response"]; import type { SessionContext } from "./context.js"; import { currentBearer } from "./credentials.js"; diff --git a/mcp-server/src/generated-contract.ts b/mcp-server/src/generated-contract.ts index 5e16cda6..eb559813 100644 --- a/mcp-server/src/generated-contract.ts +++ b/mcp-server/src/generated-contract.ts @@ -1,450 +1,438 @@ -// Generated by scripts/contracts/generate.py from docs/developer/openapi.yaml. -// Do not edit; this file carries wire types, not a second contract. - -export type ApiError = { - "status": number; - "error": string; - "message": string; - [key: string]: unknown; -}; - -export type BuildInfo = { - "backend_version": string; - "backend_git_sha": string; - "deployment_mode": "server" | "cloud"; - "frontend_compatibility": { - "min_version": string; - "max_version": string; - [key: string]: unknown; -}; - [key: string]: unknown; -}; - -export type DeploymentInfo = { - "mode": "server" | "cloud"; - "allows_self_registration": boolean; - "requires_email_verification": boolean; - "allows_admin_promotion": boolean; - "assigns_personal_workspace": boolean; - "allows_self_administered_user_creation": boolean; - "allows_database_backup": boolean; - [key: string]: unknown; -}; - -export type McpPrincipalResponse = { - "user_id": number; - "authentication_type": string; - "principal_id": string; - "client_id": (string | null); - "grant_id": (number | null); - [key: string]: unknown; -}; - -export type UseCaseBranch = { - "id": string; - "kind": "alternative" | "exception"; - "title": string; - "from_step": string; - "end": UseCaseBranchEnd; - "steps"?: Array; -}; - -export type UseCaseBranchEnd = ({ - "type": "rejoin"; - "step": string; -} | { - "type": "end"; - "outcome": "success" | "failure"; -}); - -export type UseCaseBranchResponse = { - "id": string; - "kind": "alternative" | "exception"; - "title": string; - "from_step": string; - "end": UseCaseBranchEnd; - "steps": Array; -}; - -export type UseCaseDependents = ("cascade" | { - "rebranch_to": string; - [key: string]: unknown; -}); - -export type UseCaseDiff = { - "from": (UseCaseDiffEnd | null); - "to": UseCaseDiffEnd; - "summary": { - "added": number; - "removed": number; - "edited": number; - "moved": number; - "rebranched": number; - [key: string]: unknown; -}; - "header": { - [key: string]: { - "old": (string | null | Array); - "new": (string | null | Array); - [key: string]: unknown; -}; -}; - "elements": Array; - [key: string]: unknown; -}; - -export type UseCaseDiffEnd = { - "version_id": (number | null); - "version_number": (number | null); - "draft_revision": (number | null); - [key: string]: unknown; -}; - -export type UseCaseDiffSide = { - "list": "preconditions" | "main" | "postconditions" | "branch" | "branches"; - "branch_id"?: string; - "label": (string | null); - "text": string; - "from_step"?: string; - "end"?: UseCaseBranchEnd; - "kind"?: "alternative" | "exception"; - [key: string]: unknown; -}; - -export type UseCaseDocument = { - "schema"?: "marreq.use-case.v1"; - "title": string; - "primary_actor"?: (string | null); - "secondary_actors"?: Array; - "summary"?: (string | null); - "preconditions"?: Array; - "main"?: Array; - "branches"?: Array; - "postconditions"?: Array; -}; - -export type UseCaseDocumentResponse = { - "schema": "marreq.use-case.v1"; - "title": string; - "primary_actor": (string | null); - "secondary_actors": Array; - "summary": (string | null); - "preconditions": Array; - "main": Array; - "branches": Array; - "postconditions": Array; -}; - -export type UseCaseEffect = ({ - "type": "relabelled"; - "id": string; - "from": string; - "to": string; - [key: string]: unknown; -} | { - "type": "removed"; - "id": string; - [key: string]: unknown; -} | { - "type": "rebranched"; - "id": string; - "from_step": string; - [key: string]: unknown; -} | { - "type": "created"; - "id": string; - [key: string]: unknown; -}); - -export type UseCaseElement = { - "id": string; - "text": string; -}; - -export type UseCaseElementDiff = { - "id": string; - "kind": "precondition" | "postcondition" | "step" | "branch"; - "changes": Array<"added" | "removed" | "edited" | "moved" | "rebranched">; - "old": (UseCaseDiffSide | null); - "new": (UseCaseDiffSide | null); - "text_diff"?: Array<{ - "op": "equal" | "delete" | "insert"; - "text": string; - [key: string]: unknown; -}>; - [key: string]: unknown; -}; - -export type UseCaseListRef = ("preconditions" | "main" | "postconditions" | { - "branch": string; - [key: string]: unknown; -}); - -export type UseCaseNewBranch = { - "id"?: (string | null); - "kind": "alternative" | "exception"; - "title": string; - "from_step": string; - "end": UseCaseBranchEnd; - "steps"?: Array; -}; - -export type UseCaseNewElement = { - "id"?: (string | null); - "text": string; -}; - -export type UseCaseOperation = ({ - "op": "set_header"; - "title"?: (string | null); - "primary_actor"?: (string | null); - "secondary_actors"?: (Array | null); - "summary"?: (string | null); -} | { - "op": "add_element"; - "list": UseCaseListRef; - "after"?: (string | null); - "element": UseCaseNewElement; -} | { - "op": "update_text"; - "id": string; - "text": string; -} | { - "op": "move_element"; - "id": string; - "list"?: (UseCaseListRef | null); - "after"?: (string | null); -} | { - "op": "remove_element"; - "id": string; - "dependents"?: (UseCaseDependents | null); -} | { - "op": "add_branch"; - "branch": UseCaseNewBranch; -} | { - "op": "update_branch"; - "id": string; - "title"?: (string | null); - "kind"?: ("alternative" | "exception" | null); - "from_step"?: (string | null); - "end"?: (UseCaseBranchEnd | null); -} | { - "op": "remove_branch"; - "id": string; -}); - -export type UseCasePublishResponse = { - "version": use_cases_VersionView_response; - "removed_links": Array; - [key: string]: unknown; -}; - -export type entities_User_response = { - "id": number; - "username": string; - "name": string; - "email": string; - "creation_date": string; - "last_login": string; - "is_admin": boolean; - "email_verified": boolean; - [key: string]: unknown; -}; - -export type external_ProviderDiscovery_response = { - "password_enabled": boolean; - "external": Array; - [key: string]: unknown; -}; - -export type external_PublicAuthProvider_response = { - "id": string; - "display_name": string; - [key: string]: unknown; -}; - -export type use_cases_BaselineUseCaseView_response = { - "use_case_id": number; - "reference_code": string; - "title": string; - "primary_actor": (string | null); - "version_id": number; - "version_number": number; - "approval_state": string; - "current_version_id": (number | null); - "current_version_number": (number | null); - "link_count": number; - [key: string]: unknown; -}; - -export type use_cases_ClearSuspectResponse_response = { - "status": string; - "cleared": boolean; - [key: string]: unknown; -}; - -export type use_cases_DraftChangeView_response = { - "use_case_id": number; - "revision": number; - "base_version_id": (number | null); - "updated_by": (number | null); - "updated_by_name": (string | null); - "updated_at": string; - "document": UseCaseDocumentResponse; - "labels": { - [key: string]: string; -}; - "effects": Array; - [key: string]: unknown; -}; - -export type use_cases_DraftSummary_response = { - "revision": number; - "base_version_id": (number | null); - "updated_by": (number | null); - "updated_by_name": (string | null); - "updated_at": string; - [key: string]: unknown; -}; - -export type use_cases_DraftView_response = { - "use_case_id": number; - "revision": number; - "base_version_id": (number | null); - "updated_by": (number | null); - "updated_by_name": (string | null); - "updated_at": string; - "document": UseCaseDocumentResponse; - "labels": { - [key: string]: string; -}; - [key: string]: unknown; -}; - -export type use_cases_LinkView_response = { - "id": number; - "element_id": (string | null); - "element_label": (string | null); - "target_type": "requirement" | "verification"; - "requirement_id": (number | null); - "verification_id": (number | null); - "target_reference_code": string; - "target_title": string; - "rationale": (string | null); - "suspect": boolean; - "suspect_at": (string | null); - "suspect_reason": (string | null); - "linked_requirement_version_id": (number | null); - "current_requirement_version_id": (number | null); - "created_by": (number | null); - "created_at": string; - [key: string]: unknown; -}; - -export type use_cases_NextCode_response = { - "reference_code": string; - [key: string]: unknown; -}; - -export type use_cases_PutDraftRequest_request = { - "base_revision"?: (number | null); - "document": UseCaseDocument; - [key: string]: unknown; -}; - -export type use_cases_SetApprovalRequest_request = { - "state": string; - [key: string]: unknown; -}; - -export type use_cases_UseCaseCommentView_response = { - "id": number; - "use_case_id": number; - "use_case_version_id": (number | null); - "element_id": (string | null); - "element_label": (string | null); - "author_id": number; - "author_name": string; - "body": string; - "created_at": string; - [key: string]: unknown; -}; - -export type use_cases_UseCaseDetail_response = { - "id": number; - "project_id": number; - "reference_code": string; - "title": string; - "created_by": (number | null); - "created_at": string; - "updated_at": string; - "current_version": (use_cases_VersionView_response | null); - "draft": (use_cases_DraftSummary_response | null); - [key: string]: unknown; -}; - -export type use_cases_UseCaseRef_response = { - "use_case_id": number; - "reference_code": string; - "title": string; - "link_id": number; - "element_id": (string | null); - "element_label": (string | null); - "suspect": boolean; - [key: string]: unknown; -}; - -export type use_cases_UseCaseRow_response = { - "id": number; - "reference_code": string; - "title": string; - "primary_actor": (string | null); - "step_count": number; - "alternative_count": number; - "exception_count": number; - "approval_state": (string | null); - "current_version_number": (number | null); - "has_draft": boolean; - "link_count": number; - "suspect_count": number; - "updated_at": string; - [key: string]: unknown; -}; - -export type use_cases_VersionSummary_response = { - "id": number; - "version_number": number; - "approval_state": string; - "title": string; - "primary_actor": (string | null); - "change_note": (string | null); - "created_by": (number | null); - "created_by_name": (string | null); - "created_at": string; - "reviewed_by": (number | null); - "reviewed_at": (string | null); - "approved_by": (number | null); - "approved_at": (string | null); - [key: string]: unknown; -}; - -export type use_cases_VersionView_response = { - "id": number; - "version_number": number; - "approval_state": string; - "title": string; - "primary_actor": (string | null); - "change_note": (string | null); - "created_by": (number | null); - "created_by_name": (string | null); - "created_at": string; - "reviewed_by": (number | null); - "reviewed_at": (string | null); - "approved_by": (number | null); - "approved_at": (string | null); - "document": UseCaseDocumentResponse; - "labels": { - [key: string]: string; -}; - [key: string]: unknown; -}; +// Generated by scripts/generate-api-types.mjs; do not edit. +export interface components { + schemas: { + ApiError: { + error: string; + message: string; + status: number; + } & { + [key: string]: unknown; + }; + McpPrincipalResponse: { + authentication_type: string; + client_id: string | null; + grant_id: number | null; + principal_id: string; + user_id: number; + } & { + [key: string]: unknown; + }; + UseCaseBranch: { + end: components["schemas"]["UseCaseBranchEnd"]; + from_step: string; + id: string; + /** @enum {string} */ + kind: "alternative" | "exception"; + steps?: components["schemas"]["UseCaseElement"][]; + title: string; + }; + UseCaseBranchEnd: { + step: string; + /** @constant */ + type: "rejoin"; + } | { + /** @enum {string} */ + outcome: "success" | "failure"; + /** @constant */ + type: "end"; + }; + UseCaseBranchResponse: { + end: components["schemas"]["UseCaseBranchEnd"]; + from_step: string; + id: string; + /** @enum {string} */ + kind: "alternative" | "exception"; + steps: components["schemas"]["UseCaseElement"][]; + title: string; + }; + UseCaseDependents: "cascade" | ({ + rebranch_to: string; + } & { + [key: string]: unknown; + }); + UseCaseDiff: { + elements: components["schemas"]["UseCaseElementDiff"][]; + from: components["schemas"]["UseCaseDiffEnd"] | null; + header: { + [key: string]: { + new: string | null | string[]; + old: string | null | string[]; + } & { + [key: string]: unknown; + }; + }; + summary: { + added: number; + edited: number; + moved: number; + rebranched: number; + removed: number; + } & { + [key: string]: unknown; + }; + to: components["schemas"]["UseCaseDiffEnd"]; + } & { + [key: string]: unknown; + }; + UseCaseDiffEnd: { + draft_revision: number | null; + version_id: number | null; + version_number: number | null; + } & { + [key: string]: unknown; + }; + UseCaseDiffSide: { + branch_id?: string; + end?: components["schemas"]["UseCaseBranchEnd"]; + from_step?: string; + /** @enum {string} */ + kind?: "alternative" | "exception"; + label: string | null; + /** @enum {string} */ + list: "preconditions" | "main" | "postconditions" | "branch" | "branches"; + text: string; + } & { + [key: string]: unknown; + }; + UseCaseDocument: { + branches?: components["schemas"]["UseCaseBranch"][]; + main?: components["schemas"]["UseCaseElement"][]; + postconditions?: components["schemas"]["UseCaseElement"][]; + preconditions?: components["schemas"]["UseCaseElement"][]; + primary_actor?: string | null; + /** @constant */ + schema?: "marreq.use-case.v1"; + secondary_actors?: string[]; + summary?: string | null; + title: string; + }; + UseCaseDocumentResponse: { + branches: components["schemas"]["UseCaseBranchResponse"][]; + main: components["schemas"]["UseCaseElement"][]; + postconditions: components["schemas"]["UseCaseElement"][]; + preconditions: components["schemas"]["UseCaseElement"][]; + primary_actor: string | null; + /** @constant */ + schema: "marreq.use-case.v1"; + secondary_actors: string[]; + summary: string | null; + title: string; + }; + UseCaseEffect: ({ + from: string; + id: string; + to: string; + /** @constant */ + type: "relabelled"; + } & { + [key: string]: unknown; + }) | ({ + id: string; + /** @constant */ + type: "removed"; + } & { + [key: string]: unknown; + }) | ({ + from_step: string; + id: string; + /** @constant */ + type: "rebranched"; + } & { + [key: string]: unknown; + }) | ({ + id: string; + /** @constant */ + type: "created"; + } & { + [key: string]: unknown; + }); + UseCaseElement: { + id: string; + text: string; + }; + UseCaseElementDiff: { + changes: ("added" | "removed" | "edited" | "moved" | "rebranched")[]; + id: string; + /** @enum {string} */ + kind: "precondition" | "postcondition" | "step" | "branch"; + new: components["schemas"]["UseCaseDiffSide"] | null; + old: components["schemas"]["UseCaseDiffSide"] | null; + text_diff?: ({ + /** @enum {string} */ + op: "equal" | "delete" | "insert"; + text: string; + } & { + [key: string]: unknown; + })[]; + } & { + [key: string]: unknown; + }; + UseCaseListRef: ("preconditions" | "main" | "postconditions") | ({ + branch: string; + } & { + [key: string]: unknown; + }); + UseCaseNewBranch: { + end: components["schemas"]["UseCaseBranchEnd"]; + from_step: string; + id?: string | null; + /** @enum {string} */ + kind: "alternative" | "exception"; + steps?: components["schemas"]["UseCaseNewElement"][]; + title: string; + }; + UseCaseNewElement: { + id?: string | null; + text: string; + }; + UseCaseOperation: { + /** @constant */ + op: "set_header"; + primary_actor?: string | null; + secondary_actors?: string[] | null; + summary?: string | null; + title?: string | null; + } | { + after?: string | null; + element: components["schemas"]["UseCaseNewElement"]; + list: components["schemas"]["UseCaseListRef"]; + /** @constant */ + op: "add_element"; + } | { + id: string; + /** @constant */ + op: "update_text"; + text: string; + } | { + after?: string | null; + id: string; + list?: components["schemas"]["UseCaseListRef"] | null; + /** @constant */ + op: "move_element"; + } | { + dependents?: components["schemas"]["UseCaseDependents"] | null; + id: string; + /** @constant */ + op: "remove_element"; + } | { + branch: components["schemas"]["UseCaseNewBranch"]; + /** @constant */ + op: "add_branch"; + } | { + end?: components["schemas"]["UseCaseBranchEnd"] | null; + from_step?: string | null; + id: string; + kind?: ("alternative" | "exception") | null; + /** @constant */ + op: "update_branch"; + title?: string | null; + } | { + id: string; + /** @constant */ + op: "remove_branch"; + }; + UseCasePublishResponse: { + removed_links: components["schemas"]["use_cases_LinkView_response"][]; + version: components["schemas"]["use_cases_VersionView_response"]; + } & { + [key: string]: unknown; + }; + use_cases_BaselineUseCaseView_response: { + approval_state: string; + current_version_id: number | null; + current_version_number: number | null; + link_count: number; + primary_actor: string | null; + reference_code: string; + title: string; + use_case_id: number; + version_id: number; + version_number: number; + } & { + [key: string]: unknown; + }; + use_cases_ClearSuspectResponse_response: { + cleared: boolean; + status: string; + } & { + [key: string]: unknown; + }; + use_cases_DraftChangeView_response: { + base_version_id: number | null; + document: components["schemas"]["UseCaseDocumentResponse"]; + effects: components["schemas"]["UseCaseEffect"][]; + labels: { + [key: string]: string; + }; + revision: number; + updated_at: string; + updated_by: number | null; + updated_by_name: string | null; + use_case_id: number; + } & { + [key: string]: unknown; + }; + use_cases_DraftSummary_response: { + base_version_id: number | null; + revision: number; + updated_at: string; + updated_by: number | null; + updated_by_name: string | null; + } & { + [key: string]: unknown; + }; + use_cases_DraftView_response: { + base_version_id: number | null; + document: components["schemas"]["UseCaseDocumentResponse"]; + labels: { + [key: string]: string; + }; + revision: number; + updated_at: string; + updated_by: number | null; + updated_by_name: string | null; + use_case_id: number; + } & { + [key: string]: unknown; + }; + use_cases_LinkView_response: { + created_at: string; + created_by: number | null; + current_requirement_version_id: number | null; + element_id: string | null; + element_label: string | null; + id: number; + linked_requirement_version_id: number | null; + rationale: string | null; + requirement_id: number | null; + suspect: boolean; + suspect_at: string | null; + suspect_reason: string | null; + target_reference_code: string; + target_title: string; + /** @enum {string} */ + target_type: "requirement" | "verification"; + verification_id: number | null; + } & { + [key: string]: unknown; + }; + use_cases_NextCode_response: { + reference_code: string; + } & { + [key: string]: unknown; + }; + use_cases_PutDraftRequest_request: { + base_revision?: number | null; + document: components["schemas"]["UseCaseDocument"]; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseCommentView_response: { + author_id: number; + author_name: string; + body: string; + created_at: string; + element_id: string | null; + element_label: string | null; + id: number; + use_case_id: number; + use_case_version_id: number | null; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseDetail_response: { + created_at: string; + created_by: number | null; + current_version: components["schemas"]["use_cases_VersionView_response"] | null; + draft: components["schemas"]["use_cases_DraftSummary_response"] | null; + id: number; + project_id: number; + reference_code: string; + title: string; + updated_at: string; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseRef_response: { + element_id: string | null; + element_label: string | null; + link_id: number; + reference_code: string; + suspect: boolean; + title: string; + use_case_id: number; + } & { + [key: string]: unknown; + }; + use_cases_UseCaseRow_response: { + alternative_count: number; + approval_state: string | null; + current_version_number: number | null; + exception_count: number; + has_draft: boolean; + id: number; + link_count: number; + primary_actor: string | null; + reference_code: string; + step_count: number; + suspect_count: number; + title: string; + updated_at: string; + } & { + [key: string]: unknown; + }; + use_cases_VersionSummary_response: { + approval_state: string; + approved_at: string | null; + approved_by: number | null; + change_note: string | null; + created_at: string; + created_by: number | null; + created_by_name: string | null; + id: number; + primary_actor: string | null; + reviewed_at: string | null; + reviewed_by: number | null; + title: string; + version_number: number; + } & { + [key: string]: unknown; + }; + use_cases_VersionView_response: { + approval_state: string; + approved_at: string | null; + approved_by: number | null; + change_note: string | null; + created_at: string; + created_by: number | null; + created_by_name: string | null; + document: components["schemas"]["UseCaseDocumentResponse"]; + id: number; + labels: { + [key: string]: string; + }; + primary_actor: string | null; + reviewed_at: string | null; + reviewed_by: number | null; + title: string; + version_number: number; + } & { + [key: string]: unknown; + }; + }; + responses: { + /** @description API error; use cases may include code and structured details. Guard/fairing errors can have an empty body. */ + ApiError: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiError"]; + }; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} diff --git a/mcp-server/src/remote.ts b/mcp-server/src/remote.ts index 4d517c49..40ff9138 100644 --- a/mcp-server/src/remote.ts +++ b/mcp-server/src/remote.ts @@ -1,4 +1,4 @@ -import type { McpPrincipalResponse } from "./generated-contract.js"; +import type { components } from "./generated-contract.js"; import { randomUUID } from "node:crypto"; import type { Server } from "node:http"; import type { Request, Response } from "express"; @@ -10,7 +10,7 @@ import { loadContext, type SessionContext } from "./context.js"; import { MarreqAuthenticationError, MarreqClient } from "./client.js"; import { withBearer } from "./credentials.js"; -type Principal = McpPrincipalResponse; +type Principal = components["schemas"]["McpPrincipalResponse"]; type PrincipalResolution = | { kind: "ok"; principal: Principal } diff --git a/package-lock.json b/package-lock.json index 4372a619..43e773a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "commitizen": "^4.3.2", "cz-conventional-changelog": "^3.3.0", "happy-dom": "^20.14.6", + "openapi-typescript": "7.13.0", "playwright": "^1.64.1", "purgecss": "^8.0.0", "stylelint": "^17.16.0", @@ -513,6 +514,70 @@ "dev": true, "license": "MIT" }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.22", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.22.tgz", + "integrity": "sha512-dEyx4uHh6Pc6ZkdU9Xe8rA4h12Odq6QKU/fqseBn07xwVwskhHiF3KHhqmw5BR+jAiFTvKTmwCafFql/k1pSzA==", + "dev": true, + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.2", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", @@ -629,9 +694,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -650,9 +712,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -671,9 +730,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -692,9 +748,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -713,9 +766,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -734,9 +784,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1047,6 +1094,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1064,6 +1120,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1323,6 +1388,12 @@ "node": ">=4" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true + }, "node_modules/chardet": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", @@ -1400,6 +1471,12 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -2183,6 +2260,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -2279,6 +2369,18 @@ "node": ">=8" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2547,6 +2649,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -2773,9 +2884,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2798,9 +2906,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2823,9 +2928,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2848,9 +2950,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3261,6 +3360,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/openapi-typescript/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -3469,6 +3629,15 @@ "node": ">=20" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.5.30", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", @@ -4417,12 +4586,10 @@ } }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "Apache-2.0", - "optional": true, "peer": true, "bin": { "tsc": "bin/tsc", @@ -4462,6 +4629,12 @@ "node": ">= 10.0.0" } }, + "node_modules/uri-js-replace": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.2.tgz", + "integrity": "sha512-VeBBt/nMxedczht3KDL2yBBMJqIxpCQLMZp3UJ1otcWwUHdQW4IiyHpJivExzSK+FdxTowRfGL4FmwDqH9+S8Q==", + "dev": true + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4819,6 +4992,21 @@ "optional": true } } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } } } } diff --git a/package.json b/package.json index 1d48e1c8..753985ad 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "commitizen": "^4.3.2", "cz-conventional-changelog": "^3.3.0", "happy-dom": "^20.14.6", + "openapi-typescript": "7.13.0", "playwright": "^1.64.1", "purgecss": "^8.0.0", "stylelint": "^17.16.0", diff --git a/scripts/check-api-compatibility.sh b/scripts/check-api-compatibility.sh index b749d780..93f662a4 100755 --- a/scripts/check-api-compatibility.sh +++ b/scripts/check-api-compatibility.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash set -euo pipefail -exec python3 "$(dirname "$0")/contracts/compatibility.py" "$@" +exec cargo run --quiet --locked -p marreq-contracts -- verify "$@" diff --git a/scripts/contracts/__pycache__/check.cpython-314.pyc b/scripts/contracts/__pycache__/check.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2df299a0033b5b03166e6a2eedb2b039b0377e77 GIT binary patch literal 8966 zcmbtaeQX=Ym7iTMze$mlB~g-PX>HkZXv_NWw=Fx4EZLSU%VD=N?WRgtid;!+QRI4e zX-TwmQG)|2Cn@X%zKhc45C>PFz9L0cpaoQ*z{PNYxQ8NLh1AEb6{Lr|`=ftd68rAp z{BgK9`$dYDo%U`Z&hE^-c{B6o?VI0wqn%}57lKs#r$1i2xB;Pm#RVf#`pbil;AI*y zNJ1|mhUg_ELVc1FsXi@|MSW5d1y8cq+GCS!4Vc!_YwvMLjvlAv>~Tr19=GJ~@kpK? zujEzRsot_4pX5{Dt-b!9fD~vzj4gm8jgD6Qz8yE+aD_*Q*oChx;l&D3h4!uZ) zn~iA}o}1B&6`Rpsy<#t+y#Pm|QT>J7*|U*Yl1s!SmVTaRQ=Q@7W?D#RpcqeIW`)=Y z+YE2xd`#koxFjb{(c`QX1NvAq9ZMx>fxXPJ6Ex%rHWg=E&V~rZaam^>vMChv{PP2f zw=12JgjifUAqZ(fp_1uXLb37bBp07j>@beVrBjMsV8wLuG7Bu6BWwyrm)Hapyka^d z#M!7iFEL(N9zqT%Sm5_yp2RycsIL&2YvvK-@S~Ia5aNjl3S-)m#1KI=XzVPC5Y6ra zLD*h+H{KZ1kfYxm#i(gOeFqS7qWU6^jO8!kc=839DH$Qdcx5GtaUO!25m-YV>Z?MC zp@2@+*0qF(Kngll2qB>Hu5HI9Lnn9(V~to;zQCytbWJa5qx3?56!tydKVnJ&);y@K z`3dp`tz}&6$!aiQ@Qoltf_`+4%#f`pL!RUiy0j$>-(YDn=DuBT(ANsz!C<993#$$q z?}cY!pN;X1=U}x#6$U<~X)~7MrqbGi6b&xnh9}buk$q#DoH8U&Ho^)U;K!333c0`b zjmHFmy|gQy7T8u%h{sJu28LKU_rUMLDr8$w11S0cIynU53521hG%?m9H35MzM1%;# z=TVx9)Z`i1W+H5U1O>{ce*ne?Bs68|dlDDMa&W>_$(JzIB$~E@;v_VIFjOPjXW@vD z?Q2VmBr48mRANO*j7GDL7W@X4*-FzSfRr?A0@Y_jq)-O)&01OnF+?dgF*7{OO)6GC zCIBZY6&q(22b)TW6Pz>}A{DZwO`(RlBr9T(G||ez*3tAh+X^-d>RKn#!kEa%;%uw7 zBduaw;CM-FHO7RuQ8qr-!cQsABX~-ayzs2DMK@8V9Xygu$74zH*%p%qE4%n3$o>TT z_XR?g7c2`+U9*XguiUvJM-mIC5;q5APJ)(~iB9sr@#7TvrTMrBrJDoL8YTyDYnc2F zZp9C^G^CY=NFa%m<631(>mrg9xGa)aF%fXu_`3`RpwYi+Ks8Wp-~AYC;GI9UYD4mKRxkN`|zDUdbM0vJOi1aXEx;~Myh zn<3LB9k^(Nh3?>Nw<>N{%u;V}d~4$awfsoLD*tM29TBn$9(c_L^zTq)1vr)P60?5e z(@dr_>Y}s$ViDlAXUoRp{H{VDLnkRv(8-V|TV=4&zi#NmgSuunmDUesxMU*ArH0eE zxwfX-k`S^CfvG~VB_Ni1@1MEH;%`+ z6lPrg*uX~1MpI;Kz(mVMF`fE)yU`7c#~_S8P6+ZUi23p6kYBNgtfbg7JjOGsvM5$z zB$*x({IIz~05ZiXu(1S~mq|&n#1eo)#Iyh~L$PyW6q^O$Aqk3GV3PoOF0)Z7t=LCn z;%Jf^0@~5o?tS|em&#*k3QUvY9AzgH+z1#cus&ARuqe(1C$MoTEldgcKvL}3c!2O# zb)zAv3aj`^jD~;_95!ZRGnoHrwCeG_eq#E>{mO|M=ZeF7-E++YdJJ~sC(>&0Mg2X+c$!#6t%vubM4e3^XnKcP;pL-HtE$ch68OZr}Be zYaO$#OYYF^u@5UguDw&c)Y7+7Rr_}8%~YX?fKT5P9o+_Zcy}C>Q<5_i#2cO5KbBwK+cf`uri_3e+h!wCC zSa*!8c^hwy5R9Xk;%OKW#q?4vLlaXh`>N!UY`@<->VU=FS! zOCLxT&!KOtJ{LFd3gom--i2G@+%|!?v^as|-dm_|&=#_CsJRY>W z7i4dHtlD@&n-|gHN}>k_ecDH)>=USz+#$u zAMqO_4mKK-GHi#c5dd|csEf5m-v(?2HW>X2PlJxU){=SUb~fdX_j9v zQ)<}`p8S^V#zZ&FyrBN|kmr22-G8q1|36qbC z=nvD`ZAEQr9X-y8B9|JWxs*YCn1(1_OyFWEfY$(m1pv%8&3#a;aqMPf1CzUS7_Sq8 zdsFZ#XuT5i9*=>W2m|}f9wV8UkPsD{_RKcKGSVnamWu-fPp4?Ly11v`>^TZC$RsOZ zh_1e5%k`IH@>(-;KtoV~8-_R}oe;QT2`16lK?|7_{kWHJ^Z`fz>z0x|h5a;R5W6%G zze@{*st!%|9)wY8t)7m z#t6u^skm3m7Ykli4hj>JPlQ;v_!{fF*77e zoP=Y%!)XDQKFW^AXn5m5KSm(vie;{j*^=F0N;;DQ_2h;**m;7^)|&Okia;Rae3F&e zfh@xd=|m>Z3Wepu;1DQ*a4#n%r(ht1xPu<%Qk*!77e|NN=$I;wAr>aa(tKtJdZ_bR z1LwiOYe5CZ>TqVq8HF1 zP;BfZ2k|ojha&|*t4MV%6>A0v1>A}QIH7XI4>89K=$`6q=7VSK_T8l9;NC_5-kf{iboVOdp6Sk0!BxsL zGw|1E?^m|YI9DBIZ%8-0XAdn_Y?ZfnELL>nd`FfXN3na{_HPx{?^eik?_y1FuHy7` z|Ek?Jb2MkKnH|6VqP(diZ}0f+#D_1-=Y|$f#dF6J`H_^Iz6vhqUlD!ee_};DLxOiZ zO7xQtA(94<)v}Fp-RWFepX}&c@%pAuuI_p6SI2VRu3sLz9lKHW!>QTLvps*eQ!Z<| zO?+Aw`sK0dld`vKrMYb``1Ahd=3@)Z$MVgcOM$_K=Faznw*vR1KV*NG{p9K$_QTkX z_8U#JTi)97yQ^*a>sIrpBq$-Mj6il_W~|FwR(?$CVWe5G7_Ebn=44P{%--98uj^>;t4Ufg#&?>=pI z=$P-C-zV30<~_&PbZF1Hch3#~dhEl{;{G#B?lWeGmfPZOR<7N<OB?^tf{Txjpiw|6bN*@gD5_Yc3n|5oSi-5+1Rb6GwcUp|${ zd+KK{U7xr%@%q%v*lb|7eYP>@sRz>O?oZw9=l0-i_j^6>^vK&fb9Kk>ZOzr4HB_ML zV%{+Z1n+h0H4E4_RU{kmUB-Xn%Gk{^@xQfo<=+F{GBg|+D z#M{j{htUqVPJZJ(j7?cdf>oQb0b>p?k&NR2w(yV`CqBlERKT)>5DCx{%-6h~GU{MG zy8KiG-&pX#zG*g$D?%D;-GLbQpn0!V6f5#X$W}}7ERE4%7tG$;wa-g%xV{8Y`j8i{ zYTXG|gHnJYvQ_Kj6U8M$XcQu=LWa#AK2d-LMTo^fPbwY&h^E~#!~Jk3DPd@u;D(3c zEVTiY!#Bs&D4XQ7<#;^xJTt*aww+;(x*6l3+K$f>}?KMXk+qgL2OmXvtGo!MrTH z>lWN~Id?snM7w+0PQ#yUr&pa`+5g<4^SNcmnFYt0-w*uZg@1Y>e_-!I7% zR~GxPTd77N%=cb zx$=85cQNl8%TvkE{Z%vVGPP;86EZldK|YRX6t6ZK4G||pK>?dFA%-*T#|opE#3BNk zs?t5SC`Lk2{a+cbDAorVz{FH!yAC- zhc7EZO(;Nbl7Mf_gf!0Z?Uv#=0yjUIB>Su|2}S(vi{FCm3kyLI|AGeYBj0^gb|1O# zBiFy9ou8xrWz_!}s{af%er59z-Q>giMxy$wGlY|<|DqZJPRiyjH7k_;2R*O$TpM^j OvP4zDkx$jCL;f$UI>}-H literal 0 HcmV?d00001 diff --git a/scripts/contracts/__pycache__/check_baseline.cpython-314.pyc b/scripts/contracts/__pycache__/check_baseline.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74006e60254830bd3c99e11b53c67a4805024417 GIT binary patch literal 2385 zcmahKTWl29^~~%%cGuW7#*()V#S?F1x-RyXVhd<33IU8^!EDBOA;M-g-nn=OW@pCt z&Ki5AN>=(&nx7<6KTJeb=MVn)3{s_%^tT^tb_rxiq*PTD_0OsM@?X#0-LXx6a40`C4;%YdUj6C^aI4;0F)!1>`8b&(>lxi>_t3rO4XwSsF>s3 zdHx;?(X*C?dqb~skNr-ZK!f-dKjw3x&g_q9B=n)_`pTjvP(*n-raO7GH8$)J=h426}3o04Y} zp9Jwnf%(>)O(uiboa_1wYtg$BD1=g%vOvL-X0B7oXDL~<+%nTHd&D`FJ+FC`n1+u< zzDTHb)4~!D)nkD#632Jx@-H>V)ruCwy;U%5tCW6ri%k(k4Qj2A^# z*|%+M8I?2@7h02{5k&>z5&UmXA{Z0yr!jZ6C`L7q3$|M}Q%WEeTzofb65=&>GR~?J z3nKHxM`oAe$zLqPpTTtP|@&F4#o<>d2~*G#v- z(k59XHva^r5jcit4KEud8^1S-<(Sy3jkKlq25LT!{P{fX;rWtoVe1Df&$MmLQ5@)i z)v(Nh_E#F2@-A!i4dw07$S=3RF-jz#4?Hc#QOuQiRWGY^{)Qkz4bu&Od9-F1*w-FMl=2oQ4>3Q literal 0 HcmV?d00001 diff --git a/scripts/contracts/__pycache__/compatibility.cpython-314.pyc b/scripts/contracts/__pycache__/compatibility.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57d09d00c3473d3f7b0b34edc75ca5dca0cda550 GIT binary patch literal 23840 zcmb_^YjhjOm1Z~I1V93Ofo~EN9}+1(^nOqhB~cVbiF(i=1?(ioe_2HIYuU)V|wFRGbf%6eKxzHXEP_7o!Q+c8x%OOW;b&(^CM^Xhb?(` z#@RpnRW-WNpdiWFY)foZb-k-@Ro(jTy|pzj*T~>0|IM$iynK*heoZ{mqZJ+OqYEs< z%rFin!dzw?>`As)6;ag?x9X%iqNcx^h=%@ZBU<{ai|FvHKB@0DL=368IguP1W{eo= zuPI`}ujZt=H#d@3!(1}dGM6m7P!@UmYsFvXdsoWSze)L3bYE7rl&8N&3brwh!YMXV zSj^Ni4s8kJ&|NCpr50mEiXHkACSr3K@Gf!W;9cq{au`b(KO3<+Oz@XE%y^eOa`CQk z+TFpakSpRF^Z9*|n|Al4*ZpGHJ|5)kzNx9{h-=I* zMhOPIfrx!h@*Uoueo&#`2 zeB&;6#O?`R5BP&Fk1sIM?wXEFqFkRFRSyR2V_ttSFcG##g7#x4+mCg(8JpOo$rT6$ zBQ!$zEc}{&S7Z{uhHEar&*O@C-BRa~-xNKtk8JSFFqfDJ3rbX7E{&)iY(zu)u}PiG zJr)c^IMn7i#|63fnX^sWq+xV4;F|J|jwX$xqfLib}PX?#F?Wildqy2i2dohge^R`d9 z(4SY^!*0$OiiF$UsrnF?Vj$Z>H#r@$p|4Un!eo|xi}_Sz;Buw!?o^xMXBbj4=$3u- zZ{V0=M&vLjBT-X-impj?>5(Yz06WS~tESbXG7W~<5seh9pVVc;Q_DD6Dd%bRw90{* z?_i|4Q^V9Tc4jNHIhATcMQGoL^`MHWq5m9+aX?+g_}KT^vrQU~R4%CrhP;8K=B8`P zpUern#=WD2VwzM*4TyX)sS8IyR8wJO#x&ZwJeq{W;K-;~;xD`nuNme+PVTJd)$-SB zZr3Do%HuiZ^WzUJg|FTE&aL@z!P4;0YQ|WB0Q?tn%6}U+A<>`Oj;ghvs7*(UaxyyB zfH2yz_i4_`jvXQOpwF(9OKPsCnpEdm9q3>%I2%$`8Wn z)R#UZgcw8@=?wZfnf`pqo&I&m>7^2-{K&+JZrQT!<>BqE`!9`nnwv)2p6rWm^4ri+v&(u_xKKrkteF7xo1ZC*bxb)fl*>it{FO#;5)M zDObcjNl81YjZBC92%mC=l4@TdlGKJnSjjNWwU|27-Y}u#MscL|sdb0jgm1O@3qOa~ z4D*GKG27<6Zx19)wX3Gu4+=i4{MVKI9vAPPOf>mcn|u%QD){On%f+AM@Rg^8yguI0 z_rPR*v+&J<`G$A4-rX8=3+2s1{#L=%!fRUS1j`@+G7l&TG@>?{bL%N2P(cSWwL3Co zs7YJ!V4kqsR4sHz6~D-2W2R}yT&7QY&?d2(Ihp;)!Lu=rTFMuMA(M?0r6_kgDNcwb z1hpTzdkQlN4Jp`cDbBDCO-SWbc?(GVp@MyHW~qZ#%72m_D%eTZT3xaT>)f=D!@4+z zeZw2@j86Fi*}l4NL>qhox~gy3&ZfK(7gl-~HWjbm6OQi6SS0Oouv8MZoX3ki8*SK- z%op%YO;6dqH{4#YCyXuZ2Jts(xia)iQseQtBcNDsL|lRiwKb_ZLU2j7KX^TXiqIdx=_&XbKm5eNnHGezl#@^AyYBvaxh`qv})S)u6v>3hgbMrr})!j{7!eG z(H(De6Vj|aCggSVhVFHJRqT9R-%P(bguDUXFz~>bKi9yQwyhPk3&w3T$JTfE-J6c@ z9{ld!x!rtO`&!|)xM@3o@$w8(n^yHD^T*ykm64|@Z+1VgDH4gA)LD86`N6M!^p9A9 zzA{l)5h8FhR^~jG{5ED<)nj3pka|QW^P%ldm9z<*RrO&d%3-PzFQl%^*px=(Ek#<7 zq)9M=T9Goq_(-Js58{_h0u5=Lntqw!rDkDz$t#_dqX!cTq}xTw9`(h_EU%yrwNsP1 zx(>1-tyAOBC?`X|yv{1hk%`{Q^lU{h=!TsA$bIAE&^feH8?%P%9R?{}S!PJv*DChj z2=)Z=>Bl78_+TTJYZB59YWs^f#vqGNf63!trOh9ssXMQqR=1P!L|NBC5!{4R=V13; zXBcrj`mnzYO^m7fPy;%m>X1I9lcs$Qqo9E2+8C$m9P8AaWq0c&TB!S7>VPt&IwwpFczmk*vnl5+jq_Dzu6Ym)FX0KRo5PEoeqSjL!lr?_C+}> zu*d045Y@|hu^D#QJAQ)>X3m~Xvh7jTt?l^RmSk@=nN#bJ-xrP~^;kHAe(ZRSh?+EF zALSz9>%NG%Z*q3>6>-40V|&%OgQ1&AgU9O)QI~EyL>T0x))(MU?7ir{Nte1X;d>ezvBKe@`!7dWEBe|EM|dChq8a8N z_4#x4Z)+0zs#Sf}-*t2fmct*{FV?@QpVusC?ps?wu3xbn<_(9*O6YmJTil`7ErqkD zb$#w!(Km0c=M~KD`9uG#dfig++O6BS<~<2Z-KwQ77Jm1o2L(0!rgMD%z`beSeqJbW z@@D6He%Y*Uz0fwteShzrL$4o-72Ypwm_4>$T=7od-M$6Qf?KHEk*Mrgsq9$VwOlAv z9$zc&`A*O0YNn|1AM=avRDG{%{+f{Ah)fK5ujSs(UF`npsUM&E=ycrX`0?oioDaJ#W(l#umhzb9M6oypdqUZ!`=}_-bwXqwPXF{R$k(MTTF-6_6fMWE; z$H_jK2TGMk%Fu{q;JjTn#fH@QEyu4WJ-yTXk>ZpGZq>>OH$F2&QLJ%n{|=#(328w(ybU!voX z(W!T6DINnl09#O^jC;nA>2G6$`A(x3M?yOyyJP zt&q^$4*4_yYI`?DZI$RxpmvY<=TL~8C?;@tMnCA4{gBxU`>-LW>VWO&DRn4%tPY*2 zI#m7Mb$IjltwWmflq*cO+3qm3$p>?3EEH+7Q(=}g%9TmV2mJsX5bMyB=T0$QIcym# z2fz(7N3*Y94t={^-;gQ$3YHPa{Dgdr4pyEgxlhPP87D7mpPHn$D*DVUeN(RF(l_+U zS=uM2Y@;#sid-j))0{QNEjfnOX~9m%rl=JRH2OIUQ$8X?BuzF^O{a30R z=25@c_k&q;a^@;`+B13SoQM7!&9?;5&0~2wa>NyD0{eO;10PEdCr60ynKjN%e>v%~ zd&0;mV~`!HG+WJ_I-K6CqmTNzAXs050^hWUgVUTFxGzCuC`{~jdqafP9lL248Akxp zoP8VswOs_L0Wmvp7WalD;SMmteynmuo|caEP$j zNb*=HN%B}eZN~EozS}+N3QVL*0mut92`oDWk?k(G+vfq}3pDno9hCxbDgNMBZw5p~PSXH%*hkmk zm?6uA%%I^z>TxjXWexCdiYOVy*$vFx6VK%-hEZlw2vgan@VyU+K5{eUZPFywI7)D2 z^P(pjt{pC5xH_@J#N!3wFC_?9OOl0?#o_5Ope}6(sURWT*+xH)AYynYUNg+P)pp1D zx^aF_Y=>ZNoYg+C6uf;ZHvNGoQMq%qa_8J`!E%&099?QyDiK|eLwTgl$c%k-1&)L4 zCNN7g%plXVg<-Dd52(m?cz~6*T@R3V%1!}%+t-^Bf?gsa4i;&Ula!*Fxxvh6v1EWX z6j`b|X1ChMLiC^qJ$fg+Tv7)*8S`>ysjvV6sy3YdB1v{E1u4ylXlKsB32y)-z;!0e zHg=30^a9Eb@51X}euH-Khxzl)*wMQkiHd%{qJQD);>q`KCECyN?dO)a+;e@> z8gK58SM&=PhrbLH?(rLzo$MRtoouXdCmYpw*h!z;qeWzvL;8}M1>#IUn?Z4MPl6Lv z_9U1psV7r?0j7-h1&$%4FF1l`q#Dm**=M*8ypjfS!h!Hf&W(1l8#n^-at^%4@fXIa zC)Ly~=l{BSxqk7&{iZ{I)BI&vgMe@37qL-G2ZUA<-UoC#YDSxY*G15oIMWkZO0CzoOuIN2M#}4ns@j^Ps^LqW1Stbh?TY}Y)_c6;u4zBc=iVExDFR&^ zQVu%NT^=F{OXghXaI~gQBcyBq0dN}kBuyT#DEGna|E<1WoDRP=-)QBg!;w~qp1l4} zvR843$*YIFj*u5!H}ol(GN}iQO7P000bfp%13p~70CjO)h@Z?sz-ez_0uqxPgosWK zCk7^&i=gzf1FtWT_HxG%rAZ|ge}uR-+za^OJmfV-USt&Tb(FZ>a)$XVk1^!M zfy(ZQ4ZhX6;9TTB7+G>IPyf5obxZzi4uL?5zwx#C##r}TZR>>#^0X#~zFMP{mmKR5K^=n}1F{a}w*~+@4oodV3&d zduw>jvSs1qTHfv@{aW7PnG@@|mYJRhmg2dIKl&O#7gPRg9k)B?+^-&Y=+0 zG2L6OGu<;sXRiKtz2Q#no$wpsI9-f{@{v2?JK^u8y@`^M)sm4E_%dHQZ;WmJpm8z( zM@%*wLgAosqx=$U8?{>7b(4Se~&`{wgC$6~wrEoXS!+53ibfW55G zv%e}VyVLP{$Gm@`ODJrgIlZ3U&sXnS^azCq(7sF| zcInvCZociPP~LUleC+ca#(L;8BcnBa`^?wR%$paqLTT&kn?BmJ)WSdS;xCNxV;zI6UX>{`6CHP!q#$LF2zT)ca6LBCieRP9;P{oMGmae42(ZeibZ zyz?@Dev}_|@dIOm%^f#*K>0IgME*d987n;m8p7OVABpg0I|I?z22olz&Wdcok9zU9<c?9gyHa7XLzM4GN+p#pSuNC_K}_`X~A4AX~7gQ7f6H&LX;oEt5L%2 zf=el`8%m%2)oE0AD2;b%(u{r@ZG^x^&ONQiks-#}A(2%`n?A=6b;)0KKw9-qZ8p-8 zNZZOB*Y>=hnh>|01}fC~ku&QlUrK{>DdRK}Br9Yd`WBUVQ@tstJ}c5{Mtqi@vsD>* z32>*O$&5*qJMf2Yb>0I??yFyt^5DnyE`?Qs16E29Wy6g_=@_Xy(b4i$uo zodwQ9I$v?X2Bj5er4U(jQ<`C-R^(IY(&{WgFXw0UGFbjT9Fo3r8Q51)qe5qavPZvK z{U*@kPn|bLTE7Bi{m`1HV~$^?CWUEC_X+J(#vtop5vfU51anQKnh|n@p^vjrnhlC? zMbbAVZY__~v(}GdUVv#rmXT{S(ztYZ8l`+1*M`@YN z`*F4h5{FQwv)q}{#!!{BV#AkeXI0j|tWHyvv-(*vXj`APJ~7Tpj7g1>Ql3z@f(D;Z zwjxHz9;$KHJZ&VL_NRsZ4t2L@)cyA;Gjp_h04s({pE+afzvB$3`5ohv8Bmt^fxTZbJmTqp== z+CJqALtl3S=&3zwu%}cuqlNMTZaRQdn-7YZp6Ce(D^hyv_DSePrGuc!Kk2&W3vvfG z2rW`_m@!C{BV+KOiDmRIgj6}I!Mz}IdV9#GD#IwQZ+=$pCYFOJq~!Ur_tf45k?QG z^J%m^?M6G@NF+z(2~4pCg%}GiYds|fCUo-8hz0f^xTYAi-0t|_A&T3 z=AP7(?uPg(YE4I+@_KwO(wJM1e2jp8b}~@X=zV$b0mjc z5-ts7IGIZ#rM)nVAS0VJK+=H8C%Q!GYSKhfrpPC0BI#0|a4j;H6#Qg3d!r+zc~1o@ z7_dJ_n%npbzXd=G`pu;^F@sRLlh+hKD5;*mE|hG8uJHp+{@j+hrh@!#j%&&vSjzbF zt%7CuOz(rdQogK7$lI9$qSuQm@0@-8Y@(?CwgF&bN%?Iv!4PeC%&(i@$m1iFEvvns&AaZ{B9x;o~&dHbHl>x;gn8+>_h+N`|EQhx>FC1O9wIX2F$QM7izPICd zO;NfjQMzTNbj$tH=G(oqM`x$kE!KHs+_GuCu=LLU*Z0p~OcZWeE!?tRRDS2o>t|w) zSoeZuiCqjYHS?Q}-!GyHnoX!|QJFIOV)@e1zo<%V?^@a3wcL@|es*>HS!EWgh3yM< zeCr8*Q*XTJWU4FP9tUd9{@JBO!=9CfJwWIRgof^A?=O9y`1t+{{KXNW=MsUFU0&WB z;;(XiD9n#WgzD-0)@z?zn6j;3SQ)crHu~yeNKne(8cmp>;n|WgFo*c^&bayTdX@d1 zp}Rw|ON(7X)!wDTpO<`GvRuXYJB0lMb6aK&5)l|hA0WK=&d9Kn zFFzbNA6c(%e8+#+zp#ICOsL+Us6M;`Sw!{G+h=Ew&+T2e6wUX(bNcS-g))>^xi@av zs~CK~e%Ipp#S=^W`0DQa*5khtr5fmqq4$P=FuYj0v_ojZjtjAlub!B&^BZCSBx5kP5|60L_biQJ?Z{1QZ zN-P9RZEQHvuzRIpx6rVmGo?(A^VL0Z>xuQU`b1gN3RbSNtqU(E+B#R-I)%0)iLxVn z*^xPIn(l@di`N?VCK?W`G#ps!MQs~S-aEh#jjZ)uO7yu_`hWmm;r#)jFUUu(@!{*A zG0Y9NONF0fs*~iWOLdC;aBQFI5yRA+R{d{=$v>m|Ld}#PWxvR0N+5O{VwvKGFRrja zir=i88-D|KOVzxl`pbvMS;ppIzf2ZCM@9R;Y^c0YqPtg8d7)C5A-c!fEn>+x-~g74 zxA^ocMIotgwMz96*@o$9Ctiv3%%-U&o#E;jss;c_fgT*>q~A9N!N89%A)t{M zkB*Mrgjr=WXVQDaX-A zynUs-T`1o!n06#g2Uko71yd)l>3rNMMvufexw4POG$a>jDHjuMV%vWs9hv6gB-0mKTmCp|zPS~~?MjC!Dbsjv+qF{vKZBuB=Wq}XXums}R~ zNul+ohiIxO9ZPaiO}K2eF{DrJursS~fntgDz+^}JG!vKpGAU;I*Qt713aL$UowL8{H|pBkZQvi6i}_(!Zg+}$&&N-hPoznF`Cp!X;aXrihU^qJbC+dER108l%9`xF$9$hi>ePk}Jh3hgk< z7cT?wz;e?a334~1TjeC-n^Q1=e-TG`w~L(3vqVOfB_LrEW=ug2CjS2P9RSgq1F4$@ zh~^Jes$e&72Lu4aC?F6}*;KS?&#qUZWpdeQNh@{-k~=jYPU>*U0bJ1nQNY|rjy1%x zzwO%gWR46LkY2gY;c@}F*)$ckwx?)9G)FEXT4VS4!mbI;8wTXPC?t%>(QKCKR1PP~+$ZEksw8)5ip2-&;b&cU{icTQ{bQpYz z=i)pBK=+l}osC9<(25GIm*Hk>7vDU-+Lu$TLjR(t$%A6qT;LTc0p(UNvq0 zVCO>gM~9at<6C=RhdO_A{zbm&#NX=f-4J>&@F$+g&I+`$bt(v;z^u2uLNg?kPZ#V@b{<%~0*Vc*}10rWiXw`H`PCCd}o)_|*yuq1DLO2RJ^u#^Q@-_a9pRb$}@&ddepln3% zTGOF~sdLrT`HRMTMgOsdcZ~3tylal}c-Od`Lzis%`ydxqbgy278iGFe+tIH_=e7v? z3chA%T)%Vi+E1fDjxKK%_MYV(!|}bta`JSqd}`T3RFu-HTvz^tC~?7jF1A0mb>aEN z@S>MLaFMqU3k4&*d4y;vH9^ftYo6_y>sT{a$8@pintexHvy+_J8|TZ{3hLvU28E7H zYT3fbGYr|7D3kOv%mSB6sf>_jd6fFa&?+YLg+9_EDQ$--GjzQao>oQx^Hnk>ZU`t5 ztO?Q0p^rm0sluiU-CX6O4AUO6-Ow!rVHqavj#4UG*_fMb#R?<)GKghWDY0x5G+8&L zEro|NgojR5`tF5vtFtj*k|+ei+pQvS8&X57NS(>bQPxRju%wp4npxJc-q;$tX>V+c z4F68DP|wgRfMJcppzJ4U%f$w+$$DQzPFfHV(kr$~xdeyNVcMq3S_|3O18j`U4#Mop zn5_mGF-+NFaPvdA71s`==^$5X1?a$_$VZ*Y>Sk~GwCaFrUk4N;41~IzIm9iRRi0t7 zJ!iF}PO~-8{VoR+@;bu=F_r@vYb~Bdsh~NOD&+PF+Ur6k(ye8u|Fs~#EPaD5U zt%4>zHYKY4UF6hCbH16V>}<%xQS4Or%dBde{w4OA%**q+DI0YhacF2KSfCtALjlTuA~9?91*156Fs)^cBB*=MK4zZ z-_ZhV7^>0MBFRcx$R$ZMF*P5ba)+W-Dc&TVSF3_ei8e~NUZlfC=1XL4aCs0dA=;9{ zsndLvB0(yW%ovGmq)n;|;9kYOLP--8zDFU*A@?`IHGAPsI#weNw#X<`{x=Sjc!ycTDP_8qT$L6qIR`VtjCJN)*X%5NUjCK%v z8L42%Wstp;mB;J$L$m-@e^PJ;>_PURo#b#24K1a`0@lzF>^LxAV9`R3-YL5UP#L9M zHf6)1;=YT7lHCS1T}W!F4mySTAGcdbZlWTyo1;^%fNvc5McTAQw%|}FJ@6th zmff#LMP_#LT}{H&uxe`fpm5>BkE&KXrs7Rg;Jf%rmykEc8^*wQ&KcJ9Wqj4axc;C7 zstyO@M*=ijw~%+7Hyr;vXGF-GUU5e5dEOk3mHt`vV$nyDrS6|Z6MK7C_Vxe z5j=B(&zpwqV$QdgXNP1&pZD#TzW&nOegPN4lrA)_)$WSxchQVfw-<>@{I;t3iI{J# zdYfR|K67rpxbn{M>%)BQejvTYoik@2=#8_Tg1&tIm4zw3npBi#F(Bj(@`k|&xkYoA zV`tWicL=#VKN?!PeDBQKp^L)qVF+6EIp4fa!w`XYfus^Un^@_cz&&H zuaLj*zNvGm3$K*&HE%l|H=Oy@S_kn>Vfp;7?;n~qJSZxk-}lbJy9XC6Ld8CzX#edT z!lstTP4?K}YQyfO_IO9X1Q!p^_r#hPh8CY+Ydy@j9N~{#UVQ7LiKWTq z=vt?P9~|aK#@7acTVLh5m)3kQ$M?J<6yLgUxCN#C{QA$#Om5ly_}k4fA7JmMwe8)@ zSAeM=QJqlzGx<5A`ds|^g8ZCQ;b(yD7eAg;tps1a93Y+@|eaL>aQnjghrKiS%TvQTqxU(v}@s~hwtKZN0> zOmq~>(pSJ>5leg=%-$3;9wKIbGUF_T6PD4T8Ua(2im%>_a>xUwCVPCSf;&piu@3EC zOope%lv(8`<|VU-PmEzu)Ga5YLr69dk-ks^>iDQY&6?X0ErEc)Oyd!-?@Ke91#7J z#t*ZWBK6M&jl-gBN!B{cm?845m8$!Ue%Fq)OEE{bNv?jv4QzPGl}eYvJWA9l)r=eq z9Qn$+$vuNPR*Ab#>zK=67wpUk*$wG4QZKuRy##$m$hJkFof;kEp*O{abdG#SfunE) zheYuilxJHT+pY>}tDxZ-(jC^F_K3eecBDm$i(IHqqm+xIcu)sc2KTyTr2M*+(wDk> zv8fa?l@a6;HMYW3L)@DO!6tO!8#O_U7Pq#_Hg#ZMQ(8Erf6mjd7+ix2RXa8q-@% zJwc#(#FSOOfQjOk6hwvb`UJHYmuv>fbx`BeR1XSwk&4xD0TrYQA%G~Q%S@v!>B@?2 zP}Ek+%uY?tq1{9=!syX-66p{i8J!X!HPuD1_!V!FN@Yu~a*Y5=B~|7A7X8#i*}SvS z0_pG|W!7QGnCN@~V&~5NSHx^8hfysz0Uj;vP39ftCg|Ny{lp99n9)((21PD9g+2Nx zqS3&V@Qo%RAH&H)xz_X~K`~S4(nxKTxwNA6C$S|2!RCCZ224Kvei8b#!ON|XZ`O45 z6b<}@gb}tZX>^i%2Ivi~N&UG2(P8NG-V|LqgD_F1PL3?H$(-Z3$1g<9MYSQ0+##vC z62|F9>zSSkg<&~FxKz?)4Q)`4Tr#Z*_a8{P-ypC5gcr3OO0kb3>!=~HG>09Y(P1x0 z;hqn2ixau^tGV?bmBe%R1Gm!{6Pl70O$jX5B0sC=H6?M)VfiCp+P=Dfh#-BAlO{I)%BpHV!r#GGk4DjCC!ky zm(?aVb*yaaSSvdKBT7rzeAAkxenFGi(z&vwGj8czZoW6N)^(A;JeIh8edY4?c-M90 zhW=QSOD%@+twYo zbkn*`2)0SyI{6^q9xHmU>IYT0L2Fj`1?&Nh->rYG`8&;XuxXDQ8{?YI5Vp^~oUqo% zt@X3pdCk^^TD<<{3lo!D^0xln(hv6YrrlpKtoHDFL3K}=Ke#cpU`#CD1*2hJ(8+VK!GlTsCr_t}EoD3U#9?^*V?J2sg>SL1Lh~M3jIM$c{*I zBPY6WQsX#xZjb{%#Bl#aUi9Qg=P%jW=tY;J0^YZXm!9+J4E{^vCDWKr8IG`_xEkYz zFG5>JCGO(9G8`1|TJGe?-Oc#VgctEb@3ZXZ8V#%YUq*(l`!!?vlreqEpkRHz_9Q=_Aa5Suqb-6iXB0O>U%Ec?Pa5Z-#5x85?iudLE%j9YPK0j44=aY*XmH z=uDXxb>=2Isq77`Mz!cazcqE37bTpCB*P*%G$o0HVH}kd?ot$soC0H+3&kT`I58nb zV`8M8PlPXs3WpO(MdZXuNen9?C7OuK9Vh*?#_*?34QWg)5sHL!d+9q8jhNu~u-uGL z2qYf_*^i=emT*3Z0!WtwC>nIG;3<6+MY3ZPaLNp#eeimO%hL1AE|A%1!Q!2Alf2oG zmETcJ3v2DHK>d9PNt8fkR~^^)IR>^8sGLT#J6lgsJS)+BnLr0&rnK=kH}Eb{#p_pm zM%TNFUgPb>qa@vgT2L9UB+&bY1nSe3Z`MuUbc18*?vcE9BI(4x+yOLdXnNR+$hEh zrpQznDOhLkhRbjeWtB2BY?L6O_lx@r|(=n?8>q7D4A*k$l%(L&Vl&a z-PyI=WUZ{H@>_HI{V!R$14-bQn`&umVZ~sjv3sF6&Leo@R}i`c8pDEMe#Bayjy!h- zIr%#9&3efyFu|(Ay2c&f@I^i(r*x_n}GeggL@8F0gA0Y)-#1p%LCzCO1LMJ=|n_q3q=d9)}yu&y1Et{PUfiY-` z&nfRK9zF78Y&YJ`bCdN^h_u6QLw>$>5IN9p+Mqp0`5zn7tjYL=c;ZsLUo*$Wiy}@j zJ}i!DR6sL_VzE5XRz*0a=F&IS@|76&wmHa$sDHi#Vi+9nzFm)=Nf{5jr@7R<#ZV@!H4MDG&SG;UN4CNYix+j%(=*UXXRge2o~n9HdLlhchs zp9qt4xRpGCfR`1=)Ay6hi^>h(+ih>PEex&KwW)P&KfRn2s%1N+F$7>Yh(ZFsZqI(W z>80|*fQpJO8bE{evlI{P2dHODMF1vr>w@DE>-uR-!15#}A=+=kBpPaFuxC=#n0O*0 z$^_e5x#<#FKz~SXh0DKn+js_k)49C;Mzh-3xxkRi*rK@7*tuS}N3GlQyUwgsuW$2$ zRPkgirdhyBqap?m#yB*UvOGh&Ud^ogmPQ@WEQ!(ckUeNN$UY zY1Gpxn&VP*H#mKdRvn5&q9oA>#e#YIR3*o~a6z2v=41s&NKQ@O*{lcHa; zX_PHR+g?spO}1wpxIGz%0~Z~Oijh=PGA<`2DS;I+Qk)CP;dA0dsGIxvf6)9U&76pf zr^Z0C_|&N}3^@#V2yTK-vqs~JI4)xF7)zEV%{rP$#KchC@6gP$6pMlbpHD>Nn79h| zke5v3B4{BhN5Q*8@i15=7KN9cguqJ(EFnn};}MOCM2WrxV!A^ZC$PT^gTKNGpaZ;{ z+%XrTKxmFd2-<}dxAJ?+}5vODv;S6@xdrdB+C zOUiQp&o8GveQ8&p%JyaKuB%U6u*PC4nhZp&D{p0Da zPpIzQOJ3F0sj{7fY;x7=p6^}kT&)jeDz?m+Gxbe6{qd#dbp4+7`ks~go;&sXXODq4 z8odicH=lp!`Ni|g&FSs?R~z?V!?&KC8OS&)uO6R0KEL}$HKRLG;4?Uf$jy<5GIpzA)wW)=gw0p;zYscdD zHCI4o10dWRlV6)uE4Hs&TW?!i!6ok5s#W(>OUlxiTC+cGJD@TL9)bs<4S|isOcG}v zI>4_;3n(CSTq%BMJ2Ey?|%o+xcbc4b8N&t{gzc+Q)PM-mNLoeB1| z6z>`QoLwM?`*JRaJ0zIDfU-A7IWCxj)>6I^;;n*ZvwYH!h-U=`su622pLrM7;BCB} zcXZH3h7~Bvk&Ye6Lc%e~HVHHovUR8fiq;b7q9^5#eBvs8GE6M1w;-e)r410)be85T z_U6!cqqYN(Vf!mn-wy=1!?~;>!i`RGpPN*0V>mIvMG|2-5P@VVM!+Kgmv~5uc1(pP zVjVVEuP?#H6AC8+X!LN(xv0#ILH){w0gV7?iptwvI$em9MMy75p_|QPI&1_eY#V%- zSWE}!qi~alW{BZ>2ET>CF=W!kiB~?)gL!(lIhTF`vg!a0((3@tq#TeV0qp1nH%avN zb9g80u>rF&I1gY!7L0P3%@r_PYcLY8$R>H#Q}C`#5Q1R&eaL){(hm?CrjF2m=>2`? zLhll@cyVd)!!67GAJi>7ZauT+?T0f=p6AjPZwsZ_$X5WszPKW5)*K(Wjc{d_LdrQ= zvrLG}xdZ?e8EWjs5RQg`I8~WPTVo335HSERBz3VwGLmWqqXKlsBLNwQ17OK~8X0NN zn*>(!O@yWP?5jFuUgix{J4(C**2_O{*FRmz9VDUiQ*%lJE+$IT z%#aa?7!$i=;=~2u_OluzL;j{&&WWK2kwyPrqFJ&#-~mES&ICRT7p;sqYW7Avy4J10?WZEE<(Z>FNHAW6PXy{e^2UfZf1)t1pDq(J^&gUK>@^ukl>P@o&Z9K=zzO;}eefJTeJJB}!4|c3^LxKH zv0m$6srBEfeQee?!(_bMZ`Qq2_oIdtN8=1(;_8Xn6Z2gQ!p-4#hJSc=Nmw4b)&2Xa zKfe5jm(??pD!&8^zd{`%>&sa-g4AI$svn^L2|FL5ADU2e#{&~`@A{NQWlq(#ca7b< zTyu|gsIEP0?4G5he%rOic75pkb=xo7ZVBqpmsh(5_4Jvw?lbqTjKgvtF=h*X8nhIK zU%nB&>L4?wgUoRL!RE<-p-AS@-dwdLApva~nkk;!I}~R7#h6hX`#k|Qrr~G z7psML3|fKdx&)u$9@i=9AW2_K+At{_^ad-D5usx;e2y~(NmgCb z3Pn0lemxsX1v)PUunM2hNZzbB-ppHqEyk$O3;EDo4FAJEgCa;OpOVH~3-&(?)zw+3 zpZzQZ5jzx7dL0b)Ny^&Z4)sF#f@YJI1QtgWoD@l*f#R4vt}lsA#mJR9RDeMv>TwaM zP5{GjGR{d;5MJX?em*{S+w_v*`O`jUhWjxuk?*q8>&yD^Ujl z9+HXakyA%LjhP5REfs^wgYc981ea-a&snqX^xby)e&$|v?t{3-*wz{M3gezHTim5G z?p0=Q#$LH@Z&KGrBj_STb{mX$^=RMY`mXip#etLA} z^r#v>mp&a`E03;9^175-ky2~yOLturckK>6>S^rA2$6hb#P7f)%;B$+i(XTX!JXzL zPz~xIjY7pal{KAiM7_uxBE&>#WF1l@f(I~QOyxbKFQ3&d+dBb&3&g|&7y~JZkMc8c zxo@H<>M!WTN67UNvVVkZe?z`cEIX*DKN+Izl;x9#a>{wX88MET)T+soVXSWqyf!d9 Q^iAOoQw>^R>hvxC5BH5j#sB~S literal 0 HcmV?d00001 diff --git a/scripts/contracts/__pycache__/inventory.cpython-314.pyc b/scripts/contracts/__pycache__/inventory.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6ed2a49888974783ce886a844c466d6687dc3c3 GIT binary patch literal 2736 zcmai0O-vlg6|Vl78D?Pkx3R%24bBD{0~33#D9bvKu?SvR8z#+oZ0v#Ap6P;V)6?Cq zt^qSUagcH$H+)*hisa293P~B)gE^D5Cz0KMwN|}D3?`EICc^#m*myVv?1{- zDXHtddhdHxuimSA)n^)`5d`i1FaCJzCqab%%m;pxYRt1cz-%B5QM8COVM=&Yq+$=} z#VLtO3t}%?kk1QMbfVrjKcwetY@*>%=5veUP^Zr$L_-Uq*91-KK$bx3H1RZ2<r_9`W?j~V7`P2f!RDvOx-d|IyD_z z@p5@w(7h3u5-Y&N+QzVHE3+=PFCvfq2yz9jqZxf$m^2Yu0~tA;eu2Lx4jqYl!q3}bSRreE zetcS)YYZV*PRWVD5onF|bLIv#(G8~IY*bsK&gTwkFS>@0ASx&7P|LaVHJ_T%s)^QMHSz=U3-TNAt?^}?CVk^NLI%uv3Tg5cK?}Hzsc@p@OOQc-ziWVY zo7`r%CDnMu*VdP9dXW~=>QYU<&qGHbZqpIl_9KcK3%21@!+FRLZ*FT+NOV;BqrDbM zHfic8WP=yD4+AQsJ%RRk@S^ zxBbLF+X^~dx7)*Ty>ZMVxtQ*nC3$d!cqKUVHwTt6ExV2fT@gN==JI6&TbN=sJ&^VW z`vz_<_AQ)I`vyh^$JMl&UI3w6%qj!pqdMH`oT82|*~&nA?TiYV^nj1^1_3cV2C|Ao z9p~D2Sis@8m!Xo(9bv+FsrACZ&GZ^90r>PtdPE%`E6MMq&wjV`^K=$uao4;GwF0!>Vl6ij>8w${rD;@rYHdNV$JV}W=2aA{%CmwFo3 zjq)5dOy^3@jq`#S2lvYVJxCDK&Y7-mRkTw!#RL}gQEEENWC&fJ1q&ETlfgoH-8L*t zO5*4!3wo7$%R;Lr+Vo) z?;&>Igs%@mwKxW$8tYRfRU|STIG8>UK`~UZ)b6L+a*BqZYZl1yA*hel)DW{(OkU`N z)v?;d9VJlwnY?T{Sr#S`n+%2O&P0M)-I8fTcranY9JmN?feS8r6cn(WD!shGYgLscx3AZOn5OQywWi_tHb_yk!3=LG;w(| zbY2^DUsYae>_RN>6mV=6+{a#Q%^@qEtLJd68oQX61GlLc^WPgUcK95+>nvJLb*8!< zauych`|!Sid%A)4qfHx=&qdTTE$-HJZiYW^@4P?p_|(vou5-Ix=N^51uWR&4`x~1R z`)wWXj@=vEYdgI;aUi3P%i`vx&rfze?0V4k-nCy|{ME$!6Tdt8`c`0nV(R0|A71`w zX16)M6}*3AdwsvV_u>2x=ePSm>ppvrJX+n7_j|wc)9xR4Z+m;auib0hlDG8x$M!qA zew2N<^k8Y{)zJ?w{W`h#%Jk!o=`ESB(fOeB0HIek3Hqe8AU#8?x_{3mbiT$|syvA8pz*_nQOCwuEN%h_@1KM=Yt#Koso0#jWS zCqL;fgLAcHzj2`#E@1vDM*A9DwR38D^18iCgscWbA)E(gbe00@^$&I zRRX$snQ#b|@5(UQjQl-5>iJdxfGzRB^C;>ZeI7=~EvJO>`%baL9&P6nAh#1(tF=7NZ9ycB1BYe5`%p5VZXO@VCVQa(+Y&tF+vqfyj z>=FAhN5pZ=8F3zSMO??0MV6IwBEOQ0bDXb`<4QTCu1TfRuU_El6PI|SVLkh=a{Pkg+N%sI-{Fn)S6Nh- zI~0t_-rmR&Sq{l6FP=KpsaktO17WW$sg`gk;O`w)Ey`ezB1P1^>yqs68}BLI0N-RPCYH^ELgIG$% zuji_^s(fTPjIIgwye#!bRC`zsg(W%SmlW0L_o-HYFd|)%WYw%hyb+{RBCQwg%{yzi*KSjJ zf3Pp4nj`*5KvJ!J{-MYq2Fi+oQIPkk8EKHp9LDe}PxI@Lvo6w)3Mh47_4{tBd^n`w z{fg8(DElMBs=Y_TK+8>ok$yDAD|-i|2#P{ef=G)(R6d9}G^4j)8t|$PsxL;qIpiCr zd4k3UBTw_u9!wN3Rln|KB@}E?ZL*|Qxes9s#P`BZ`-aq z&j!7Cbx8I{(FJyzccCCkRMb4Zif8D>D24BjM8b)Bpgvl??{JvQCJcpQz|5;qkJR;t z2Bf;{-atT7>TZPOt4i40E7c8nWmLRQtASES^`=R$)J4!151Z~0e~&+aE~yO!?uX8zksD){@s$%*lNUa; z#wt5v;)PguFm~+*l6=*0gi;5ac)!1YL za+{74I&L~c=&b1ip_fd(g!)VYLP66FLXv5SP{24$=&CVF=oQm(9;l7)B6ObbCUlv< zLg;1w6+*A^N6kPd%pHV8^F>14<~~AxbC^)S`5Ga`{2HNV%W(@(n?)qlY3U}^YPm$H z$#R*{S1i{F4OwmyI&OZI(5w7wg#7#=D^M%nM96JzCv@7{Md*UHk5IogKqzRvK`3f% z6M#+$=LlU8x(T%lmk148FB3X%^$_w3eT4kNbwWcziw&q>I7+C)dW=xiaGcOJ!wEv& zhE_r+Z5P;`ZGceN_6nibY)9-sN9`vGowA=JbiwW=^s23gP{`Ix=p~zvkW(UPmh4Y3 z1-V|D-+I7w)Hv-nT)fV8aUxf*Ux|0p*;_4IEORv6YN^gwFr{AGzX1N+Kks;hUxspH?0g|P646Oe0K3u1_5ByhS zQ}Q7^DWa#8@pbNzqj2Q>v}5h)SL2Q?<459-9TN?4$AQ?vj=1A=OuQI(ycFYKTA;Zd zte7vexo99SZ$`0m+9tn9adTT{O>k!_G&?(vhU7`bUndk(}6J3D$;xfPjF%NLLm=Cx@EC4JNmjf1wD*%hdLco;} z;zcXLb0Z~UG45B1D*;!FC4g(hRe+`9YQQpa4d7a_6tG+@1FR6&0=^)Y1FjP*0N0Bz z0B#W10d5r6M>chFkT|RU|Vkp^WxbP&1{&^i-fI5qUu7P!N!DL)F zuRQ|`vFKMR`~rx%L+|fYbdpj~QZm2Dt;e)($D}7g1t}QtseEq;;!E&JQkY$tg3=9; zHm^v5zVvBthBkLLtd`7gSuJ})%t0aMfcdgw<+pd=;@=Q(@z#NF9T+S8pyd6M=@r$t z_}>%e&wo;+LGHqn!z_2IGqN`fvB?G@Ln4TruIyp}3=sN~3Ad=wP(QyxR6W+97M2ecPL3ra5&^Nq z zvW+NC=hT98Rw$VgN=6IcTm7BY-z^=JKNhOzw=_9)GV;F8)zMtU)!ZO*n-G<~#i9Z6 z`dTV2quvvjGXzyyuNX1VnqZ3NOF>^a1U0&1z^^D!UEA@5h8t=#>#yU%g*r=?21v8^ z$e}@~-#I%gBN?%Lg=7W9TIt$b{5OT$!pO=gVbw#y`AyetSFE^le8Uf_?^OTunu%-Q zulrbNoZoxNdGd8u4wepBGAuUzlM*mPNQE^HpRP6^wn^Yd=!jq>l=zhnQd^J8J-Z&a_7uB7$) zGF(xwAEFq>xVq^oE5(;*>uya&(fWJG68k$X&o;bG@9-UaCo5yy-ERmZg>RL-Su!oG z9^D@oD#r__gxY87@V8NDy0F8bS6RR;Za0 zYR1pc2)mzYm~VYa!wlkiW@(b4x1`CFwA3#~{MTD?M$EHoJC@FgigJc?qf-k-5D|31-H#!#lHOSSfdLIKGeWP(aHOGB`W zB?;^hxn=r4e9`cw~yn z%DAw7VpCjbm=z9935V{ko)OxfiG%)(6->ljYh3bJ&(X)z?Mxz1+PTQpW@`7uJmv&l z9EyvU_M8e2BWTs&RSi8UJ)g~sq?}AGnsd*Fvczm)7Jd&KKM*SRFM65Pep~J#&z^&3 zph8>&M7{sTyDKIe=j-`#0k*>_VJ{L)?pomSv#bTKvsca=p@q~_!J&D4bnyioV*CWB{IL39$* zHZiM=as^)9F5d*4XDlgY)0g@RM(o6HRI0z~a?Rcx8P4Dp%4Ps`S#?cXe~q4)5;i|m zcg?U;mdtxHH=gl=b^28q&rzR$50kqq*k0xfaNT&-99Dkc*gM}8xbQ{lC88mn$2L}oix{Lt2~0_tpfiLK1&~`R!lN2epM^>gWx9- zj2gXLm>r`1j!|QE0v^ws=9-(qAav(&I3!14d>};{oeL|sr?=l5ydo*Ampvdd1)dPi zx%+jgWk{%qF26M8=^YE)3WQks_KB9sV|V?r(`RElyJF|P(a94i96O9lf0wq9wM-^|u^+N`8h-X3OpPS`aQYm;Con0%%$PIAx6 zWYXmNbAe)L>6E1dOD1;5DjQ?v`zHJD_QwvKik*IGx}^JK;nMsLw4($z6Kk+#L>nsF zEJN3zb3|S=W44iJ6#Sy8fn)Y9Z}1z)+`ndc3_?Vcmmz5R96(*A6{3?_f?&SyFh11FQ#K%f)_L0$tZRnqJJi*GSj*jgICOj$Z0ZJ&;bq zCjT+?_&3}>XFRXL3Z{zL9d72TXp=o zBhOh}6_mxdX3*nN#B-j5{#9Gap3_D&&AbrhHI&z`ga+ka z$)ogpRY6avI>3!Q39n1sc!%^GnKs72Oa2MvE$)+ICZQh8PZvu|)9tf&?VjLs*i4rbKw^bCU|s@8t5(jV~m$UmTbwti{I z=Z78xmsl-T1ct;a2ji)#%?D228wtt7sxy6GSk2R!yWW*NHB$b7iZmfkzQZcjGZ^%t zCwOAO@&-gHH2pUrRA@ot)b`Y5P|LC~L;$=ZTQe1yU-oG{BkT1`4kLJa}0GGTZvB>w;6A>Jj^Q@JwB7S z@nL@PTdp@ww{>pYOFdiVi|dDDt|LM z;xvA%akOuG#m3ncHB&2UrdQP7I{eU8Ji2d8imj@NyJ}{g4O7mB$qh5k7ax{y9IqN* zGjSxgxpDH?A7A;?;Qe6i?1kx;i?c1hsTSXKOJA)2T1>f)iVbV-rAIZF)$$JGt;4^2 z>X$3I@;<|-C0zch(S7l}P3WV9LW!OEx9wx~(8OCa*WposghT zMV>%^$^VqVzXHIjUlk(KK$!8V<(;?+VgG3OuuknVjfi}o0CWM4Es@k^n*P-u(mbE; zm^2>2Q$e)Js|*Gr3MB_|YjnLYu{70|N*Urz?R{s zn54wL&{W{MfwWrqyP-tV0CgQnr>+I4F=Nj}PhZY;W~G>J^7CL}pW!rrg7d8>tqqzF zyiq|f8T6B%Tr{CtMCw!@YjJ}rt1R!IA@rWQZ&XZ< zfR$${_94LU(UVl&PfZEi3|B6)YRf7DdlNT>3-bm}h$9ndzcV#;IqW zrBlw*(XO$!c-i(D=Z*yPe$V|~_f&c3-Q6Ene(XLoUEcYg`#tyDnNicFoiom}85vH& zN_76?gYDDhr}PYIQPZWTW}F>!nK(ILzuQ!ESTNsr?E-vYDj~SG0`QLo$Kh2u*!pLz zxxblg9ZPBa(pVBU9TrhmkRrj7cx}yPkpDe;fU%`^c#q|pzVahVHGi^hWlDjr>1gIy ze-KLs*q*Su3=DfheMy_Rq8`e{V3u0K2&$2Ad09-6h@C!9bM(S=(Z#HD^mB8j8bJE( zy9b*#neT0KH0{YTU51`pABXmu)X4G1M95}L)ERx)nC&Sv?=+2kAtz6!ji;qezLOLm z7T`uotPf%S3Zk6rPFhO3jC!3Q4~_Ion|$-`7n_W2tF@`P$drYLKw`?8q5?QJTg*q- zB>dbf`uz%)$k@3KR6x%8`|yQgCLg|!W-&z?%k*HtAH1p-q}wt@e0Vyf88nGw!)=}! zH2)Lj+5nKroGaLTPrTccdbdY^x4JXe6kj0I@6&st09hFCLo8fheDlTc)sOT4xRI?{ zAGr!fHvI0z(dxKs!>qGr%2_kxtcB%rwqWyA!RCMAitXu~EjSx5IQuBSe2kyT-w12v z@-D;YR?PP)SM}!>x~C>$^>>nk&us_Bfl1Z1+t9uJ0E)nm4-cSFr0! zM{`3Cy7-IVAi797P0ye0VN;hW+e#NRJ)w4yew@j6v#&=}+BG)E7NRcN8)G4Zy5_uN z?mNXrS$$jEQmCtK>rztD+Y&Trj=FU9k`bajgu1JSiuP#!nPW{`VREA{P?)z(l`T!2 zq^OXmZ*o*Kvvt58kp_^O8j|214?=N^MVsd^;Fkr7&Y!k{oZZj-D}PF)-7+| zYr9v!3D@^mJKYZ3{Y?U0?{&BZ*8>w>KX5qQYg`YC+4Wk7yDkTn{_NL7rHfhdjoIuu zy475#yD$z#=rYZUZ00-((=DUwES-fWRfg=@-pS6{2!2L4!t%qsi_}k5R!GXzo*w542B{e zIw=E(GvM1Z7^gyTMUzuBeKMCk{X)fkk19u-l}nSf38S&^z2J9((`##|SJc5|T)q9) z;gQvGtamO6)gEVe^ z%3>1I_z9E7&CA?n=KD2Hx7~JsuYl_Zc87bl>p>B_E_1kR|396l?~47uN9T^(+B#h& zX2xatUt(@YSNY+K?vpA&Nfop;1gcN$B(=+*B7uzk?zuFot>m;lBM%*7q7x^MAn}GJ z50&FdmXmuWx6~58PgVFw0M9{*>p+epMc-&0wZJUAI3bF&1!v*~XVM9=GlLL2L5OEE z2vH{Y#SAl4(jYwZ_duBb9g9!n7Z9J%uLv(~nVC{?dFBDg_M)Tti5>BzQk$EAs+o{W zZp~^t={gbhU@JT+q^ndR#}TewD&#Bi}&=)j1*A0%!Ur zZqDNg$YaltW+1{2#UiF2q)4c+%Vbit+n=tRfBIB!x>E=ao_cEEiqA!~v(=(STem`| z^!dph1FkK)PYP|mMpvXiOX$ON_7_h7iWa9o(|lL9!h@s}TPDjO{^-0g56(si4@Tf7 z2YonNEDgcZR1xuBxgz;0lJAsNyh*L>Sz@Tl&!&T`CS_Q`#?4?44m|6X6r2Vo4+d4c zG=vk#BIMGE6cJe_J0knQP_@evs>u9J zhxvqPtLtryktLg?N&W$R%!bcT`>@~+!mmebw@2$K)t!r-JwqWsp_=>@U`hJGC(hzW zMdfc@{?_HOGap=j|MCyd{J1c7$QN($P4ALoSCpB-q1f=v>A_c_?lc+ClmEy?V=p1e z7$x+I(M^YQ9Wh-ZCOus>dZE_VJxy#C^h^Cg`#X6!e+P3|q`2U{HO{h3UV zJTw>+jIpi&;RDYN+0IQ#Z45H9r!k!qx-EKyPRR4^Uo0=FICDaM;vVbQ=OJsOT!Mn= ziv*ptU)f^df}?35WD~d?9$c(gHFJi*C}5ft^T~drm$> zA+#H!>;r(q6TEkDz{xkAx1A$hGeYS@SHY}n?UZZnJHxZCs<^9)1nG&}C*Ej-5oj5F z3##z8|TZ3~b6b9u)^`3+zo z(F!_~P-(2I0|zBP;96ogIAxHIO4Lk>LPt@;de|euIg$(y!ye|N?8SBh++reh5TeK1 z%lL0sE(Z5}%Vt0w)4cSt2ORw&6sd7 zA-G!c_mQhrb7;B86*R3e-&^Bos>z&kq}?zE^XoAN{(7~nT4sC`&0kQnN;{GYq>@Fm zdQQD6!wFkw($C3qt(qn!|1A;6{{iTf4J7&h1ee*>51DBO7D{9#AP}$-Afck2Xke$? zZn{ls8nT1poB(qb3e6KumT+c*B6+SrHScD{l&Q zR{j~lm#b#PHqc3?r?nGCyKzn_X%1d;SS7DD%{gL@<^bomgwgQ&2RGlpIbFJIA`mZi z&o~cfRT!FgkpXbN(Jx8Qe}$fZJ_j|-X&T`bq6ClDZT zjldv*D1lcA{1$<)6Zi)NzCqwy1ZZPaexCr_w*4=3MH;I7-wFH`f&WPWOpcTPn!x`i z;6h{M0s@5uRuWhZpcZ*N^bxwh*Ml!WWq%JgdK9#;z=KaWp}G5TRz|NB2zWgB^vsnO ziEPNu9b)TJ`7phMKpFm(9|QdL?_f$iHX6-a9t#HZuE!Pwt}I6LzQ=;myeS=+aBFAj zOh|$&JWa%9q~py8A3KczD9>l00xtua%xgch=bH;3Z!nuL8Xhk*n@<&D^AW@6`4)4F;Zu$vbNXk^q0t|}SFLFNO8FQya*_ZE zR`$&-`(A`hK{Aucv?x)9!Jr=>)4~eyeR{wXVA7e^caE?#7Wq|*UEBS;g z_=L;D-_N;ov)nlhgF&)Cwi^r$=u4dXLhzj7GojgJXfceI%&w}QT2=iS$8JA8Vc~e^ bt>}zt)kEI-J14((^6RI5Y2x_RtRDXtDmL6U literal 0 HcmV?d00001 diff --git a/scripts/contracts/__pycache__/test_contract_tooling.cpython-314.pyc b/scripts/contracts/__pycache__/test_contract_tooling.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a19fddea072ca4b8bad322d2f694579575bfd399 GIT binary patch literal 7690 zcmb_hTW}NC89uAsl~%T7TfSfeHWJ|yWWc82m^%amu1$(@7GcuFuvtrMydX=;*%ii~ zbjswRF>PnUluqjgrliwO2ReP=r87*QfG09}v5_xy1Jh0~)5#l!%+T=E|3AAcSw?|` zva+$|NpM72$m7Je*dq(ovaHG@(;|om*^xOJ%Gena-7IqH!Cs~KI@5cme8x3LvR-}su>@O@>XzfI%QHd zZtkZvO)Wt_bSP#C$+VilTJa$*ehL!)6Ix26s;MQS9!oGXTHKPTW@OSSLxW=PX^rZG zqj7f-c;}qVqh3g;u;#~z$;rg@9IrC@MI;R)t0$%)dq9dZ57H|OAbqk3Qj)zO{jv{a znJj@Um;Gj-kC;KZYz2vyTfttMPGsX6jcJBy#NF57TLt)sMUx&SAaR!T=gZus8SxN8 ztNTc=v$WXAz52Ko(gx*tg$dMgp<_Q{vd`1z^a+bhU^rY)l&2DuS>>u>Xw=;QURF(7 zWp*Y;v)WZ2)=f(eT3!to+OUKZYQ`{d0IMSrtE+V=J*>5!R+CB1XdOw@Q$|LOYpug7 zrP_O~Mx5#y(`b!5n`N|`Fl)ulDrP#J)Ke!~GNaZuR+7$WDK(=jL#m-bpNf&r(zvG3 zbQX#gHI-2EJF2ME)CU0phSgQHrY$etQwXMIyb9th`DcCOg^_cCaqnFzFef!kNevT= zKB=CTRzm)x%5#+y!mQNv2&$5zJ;cRid$<8s+Ev&tF2li|a0QTOJpK9okv)ALnP0aA z`?4=t*zD}&c2424o-D7-Q;&Y1TdxQgZ86g8cS{QQenu{s-_E@$93Y8CR6^K)fNLLL zmG8F`s0U|`*9n+GBYC5okYz;n9t`>M?mHL+gET&xa|wXa!(J`4p7a^(v$))%xXp3 zfjHd+|Bu#zcm-Z|mc&RWEJ+4*x{uomPtTCEVwk)E(`VBJ?s<)ep?JntB+~Khu$BV$ zh%f*~hIPXLxJL9;WI&Cd(o%_@tK_Q3^0aKQL|Z>_{Hr5$5!@{Q4g~9&4BcfND}m5L z_cp#2r+a0PriT>(HmIVj0J@=2?W7imv9#<+rsHbT*wvEn9=4Pii$Qz=^P3FEr}_sb z55F}T*?+rkHqw*pANcB#0Hq&P*K)DWo~YOI>#5T)u{0f}*p^}`;{l2qVDV~d^r}c} z@F|vCURwbv5IC_o#X#(W`bV6J?qC}Cg7dKsyu7ukhjAJ2f;da=OKblUT6Q5k?!6bR zo~&7O?ez75KaI@=J7$9&_Zpfm`p$Kacis(DeRSyDq04(O?ww-GmJ7!(@4dYD<3cvq z7@rO#9*d-6F9)sR?@qAeiGl0%hqewuyt#Za$Xh~rN1eY2dU+-=pJIZ3TyJ*udfimv zzPyMjT+PaOWKVxyzwzMd^Axn#ttAVqp`(7#T>Fwm;1=&Q*(B)JeTq#g-IBt+zsgN7 zU=uj-z7d;94x9KHn=EMM&ceL|H=FB0lEOd~sN@n!x@zq|xAzhQ}cd-twJRsjj9>utAdPY@&z00)0n4 zbR`Z#wAE-HM^M}@tG|Q>u;vaisPVX-0DXlMaa4f=av-TClypjg6B{fT<=N!09Nu7{ z7Bg}PbpFPBK6}kS)4c8aiK*uOA07Gek>7g1G7zmFR7SX8ltnm-j7ghNgh7;Evlk%L zr3o#QOphX^Mx13rN*zX%&R!_G0^VmVTp!vFPjLYaSZ{~p-kFNMefzb^$j%!;ahA&Ar%_1R#3jVjU|UEDP0O&90DNpNpgW4jD_sj>A+K#`y!K){VRgT>O-Ow2R?gV!|F#z=BW}vNOwRnqPjB<5($L6o$y8z?6g7qqc z$t?R?Iob0}9ztzoR}l|YgH@Bal-wG?aCzQbz(dH3T!9zA)~XR49ugT3EokNPk9!9m z^5SnE4|$)$aA;{712Oj$Tm#mPup*-nvAqv7iS|oKWopcto8cf z|9M4)Qc4ZZ4YOg;Y@zFMEC@0%Tx|QSovHV?o~O0%Wvmtwzi|Y_S;CaH`TDxs(&W}R z=YsNVP%f>puztC)>?*9C%Qec=fy1b;hrvFP;qOjRw(XONwmlogo0W@EZY=NMi}e*X z2$n;7^mjIhVtw5VI*ivdJRg>%j=ja}w1@w3vAk5NqIpXM53kf-Q471=m>&G<8WPeC)> zlj-RvbUC_{GKX5-pVx#!yhZlRw)_fz!g+*7^elaBMSR5qaRSl4kkr&)Xus5Uv3>mT-F4p?=O+VerljaSsp@>qPlD%9&PdDWBzUssO6|1N;vnZz z#V1CtVZ*hCnTBn%()MQnWZ42!ryosMc7X$tcfv@X#xh(X?9<1&+Xb#27)Pxf0BAuN zX#VEPVo}apl=C6*zEyi^e*02zlYQ>^SU557dO8!t_st7p6=vf?6KK~DAS@eO3V{Km zHKCo>k`Ugbtxk-uWmFwbT7o&6(JY@1QI-=LwaO5k=@fVfhUGH=bHl1(`8%?DGSQtH zOj~6cihY>CcUAykmxmHP00Tn&qT*20V2UHQylMtQiV3U24r%tJ?GUGnU&#$xAli*= z(k#aDT8867$Pot%Tnr&kJ4j;=Ax+zXXlrW;?0&PR=-(J4AQ)aR36tR+GYva(H9N

3rjiv}|H@Mv7ipJ0rDUdw)jScjNe!6uS$!UQjkR zBX!J4-BVKc?Urfj`z)HdbV^z}(fPaXD?_=Jn`fmhPa_=Hju{6BY=lGBzIyWrcb^dW z4&V~w)8Wd}h%eTT3Bi){&`0J z_m1rMZvErPD!Pr4C>S{@$^ik6@_A~lA?u1x1-pD;=3sD!xKG@VhDs)rznOA+Ynb& z6AZlp_9aBILW&Yk!iFdGxEa^rQ;7oRZE)U0Xog1o>&(}o71$pN4B#0^IRE_+mi3`W z5SKj+h@!?ltniA*xQA7|7~>w6@nRoHLEM3bTOU`5V$%Zxf>8zQ2xmR4>M3~fV!8!7 zv;+jSiyIO8C1^!v7|lrpTuIr2anHc%Ll$D_MV^to+iuk!w2N qHCMZ8s&>_v1Tqg!azqG>k4^K7?+Sqr4}Ng)M~D8y6JZH^@&5o2@e>#T literal 0 HcmV?d00001 diff --git a/scripts/contracts/__pycache__/verify_transcript.cpython-314.pyc b/scripts/contracts/__pycache__/verify_transcript.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d06712bbe1eb7a3a07cf21c3206980cd04ab2de GIT binary patch literal 8050 zcmb_heQX;?cAw?)OZ=w3zi3gGY)jS$C0n*^*|A|opRq+VYnf7PO7x0cN$XJLdUq*V z^cfNImy%v;rI%imONuCH(b~OB<3s+aTcD`ZT&_B_#Vt)KR5m&!=m*-Oe>AaET%G>Y zH%l%_>4c^zI)Y|qXWq=bdGF24?>)BItR@60{x4r%|JaGpKj05F8M2ipuM!9?Aqt6T z5>do3AreZd5j9FliX@bpVeODk)YWBZ^rBwrGl&MIG>S$j$>E|QlV}<;i)MvRJ8T)U ziq;{UXdAMN_92Jp7;=ivIz;IzkXTIVAupLIr3@#=5ehFMREuZNtu#US4y(-O%X&0{ zC}TJ9jFVVCQSS3(>V-K~G@+wR-I`2W9RjM(!v0LXE88SiO%%htRX(+$fV4|psb-?& zyJ#kADDy!WJ5dXidS6e$G}$V&*Gy_Ci^`cgJII7+Q2v{(iTC_;gbmXojX&z%|A?jDZLLr08XY>P!Bi|!c5WCmyGLa{i^7j_E_M?*UkS!_3X zn+aL#qs7@~ja=Ht)6?RSjw2nNN3x8^c-fw<0uTK>ALE;KvX;7daa6WG8;goO9TJsh zSsRJbVcC>lQ#QaxW083#loh-pIS1q<{6F~~nC%h@gaS5|%7^#4E7c=EsqV(BIe!G9BD4>2S`LooBnp(MEy6PkaM`6!i>yG`` zchgW#l{Z!2oWt*!hR3x~LVg=aq}Xo<8RX6`$iPUI6!O41{f@o(ph`VRoqf;#vhV0G zS9#=${l!d;!dve7^B1eVzN2}Wt2KXae|2yg_FTO}9nXAU(Td%7OdXvssY;b9r$f*b z=x}e1SAEBh{{JLW;xC!D@1d>gBN~-dvZc3CBW$C(O(<5&Ud> zAdAu*+rCK8MP$9e@be5W>*pA8HWp441#VrPygktpxSh~lo%Bq!Bs76rGI0xv+cI%m z)^dO%Wa8|O*Io-;W7~W(1lfL62QU688i3s?w??s_@MX`nx;tD_xi|kNS^}VoUsn85 zVqkTQWQk@CkM9l)U33bjX(nY&dxuPLG7%K;18~somis)ju{p*wPe&q*;8}?AFA5wT zVmxy+50J|vgm{({1y3joVgwItmYrS<0;Gv%I@`HLx#;u^6J>64{293_L(qOY5(@$J zI@6x(!tbJhAHRQv{YkE~KV7&d-o3F(zdQ5R%v$K($ZF)ng{K0R#mnyfM52M7n__3; zvAE!l@$NZR5ZLHU9;~^ip;ac_iISUb8D`t!fa`!cmI;GeL|Tjsa#0RN%7!pAO~)f* z!qwxBM?t6YIY5B`^Z=X10d{6axMia{L^j}E15Ls~b6kWCDG1YZU5G_t{?IHlM`Q4& z;8K}5BAc`I-dK1sQL}52=Pq6JsvtJewga)|Bz57(Mdqd`n*`92 z7-6Fz9GxJ7-O5@;S2_&21(_5WQP!~{GY9=~4ow;28=I5$Cu)% zh&fpk<79)f1=zY7PYs74#EOfu72h@>o;lcx$nf|C@&Jc`fhjOjz7AJyS+fmhO&aX~ zMx4c58M22?CbAQ#Q}z=}LtCvKA9(M1Q>~}dt%J$d!Hw48l!4$4P_kXwYCN!Z<0lP2KCn8vG`L+{wXEN2JNCiky~&NXXZ{1Bk}#QU zd*-dgTk~tg8voZ7A60x@wLUFfp4jwGCeKa2(Y+C3(xKR+P;7(a)7<AR2a~l^gX?jUzmS=HsgXklnae%ylixKh?2|E}dM@d+HfV@J&3b=s2tK=jeFhB(Pz!OZNuf zJGRz&|Hhv#{B+@g@Zt9!-b!{;Z!D}5KUlaoxICCFzqD<(Eie4m>DA5+bM1C)eUE3D-T6xOS zl(uvxEuHrpH!R)Iai`~%o)tD_c0V$=-Y-vg_9i=fA0AD24nFD}+;Y{u>wn9?Hgf++ z%5^;L>PfnK9-MqQm2wTIT_Z`?h~&K{O@&h~ChcOAE>^nEr(D9acgya2WIwQ7-mrFi zEhZiFNsZ5^$|=b~VHWnKEFEdf$)x4vgM$yNHha!*ST1ZC?JJ$HS(Xi3M#~-JE5?g%JZr226eat!L=5ZTICOJ9pE4Sr#;ZQE+3edqsnK)Q70Z!Svb z15)io$~Gw(C$}v1Yu!@oz`9K`k4f6Gr;pDO$bOZ0`i&l&%F`!$R53~j`1bzDQsHgY ze$-SA`Cql{_jZ~-F773u_@vSR`Oh`g-V?^3*Bc<;0b=ld498yk#H}2D2rldaT2vR$ zhcC=sN$mrdk0=N(IDPoe-<4Ea6$%yv7x2xjBp-ezb|rA42^AO?1Q#?M(T+HhvN_NI z08maF$U#n@W!Dt&3A5xNB@RAN8YQCVR^TYU;FUy?;MVH)aBKCGxBPlaTL`2n9e#{Q zIs*C85G~w$G^|G`>j7ujNu&%rW-=Plh(%dpS36!I;K=ZXkPlzfyHb751wTauK?iX% zm}}t-fijicZ%}0j;RCbUssvw*dxcOaldmdIrPdU;e-HUkrdE{%N_C|=RD%NG+GUd8 zm~Ri@7fngvsB3q^eGG{GWDe>REOMhjF1VscVc1hG0Paz8uaL=T&Y`bEE;OQL(J9h} zSfU2pfBdJ0QNVE_&cm#3)Piyr2_dCg#Da_Ag8A_u&~%{e49)h$U>`FF@l62IO5hVC zEF8d$D=-&u03YWtIt|4*7Gj{WX%(kV0Z3#ZA_@bi;hAs$wvmU4=7Dr34gpI*tWZ@k ztQ!y;ARTe>TfmE<*c{*ib}GWSN1pYmXmrOR#_0Pf{6F~v$a0>@0K8L9h&N*VkWCpA z%pN;1B|otf(Q)6U7(*V9KYA)K;^`p6Czv_5Hsw>FQ-IHhZ}G&Q?< zjZI(Ulh=4jcnO|snmDI<*ZCvo?+-o|@S%Fye$GMs&T+1!c@5} z!s&qEfx^R4s6aI(o4g-~djb8Cs4?p+O zp$E_Zr0av8Kkm6tfAGxRXFfiZ^;a7X5Q!#rOK@Z%;vb8+7ihsPu;49-@GAoy3NiRV z_qe^yB#(EIsNQiTb4ONOgcCaf1kFw!>zT)o9e)gGm~FlrGFc1v!Xow{7*45a!TlCx zBZm6Zu%ui(3bhzd^NW3e^Yu{w7eJfPaAo$gqUI{6D${7TZ$)=T2c+w_RNRX4~(ytr=3qO@N`y4ym+#!`!}I zS_yDkIk8Wa67sTM&2s-%Q_FJyO7o_rc6IEL<-k9dRwl7+rz*XvQZIDdOQp&)8}>6> zrB$nkH%kvl2fLG{C%)|NSm+t#wQwds+yDQ!KxVLg18-L9-%ec^4p zbhu};vS)dC%TkuMxRU_+O8=+$s&snMS*ACnwoUmrrnBJoudvN)G6Jbm1TLEL|S(}+qNzCnbwam!Km zrsZ|Z>g7)z4PTMa@a>kP;n!AVYXPP*e0?jN0#UzL(QnlLMKxp}H=iT=PZ@sJV(z!< ze|Fpk^-rv|{jEixG;46Zg~0Vz64y_e2drk<7!1zQY%~~zo7Ef!e;LP!;8e?|d{hYk zV1^A@uI3s1v@zk}Z|EIp^lTLWNY}hiHUxtZlL-c8BhAmiFK-Z6yAFKvbCAgf?8ipf zDaC{{rvPsPeD6g;HsLEITAb>_r92k8(?Ud2_xhp2pQ|g28g#6rRNx6 z!FGtmAkH^YAQQlYtD3 z4Qh(NY&<;|3&$hO8UDAS4Z~yseuzS#FoO76OA^{AMnpJ1N9NCw{a2_`sT6&VO#h5d ze}RTJ(a>Xk12IlKzCctG&aaLmtz|i}L00`rTf2pAudGYm&DQn}>z-KL|AB_HG@Xvy&mXW0a n$$TiOZTd`GwA^^-@T-Sc{A=enOb0*J9{SpjwAK7;!18|s7eV`8 literal 0 HcmV?d00001 diff --git a/scripts/contracts/bundle.py b/scripts/contracts/bundle.py deleted file mode 100644 index 0414620e..00000000 --- a/scripts/contracts/bundle.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -"""Export the exact checked-out component's contract for retention with its image. - -This records evidence; deployment verification still rejects unverified operations. -Never use this to manufacture contracts for older releases from current sources. -""" -import argparse -import json -import subprocess -import sys - -from compatibility import ROOT, ContractError, digest, load, policy, version -from check import check - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--component", choices=["backend", "frontend", "mcp"], required=True) - parser.add_argument("--version", required=True) - parser.add_argument("--variant", choices=["server", "cloud"]) - parser.add_argument("--output", required=True) - args = parser.parse_args() - version(args.version) - errors, gaps = check() - if errors: - raise ContractError("; ".join(errors)) - if args.component == "backend" and not args.variant: - raise ContractError("backend bundle requires --variant") - sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip() - # Verify tracked contract inputs match the commit whose identity is exported. - paths = list(load(ROOT / "docs/developer/api-contract-source-review.json")) + ["docs/developer/openapi.yaml", "docs/developer/api-contract-source-review.json"] - tracked = subprocess.run(["git", "ls-files", "--error-unmatch", "--", *paths], cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - if tracked.returncode: - raise ContractError("commit all audited source files before exporting a source-bound bundle") - result = subprocess.run(["git", "diff", "--quiet", "HEAD", "--", *paths], cwd=ROOT) - if result.returncode: - raise ContractError("commit audited contract inputs before exporting a source-bound bundle") - contract = load(ROOT / "docs/developer/openapi.yaml") - # Until consumer call coverage is complete, use the entire backend surface. - # This is conservative and documented; never silently omit dependencies. - bundle = {"format": "marreq.contract.v1", "component": args.component, "version": args.version, "source_sha": sha, "contract": contract} - if args.component == "backend": - bundle.update(variant=args.variant, frontend_compatibility=load(ROOT / "marreq-core/frontend_compatibility.json")) - else: - directory = "mcp-server" if args.component == "mcp" else "frontend" - bundle["compatibility"] = load(ROOT / directory / "compatibility.json") - policy(bundle["compatibility"]) - from pathlib import Path - Path(args.output).write_text(json.dumps(bundle, indent=2) + "\n") - print(json.dumps({"file": args.output, "sha256": digest(args.output), "version": args.version, "source_sha": sha})) - return 0 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except (ContractError, OSError, ValueError) as error: - print(str(error), file=sys.stderr) - sys.exit(1) diff --git a/scripts/contracts/check.py b/scripts/contracts/check.py deleted file mode 100644 index 8b98ceeb..00000000 --- a/scripts/contracts/check.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -"""Validate OpenAPI, route coverage, compatibility metadata, and review evidence.""" -import argparse -import hashlib -import json -import sys - -from openapi_spec_validator import validate -from compatibility import ROOT, ContractError, load, policy, version, resolve -from generate import generated -from inventory import source_routes - - -def source_evidence(): - files = set() - for directory in ("marreq-core/src", "marreq-server/src", "marreq-cloud/src", "frontend/src/api", "mcp-server/src"): - files.update(file for file in (ROOT / directory).rglob("*") if file.suffix in (".ts", ".rs") and "__tests__" not in file.parts and not file.name.endswith(".test.ts") and file.name != "generated-contract.ts") - # Direct frontend calls outside the centralized API modules are still - # dependencies, including binary downloads/imports and OAuth navigation. - for file in (ROOT / "frontend/src").rglob("*"): - if file.suffix in (".ts", ".tsx") and "__tests__" not in file.parts: - content = file.read_text() - if "/api/" in content or "fetch(" in content: - files.add(file) - files.update(ROOT / name for name in ("marreq-core/src/app.rs", "marreq-core/src/build_info.rs", "marreq-core/src/api/mod.rs", "marreq-cloud/src/routes.rs", "marreq-cloud/src/deployment.rs", "marreq-server/src/deployment.rs", "marreq-server/src/routes.rs", "marreq-server/src/main.rs", "marreq-cloud/src/main.rs", "frontend/compatibility.json", "marreq-core/frontend_compatibility.json", "mcp-server/compatibility.json")) - return {str(file.relative_to(ROOT)): hashlib.sha256(file.read_bytes()).hexdigest() for file in sorted(files) if file.is_file()} - - -def check(require_complete=False): - spec = load(ROOT / "docs/developer/openapi.yaml") - resolve(spec, spec) - validate(spec) - for component in ("frontend", "mcp-server"): - policy(load(ROOT / component / "compatibility.json")) - backend_range = load(ROOT / "marreq-core/frontend_compatibility.json") - if set(backend_range) != {"min_version", "max_version"} or version(backend_range["min_version"]) > version(backend_range["max_version"]): - raise ContractError("invalid backend frontend compatibility range") - errors = [] - source = {(r["path"], r["method"]): r for r in source_routes()} - documented = {(p, m): op for p, item in spec["paths"].items() for m, op in item.items()} - for key in source.keys() - documented.keys(): - errors.append(f"undocumented backend route: {key}") - for key in documented.keys() - source.keys(): - errors.append(f"documented route missing in backend: {key}") - for key in source.keys() & documented.keys(): - variants = [source[key]["variant"]] if source[key]["variant"] != "shared" else ["server", "cloud"] - if documented[key].get("x-marreq-variants") != variants: - errors.append(f"deployment variant mismatch: {key}") - if documented[key].get("x-marreq-guards") != source[key]["guards"]: - errors.append(f"authentication guard mismatch: {key}") - if documented[key].get("x-marreq-handler") != source[key]["handler"]: - errors.append(f"handler mismatch: {key}") - for path, content in generated(spec).items(): - if not path.exists() or path.read_text() != content: - errors.append(f"generated artifact drift: {path.relative_to(ROOT)}; run scripts/contracts/generate.py") - lock = ROOT / "docs/developer/api-contract-source-review.json" - if not lock.exists() or load(lock) != source_evidence(): - errors.append("API source changed since contract review. Audit wire behavior, update OpenAPI/tests, then run check.py --record-source-review. This acknowledgement is not a substitute for schema or integration tests.") - gaps = [f"{m.upper()} {p}" for (p, m), op in documented.items() if op.get("x-marreq-unverified")] - if require_complete and spec.get("x-marreq-contract-complete") is not True: - errors.append("producer contract is explicitly incomplete; finish the P0 audit before publication") - if require_complete and gaps: - errors.extend(f"unverified wire contract: {gap}" for gap in gaps) - return errors, gaps - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--record-source-review", action="store_true", help="Explicitly acknowledge source audit; review resulting diff") - parser.add_argument("--require-complete", action="store_true", help="Release safety: fail on inventory-only operations") - args = parser.parse_args() - if args.record_source_review: - (ROOT / "docs/developer/api-contract-source-review.json").write_text(json.dumps(source_evidence(), indent=2) + "\n") - errors, gaps = check(args.require_complete) - print(json.dumps({"valid": not errors, "unverified_operations": len(gaps), "errors": errors}, indent=2)) - return 1 if errors else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/contracts/check_baseline.py b/scripts/contracts/check_baseline.py deleted file mode 100644 index 4c34a673..00000000 --- a/scripts/contracts/check_baseline.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -"""Reject breaking changes to audited operations in the PR base contract.""" -import copy -import json -from pathlib import Path -import sys - -from compatibility import ROOT, compare, load - - -def check(previous, proposed): - if "x-marreq-contract-complete" not in previous: - # The pre-bootstrap document is explicitly partial and has a different - # /api server base. It is not evidence for a historical consumer release. - return {"compatible": None, "bootstrap": True, "errors": [], "reason": "Previous OpenAPI predates verified contract inventory; no historical compatibility established."} - audited = copy.deepcopy(previous) - audited["paths"] = {path: {method: op for method, op in item.items() if not op.get("x-marreq-unverified")} for path, item in previous["paths"].items()} - audited["paths"] = {path: item for path, item in audited["paths"].items() if item} - errors = sorted(set(error for variant in ("server", "cloud") for error in compare(audited, proposed, variant))) - return {"compatible": not errors, "errors": errors} - - -if __name__ == "__main__": - result = check(load(Path(sys.argv[1])), load(ROOT / "docs/developer/openapi.yaml")) - print(json.dumps(result, indent=2)) - sys.exit(1 if result["errors"] else 0) diff --git a/scripts/contracts/compatibility.py b/scripts/contracts/compatibility.py deleted file mode 100644 index ef89be12..00000000 --- a/scripts/contracts/compatibility.py +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/env python3 -"""Fail-closed, offline compatibility checks for immutable component bundles. - -The deployment plan is an input, never inferred from package versions or main. -Only local files are read; artifact downloading/authentication belongs to CI/CD. -""" -from __future__ import annotations - -import argparse -import copy -import hashlib -import json -from pathlib import Path -import re -import sys - -import yaml -from openapi_spec_validator import validate as validate_spec - -ROOT = Path(__file__).resolve().parents[2] -METHODS = {"get", "post", "put", "patch", "delete", "head", "options"} - - -class ContractError(ValueError): - pass - - -def load(path): - with Path(path).open() as stream: - return yaml.safe_load(stream) - - -def version(value): - # No coercion, wildcards, prereleases, or lexicographic comparison. Release - # candidates need their own explicitly verified deployment plan. - if not isinstance(value, str) or not re.fullmatch(r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)", value): - raise ContractError(f"expected stable SemVer X.Y.Z, got {value!r}") - return tuple(map(int, value.split("."))) - - -def policy(value): - if not isinstance(value, dict) or set(value) - {"requires_backend_min", "requires_backend_max"}: - raise ContractError("invalid compatibility metadata fields") - if "requires_backend_min" not in value: - raise ContractError("compatibility metadata requires requires_backend_min") - low = version(value["requires_backend_min"]) - high = version(value["requires_backend_max"]) if "requires_backend_max" in value else None - if high is not None and low > high: - raise ContractError("compatibility minimum exceeds maximum") - return low, high - - -def resolve(node, document, seen=()): - """Expand local refs; reject external refs and recursive schemas conservatively.""" - if isinstance(node, list): - return [resolve(item, document, seen) for item in node] - if not isinstance(node, dict): - return node - if "$ref" in node: - ref = node["$ref"] - if not ref.startswith("#/") or ref in seen: - raise ContractError(f"unsupported external or recursive reference: {ref}") - target = document - for key in ref[2:].split("/"): - target = target[key.replace("~1", "/").replace("~0", "~")] - target = copy.deepcopy(target) - target.update({key: value for key, value in node.items() if key != "$ref"}) - return resolve(target, document, (*seen, ref)) - return {key: resolve(value, document, seen) for key, value in node.items()} - - -def subset(source, target, location): - """Prove every source value is accepted by target, or fail closed. - - Requests: released consumer -> proposed producer. - Responses: proposed producer -> released consumer. - Unsupported changed constraints cannot accidentally establish compatibility. - """ - if source == target or target is True or target == {} or source is False: - return [] - if target is False or source is True: - return [f"{location}: schema acceptance changed"] - if not isinstance(source, dict) or not isinstance(target, dict): - return [f"{location}: unsupported schema change"] - ignored = {"title", "description", "examples", "example", "deprecated", "default", "readOnly", "writeOnly", "$schema", "x-rust-model"} - supported = {"type", "enum", "const", "properties", "required", "items", "additionalProperties", "minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"} - unknown = (set(source) | set(target)) - supported - ignored - errors = [] - for key in sorted(unknown): - if source.get(key) != target.get(key): - errors.append(f"{location}: changed {key} needs explicit compatibility evidence") - def types(schema): - value = schema.get("type", []) - return set(value if isinstance(value, list) else [value]) - a, b = types(source), types(target) - if b and (not a or any(t not in b and not (t == "integer" and "number" in b) for t in a)): - errors.append(f"{location}: type {sorted(a)} is not accepted by {sorted(b)}") - for key in ("enum", "const"): - if key in target: - candidates = source.get("enum", [source["const"]] if "const" in source else None) - accepted = target[key] if key == "enum" else [target[key]] - if candidates is None or any(item not in accepted for item in candidates): - errors.append(f"{location}: incompatible {key}") - for key in ("minimum", "minLength", "minItems", "minProperties"): - if key in target and (key not in source or source[key] < target[key]): - errors.append(f"{location}: tightened {key}") - for key in ("maximum", "maxLength", "maxItems", "maxProperties"): - if key in target and (key not in source or source[key] > target[key]): - errors.append(f"{location}: tightened {key}") - for key in sorted(set(target.get("required", [])) - set(source.get("required", []))): - errors.append(f"{location}.{key}: required property is not guaranteed") - source_props = source.get("properties", {}) - target_props = target.get("properties", {}) - source_extra = source.get("additionalProperties", True) - target_extra = target.get("additionalProperties", True) - for key in sorted(set(source_props) | set(target_props)): - left = source_props.get(key, source_extra) - right = target_props.get(key, target_extra) - if left is not False: - errors.extend(subset(left, right, f"{location}.{key}")) - errors.extend([] if source_extra is False else subset(source_extra, target_extra, f"{location}.*")) - if "items" in source or "items" in target: - errors.extend(subset(source.get("items", True), target.get("items", True), f"{location}[]")) - return errors - - -def operations(spec, variant): - result = {} - for path, item in spec["paths"].items(): - for method in METHODS & item.keys(): - op = item[method] - if variant in op.get("x-marreq-variants", ["server", "cloud"]): - result[(path, method)] = {**op, "parameters": item.get("parameters", []) + op.get("parameters", [])} - return result - - -def compare(consumer, backend, variant): - consumer_resolved = resolve(consumer, consumer) - backend_resolved = resolve(backend, backend) - validate_spec(consumer) - validate_spec(backend) - required = operations(consumer_resolved, variant) - offered = operations(backend_resolved, variant) - errors = [] - for key, old in required.items(): - path, method = key - at = f"{method.upper()} {path}" - if key not in offered: - errors.append(f"{at}: endpoint missing in {variant} backend") - continue - new = offered[key] - for op in (old, new): - if op.get("x-marreq-unverified"): - errors.append(f"{at}: contract has unverified behavior; compatibility cannot be established") - old_security = old.get("security", consumer.get("security", [])) - new_security = new.get("security", backend.get("security", [])) - if old_security != new_security: - errors.append(f"{at}: authentication/scopes changed") - if consumer.get("components", {}).get("securitySchemes") != backend.get("components", {}).get("securitySchemes"): - errors.append(f"{at}: security scheme definitions changed") - for name in ("x-marreq-policy", "x-marreq-csrf", "x-marreq-bearer-scopes"): - if old.get(name) != new.get(name): - errors.append(f"{at}: {name} changed") - old_params = {(p["in"], p["name"]): p for p in old.get("parameters", [])} - new_params = {(p["in"], p["name"]): p for p in new.get("parameters", [])} - for p, param in new_params.items(): - if p not in old_params: - if param.get("required"): - errors.append(f"{at}: new required {p} parameter") - else: - if param.get("required") and not old_params[p].get("required"): - errors.append(f"{at}: {p} parameter became required") - errors.extend(subset(old_params[p].get("schema", {}), param.get("schema", {}), f"{at} parameter {p}")) - if set(old_params) - set(new_params): - errors.append(f"{at}: accepted parameters removed") - old_body, new_body = old.get("requestBody", {}), new.get("requestBody", {}) - if new_body.get("required") and not old_body.get("required"): - errors.append(f"{at}: request body became required") - for media, content in old_body.get("content", {}).items(): - if media not in new_body.get("content", {}): - errors.append(f"{at}: request media {media} removed") - else: - errors.extend(subset(content.get("schema", {}), new_body["content"][media].get("schema", {}), f"{at} request")) - old_responses, new_responses = old["responses"], new["responses"] - # Even error status changes matter: clients branch on 401/403/404/409. - if old_responses.keys() != new_responses.keys(): - errors.append(f"{at}: response status codes changed") - for status in old_responses.keys() & new_responses.keys(): - previous = old_responses[status] - proposed = new_responses[status] - if previous.get("headers", {}) != proposed.get("headers", {}): - errors.append(f"{at} {status}: response headers changed") - old_content, new_content = previous.get("content", {}), proposed.get("content", {}) - if old_content.keys() != new_content.keys(): - errors.append(f"{at} {status}: response media changed") - for media in old_content.keys() & new_content.keys(): - errors.extend(subset(new_content[media].get("schema", {}), old_content[media].get("schema", {}), f"{at} response {status}")) - return sorted(set(errors)) - - -def digest(path): - return hashlib.sha256(Path(path).read_bytes()).hexdigest() - - -def read_bundle(descriptor, component, directory): - required = {"file", "sha256", "version", "source_sha"} - if not isinstance(descriptor, dict) or set(descriptor) != required: - raise ContractError(f"{component}: descriptor requires exactly {sorted(required)}") - version(descriptor["version"]) - if not re.fullmatch(r"[a-f0-9]{40}", descriptor["source_sha"]): - raise ContractError(f"{component}: immutable source SHA required") - relative = Path(descriptor["file"]) - if relative.is_absolute() or ".." in relative.parts: - raise ContractError(f"{component}: bundle file must stay inside the plan directory") - path = directory / relative - if not path.resolve().is_relative_to(directory.resolve()): - raise ContractError(f"{component}: bundle symlink escapes the plan directory") - if not path.is_file(): - raise ContractError(f"{component}: missing historical artifact {path}; never substitute current main") - if not re.fullmatch(r"[a-f0-9]{64}", descriptor["sha256"]) or digest(path) != descriptor["sha256"]: - raise ContractError(f"{component}: bundle SHA-256 mismatch") - bundle = load(path) - if bundle.get("format") != "marreq.contract.v1" or bundle.get("component") != component: - raise ContractError(f"{component}: wrong bundle format/component") - for key in ("version", "source_sha"): - if bundle.get(key) != descriptor[key]: - raise ContractError(f"{component}: bundle {key} disagrees with deployment plan") - resolve(bundle["contract"], bundle["contract"]) - validate_spec(bundle["contract"]) - return bundle - - -def verify_manifest(path): - manifest = load(path) - if not isinstance(manifest, dict) or set(manifest) != {"format", "variant", "backend", "frontend", "mcp"}: - raise ContractError("deployment plan requires format, variant, backend, frontend and mcp") - if manifest["format"] != "marreq.deployment-contract.v1" or manifest["variant"] not in ("server", "cloud"): - raise ContractError("invalid deployment plan format/variant") - bundles = {name: read_bundle(manifest[name], name, Path(path).parent) for name in ("backend", "frontend", "mcp")} - backend = bundles["backend"] - if backend.get("variant") != manifest["variant"]: - raise ContractError("backend artifact deployment variant mismatch") - errors = [] - backend_version = version(backend["version"]) - frontend_range = backend.get("frontend_compatibility", {}) - if set(frontend_range) != {"min_version", "max_version"}: - raise ContractError("backend frontend compatibility metadata is incomplete") - lo, hi = version(frontend_range["min_version"]), version(frontend_range["max_version"]) - if lo > hi: - raise ContractError("backend frontend compatibility range is inverted") - if not lo <= version(bundles["frontend"]["version"]) <= hi: - errors.append("frontend: outside backend's supported consumer range") - for name in ("frontend", "mcp"): - consumer = bundles[name] - low, high = policy(consumer.get("compatibility")) - if backend_version < low or (high is not None and backend_version > high): - errors.append(f"{name}: backend {backend['version']} is outside declared range") - errors.extend(f"{name}: {error}" for error in compare(consumer["contract"], backend["contract"], manifest["variant"])) - return {"compatible": not errors, "variant": manifest["variant"], "versions": {name: bundle["version"] for name, bundle in bundles.items()}, "errors": errors} - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--manifest", required=True, type=Path) - parser.add_argument("--candidate-component", choices=["backend", "frontend", "mcp"]) - parser.add_argument("--candidate-version") - parser.add_argument("--candidate-source-sha") - parser.add_argument("--candidate-bundle", type=Path) - args = parser.parse_args() - try: - candidate = (args.candidate_component, args.candidate_version, args.candidate_source_sha) - if any(candidate): - if not all(candidate): - raise ContractError("candidate check requires component, version and source SHA") - manifest = load(args.manifest) - descriptor = manifest[args.candidate_component] - if descriptor["version"] != args.candidate_version or descriptor["source_sha"] != args.candidate_source_sha: - raise ContractError("deployment plan does not reference the exact tagged candidate; refusing to check a different artifact") - if args.candidate_bundle: - if not all(candidate): - raise ContractError("candidate bundle requires candidate identity") - if digest(args.candidate_bundle) != descriptor["sha256"]: - raise ContractError("candidate contract is not the source-exported bundle for this tag") - result = verify_manifest(args.manifest) - except (ValueError, OSError, KeyError, TypeError) as error: - result = {"compatible": False, "errors": [str(error)]} - except Exception as error: # validators also have their own exception types - result = {"compatible": False, "errors": [f"invalid contract: {error}"]} - print(json.dumps(result, indent=2)) - return 0 if result["compatible"] else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/contracts/generate.py b/scripts/contracts/generate.py deleted file mode 100644 index b9037409..00000000 --- a/scripts/contracts/generate.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -"""Reproduce TypeScript wire types and compiled-Rocket route expectations.""" -import argparse -import json -import sys - -from compatibility import ROOT, load - - -def ts(node): - if node is True or node == {}: - return "unknown" - if node is False: - return "never" - if "$ref" in node: - return node["$ref"].split("/")[-1] - for key in ("oneOf", "anyOf", "allOf"): - if key in node: - join = " & " if key == "allOf" else " | " - return "(" + join.join(ts(item) for item in node[key]) + ")" - if "const" in node: - return json.dumps(node["const"]) - if "enum" in node: - return " | ".join(json.dumps(item) for item in node["enum"]) - kind = node.get("type") - if isinstance(kind, list): - return " | ".join(ts({**node, "type": value}) for value in kind) - if kind == "null": - return "null" - if kind in ("number", "integer"): - return "number" - if kind in ("string", "boolean"): - return kind - if kind == "array": - return f"Array<{ts(node.get('items', {}))}>" - if kind == "object" or "properties" in node: - lines = [] - for name, value in node.get("properties", {}).items(): - optional = "" if name in node.get("required", []) else "?" - lines.append(f" {json.dumps(name)}{optional}: {ts(value)};") - extra = node.get("additionalProperties", True) - if isinstance(extra, dict): - lines.append(f" [key: string]: {ts(extra)};") - elif extra: - lines.append(" [key: string]: unknown;") - return "{\n" + "\n".join(lines) + "\n}" - if node.get("x-marreq-unverified"): - return "unknown" - raise ValueError(f"unsupported TypeScript schema: {node}") - - -def generated(spec): - header = "// Generated by scripts/contracts/generate.py from docs/developer/openapi.yaml.\n// Do not edit; this file carries wire types, not a second contract.\n\n" - types = header + "\n\n".join(f"export type {name} = {ts(schema)};" for name, schema in sorted(spec["components"]["schemas"].items())) + "\n" - routes = [] - for path, item in spec["paths"].items(): - for method, operation in item.items(): - routes.append({"method": method.upper(), "path": path, "variants": operation.get("x-marreq-variants", ["server", "cloud"])}) - return { - ROOT / "frontend/src/api/generated-contract.ts": types, - ROOT / "mcp-server/src/generated-contract.ts": types, - ROOT / "docs/developer/api-contract-routes.json": json.dumps(routes, indent=2) + "\n", - } - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--check", action="store_true") - args = parser.parse_args() - stale = [] - for path, content in generated(load(ROOT / "docs/developer/openapi.yaml")).items(): - if args.check: - if not path.exists() or path.read_text() != content: - stale.append(str(path.relative_to(ROOT))) - else: - path.write_text(content) - if stale: - print("Generated contract drift; run python3 scripts/contracts/generate.py:\n" + "\n".join(stale), file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/contracts/inventory.py b/scripts/contracts/inventory.py deleted file mode 100644 index 9376ed63..00000000 --- a/scripts/contracts/inventory.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Source audit helpers, corroborated by Rocket's compiled route inventory. - -This extracts route signatures, not Rust syntax or inferred serialization schemas. -The checked-in OpenAPI remains the authoritative wire contract. -""" -from pathlib import Path -import re - -ROOT = Path(__file__).resolve().parents[2] - - -def source_routes(root=ROOT): - routes = [] - for directory, variant in (("marreq-core/src/api", "shared"), ("marreq-cloud/src/api", "cloud"), ("marreq-server/src/api", "server")): - for file in sorted((root / directory).glob("*.rs")): - text = file.read_text() - # Rocket route attributes always precede a free handler function. - pattern = r'#\[(get|post|put|patch|delete)\(\s*"([^"]+)"(.*?)\)\]\s*pub (?:async )?fn (\w+)\s*\((.*?)\)\s*(?:->\s*(.*?))?\s*\{' - for match in re.finditer(pattern, text, re.S): - method, uri, attributes, name, arguments, returns = match.groups() - base = "" if file.stem == "oauth" else "/api" - path, _, query = uri.partition("?") - path = base + re.sub(r"<([\w]+)(?:\.\.)?>", r"{\1}", path) - guards = re.findall(r"\b(?:ApiUser(?:OrBearer)?|SessionUser|OptionalSessionUser|AdminOnly|Project[A-Z][A-Za-z]*|ProjectsRead|StdioMcpAuditAuth|McpAuditAuth|McpPrincipalAuth|InternalMcpAudit)\b", arguments) - routes.append({"path": path, "method": method, "handler": f"{file.stem}::{name}", "source": str(file.relative_to(root)), "variant": variant, "arguments": arguments.strip(), "returns": (returns or "()").strip(), "query": query, "guards": guards, "data": re.findall(r'data\s*=\s*"<([^>]+)>"', attributes)}) - return routes diff --git a/scripts/contracts/requirements.txt b/scripts/contracts/requirements.txt deleted file mode 100644 index d21b1402..00000000 --- a/scripts/contracts/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -PyYAML==6.0.3 -jsonschema==4.26.0 -openapi-spec-validator==0.7.2 diff --git a/scripts/contracts/test_compatibility.py b/scripts/contracts/test_compatibility.py deleted file mode 100644 index 823c9d34..00000000 --- a/scripts/contracts/test_compatibility.py +++ /dev/null @@ -1,227 +0,0 @@ -import copy -import hashlib -import json -from pathlib import Path -import subprocess -import tempfile -import unittest - -from compatibility import ContractError, ROOT, compare, policy, subset, verify_manifest - - -def spec(): - obj = {"type": "object", "properties": {"id": {"type": "integer"}, "state": {"type": "string", "enum": ["draft", "approved"]}}, "required": ["id", "state"], "additionalProperties": True} - return {"openapi": "3.1.0", "info": {"title": "fixture", "version": "1.0.0"}, "paths": {"/api/things/{id}": {"post": {"security": [{"bearerAuth": []}], "parameters": [{"in": "path", "name": "id", "required": True, "schema": {"type": "integer"}}], "requestBody": {"required": True, "content": {"application/json": {"schema": {"type": "object", "required": ["title"], "properties": {"title": {"type": "string"}}, "additionalProperties": False}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": obj}}}, "401": {"description": "Unauthorized"}}}}}, "components": {"securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer"}}}} - - -def operation(document): - return document["paths"]["/api/things/{id}"]["post"] - - -def response(document): - return operation(document)["responses"]["200"]["content"]["application/json"]["schema"] - - -def request(document): - return operation(document)["requestBody"]["content"]["application/json"]["schema"] - - -class CompatibilityTests(unittest.TestCase): - def setUp(self): - self.old = spec() - self.new = copy.deepcopy(self.old) - - def check(self): - return compare(self.old, self.new, "server") - - def test_valid_openapi(self): - self.assertEqual(self.check(), []) - - def test_removed_route(self): - self.new["paths"] = {} - self.assertIn("endpoint missing", self.check()[0]) - - def test_incompatible_request_type(self): - request(self.new)["properties"]["title"] = {"type": "integer"} - self.assertTrue(self.check()) - - def test_incompatible_response_type(self): - response(self.new)["properties"]["id"] = {"type": "string"} - self.assertTrue(self.check()) - - def test_removed_required_response(self): - response(self.new)["required"].remove("id") - self.assertTrue(self.check()) - - def test_new_required_request(self): - request(self.new)["properties"]["reason"] = {"type": "string"} - request(self.new)["required"].append("reason") - self.assertTrue(self.check()) - - def test_optional_request_addition(self): - request(self.new)["properties"]["reason"] = {"type": "string"} - self.assertEqual(self.check(), []) - - def test_response_addition(self): - response(self.new)["properties"]["extra"] = {"type": "boolean"} - response(self.new)["required"].append("extra") - self.assertEqual(self.check(), []) - - def test_response_enum_extension_breaks_exhaustive_consumers(self): - response(self.new)["properties"]["state"]["enum"].append("reviewed") - self.assertTrue(self.check()) - - def test_request_enum_restriction(self): - request(self.old)["properties"]["title"]["enum"] = ["a", "b"] - request(self.new)["properties"]["title"]["enum"] = ["a"] - self.assertTrue(self.check()) - - def test_oauth_scope_mismatch(self): - operation(self.new)["x-marreq-bearer-scopes"] = ["things:write"] - self.assertTrue(self.check()) - - def test_authentication_mismatch(self): - operation(self.new)["security"] = [] - self.assertTrue(self.check()) - - def test_status_code_mismatch(self): - operation(self.new)["responses"]["201"] = operation(self.new)["responses"].pop("200") - self.assertTrue(self.check()) - - def test_variants(self): - operation(self.old)["x-marreq-variants"] = ["cloud"] - self.new["paths"] = {} - self.assertEqual(self.check(), []) - self.assertTrue(compare(self.old, self.new, "cloud")) - - def test_unverified_contract_fails_even_when_identical(self): - operation(self.old)["x-marreq-unverified"] = True - self.assertTrue(compare(self.old, self.old, "cloud")) - - def test_unsupported_constraint_changes_fail_closed(self): - self.assertTrue(subset({"type": "string"}, {"type": "string", "pattern": "a+"}, "request")) - - def test_nullable_response_breaks_nonnullable_consumer(self): - self.assertTrue(subset({"type": ["string", "null"]}, {"type": "string"}, "response")) - - def test_metadata_invalid_or_incomplete(self): - for value in ({}, None, {"requires_backend_min": "1.x"}, {"requires_backend_min": "2.0.0", "requires_backend_max": "1.0.0"}, {"requires_backend_min": "1.0.0", "typo": "x"}): - with self.subTest(value=value), self.assertRaises(ContractError): - policy(value) - - def deployment(self, directory, incompatible=False, backend_version="1.1.0"): - plan = {"format": "marreq.deployment-contract.v1", "variant": "server"} - for component, ver in (("backend", backend_version), ("frontend", "1.0.0"), ("mcp", "1.2.0")): - contract = spec() - if incompatible and component == "backend": - response(contract)["required"].remove("id") - bundle = {"format": "marreq.contract.v1", "component": component, "version": ver, "source_sha": "a" * 40, "contract": contract} - if component == "backend": - bundle.update(variant="server", frontend_compatibility={"min_version": "1.0.0", "max_version": "2.0.0"}) - else: - bundle["compatibility"] = {"requires_backend_min": "1.0.0", "requires_backend_max": "1.9.0"} - data = json.dumps(bundle).encode() - (directory / f"{component}.json").write_bytes(data) - plan[component] = {"file": f"{component}.json", "sha256": hashlib.sha256(data).hexdigest(), "version": ver, "source_sha": "a" * 40} - path = directory / "plan.json" - path.write_text(json.dumps(plan)) - return path - - def test_unchanged_frontend_and_mcp_with_new_compatible_backend(self): - with tempfile.TemporaryDirectory() as temp: - result = verify_manifest(self.deployment(Path(temp))) - self.assertTrue(result["compatible"]) - self.assertNotEqual(result["versions"]["frontend"], result["versions"]["backend"]) - self.assertNotEqual(result["versions"]["mcp"], result["versions"]["backend"]) - - def test_unchanged_consumers_with_incompatible_backend(self): - with tempfile.TemporaryDirectory() as temp: - result = verify_manifest(self.deployment(Path(temp), incompatible=True)) - self.assertFalse(result["compatible"]) - self.assertTrue(any(e.startswith("frontend:") for e in result["errors"])) - self.assertTrue(any(e.startswith("mcp:") for e in result["errors"])) - - def test_versions_fail_independently_of_contracts(self): - with tempfile.TemporaryDirectory() as temp: - result = verify_manifest(self.deployment(Path(temp), backend_version="2.0.0")) - self.assertFalse(result["compatible"]) - - def test_missing_historical_artifact(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp)) - (Path(temp) / "mcp.json").unlink() - with self.assertRaisesRegex(ContractError, "missing historical artifact"): - verify_manifest(path) - - def test_tampered_artifact(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp)) - with (Path(temp) / "frontend.json").open("a") as stream: - stream.write(" ") - with self.assertRaisesRegex(ContractError, "SHA-256 mismatch"): - verify_manifest(path) - - def test_descriptor_version_cannot_substitute_another_artifact(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp)) - plan = json.loads(path.read_text()) - plan["frontend"]["version"] = "1.0.1" - path.write_text(json.dumps(plan)) - with self.assertRaisesRegex(ContractError, "version disagrees"): - verify_manifest(path) - - def test_bundle_path_cannot_escape_plan_directory(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp)) - plan = json.loads(path.read_text()) - plan["mcp"]["file"] = "../mcp.json" - path.write_text(json.dumps(plan)) - with self.assertRaisesRegex(ContractError, "inside the plan directory"): - verify_manifest(path) - - def test_backend_variant_cannot_be_substituted(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp)) - plan = json.loads(path.read_text()) - plan["variant"] = "cloud" - path.write_text(json.dumps(plan)) - with self.assertRaisesRegex(ContractError, "variant mismatch"): - verify_manifest(path) - - def test_candidate_plan_must_reference_tagged_source(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp)) - import sys - result = subprocess.run([sys.executable, str(ROOT / "scripts/contracts/compatibility.py"), "--manifest", str(path), "--candidate-component", "backend", "--candidate-version", "1.1.0", "--candidate-source-sha", "b" * 40], capture_output=True, text=True) - self.assertEqual(result.returncode, 1) - self.assertIn("exact tagged candidate", json.loads(result.stdout)["errors"][0]) - - def test_invalid_openapi_does_not_pass_compatibility(self): - self.new.pop("info") - with self.assertRaises(Exception): - self.check() - - def test_external_refs_are_rejected_before_any_validator_network_access(self): - response(self.new)["properties"]["id"] = {"$ref": "https://example.test/schema.json"} - with self.assertRaisesRegex(ContractError, "external or recursive"): - self.check() - - def test_release_gate_zero_for_compatible_independent_artifacts(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp)) - import sys - result = subprocess.run([sys.executable, str(ROOT / "scripts/contracts/compatibility.py"), "--manifest", str(path)], capture_output=True, text=True) - self.assertEqual(result.returncode, 0) - self.assertTrue(json.loads(result.stdout)["compatible"]) - - def test_release_gate_nonzero_on_real_violation(self): - with tempfile.TemporaryDirectory() as temp: - path = self.deployment(Path(temp), incompatible=True) - import sys - result = subprocess.run([sys.executable, str(ROOT / "scripts/contracts/compatibility.py"), "--manifest", str(path)], capture_output=True, text=True) - self.assertEqual(result.returncode, 1) - self.assertFalse(json.loads(result.stdout)["compatible"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/contracts/test_contract_tooling.py b/scripts/contracts/test_contract_tooling.py deleted file mode 100644 index 78ef3d3f..00000000 --- a/scripts/contracts/test_contract_tooling.py +++ /dev/null @@ -1,70 +0,0 @@ -import copy -import json -from pathlib import Path -import tempfile -import unittest -from unittest.mock import patch - -from compatibility import ContractError, ROOT, load -from check import check -from check_baseline import check as check_baseline -from generate import generated -from test_compatibility import spec, response -from verify_transcript import verify - - -class ProducerTests(unittest.TestCase): - def test_openapi_has_all_source_routes_and_generated_artifacts(self): - # Review lock is checked separately and cannot stand in for route/schema checks. - errors, gaps = check() - self.assertEqual(errors, []) - self.assertTrue(gaps) # Be honest about the bootstrap's incomplete evidence. - - def test_documented_route_missing_from_actual_source_is_rejected(self): - from inventory import source_routes - routes = source_routes() - with patch("check.source_routes", return_value=routes[1:]): - errors, _ = check() - self.assertTrue(any("documented route missing in backend" in error for error in errors)) - - def test_cloud_route_accidentally_enabled_on_server_is_rejected(self): - from inventory import source_routes - routes = copy.deepcopy(source_routes()) - next(route for route in routes if route["variant"] == "cloud")["variant"] = "shared" - with patch("check.source_routes", return_value=routes): - errors, _ = check() - self.assertTrue(any("deployment variant mismatch" in error for error in errors)) - - def test_source_change_without_contract_review_is_rejected(self): - with patch("check.source_evidence", return_value={"changed.rs": "f" * 64}): - errors, _ = check() - self.assertTrue(any("API source changed" in error for error in errors)) - - def test_release_completeness_cannot_pass_inventory_only_entries(self): - errors, gaps = check(require_complete=True) - self.assertTrue(gaps) - self.assertTrue(any("unverified wire contract" in error for error in errors)) - - def test_audited_baseline_rejects_required_response_removal(self): - old, new = spec(), spec() - old["x-marreq-contract-complete"] = False - response(new)["required"].remove("id") - self.assertTrue(check_baseline(old, new)["errors"]) - - def test_generated_types_come_from_the_contract(self): - document = load(ROOT / "docs/developer/openapi.yaml") - result = generated(document) - document["components"]["schemas"]["BuildInfo"]["properties"]["new_required"] = {"type": "integer"} - document["components"]["schemas"]["BuildInfo"]["required"].append("new_required") - self.assertNotEqual(result, generated(document)) - - def test_empty_integration_transcript_is_not_a_passing_backend_test(self): - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "exchanges.jsonl" - path.write_text("") - with self.assertRaisesRegex(ContractError, "lacks critical cases"): - verify(path) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/contracts/verify_transcript.py b/scripts/contracts/verify_transcript.py deleted file mode 100644 index c513cb5b..00000000 --- a/scripts/contracts/verify_transcript.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -"""Validate real Rocket responses and requests recorded by the DB integration test. - -No credentials are recorded. Inventory-only operations are counted separately. -""" -import argparse -import copy -import tempfile -from pathlib import Path -import json -import re -import sys - -from jsonschema import Draft202012Validator, ValidationError -from compatibility import ROOT, ContractError, load, operations, resolve - - -def verify(path): - spec = load(ROOT / "docs/developer/openapi.yaml") - declared = operations(resolve(spec, spec), "server") - records = [json.loads(line) for line in path.read_text().splitlines() if line.strip()] - checked, inventory_only, seen = 0, 0, set() - for record in records: - matches = [(p, op) for (p, m), op in declared.items() if m.upper() == record["method"] and re.fullmatch(re.sub(r"\{[^}]+\}", "[^/]+", re.escape(p).replace(r"\{", "{").replace(r"\}", "}")), record["path"].split("?")[0])] - if matches: - specificity = min(p.count("{") for p, _ in matches) - matches = [(p, op) for p, op in matches if p.count("{") == specificity] - if len(matches) != 1: - raise ContractError(f"ambiguous or missing operation for {record['method']} {record['path']}") - path_template, op = matches[0] - if op.get("x-marreq-unverified"): - inventory_only += 1 - continue - status = str(record["status"]) - response = op["responses"].get(status, op["responses"].get("default")) - if response is None: - raise ContractError(f"{path_template}: undocumented actual status {status}") - body = record["response"] - schema = response.get("content", {}).get("application/json", {}).get("schema") - if schema is not None: - Draft202012Validator(schema).validate(body) - if record.get("request") is not None and status.startswith("2"): - request_schema = op.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema") - if request_schema is None: - raise ContractError(f"{path_template}: undocumented JSON request") - Draft202012Validator(request_schema).validate(record["request"]) - checked += 1 - seen.add((record["method"], path_template, status)) - required = {("GET", "/api/meta/build", "200"), ("GET", "/api/meta/deployment", "200"), ("GET", "/api/projects/{project_id}/use-cases", "200"), ("POST", "/api/projects/{project_id}/use-cases", "201"), ("PUT", "/api/projects/{project_id}/use-cases/{id}/draft", "200"), ("GET", "/api/projects/{project_id}/use-cases", "403")} - missing = required - seen - if missing: - raise ContractError(f"integration transcript lacks critical cases: {sorted(missing)}") - return {"validated_exchanges": checked, "inventory_only_exchanges": inventory_only} - - -def negative_controls(path): - """Demonstrate rejection using corrupted copies of actual backend traffic.""" - records = [json.loads(line) for line in path.read_text().splitlines() if line.strip()] - cases = [] - for label in ("removed guaranteed response property", "incompatible response type", "incompatible MCP request"): - mutated = copy.deepcopy(records) - if label == "incompatible MCP request": - entry = next(r for r in mutated if r["method"] == "POST" and r["status"] == 201 and r["path"].endswith("/use-cases")) - entry["request"]["title"] = 123 - else: - entry = next(r for r in mutated if r["path"] == "/api/meta/build") - if label == "removed guaranteed response property": - del entry["response"]["backend_version"] - else: - entry["response"]["backend_version"] = 123 - with tempfile.TemporaryDirectory() as directory: - candidate = Path(directory) / "negative.jsonl" - candidate.write_text("\n".join(json.dumps(r) for r in mutated) + "\n") - try: - verify(candidate) - except ValidationError: - cases.append(label) - else: - raise ContractError(f"negative control was silently accepted: {label}") - return cases - - -if __name__ == "__main__": - from pathlib import Path - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("transcript", type=Path) - args = parser.parse_args() - result = verify(args.transcript) - result["rejected_negative_controls"] = negative_controls(args.transcript) - print(json.dumps(result, indent=2)) diff --git a/scripts/generate-api-types.mjs b/scripts/generate-api-types.mjs new file mode 100644 index 00000000..2f3144a1 --- /dev/null +++ b/scripts/generate-api-types.mjs @@ -0,0 +1,26 @@ +// Standard OpenAPI -> TypeScript generation; only component-reachable schemas. +// Committed declarations keep Rust/global OpenAPI out of consumer Docker builds. +import { execFileSync } from 'node:child_process'; +import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import openapiTS, { astToString } from 'openapi-typescript'; +const check = process.argv.includes('--check'); +const directory = mkdtempSync(join(tmpdir(), 'marreq-types-')); +try { + for (const component of ['frontend', 'mcp']) { + const path = join(directory, `${component}.json`); + // Inventory-only entries generate no invented types. Strict release/CI + // coverage is checked separately by `marreq-contracts validate --consumers`. + execFileSync('cargo', ['run', '--quiet', '--locked', '-p', 'marreq-contracts', '--', 'select', '--component', component, '--allow-unverified', '--output', path]); + const spec = JSON.parse(readFileSync(path, 'utf8')); + spec.paths = {}; // Clients currently import schemas, not generated SDK paths. + const ast = await openapiTS(spec); + const declarations = ast.filter(node => node.name?.escapedText === 'components'); + const result = '// Generated by scripts/generate-api-types.mjs; do not edit.\n' + astToString(declarations); + const output = component === 'frontend' ? 'frontend/src/api/generated-contract.ts' : 'mcp-server/src/generated-contract.ts'; + if (check) { + if (readFileSync(output, 'utf8') !== result) throw new Error(`${output}: generated type drift; run node scripts/generate-api-types.mjs`); + } else writeFileSync(output, result); + } +} finally { rmSync(directory, { recursive: true, force: true }); } diff --git a/scripts/install-oasdiff.sh b/scripts/install-oasdiff.sh new file mode 100755 index 00000000..9cd25409 --- /dev/null +++ b/scripts/install-oasdiff.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Pinned upstream standards engine, used only by developer/CI Rust tooling. +set -euo pipefail +[[ $(uname -s) == Linux && $(uname -m) == x86_64 ]] || { echo 'Install oasdiff 1.34.0 for your platform and set OASDIFF.' >&2; exit 1; } +destination=${1:-"$HOME/.cache/marreq-contracts"} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + https://github.com/oasdiff/oasdiff/releases/download/v1.34.0/oasdiff_1.34.0_linux_amd64.tar.gz \ + --output "$work/oasdiff.tar.gz" +printf '%s %s\n' 7c88e09fd500b231af2819eb21f6fdfc7bcdfe2741d2c1937c59e7b72c8a575a "$work/oasdiff.tar.gz" | sha256sum --check +mkdir -p "$destination" +tar -xzf "$work/oasdiff.tar.gz" -C "$work" oasdiff +install -m 755 "$work/oasdiff" "$destination/oasdiff" +if [[ -n ${GITHUB_PATH:-} ]]; then printf '%s\n' "$destination" >> "$GITHUB_PATH"; fi diff --git a/scripts/contracts/smoke-consumers.mjs b/scripts/smoke-api-consumers.mjs similarity index 51% rename from scripts/contracts/smoke-consumers.mjs rename to scripts/smoke-api-consumers.mjs index abbb0821..8de0086d 100644 --- a/scripts/contracts/smoke-consumers.mjs +++ b/scripts/smoke-api-consumers.mjs @@ -1,26 +1,37 @@ -// Real HTTP producer/consumer smoke test. Run only through test-backend.sh: +// Real HTTP producer/consumer smoke test. Run only through test-api-contracts.sh: // it provides an isolated fixture DB and loopback-only Rocket process. import assert from 'node:assert/strict'; import { appendFileSync } from 'node:fs'; -import { MarreqClient } from '../../mcp-server/dist/client.js'; -import { fetchJson } from '../../frontend/src/api/transport.ts'; +import { MarreqClient } from '../mcp-server/dist/client.js'; +import { fetchJson } from '../frontend/src/api/transport.ts'; +import { getCsrfToken, loginJson, getMe, logoutJson } from '../frontend/src/api/auth.ts'; +import { requestPasswordReset } from '../frontend/src/api/publicAuth.ts'; const base = process.env.MARREQ_CONTRACT_BACKEND_URL; assert.match(base ?? '', /^http:\/\/127\.0\.0\.1:\d+$/); assert.ok(process.env.MARREQ_CONTRACT_TRANSCRIPT); +const mode = process.env.MARREQ_CONTRACT_VARIANT ?? 'server'; +const cookies = new Map(); const networkFetch = globalThis.fetch; globalThis.fetch = async (input, init = {}) => { const url = new URL(input, base); assert.equal(url.origin, base, 'contract smoke must stay on the isolated backend'); - const response = await networkFetch(url, init); + const headers = new Headers(init.headers); + headers.set('Origin', base); + if (cookies.size) headers.set('Cookie', [...cookies].map(([k,v]) => `${k}=${v}`).join('; ')); + const response = await networkFetch(url, {...init, headers}); + for (const value of response.headers.getSetCookie()) { + const cookie = value.split(';')[0]; const equal = cookie.indexOf('='); + cookies.set(cookie.slice(0,equal), cookie.slice(equal+1)); + } const text = await response.clone().text(); let body = null; try { body = JSON.parse(text); } catch { /* Empty guard and 204 bodies are valid. */ } // Authorization headers/cookies are never retained in the transcript. appendFileSync(process.env.MARREQ_CONTRACT_TRANSCRIPT, JSON.stringify({ - method: init.method ?? 'GET', path: url.pathname + url.search, + variant: mode, method: init.method ?? 'GET', path: url.pathname + url.search, status: response.status, - request: typeof init.body === 'string' ? JSON.parse(init.body) : null, + request: url.pathname === '/api/auth/login' ? {username:'[test-user]',password:'[redacted]'} : typeof init.body === 'string' ? JSON.parse(init.body) : null, response: body, }) + '\n'); return response; @@ -28,22 +39,34 @@ globalThis.fetch = async (input, init = {}) => { // Execute the actual frontend transport against the running Rocket binary. const build = await fetchJson('/api/meta/build'); -assert.equal(build.deployment_mode, 'server'); +assert.equal(build.deployment_mode, mode); const capabilities = await fetchJson('/api/meta/deployment'); -assert.equal(capabilities.allows_self_registration, false); +assert.equal(capabilities.allows_self_registration, mode === 'cloud'); +assert.equal(capabilities.allows_database_backup, mode === 'server'); +assert.equal(capabilities.allows_self_administered_user_creation, mode === 'server'); +const discovery = await fetchJson('/.well-known/oauth-authorization-server'); +assert.ok(discovery.scopes_supported.includes('use_cases:write')); +assert.deepEqual(discovery.token_endpoint_auth_methods_supported, ['none']); +const csrf = await getCsrfToken(); +await loginJson('manager', 'Contract-only-fixture-58!z', csrf); +assert.equal((await getMe()).username, 'manager'); +await logoutJson(await getCsrfToken()); +await assert.rejects(() => getMe(), (error) => error.status === 401); +if (mode === 'cloud') await requestPasswordReset({email:'absent-contract-user@example.test'}); // These delegated fixture credentials exist only in the ephemeral DB. const context = { baseUrl: base, apiToken: 'uc-write', projectId: 1, mode: 'draft_write', traceWrite: false }; const writer = new MarreqClient(context); const reader = new MarreqClient({ ...context, apiToken: 'uc-read' }); +assert.equal((await reader.getPrincipal()).authentication_type, 'delegated_oauth'); assert.ok(Array.isArray(await reader.listUseCases())); -const created = await writer.createUseCase({ title: 'Network contract use case', primary_actor: 'Tester' }, 'network-contract-create-key'); +const created = await writer.createUseCase({ title: 'Network contract use case', primary_actor: 'Tester' }, `network-contract-create-key-${mode}`); assert.equal(typeof created.id, 'number'); const document = { schema: 'marreq.use-case.v1', title: 'Network contract use case', main: [{ id: 's_net01', text: 'Exercise the real client against Rocket' }] }; const saved = await writer.putUseCaseDraft(created.id, created.draft.revision, document); assert.equal(saved.document.main[0].id, 's_net01'); const detail = await reader.getUseCase(created.id); assert.equal(detail.id, created.id); -await assert.rejects(() => reader.createUseCase({ title: 'Denied write' }, 'network-contract-denied-key'), (error) => error.status === 403); +await assert.rejects(() => reader.createUseCase({ title: 'Denied write' }, `network-contract-denied-key-${mode}`), (error) => error.status === 403 && error.challenge?.includes('use_cases:write')); await assert.rejects(() => reader.withProject(2).listUseCases(), (error) => error.status === 403 || /403/.test(error.message)); console.log('Real frontend transport and MCP HTTP read/write/scope/project-boundary checks passed.'); diff --git a/scripts/contracts/test-backend.sh b/scripts/test-api-contracts.sh similarity index 54% rename from scripts/contracts/test-backend.sh rename to scripts/test-api-contracts.sh index 4a1c66d4..64866abe 100755 --- a/scripts/contracts/test-backend.sh +++ b/scripts/test-api-contracts.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Creates a disposable DB; ignores DATABASE_URL and .env. No production access. set -euo pipefail -cd "$(dirname "$0")/../.." +cd "$(dirname "$0")/.." work=$(mktemp -d) container="marreq-contract-$RANDOM-$$" backend_pid= @@ -28,7 +28,7 @@ export MARREQ_TEST_DATABASE_URL="postgres://contract_test:disposable-contract-te export DATABASE_URL="$MARREQ_TEST_DATABASE_URL" export MARREQ_CONTRACT_TRANSCRIPT="$work/exchanges.jsonl" cargo test --locked -p marreq-core --no-default-features --test delegated_oauth_mcp_tools_postgres_integration --no-run --message-format=json > "$work/test-build.jsonl" -test_executable=$(python3 -c 'import json,sys; print(next(r["executable"] for r in map(json.loads,open(sys.argv[1])) if r.get("executable") and r["target"]["name"]=="delegated_oauth_mcp_tools_postgres_integration"))' "$work/test-build.jsonl") +test_executable=$(node -e 'const fs=require("node:fs"); console.log(fs.readFileSync(process.argv[1],"utf8").trim().split("\n").map(JSON.parse).find(r=>r.executable&&r.target.name==="delegated_oauth_mcp_tools_postgres_integration").executable)' "$work/test-build.jsonl") # A clean working directory and environment prevent dotenv/SMTP/semantic-search # configuration inherited from a developer machine from entering test processes. (cd "$work" && env -i PATH="$PATH" LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" \ @@ -38,29 +38,33 @@ test_executable=$(python3 -c 'import json,sys; print(next(r["executable"] for r # Run the actual frontend transport and MCP HTTP client against a live socket. # All database and service configuration is pinned to this ephemeral test stack. npm run build --prefix mcp-server -cargo build --locked -p marreq-server --message-format=json > "$work/server-build.jsonl" -server_executable=$(python3 -c 'import json,sys; print(next(r["executable"] for r in map(json.loads,open(sys.argv[1])) if r.get("executable") and r["target"]["name"]=="marreq-server"))' "$work/server-build.jsonl") -backend_port=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()') -export ROCKET_ADDRESS=127.0.0.1 ROCKET_PORT="$backend_port" +cargo build --locked -p marreq-server -p marreq-cloud --message-format=json > "$work/server-build.jsonl" +frontend/node_modules/.bin/rolldown scripts/smoke-api-consumers.mjs --platform node --format esm --file "$work/smoke.mjs" export ROCKET_DATABASES="{my_db={url=\"$DATABASE_URL\",pool_size=2}}" export MARREQ_PUBLIC_BASE_URL=http://localhost:8080 MARREQ_MCP_PUBLIC_URL=http://localhost:8080/mcp export MARREQ_MCP_AUDIT_SECRET=integration-test-audit-secret-32-bytes -export MARREQ_AUTO_SEED=false MARREQ_UI_MODE=api_only -export MARREQ_CONTRACT_BACKEND_URL="http://127.0.0.1:$backend_port" -(cd "$work" && exec env -i PATH="$PATH" LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" \ - DATABASE_URL="$DATABASE_URL" ROCKET_ADDRESS=127.0.0.1 ROCKET_PORT="$backend_port" \ - ROCKET_DATABASES="$ROCKET_DATABASES" MARREQ_PUBLIC_BASE_URL="$MARREQ_PUBLIC_BASE_URL" \ - MARREQ_MCP_PUBLIC_URL="$MARREQ_MCP_PUBLIC_URL" MARREQ_MCP_AUDIT_SECRET="$MARREQ_MCP_AUDIT_SECRET" \ - MARREQ_AUTO_SEED=false MARREQ_UI_MODE=api_only "$server_executable") > "$work/backend.log" 2>&1 & -backend_pid=$! -ready=false -for attempt in {1..60}; do - if ! kill -0 "$backend_pid" 2>/dev/null; then cat "$work/backend.log"; exit 1; fi - if curl --silent --fail "$MARREQ_CONTRACT_BACKEND_URL/api/meta/health" > /dev/null; then ready=true; break; fi - sleep 1 +for mode in server cloud; do + executable=$(node -e 'const fs=require("node:fs"); console.log(fs.readFileSync(process.argv[1],"utf8").trim().split("\n").map(JSON.parse).find(r=>r.executable&&r.target.name===process.argv[2]).executable)' "$work/server-build.jsonl" "marreq-$mode") + backend_port=$(node -e 'const s=require("node:net").createServer();s.listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close()})') + export MARREQ_CONTRACT_BACKEND_URL="http://127.0.0.1:$backend_port" MARREQ_CONTRACT_VARIANT="$mode" + (cd "$work" && exec env -i PATH="$PATH" LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" \ + DATABASE_URL="$DATABASE_URL" ROCKET_ADDRESS=127.0.0.1 ROCKET_PORT="$backend_port" \ + ROCKET_DATABASES="$ROCKET_DATABASES" MARREQ_PUBLIC_BASE_URL="$MARREQ_PUBLIC_BASE_URL" \ + MARREQ_MCP_PUBLIC_URL="$MARREQ_MCP_PUBLIC_URL" MARREQ_MCP_AUDIT_SECRET="$MARREQ_MCP_AUDIT_SECRET" \ + CSRF_ALLOWED_ORIGINS="$MARREQ_CONTRACT_BACKEND_URL" \ + MARREQ_AUTO_SEED=false MARREQ_UI_MODE=api_only "$executable") > "$work/backend.log" 2>&1 & + backend_pid=$! + ready=false + for attempt in {1..60}; do + if ! kill -0 "$backend_pid" 2>/dev/null; then cat "$work/backend.log"; exit 1; fi + if curl --silent --fail "$MARREQ_CONTRACT_BACKEND_URL/api/meta/health" > /dev/null; then ready=true; break; fi + sleep 1 + done + if [[ "$ready" != true ]]; then cat "$work/backend.log"; exit 1; fi + node "$work/smoke.mjs" + kill "$backend_pid" + wait "$backend_pid" || true + backend_pid= done -if [[ "$ready" != true ]]; then cat "$work/backend.log"; exit 1; fi -frontend/node_modules/.bin/rolldown scripts/contracts/smoke-consumers.mjs --platform node --format esm --file "$work/smoke.mjs" -node "$work/smoke.mjs" -# Fails if the test skipped, produced no transcript, or omitted a critical case. -python3 scripts/contracts/verify_transcript.py "$MARREQ_CONTRACT_TRANSCRIPT" +# Missing/skipped critical exchanges fail, not just malformed response bodies. +cargo run --quiet --locked -p marreq-contracts -- verify-traffic "$MARREQ_CONTRACT_TRANSCRIPT" diff --git a/tools/marreq-contracts/Cargo.toml b/tools/marreq-contracts/Cargo.toml new file mode 100644 index 00000000..fdeff3f2 --- /dev/null +++ b/tools/marreq-contracts/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "marreq-contracts" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +anyhow.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sha2.workspace = true +clap = { version = "4.6", features = ["derive"] } +semver = { version = "1.0", features = ["serde"] } +serde_yaml_ng = "0.10" +jsonschema = { version = "0.60", default-features = false } +tempfile = "3" +walkdir = "2" +tree-sitter = "0.25" +tree-sitter-typescript = "0.23.2" diff --git a/tools/marreq-contracts/src/artifact.rs b/tools/marreq-contracts/src/artifact.rs new file mode 100644 index 00000000..a5e12f7e --- /dev/null +++ b/tools/marreq-contracts/src/artifact.rs @@ -0,0 +1,326 @@ +//! Marreq artifact identity/policy. Contract hashes pin files, NOT Docker images. +use crate::{contract, coverage}; +use anyhow::{Context, Result, ensure}; +use semver::Version; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, + process::Command, +}; +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Policy { + pub requires_backend_min: Version, + #[serde(skip_serializing_if = "Option::is_none")] + pub requires_backend_max: Option, +} +impl Policy { + pub fn validate(&self) -> Result<()> { + ensure!( + self.requires_backend_max + .as_ref() + .is_none_or(|max| max.cmp_precedence(&self.requires_backend_min).is_ge()), + "inverted backend compatibility range" + ); + Ok(()) + } + pub fn accepts(&self, backend: &Version) -> Result<()> { + self.validate()?; + ensure!( + backend.cmp_precedence(&self.requires_backend_min).is_ge() + && self + .requires_backend_max + .as_ref() + .is_none_or(|max| backend.cmp_precedence(max).is_le()), + "backend {backend} outside consumer policy" + ); + Ok(()) + } +} +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct FrontendRange { + pub min_version: Version, + pub max_version: Version, +} +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Bundle { + pub format: String, + pub component: String, + pub version: Version, + pub source_sha: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub variant: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compatibility: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub frontend_compatibility: Option, + pub contract: Value, +} +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Artifact { + pub file: PathBuf, + pub sha256: String, + pub version: Version, + pub source_sha: String, +} +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Plan { + pub format: String, + pub variant: String, + pub backend: Artifact, + pub frontend: Artifact, + pub mcp: Artifact, +} +pub fn hash(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|b| format!("{b:02x}")) + .collect() +} +fn sha(value: &str, len: usize) -> bool { + value.len() == len + && value + .bytes() + .all(|b| b.is_ascii_digit() || matches!(b, b'a'..=b'f')) +} +fn component(value: &str) -> Result<()> { + ensure!( + ["backend", "frontend", "mcp"].contains(&value), + "invalid component {value}" + ); + Ok(()) +} +pub fn variant(value: &str) -> Result<()> { + ensure!( + ["server", "cloud"].contains(&value), + "invalid deployment variant {value}" + ); + Ok(()) +} +fn identity(bundle: &Bundle) -> Result<()> { + ensure!( + bundle.format == "marreq.contract.v2", + "unsupported bundle; historical consumer dependencies must be evidenced, not inferred from current main" + ); + component(&bundle.component)?; + ensure!(sha(&bundle.source_sha, 40), "immutable source SHA required"); + if bundle.component == "backend" { + variant( + bundle + .variant + .as_deref() + .context("backend variant required")?, + )?; + ensure!( + bundle.frontend_compatibility.is_some(), + "backend frontend policy missing" + ); + } else { + ensure!( + bundle.variant.is_none(), + "consumer artifacts are deployment-independent" + ); + bundle + .compatibility + .as_ref() + .context("consumer compatibility policy missing")? + .validate()?; + } + ensure!( + !contract::operations(&bundle.contract, None)?.is_empty(), + "empty artifact contract is missing evidence" + ); + Ok(()) +} +pub fn export(root: &Path, who: &str, version: Version, mode: Option) -> Result { + component(who)?; + let spec = contract::load(&root.join("docs/developer/openapi.yaml"))?; + let required: BTreeSet<_> = if who == "backend" { + variant(mode.as_deref().context("backend --variant required")?)?; + contract::operations(&spec, mode.as_deref())? + .into_iter() + .filter(|(_, op)| { + !op.get("x-marreq-unverified") + .is_some_and(|v| v != &Value::Bool(false)) + }) + .map(|(key, _)| key) + .collect() + } else { + coverage::scan(root, who, &spec)?.operations + }; + let selected = contract::select(&spec, &required, mode.as_deref(), true)?; + if who == "backend" { + contract::validate(&selected)?; + } else { + contract::validate_wire_contract(&selected)?; + } + let directory = if who == "mcp" { "mcp-server" } else { who }; + let mut inputs: Vec = [ + "docs/developer/openapi.yaml", + "tools/marreq-contracts", + "Cargo.toml", + "Cargo.lock", + ] + .into_iter() + .map(String::from) + .collect(); + if who == "backend" { + inputs.extend( + ["marreq-core", "marreq-cloud", "marreq-server"] + .into_iter() + .map(String::from), + ); + } else { + inputs.extend( + [ + "src", + "package.json", + "package-lock.json", + "compatibility.json", + ] + .into_iter() + .map(|path| format!("{directory}/{path}")), + ); + } + let status = Command::new("git") + .args(["status", "--porcelain", "--untracked-files=all", "--"]) + .args(&inputs) + .current_dir(root) + .output()?; + ensure!( + status.status.success() && status.stdout.is_empty(), + "commit source/contract inputs before exporting an immutable bundle" + ); + let output = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root) + .output()?; + ensure!(output.status.success(), "cannot determine source identity"); + let source_sha = String::from_utf8(output.stdout)?.trim().to_owned(); + let policy = if who != "backend" { + Some(serde_json::from_value(contract::load( + &root.join(directory).join("compatibility.json"), + )?)?) + } else { + None + }; + let frontend_compatibility = if who == "backend" { + Some(serde_json::from_value(contract::load( + &root.join("marreq-core/frontend_compatibility.json"), + )?)?) + } else { + None + }; + let bundle = Bundle { + format: "marreq.contract.v2".into(), + component: who.into(), + version, + source_sha, + variant: mode, + compatibility: policy, + frontend_compatibility, + contract: selected, + }; + identity(&bundle)?; + Ok(bundle) +} +fn read(directory: &Path, descriptor: &Artifact, who: &str) -> Result { + ensure!( + !descriptor.file.is_absolute() + && !descriptor + .file + .components() + .any(|x| matches!(x, std::path::Component::ParentDir)), + "artifact file must stay inside plan directory" + ); + let path = directory.join(&descriptor.file); + let canonical = path.canonicalize().with_context(|| { + format!( + "missing historical {who} artifact {}; refusing substitution with main", + path.display() + ) + })?; + ensure!( + canonical.starts_with(directory.canonicalize()?), + "artifact symlink escapes plan directory" + ); + let bytes = std::fs::read(canonical)?; + ensure!( + sha(&descriptor.sha256, 64) && hash(&bytes) == descriptor.sha256, + "{who}: contract SHA-256 mismatch (not an image digest)" + ); + let bundle: Bundle = + serde_json::from_slice(&bytes).with_context(|| format!("invalid {who} bundle"))?; + identity(&bundle)?; + ensure!( + bundle.component == who + && bundle.version == descriptor.version + && bundle.source_sha == descriptor.source_sha, + "{who}: artifact component/version/source SHA mismatch" + ); + contract::validate(&bundle.contract)?; + Ok(bundle) +} +pub fn verify(path: &Path, candidate: Option<(&str, &Version, &str, &Path)>) -> Result { + let plan: Plan = serde_json::from_value(contract::load(path)?)?; + ensure!( + plan.format == "marreq.deployment-contract.v2", + "unsupported deployment plan format" + ); + variant(&plan.variant)?; + let directory = path + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + let backend = read(directory, &plan.backend, "backend")?; + let frontend = read(directory, &plan.frontend, "frontend")?; + let mcp = read(directory, &plan.mcp, "mcp")?; + ensure!( + backend.variant.as_deref() == Some(&plan.variant), + "backend deployment variant mismatch" + ); + if let Some((who, ver, source, file)) = candidate { + let descriptor = match who { + "backend" => &plan.backend, + "frontend" => &plan.frontend, + "mcp" => &plan.mcp, + _ => anyhow::bail!("invalid candidate component"), + }; + ensure!( + &descriptor.version == ver + && descriptor.source_sha == source + && hash(&std::fs::read(file)?) == descriptor.sha256, + "plan does not reference the source-exported tagged candidate" + ); + } + let range = backend.frontend_compatibility.as_ref().unwrap(); + ensure!( + range.min_version.cmp_precedence(&range.max_version).is_le(), + "invalid backend frontend range" + ); + ensure!( + frontend.version.cmp_precedence(&range.min_version).is_ge() + && frontend.version.cmp_precedence(&range.max_version).is_le(), + "frontend version outside backend policy" + ); + for consumer in [&frontend, &mcp] { + consumer + .compatibility + .as_ref() + .unwrap() + .accepts(&backend.version) + .with_context(|| consumer.component.clone())?; + contract::compare(&consumer.contract, &backend.contract, &plan.variant) + .with_context(|| consumer.component.clone())?; + } + Ok( + serde_json::json!({"compatible":true,"variant":plan.variant,"versions":{"backend":backend.version,"frontend":frontend.version,"mcp":mcp.version}}), + ) +} diff --git a/tools/marreq-contracts/src/contract.rs b/tools/marreq-contracts/src/contract.rs new file mode 100644 index 00000000..1a829d30 --- /dev/null +++ b/tools/marreq-contracts/src/contract.rs @@ -0,0 +1,387 @@ +//! OpenAPI selection and validation. Standards parsing/comparison is delegated +//! to pinned oasdiff; JSON instances use the jsonschema crate in offline mode. +use anyhow::{Context, Result, bail, ensure}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::Path, + process::Command, +}; + +pub const ENGINE_VERSION: &str = "1.34.0"; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Operation { + pub path: String, + pub method: String, +} +impl std::fmt::Display for Operation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self.method.to_uppercase(), self.path) + } +} +pub fn load(path: &Path) -> Result { + serde_yaml_ng::from_str( + &std::fs::read_to_string(path) + .with_context(|| format!("missing contract evidence: {}", path.display()))?, + ) + .context("invalid JSON/YAML") +} +pub fn write(path: &Path, value: &impl Serialize) -> Result<()> { + std::fs::write(path, serde_json::to_vec_pretty(value)?).context("write contract") +} +pub fn operations(spec: &Value, variant: Option<&str>) -> Result> { + let mut result = BTreeMap::new(); + for (path, item) in spec["paths"].as_object().context("OpenAPI paths missing")? { + for (method, op) in item.as_object().context("invalid path item")? { + if !["get", "post", "put", "patch", "delete", "head", "options"] + .contains(&method.as_str()) + { + continue; + } + if variant.is_some_and(|v| { + op.get("x-marreq-variants") + .and_then(Value::as_array) + .is_some_and(|a| !a.iter().any(|x| x == v)) + }) { + continue; + } + let mut op = op.clone(); + if let Some(parameters) = item.get("parameters") { + let mut merged = parameters.as_array().context("path parameters")?.clone(); + merged.extend(op["parameters"].as_array().cloned().unwrap_or_default()); + op["parameters"] = json!(merged); + } + if op.get("security").is_none() { + op["security"] = spec.get("security").cloned().unwrap_or(json!([])); + } + result.insert( + Operation { + path: path.clone(), + method: method.clone(), + }, + op, + ); + } + } + Ok(result) +} +// Compact a consumer bundle to its operations and reachable local components. +// No dereferencing/schema comparison is implemented here. +pub fn select( + spec: &Value, + required: &BTreeSet, + variant: Option<&str>, + verified: bool, +) -> Result { + let available = operations(spec, variant)?; + let mut selected = json!({"openapi":spec["openapi"],"info":spec["info"],"servers":spec.get("servers").cloned().unwrap_or(json!([{ "url":"/" }])),"paths":{},"components":{}}); + for key in required { + let op = available.get(key).with_context(|| { + format!( + "required operation missing in {} backend: {key}", + variant.unwrap_or("selected") + ) + })?; + if verified + && op + .get("x-marreq-unverified") + .is_some_and(|v| v != &Value::Bool(false)) + { + bail!("required consumer operation has unverified wire contract: {key}"); + } + selected["paths"][&key.path][&key.method] = op.clone(); + } + // Include just referenced components, including security schemes inherited + // at the operation level. External refs are always rejected before tooling. + loop { + let mut references = BTreeSet::new(); + collect_refs(&selected, &mut references)?; + for item in selected["paths"].as_object().unwrap().values() { + for op in item.as_object().unwrap().values() { + for req in op["security"].as_array().into_iter().flatten() { + for name in req.as_object().context("security requirement")?.keys() { + references.insert(format!("#/components/securitySchemes/{name}")); + } + } + } + } + let mut changed = false; + for reference in references { + let pointer = &reference[1..]; + if selected.pointer(pointer).is_none() { + let parts: Vec<_> = pointer.trim_start_matches('/').split('/').collect(); + ensure!( + parts.len() == 3 && parts[0] == "components", + "unsupported non-component reference {reference}" + ); + selected["components"][parts[1]][parts[2].replace("~1", "/").replace("~0", "~")] = + spec.pointer(pointer) + .with_context(|| format!("missing reference {reference}"))? + .clone(); + changed = true; + } + } + if !changed { + break; + } + } + Ok(selected) +} +fn collect_refs(value: &Value, out: &mut BTreeSet) -> Result<()> { + match value { + Value::Object(map) => { + if let Some(r) = map.get("$ref") { + let r = r.as_str().context("invalid $ref")?; + ensure!( + r.starts_with("#/"), + "external references are forbidden: {r}" + ); + out.insert(r.into()); + } + for v in map.values() { + collect_refs(v, out)?; + } + } + Value::Array(a) => { + for v in a { + collect_refs(v, out)?; + } + } + _ => (), + } + Ok(()) +} +fn engine() -> Result { + let binary = std::env::var_os("OASDIFF").unwrap_or_else(|| "oasdiff".into()); + let output = Command::new(&binary).arg("--version").output().context( + "install checksum-pinned oasdiff using scripts/install-oasdiff.sh (or set OASDIFF)", + )?; + let version = String::from_utf8_lossy(&output.stdout); + ensure!( + output.status.success() + && version + .split_whitespace() + .any(|v| v.trim_start_matches('v') == ENGINE_VERSION), + "expected oasdiff {ENGINE_VERSION}, got {version}" + ); + Ok(Command::new(binary)) +} +pub fn validate(spec: &Value) -> Result<()> { + collect_refs(spec, &mut BTreeSet::new())?; + let directory = tempfile::tempdir()?; + let path = directory.path().join("contract.json"); + write(&path, spec)?; + let output = engine()? + .args([ + "validate", + "--allow-external-refs=false", + "--fail-on", + "WARN", + "--format", + "json", + ]) + .arg(path) + .output()?; + ensure!( + output.status.success(), + "OpenAPI validation failed: {}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + Ok(()) +} +// Explicitly bound the schema dialect whose compatibility the pinned engine +// can establish. Advanced semantics are localized to selected consumer schemas, +// never silently ignored or applied to unrelated backend definitions. +fn schema_policy(schema: &Value) -> Result<()> { + let Some(map) = schema.as_object() else { + return Ok(()); + }; + let supported = [ + "$schema", + "$ref", + "$id", + "$defs", + "definitions", + "type", + "const", + "enum", + "allOf", + "anyOf", + "oneOf", + "properties", + "required", + "additionalProperties", + "items", + "minItems", + "maxItems", + "uniqueItems", + "minLength", + "maxLength", + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minProperties", + "maxProperties", + "pattern", + "format", + "title", + "description", + "default", + "examples", + "example", + "deprecated", + "readOnly", + "writeOnly", + "nullable", + "discriminator", + "externalDocs", + "xml", + "x-extensible-enum", + "x-marreq-unverified", + ]; + for key in map.keys() { + ensure!( + supported.contains(&key.as_str()), + "schema keyword {key} is outside the supported compatibility dialect; affected operation needs explicit evidence" + ); + } + ensure!( + !map.get("x-marreq-unverified") + .is_some_and(|v| v != &Value::Bool(false)), + "selected schema is unverified" + ); + for key in ["properties", "$defs", "definitions"] { + if let Some(values) = map.get(key).and_then(Value::as_object) { + for value in values.values() { + schema_policy(value)?; + } + } + } + for key in ["items", "additionalProperties"] { + if let Some(value) = map.get(key) { + schema_policy(value)?; + } + } + for key in ["allOf", "anyOf", "oneOf"] { + if let Some(values) = map.get(key).and_then(Value::as_array) { + for value in values { + schema_policy(value)?; + } + } + } + Ok(()) +} +fn supported_schemas(document: &Value) -> Result<()> { + if let Some(schemas) = document + .pointer("/components/schemas") + .and_then(Value::as_object) + { + for schema in schemas.values() { + schema_policy(schema)?; + } + } + fn inline(value: &Value) -> Result<()> { + match value { + Value::Object(map) => { + for (key, value) in map { + if key == "schema" { + schema_policy(value)?; + } else if !matches!(key.as_str(), "example" | "examples" | "default") { + inline(value)?; + } + } + } + Value::Array(values) => { + for value in values { + inline(value)?; + } + } + _ => (), + } + Ok(()) + } + inline(document) +} + +pub fn validate_wire_contract(document: &Value) -> Result<()> { + validate(document)?; + supported_schemas(document) +} + +pub fn compare(consumer: &Value, backend: &Value, variant: &str) -> Result { + crate::artifact::variant(variant)?; + let required = operations(consumer, Some(variant))?.into_keys().collect(); + let old = select(consumer, &required, Some(variant), true)?; + let new = select(backend, &required, Some(variant), true)?; + validate_wire_contract(&old)?; + validate_wire_contract(&new)?; + // These are Marreq-specific authorization semantics not standard OpenAPI + // security schemes. Compare per operation, never globally across the API. + let a = operations(&old, None)?; + let b = operations(&new, None)?; + for (key, op) in &a { + ensure!( + op.get("x-marreq-csrf") == b[key].get("x-marreq-csrf"), + "{key}: CSRF authentication requirement changed" + ); + let scopes = |v: &Value| -> Result> { + v.as_array() + .context("scope declarations must be arrays")? + .iter() + .map(|s| Ok(s.as_str().context("scope must be string")?.to_owned())) + .collect() + }; + let old = op.get("x-marreq-bearer-scopes"); + let new = b[key].get("x-marreq-bearer-scopes"); + match (old, new) { + (Some(old), Some(new)) => ensure!( + scopes(new)?.is_subset(&scopes(old)?), + "{key}: newly required Bearer scopes" + ), + (None, None) => (), + _ => anyhow::bail!("{key}: Bearer authorization evidence changed"), + } + } + let dir = tempfile::tempdir()?; + let base = dir.path().join("consumer.json"); + let revision = dir.path().join("backend.json"); + write(&base, &old)?; + write(&revision, &new)?; + let output = engine()? + .args([ + "breaking", + "--allow-external-refs=false", + "--fail-on", + "WARN", + "--format", + "json", + "--flatten-allof", + ]) + .arg(base) + .arg(revision) + .output()?; + let findings: Value = serde_json::from_slice(&output.stdout).unwrap_or(json!([])); + ensure!( + output.status.success(), + "incompatible {variant} producer/consumer contract: {findings} {}", + String::from_utf8_lossy(&output.stderr) + ); + Ok(findings) +} +pub fn validate_instance(spec: &Value, schema: &Value, instance: &Value) -> Result<()> { + let mut schema = schema.clone(); + if let Some(obj) = schema.as_object_mut() { + obj.insert("components".into(), spec["components"].clone()); + } + let validator = jsonschema::options() + .offline() + .build(&schema) + .context("invalid/offline JSON schema")?; + validator + .validate(instance) + .map_err(|e| anyhow::anyhow!("wire schema violation at {}: {}", e.instance_path(), e)) +} diff --git a/tools/marreq-contracts/src/coverage.rs b/tools/marreq-contracts/src/coverage.rs new file mode 100644 index 00000000..2a85d84f --- /dev/null +++ b/tools/marreq-contracts/src/coverage.rs @@ -0,0 +1,714 @@ +//! Conservative TypeScript call analysis using the real TS/TSX grammar. +//! Unknown URL or HTTP-method expressions fail instead of dropping dependencies. +use crate::contract::{Operation, operations}; +use anyhow::{Context, Result, bail}; +use serde::Serialize; +use serde_json::Value; +use std::{ + collections::{BTreeMap, BTreeSet, HashSet}, + path::Path, +}; +use tree_sitter::Node; + +#[derive(Debug, Serialize)] +pub struct Coverage { + pub operations: BTreeSet, + pub call_sites: usize, +} + +fn children(node: Node<'_>) -> Vec> { + let mut cursor = node.walk(); + node.named_children(&mut cursor).collect() +} +fn descend<'a>(node: Node<'a>, kind: &str, found: &mut Vec>) { + if node.kind() == kind { + found.push(node); + } + for child in children(node) { + descend(child, kind, found); + } +} +fn text<'a>(node: Node<'_>, source: &'a str) -> &'a str { + &source[node.byte_range()] +} +fn field<'a>(node: Node<'a>, name: &str) -> Result> { + node.child_by_field_name(name).context("missing AST field") +} +fn name(node: Node<'_>, source: &str) -> String { + if matches!( + node.kind(), + "await_expression" | "parenthesized_expression" | "instantiation_expression" + ) && let Some(inner) = children(node).first() + { + return name(*inner, source); + } + if node.kind() == "subscript_expression" + && let Some(index) = node + .child_by_field_name("index") + .filter(|n| n.kind() == "string") + { + return text(index, source).trim_matches(['\'', '"']).into(); + } + node.child_by_field_name("property") + .map_or_else(|| text(node, source), |n| text(n, source)) + .to_owned() +} +fn enclosing(mut node: Node<'_>) -> Node<'_> { + while let Some(parent) = node.parent() { + if matches!( + node.kind(), + "function_declaration" | "method_definition" | "arrow_function" + ) { + return node; + } + node = parent; + } + node +} +fn combine(left: Vec, right: Vec) -> Vec { + left.into_iter() + .flat_map(|a| right.iter().map(move |b| format!("{a}{b}"))) + .collect() +} + +// Only finite string construction is evaluated. Unknown parameter values become +// a segment wildcard; an entirely unknown endpoint or HTTP method is rejected. +fn values( + node: Node<'_>, + source: &str, + root: Node<'_>, + bound: &BTreeMap>, + depth: usize, +) -> Vec { + if depth > 16 { + return vec!["*".into()]; + } + let eval = |n| values(n, source, root, bound, depth + 1); + match node.kind() { + "string" => vec![text(node, source)[1..text(node, source).len() - 1].to_owned()], + "template_string" => { + let mut result = vec![String::new()]; + for child in children(node) { + let part = if child.kind() == "string_fragment" { + vec![text(child, source).to_owned()] + } else if child.kind() == "template_substitution" { + children(child) + .first() + .map_or(vec!["*".into()], |n| eval(*n)) + } else { + vec![text(child, source).to_owned()] + }; + result = combine(result, part); + if result.len() > 128 { + return vec!["*".into()]; + } + } + result + } + "ternary_expression" => ["consequence", "alternative"] + .into_iter() + .filter_map(|f| node.child_by_field_name(f)) + .flat_map(eval) + .collect(), + "binary_expression" + if node + .child_by_field_name("operator") + .is_some_and(|n| text(n, source) == "+") => + { + combine( + eval(field(node, "left").unwrap()), + eval(field(node, "right").unwrap()), + ) + } + "identifier" => { + let id = text(node, source); + if let Some(v) = bound.get(id) { + return v.clone(); + } + let scope = enclosing(node); + let mut vars = vec![]; + descend(scope, "variable_declarator", &mut vars); + if let Some(value) = vars + .into_iter() + .filter(|v| { + v.start_byte() < node.start_byte() + && v.parent() + .and_then(|p| p.child(0)) + .is_some_and(|n| n.kind() == "const") + }) + .filter(|v| { + let mut block = v.parent(); + while block.is_some_and(|p| !matches!(p.kind(), "statement_block" | "program")) + { + block = block.and_then(|p| p.parent()); + } + let mut parent = Some(node); + while let Some(p) = parent { + if Some(p) == block { + return true; + } + parent = p.parent(); + } + false + }) + .rev() + .find_map(|v| { + (v.child_by_field_name("name") + .is_some_and(|n| text(n, source) == id)) + .then(|| v.child_by_field_name("value")) + .flatten() + }) + { + return eval(value); + } + if let Some(parameters) = scope.child_by_field_name("parameters") { + for parameter in children(parameters) { + let ident = parameter + .child_by_field_name("pattern") + .or_else(|| parameter.child_by_field_name("name")); + let numeric = parameter.child_by_field_name("type").is_some_and(|t| { + text(t, source).trim().trim_start_matches(':').trim() == "number" + }); + if numeric && ident.is_some_and(|n| text(n, source) == id) { + return vec!["#".into()]; + } + } + } + vec!["*".into()] + } + "number" => vec![text(node, source).into()], + "member_expression" + if node.child_by_field_name("property").is_some_and(|n| { + let property = text(n, source); + property == "id" || property.ends_with("Id") || property.ends_with("_id") + }) => + { + vec!["#".into()] + } + "parenthesized_expression" | "as_expression" | "non_null_expression" => children(node) + .first() + .map_or(vec!["*".into()], |n| eval(*n)), + "call_expression" => { + let Ok(fun) = field(node, "function") else { + return vec!["*".into()]; + }; + let fun_name = name(fun, source); + let args = node + .child_by_field_name("arguments") + .map(children) + .unwrap_or_default(); + if matches!(fun_name.as_str(), "encodeURIComponent" | "String") { + return vec!["*".into()]; + } + if fun_name == "withQuery" { + return args.first().map_or(vec!["*".into()], |n| eval(*n)); + } + let mut defs = vec![]; + descend(root, "function_declaration", &mut defs); + descend(root, "method_definition", &mut defs); + // Arrow-valued helpers such as `const base = (projectId) => ...`. + let mut vars = vec![]; + descend(root, "variable_declarator", &mut vars); + for var in vars { + if var + .child_by_field_name("name") + .is_some_and(|n| text(n, source) == fun_name) + && let Some(v) = var + .child_by_field_name("value") + .filter(|n| n.kind() == "arrow_function") + { + defs.push(v); + } + } + for def in defs { + if def.kind() != "arrow_function" + && !def + .child_by_field_name("name") + .is_some_and(|n| text(n, source) == fun_name) + { + continue; + } + let mut scope = bound.clone(); + if let Some(parameters) = def.child_by_field_name("parameters") { + for (i, p) in children(parameters).into_iter().enumerate() { + let id = p + .child_by_field_name("pattern") + .or_else(|| p.child_by_field_name("name")) + .unwrap_or(p); + let default = p.child_by_field_name("value"); + scope.insert( + text(id, source).into(), + args.get(i).map_or_else( + || default.map_or(vec!["*".into()], eval), + |n| eval(*n), + ), + ); + } + } + if let Some(body) = def.child_by_field_name("body") { + let mut returns = vec![]; + if body.kind() == "statement_block" { + descend(body, "return_statement", &mut returns); + } else { + returns.push(body); + } + let result: Vec<_> = returns + .into_iter() + .flat_map(|r| { + let v = if r.kind() == "return_statement" { + children(r).first().copied() + } else { + Some(r) + }; + v.map_or(vec![], |v| values(v, source, root, &scope, depth + 1)) + }) + .collect(); + if !result.is_empty() { + return result; + } + } + } + vec!["*".into()] + } + _ => vec!["*".into()], + } +} + +fn methods(init: Option>, source: &str) -> Result> { + let Some(init) = init else { + return Ok(vec!["get".into()]); + }; + if init.kind() == "call_expression" { + let fun = name(field(init, "function")?, source); + if fun == "write" { + let args = children(field(init, "arguments")?); + if let Some(n) = args.first().filter(|n| n.kind() == "string") { + return Ok(vec![ + text(*n, source)[1..text(*n, source).len() - 1].to_lowercase(), + ]); + } + } + bail!("unresolved request options: {}", text(init, source)); + } + if init.kind() != "object" { + bail!( + "request options need an explicit HTTP method: {}", + text(init, source) + ); + } + anyhow::ensure!( + !children(init).iter().any(|n| n.kind() == "spread_element"), + "request option spreads need explicit method evidence" + ); + for p in children(init).into_iter().filter(|n| n.kind() == "pair") { + if text(field(p, "key")?, source).trim_matches(['\'', '"']) == "method" { + let v = field(p, "value")?; + return match v.kind() { + "string" => Ok(vec![ + text(v, source)[1..text(v, source).len() - 1].to_lowercase(), + ]), + // Both branch methods are required; dynamic arbitrary methods fail. + "ternary_expression" => ["consequence", "alternative"] + .into_iter() + .map(|f| { + let n = field(v, f)?; + if n.kind() != "string" { + bail!("unresolved method") + }; + Ok(text(n, source)[1..text(n, source).len() - 1].to_lowercase()) + }) + .collect(), + _ => bail!("unresolved HTTP method: {}", text(v, source)), + }; + } + } + Ok(vec!["get".into()]) +} + +// Intersection of segment globs. '*' never crosses '/', so a collection call +// cannot accidentally require every nested resource beneath it. +pub fn intersects(a: &str, b: &str) -> bool { + fn go(a: &[u8], b: &[u8], i: usize, j: usize, seen: &mut HashSet<(usize, usize)>) -> bool { + if !seen.insert((i, j)) { + return false; + } + if i == a.len() && j == b.len() { + return true; + } + // '#' is a nonempty numeric identifier. It keeps a typed id lookup + // from accidentally selecting a reserved sibling such as /index_status. + match (a.get(i), b.get(j)) { + (Some(b'#'), Some(b'#')) => return go(a, b, i + 1, j + 1, seen), + (Some(b'#'), Some(b'*')) => { + return go(a, b, i + 1, j, seen) || go(a, b, i, j + 1, seen); + } + (Some(b'*'), Some(b'#')) => { + return go(a, b, i, j + 1, seen) || go(a, b, i + 1, j, seen); + } + (Some(b'#'), Some(d)) if d.is_ascii_digit() => { + return go(a, b, i + 1, j + 1, seen) || go(a, b, i, j + 1, seen); + } + (Some(d), Some(b'#')) if d.is_ascii_digit() => { + return go(a, b, i + 1, j + 1, seen) || go(a, b, i + 1, j, seen); + } + (Some(b'#'), _) | (_, Some(b'#')) => return false, + _ => (), + } + if a.get(i) == Some(&b'*') { + return go(a, b, i + 1, j, seen) + || (j < b.len() && b[j] != b'/' && go(a, b, i, j + 1, seen)); + } + if b.get(j) == Some(&b'*') { + return go(a, b, i, j + 1, seen) + || (i < a.len() && a[i] != b'/' && go(a, b, i + 1, j, seen)); + } + i < a.len() && j < b.len() && a[i] == b[j] && go(a, b, i + 1, j + 1, seen) + } + go(a.as_bytes(), b.as_bytes(), 0, 0, &mut HashSet::new()) +} +fn path_glob(path: &str) -> String { + let mut out = String::new(); + let mut param = false; + for c in path.chars() { + if c == '{' { + param = true; + out.push('*'); + } else if c == '}' { + param = false; + } else if !param { + out.push(c); + } + } + out +} + +pub fn analyze( + source: &str, + tsx: bool, + declared: &BTreeMap, + transport: bool, +) -> Result { + analyze_file(source, tsx, declared, transport, false) +} + +fn analyze_file( + source: &str, + tsx: bool, + declared: &BTreeMap, + transport: bool, + user_content_links: bool, +) -> Result { + let mut parser = tree_sitter::Parser::new(); + parser.set_language(&if tsx { + tree_sitter_typescript::LANGUAGE_TSX.into() + } else { + tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into() + })?; + let tree = parser.parse(source, None).context("parse TypeScript")?; + let root = tree.root_node(); + if root.has_error() { + let mut nodes = vec![]; + descend(root, "ERROR", &mut nodes); + // The upstream TSX grammar treats a bare ampersand in JSX text as an + // error although TypeScript accepts it. No executable expression is + // inside such a text fragment; every other parse error fails closed. + let harmless = |n: &Node<'_>| { + tsx && n.parent().is_some_and(|p| p.kind() == "jsx_element") + && text(*n, source).starts_with('&') + && !text(*n, source).contains(['{', '}', '<', '>']) + }; + if nodes.is_empty() || !nodes.iter().all(harmless) { + bail!( + "TypeScript syntax errors prevent reliable coverage analysis: {:?}", + nodes + .iter() + .map(|n| (n.start_position().row + 1, text(*n, source))) + .collect::>() + ); + } + } + let mut calls = vec![]; + descend(root, "call_expression", &mut calls); + let mut result = Coverage { + operations: BTreeSet::new(), + call_sites: 0, + }; + let known = |name: &str| { + matches!( + name, + "fetch" | "fetchJson" | "fetchBlob" | "fetchDownload" | "request" | "download" + ) + }; + let mut aliases = BTreeMap::new(); + let mut imports = vec![]; + descend(root, "import_specifier", &mut imports); + for import in imports { + if let (Some(original), Some(alias)) = ( + import.child_by_field_name("name"), + import.child_by_field_name("alias"), + ) && known(text(original, source)) + { + aliases.insert( + text(alias, source).to_owned(), + text(original, source).to_owned(), + ); + } + } + let mut variables = vec![]; + descend(root, "variable_declarator", &mut variables); + for var in variables { + if let (Some(id), Some(value)) = ( + var.child_by_field_name("name"), + var.child_by_field_name("value"), + ) { + let original = name(value, source); + let original = aliases.get(&original).cloned().unwrap_or(original); + if known(&original) { + aliases.insert(text(id, source).to_owned(), original); + } + } + } + for call in calls { + let fun = field(call, "function")?; + let fun_name = name(fun, source); + let fun_name = aliases.get(&fun_name).cloned().unwrap_or(fun_name); + if !matches!( + fun_name.as_str(), + "fetch" | "fetchJson" | "fetchBlob" | "fetchDownload" | "request" | "download" + ) { + let args = call + .child_by_field_name("arguments") + .map(children) + .unwrap_or_default(); + let mut endpoints = vec![]; + if let Some(first) = args.first() { + endpoints.extend(values(*first, source, root, &BTreeMap::new(), 0)); + if first.kind() == "object" { + for pair in children(*first).into_iter().filter(|n| n.kind() == "pair") { + if pair + .child_by_field_name("key") + .is_some_and(|n| matches!(text(n, source), "url" | "path" | "uri")) + && let Some(value) = pair.child_by_field_name("value") + { + endpoints.extend(values(value, source, root, &BTreeMap::new(), 0)); + } + } + } + } + if endpoints.iter().any(|p| p.contains("/api/")) + && !matches!( + fun_name.as_str(), + "base" | "project" | "useCases" | "withQuery" + ) + { + bail!( + "unclassified use of an API URL by {fun_name} at line {}; add an explicit transport adapter", + call.start_position().row + 1 + ); + } + continue; + } + // Exactly the shared transports forward arbitrary paths. Their callers + // are checked here; adding another forwarding layer is not auto-excluded. + if transport && fun_name == "fetch" { + let owner = enclosing(call) + .child_by_field_name("name") + .map(|n| text(n, source)); + if matches!( + owner, + Some("request" | "fetchJson" | "fetchBlob" | "fetchDownload") + ) { + continue; + } + } + if fun_name == "download" { + let mut defs = vec![]; + descend(root, "function_declaration", &mut defs); + let forwards = defs + .into_iter() + .filter(|d| { + d.child_by_field_name("name") + .is_some_and(|n| text(n, source) == "download") + }) + .any(|d| { + let mut inner = vec![]; + descend(d, "call_expression", &mut inner); + inner.into_iter().any(|c| { + c.child_by_field_name("function") + .is_some_and(|f| name(f, source) == "fetchDownload") + }) + }); + if !forwards { + continue; + } + } + let args = children(field(call, "arguments")?); + let Some(path) = args.first() else { + bail!("HTTP call has no path") + }; + let mut paths = values(*path, source, root, &BTreeMap::new(), 0); + // A forwarding download helper is covered at each concrete call site. + if fun_name == "fetchDownload" + && paths == ["*"] + && enclosing(call) + .child_by_field_name("name") + .is_some_and(|n| text(n, source) == "download") + { + continue; + } + paths = paths + .into_iter() + .map(|p| p.split('?').next().unwrap().to_owned()) + .collect(); + paths.sort(); + paths.dedup(); + let method = methods( + if fun_name == "download" { + None + } else { + args.get(1).copied() + }, + source, + ) + .with_context(|| format!("line {}", call.start_position().row + 1))?; + for path in paths { + if !path.starts_with("/api/") { + bail!( + "unresolved HTTP dependency at line {}: {}", + call.start_position().row + 1, + text(*args.first().unwrap(), source) + ); + } + for method in &method { + let found: Vec<_> = declared + .keys() + .filter(|op| op.method == *method && intersects(&path, &path_glob(&op.path))) + .cloned() + .collect(); + if found.is_empty() { + bail!( + "undocumented consumer operation: {} {} at line {}", + method, + path, + call.start_position().row + 1 + ); + } + result.operations.extend(found); + } + } + result.call_sites += 1; + } + // Browser navigation to fixed API paths is also a consumer dependency. + let mut attributes = vec![]; + descend(root, "jsx_attribute", &mut attributes); + for attribute in attributes { + let parts = children(attribute); + if parts.first().is_none_or(|n| text(*n, source) != "href") { + continue; + } + let Some(value) = parts.get(1) else { + continue; + }; + let value = if value.kind() == "jsx_expression" { + children(*value).first().copied().context("empty href")? + } else { + *value + }; + if user_content_links && text(value, source) == "n.href" { + continue; + } + for path in values(value, source, root, &BTreeMap::new(), 0) { + let path = path.split('?').next().unwrap(); + if path == "*" { + bail!( + "unresolved browser navigation at line {}", + attribute.start_position().row + 1 + ); + } + if !path.starts_with("/api/") { + continue; + } + let keys: Vec<_> = declared + .keys() + .filter(|key| key.method == "get" && intersects(path, &path_glob(&key.path))) + .cloned() + .collect(); + if keys.is_empty() { + bail!("undocumented browser API dependency {path}"); + } + result.operations.extend(keys); + } + } + Ok(result) +} + +pub fn scan(root: &Path, component: &str, spec: &Value) -> Result { + anyhow::ensure!( + matches!(component, "frontend" | "mcp"), + "consumer must be frontend or mcp" + ); + let directory = root.join(if component == "frontend" { + "frontend/src" + } else { + "mcp-server/src" + }); + let declared = operations(spec, None)?; + let mut result = Coverage { + operations: BTreeSet::new(), + call_sites: 0, + }; + for entry in walkdir::WalkDir::new(directory) { + let entry = entry?; + let path = entry.path(); + if !entry.file_type().is_file() + || !matches!( + path.extension().and_then(|x| x.to_str()), + Some("ts" | "tsx") + ) + { + continue; + } + let relative = path.strip_prefix(root)?.to_string_lossy(); + if relative.contains("/__tests__/") + || relative.ends_with(".test.ts") + || relative.ends_with("generated-contract.ts") + { + continue; + } + let source = std::fs::read_to_string(path)?; + let transport = + relative == "frontend/src/api/transport.ts" || relative == "mcp-server/src/client.ts"; + let coverage = analyze_file( + &source, + path.extension().is_some_and(|x| x == "tsx"), + &declared, + transport, + relative == "frontend/src/components/StatementText.tsx", + ) + .with_context(|| relative.to_string())?; + result.call_sites += coverage.call_sites; + result.operations.extend(coverage.operations); + } + // Protocol navigation/discovery isn't fetch(): explicit, small, tested roots. + let protocol = if component == "frontend" { + vec!["auth_auth_external_start", "auth_auth_external_callback"] + } else { + vec![ + "oauth_authorization_server_metadata", + "oauth_protected_resource_metadata_mcp", + "oauth_register", + "oauth_authorize_page", + "oauth_authorize_decision", + "oauth_token", + ] + }; + for id in protocol { + let key = declared + .iter() + .find(|(_, v)| v["operationId"] == id) + .map(|(k, _)| k.clone()) + .with_context(|| format!("missing protocol operation {id}"))?; + result.operations.insert(key); + } + Ok(result) +} diff --git a/tools/marreq-contracts/src/lib.rs b/tools/marreq-contracts/src/lib.rs new file mode 100644 index 00000000..d0ca8ac9 --- /dev/null +++ b/tools/marreq-contracts/src/lib.rs @@ -0,0 +1,4 @@ +pub mod artifact; +pub mod contract; +pub mod coverage; +pub mod traffic; diff --git a/tools/marreq-contracts/src/main.rs b/tools/marreq-contracts/src/main.rs new file mode 100644 index 00000000..0f47dc2c --- /dev/null +++ b/tools/marreq-contracts/src/main.rs @@ -0,0 +1,204 @@ +use clap::{Parser, Subcommand}; +use marreq_contracts::{artifact, contract, coverage, traffic}; +use std::path::PathBuf; +#[derive(Parser)] +struct Cli { + #[command(subcommand)] + command: Action, +} +#[derive(Subcommand)] +enum Action { + Coverage { + #[arg(long, default_value = ".")] + root: PathBuf, + #[arg(long, default_value = "docs/developer/openapi.yaml")] + spec: PathBuf, + #[arg(long)] + component: String, + }, + Validate { + #[arg(long, default_value = "docs/developer/openapi.yaml")] + spec: PathBuf, + #[arg(long, default_value = ".")] + root: PathBuf, + #[arg(long)] + document_only: bool, + }, + Bundle { + #[arg(long, default_value = ".")] + root: PathBuf, + #[arg(long)] + component: String, + #[arg(long)] + version: semver::Version, + #[arg(long)] + variant: Option, + #[arg(long)] + output: PathBuf, + }, + Verify { + #[arg(long)] + manifest: PathBuf, + #[arg(long,requires_all=["candidate_version","candidate_source_sha","candidate_bundle"])] + candidate_component: Option, + #[arg(long)] + candidate_version: Option, + #[arg(long)] + candidate_source_sha: Option, + #[arg(long)] + candidate_bundle: Option, + }, + VerifyTraffic { + transcript: PathBuf, + #[arg(long, default_value = "docs/developer/openapi.yaml")] + spec: PathBuf, + }, + Select { + #[arg(long, default_value = ".")] + root: PathBuf, + #[arg(long)] + component: String, + #[arg(long, default_value = "docs/developer/openapi.yaml")] + spec: PathBuf, + #[arg(long)] + output: PathBuf, + #[arg(long)] + allow_unverified: bool, + }, + Compare { + consumer: PathBuf, + backend: PathBuf, + #[arg(long)] + variant: String, + }, +} +fn run() -> anyhow::Result { + match Cli::parse().command { + Action::Coverage { + root, + spec, + component, + } => Ok(serde_json::to_value(coverage::scan( + &root, + &component, + &contract::load(&spec)?, + )?)?), + Action::Bundle { + root, + component, + version, + variant, + output, + } => { + let bundle = artifact::export(&root, &component, version, variant)?; + contract::write(&output, &bundle)?; + Ok( + serde_json::json!({"file":output,"sha256":artifact::hash(&std::fs::read(&output)?),"source_sha":bundle.source_sha,"version":bundle.version}), + ) + } + Action::Verify { + manifest, + candidate_component, + candidate_version, + candidate_source_sha, + candidate_bundle, + } => { + let any = candidate_component.is_some() + || candidate_version.is_some() + || candidate_source_sha.is_some() + || candidate_bundle.is_some(); + let all = candidate_component.is_some() + && candidate_version.is_some() + && candidate_source_sha.is_some() + && candidate_bundle.is_some(); + anyhow::ensure!( + !any || all, + "candidate verification requires component, version, source SHA and source-exported bundle together" + ); + let candidate = candidate_component.as_deref().map(|who| { + ( + who, + candidate_version.as_ref().unwrap(), + candidate_source_sha.as_deref().unwrap(), + candidate_bundle.as_deref().unwrap(), + ) + }); + artifact::verify(&manifest, candidate) + } + Action::VerifyTraffic { transcript, spec } => { + traffic::verify(&contract::load(&spec)?, &transcript) + } + Action::Select { + root, + component, + spec, + output, + allow_unverified, + } => { + let spec = contract::load(&spec)?; + let coverage = coverage::scan(&root, &component, &spec)?; + contract::write( + &output, + &contract::select(&spec, &coverage.operations, None, !allow_unverified)?, + )?; + Ok( + serde_json::json!({"component":component,"operations":coverage.operations.len(),"output":output}), + ) + } + Action::Validate { + spec, + root, + document_only, + } => { + let spec = contract::load(&spec)?; + contract::validate(&spec)?; + for dir in ["frontend", "mcp-server"] { + let p: artifact::Policy = serde_json::from_value(contract::load( + &root.join(dir).join("compatibility.json"), + )?)?; + p.validate()?; + } + let range: artifact::FrontendRange = serde_json::from_value(contract::load( + &root.join("marreq-core/frontend_compatibility.json"), + )?)?; + anyhow::ensure!( + range.min_version.cmp_precedence(&range.max_version).is_le(), + "invalid backend frontend compatibility range" + ); + let mut counts = serde_json::Map::new(); + if !document_only { + for who in ["frontend", "mcp"] { + let c = coverage::scan(&root, who, &spec)?; + let selected = contract::select(&spec, &c.operations, None, true)?; + contract::validate_wire_contract(&selected)?; + counts.insert(who.into(), serde_json::json!(c.operations.len())); + } + } + Ok(serde_json::json!({"valid":true,"consumer_operations":counts})) + } + Action::Compare { + consumer, + backend, + variant, + } => { + let findings = contract::compare( + &contract::load(&consumer)?, + &contract::load(&backend)?, + &variant, + )?; + Ok(serde_json::json!({"compatible":true,"findings":findings})) + } + } +} +fn main() { + match run() { + Ok(value) => println!("{}", serde_json::to_string_pretty(&value).unwrap()), + Err(e) => { + println!( + "{}", + serde_json::json!({"compatible":false,"error":format!("{e:#}")}) + ); + std::process::exit(1); + } + } +} diff --git a/tools/marreq-contracts/src/traffic.rs b/tools/marreq-contracts/src/traffic.rs new file mode 100644 index 00000000..03f73598 --- /dev/null +++ b/tools/marreq-contracts/src/traffic.rs @@ -0,0 +1,103 @@ +//! Real request/response evidence collected by the established Rocket test. +use crate::{contract, coverage}; +use anyhow::{Context, Result, ensure}; +use serde_json::{Value, json}; +use std::{collections::BTreeSet, path::Path}; +pub fn verify(spec: &Value, path: &Path) -> Result { + let mut checked = 0; + let mut unverified = 0; + let mut seen = BTreeSet::new(); + for line in std::fs::read_to_string(path)? + .lines() + .filter(|l| !l.trim().is_empty()) + { + let record: Value = serde_json::from_str(line)?; + let mode = record["variant"].as_str().unwrap_or("server"); + crate::artifact::variant(mode)?; + let operations = contract::operations(spec, Some(mode))?; + let actual = record["path"] + .as_str() + .context("request path")? + .split('?') + .next() + .unwrap(); + let method = record["method"] + .as_str() + .context("HTTP method")? + .to_lowercase(); + let mut matches: Vec<_> = operations + .iter() + .filter(|(key, _)| { + key.method == method && coverage::intersects(actual, &route_pattern(&key.path)) + }) + .collect(); + matches.sort_by_key(|(key, _)| key.path.matches('{').count()); + let (key, op) = matches + .first() + .context("actual HTTP call missing in declared contract")?; + if op + .get("x-marreq-unverified") + .is_some_and(|v| v != &Value::Bool(false)) + { + unverified += 1; + continue; + } + let status = record["status"] + .as_u64() + .context("response status")? + .to_string(); + let response = op["responses"] + .get(&status) + .or_else(|| op["responses"].get("default")) + .with_context(|| format!("{key}: undocumented actual HTTP status {status}"))?; + let response = if let Some(r) = response["$ref"].as_str() { + spec.pointer(&r[1..]).context("response ref")? + } else { + response + }; + if let Some(schema) = response.pointer("/content/application~1json/schema") { + contract::validate_instance(spec, schema, &record["response"])?; + } + if status.starts_with('2') && !record["request"].is_null() { + let schema = op + .pointer("/requestBody/content/application~1json/schema") + .with_context(|| format!("{key}: undeclared JSON body"))?; + contract::validate_instance(spec, schema, &record["request"])?; + } + checked += 1; + seen.insert((key.method.clone(), key.path.clone(), status)); + } + for (method, path, status) in [ + ("get", "/api/meta/build", "200"), + ("get", "/api/meta/deployment", "200"), + ("post", "/api/projects/{project_id}/use-cases", "201"), + ( + "put", + "/api/projects/{project_id}/use-cases/{id}/draft", + "200", + ), + ("get", "/api/projects/{project_id}/use-cases", "403"), + ] { + ensure!( + seen.contains(&(method.into(), path.into(), status.into())), + "critical real-backend evidence missing: {method} {path} {status}" + ); + } + Ok(json!({"validated_exchanges":checked,"unverified_exchanges":unverified})) +} +fn route_pattern(path: &str) -> String { + let mut out = String::new(); + let mut param = false; + for c in path.chars() { + match c { + '{' => { + param = true; + out.push('*'); + } + '}' => param = false, + _ if !param => out.push(c), + _ => (), + } + } + out +} diff --git a/tools/marreq-contracts/tests/compatibility.rs b/tools/marreq-contracts/tests/compatibility.rs new file mode 100644 index 00000000..036b940c --- /dev/null +++ b/tools/marreq-contracts/tests/compatibility.rs @@ -0,0 +1,459 @@ +use marreq_contracts::{ + artifact::{self, Artifact, Bundle, FrontendRange, Plan, Policy}, + contract::{self, Operation}, + coverage, +}; +use serde_json::{Value, json}; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::Path, + process::Command, +}; +fn spec() -> Value { + json!({"openapi":"3.1.0","info":{"title":"fixture","version":"1.0.0"},"paths":{"/api/things":{"post":{"operationId":"create","security":[{"cookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"],"additionalProperties":false}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"state":{"type":"string","enum":["draft","approved"]}},"required":["id","state"],"additionalProperties":true}}}}}}}},"components":{"securitySchemes":{"cookie":{"type":"apiKey","in":"cookie","name":"session"}}}}) +} +fn request(s: &mut Value) -> &mut Value { + &mut s["paths"]["/api/things"]["post"]["requestBody"]["content"]["application/json"]["schema"] +} +fn response(s: &mut Value) -> &mut Value { + &mut s["paths"]["/api/things"]["post"]["responses"]["200"]["content"]["application/json"]["schema"] +} +#[test] +fn valid_openapi() { + contract::validate(&spec()).unwrap(); +} +#[test] +fn missing_endpoint() { + let mut new = spec(); + new["paths"] = json!({}); + assert!(contract::compare(&spec(), &new, "server").is_err()); +} +#[test] +fn required_response_removed() { + let mut new = spec(); + response(&mut new)["required"] = json!(["state"]); + assert!(contract::compare(&spec(), &new, "server").is_err()); +} +#[test] +fn response_type_changed() { + let mut new = spec(); + response(&mut new)["properties"]["id"] = json!({"type":"string"}); + assert!(contract::compare(&spec(), &new, "server").is_err()); +} +#[test] +fn new_required_request() { + let mut new = spec(); + request(&mut new)["properties"]["reason"] = json!({"type":"string"}); + request(&mut new)["required"] = json!(["title", "reason"]); + assert!(contract::compare(&spec(), &new, "server").is_err()); +} +#[test] +fn additive_request_and_response() { + let mut new = spec(); + request(&mut new)["properties"]["optional"] = json!({"type":"integer"}); + response(&mut new)["properties"]["extra"] = json!({"type":"boolean"}); + contract::compare(&spec(), &new, "server").unwrap(); +} +#[test] +fn response_enum_extension() { + let mut new = spec(); + response(&mut new)["properties"]["state"]["enum"] = json!(["draft", "approved", "reviewed"]); + assert!(contract::compare(&spec(), &new, "cloud").is_err()); +} +#[test] +fn request_enum_restriction() { + let mut old = spec(); + let mut new = spec(); + request(&mut old)["properties"]["title"]["enum"] = json!(["a", "b"]); + request(&mut new)["properties"]["title"]["enum"] = json!(["a"]); + assert!(contract::compare(&old, &new, "cloud").is_err()); +} +#[test] +fn authentication_requirement() { + let mut new = spec(); + new["components"]["securitySchemes"]["bearer"] = json!({"type":"http","scheme":"bearer"}); + new["paths"]["/api/things"]["post"]["security"] = json!([{"bearer":[]}]); + assert!(contract::compare(&spec(), &new, "server").is_err()); +} +#[test] +fn unrelated_security_scheme_is_not_global_coupling() { + let mut new = spec(); + new["components"]["securitySchemes"]["unrelated"] = json!({"type":"http","scheme":"bearer"}); + contract::compare(&spec(), &new, "server").unwrap(); +} +#[test] +fn per_operation_scope_change() { + let mut new = spec(); + new["paths"]["/api/things"]["post"]["x-marreq-bearer-scopes"] = json!(["things:write"]); + assert!(contract::compare(&spec(), &new, "server").is_err()); +} +#[test] +fn unrelated_backend_endpoint_can_change() { + let mut old = spec(); + old["paths"]["/api/unrelated"] = json!({"get":{"responses":{"200":{"description":"old"}}}}); + let mut new = old.clone(); + new["paths"]["/api/unrelated"] = + json!({"delete":{"responses":{"204":{"description":"different"}}}}); + let required = BTreeSet::from([Operation { + method: "post".into(), + path: "/api/things".into(), + }]); + let consumer = contract::select(&old, &required, None, true).unwrap(); + contract::compare(&consumer, &new, "server").unwrap(); +} +#[test] +fn cloud_operation_not_required_on_server() { + let mut consumer = spec(); + consumer["paths"]["/api/register"] = + json!({"post":{"x-marreq-variants":["cloud"],"responses":{"200":{"description":"ok"}}}}); + contract::compare(&consumer, &spec(), "server").unwrap(); + assert!(contract::compare(&consumer, &spec(), "cloud").is_err()); +} +#[test] +fn required_unverified_operation_fails() { + let mut new = spec(); + new["paths"]["/api/things"]["post"]["x-marreq-unverified"] = json!(true); + assert!(contract::compare(&spec(), &new, "server").is_err()); +} +#[test] +fn external_refs_fail_without_network() { + let mut new = spec(); + response(&mut new)["properties"]["id"] = json!({"$ref":"https://example.test/schema"}); + assert!( + contract::validate(&new) + .unwrap_err() + .to_string() + .contains("external references") + ); +} +#[test] +fn actual_json_instances_are_checked() { + let schema = response(&mut spec()).clone(); + contract::validate_instance(&spec(), &schema, &json!({"id":1,"state":"draft"})).unwrap(); + assert!(contract::validate_instance(&spec(), &schema, &json!({"state":"draft"})).is_err()); + assert!( + contract::validate_instance(&spec(), &schema, &json!({"id":"wrong","state":"draft"})) + .is_err() + ); +} +#[test] +fn invalid_policy() { + for value in [ + json!({}), + json!({"requires_backend_min":"1.x"}), + json!({"requires_backend_min":"2.0.0","requires_backend_max":"1.0.0"}), + json!({"requires_backend_min":"1.0.0","typo":"x"}), + ] { + let policy = serde_json::from_value::(value); + assert!(policy.is_err() || policy.unwrap().validate().is_err()); + } +} +fn plan( + dir: &Path, + backend_version: &str, + mcp_version: &str, + incompatible: bool, +) -> std::path::PathBuf { + let mut descriptors = BTreeMap::new(); + for (who, version) in [ + ("backend", backend_version), + ("frontend", "0.2.1"), + ("mcp", mcp_version), + ] { + let mut document = spec(); + if incompatible && who == "backend" { + response(&mut document)["required"] = json!(["state"]); + } + let bundle = Bundle { + format: "marreq.contract.v2".into(), + component: who.into(), + version: version.parse().unwrap(), + source_sha: "a".repeat(40), + variant: (who == "backend").then(|| "server".into()), + compatibility: (who != "backend").then(|| Policy { + requires_backend_min: "0.2.0".parse().unwrap(), + requires_backend_max: Some("0.2.99".parse().unwrap()), + }), + frontend_compatibility: (who == "backend").then(|| FrontendRange { + min_version: "0.2.0".parse().unwrap(), + max_version: "0.3.0".parse().unwrap(), + }), + contract: document, + }; + let filename = format!("{who}.json"); + contract::write(&dir.join(&filename), &bundle).unwrap(); + descriptors.insert( + who, + Artifact { + file: filename.into(), + sha256: artifact::hash(&std::fs::read(dir.join(format!("{who}.json"))).unwrap()), + version: bundle.version, + source_sha: bundle.source_sha, + }, + ); + } + let plan = Plan { + format: "marreq.deployment-contract.v2".into(), + variant: "server".into(), + backend: descriptors.remove("backend").unwrap(), + frontend: descriptors.remove("frontend").unwrap(), + mcp: descriptors.remove("mcp").unwrap(), + }; + let path = dir.join("plan.json"); + contract::write(&path, &plan).unwrap(); + path +} +#[test] +fn compatible_backend_only_and_mcp_only_updates() { + let dir = tempfile::tempdir().unwrap(); + for (b, m) in [("0.2.4", "0.3.0"), ("0.2.3", "0.3.1")] { + let path = plan(dir.path(), b, m, false); + assert_eq!(artifact::verify(&path, None).unwrap()["compatible"], true); + } +} +#[test] +fn incompatible_backend_only_update() { + let dir = tempfile::tempdir().unwrap(); + assert!(artifact::verify(&plan(dir.path(), "0.2.4", "0.3.0", true), None).is_err()); +} +#[test] +fn missing_historical_artifact() { + let dir = tempfile::tempdir().unwrap(); + let path = plan(dir.path(), "0.2.4", "0.3.0", false); + std::fs::remove_file(dir.path().join("mcp.json")).unwrap(); + assert!( + artifact::verify(&path, None) + .unwrap_err() + .to_string() + .contains("missing historical") + ); +} +#[test] +fn descriptor_source_and_version_mismatch() { + let dir = tempfile::tempdir().unwrap(); + for key in ["source_sha", "version"] { + let path = plan(dir.path(), "0.2.4", "0.3.0", false); + let mut p = contract::load(&path).unwrap(); + p["mcp"][key] = json!(if key == "source_sha" { + "b".repeat(40) + } else { + "0.3.9".into() + }); + contract::write(&path, &p).unwrap(); + assert!( + artifact::verify(&path, None) + .unwrap_err() + .to_string() + .contains("mismatch") + ); + } +} +#[test] +fn tampered_contract_hash() { + let dir = tempfile::tempdir().unwrap(); + let path = plan(dir.path(), "0.2.4", "0.3.0", false); + std::fs::write(dir.path().join("mcp.json"), "{}").unwrap(); + assert!( + artifact::verify(&path, None) + .unwrap_err() + .to_string() + .contains("SHA-256 mismatch") + ); +} +#[test] +fn cli_exit_codes_and_json_diagnostics() { + let dir = tempfile::tempdir().unwrap(); + for bad in [false, true] { + let path = plan(dir.path(), "0.2.4", "0.3.0", bad); + let result = Command::new(env!("CARGO_BIN_EXE_marreq-contracts")) + .arg("verify") + .arg("--manifest") + .arg(path) + .output() + .unwrap(); + assert_eq!(result.status.success(), !bad); + let body: Value = serde_json::from_slice(&result.stdout).unwrap(); + assert_eq!(body["compatible"], !bad); + } +} +#[test] +fn type_script_grammar_resolves_helpers_and_union_methods() { + let mut document = spec(); + document["paths"]["/api/projects/{project_id}/things/{id}"] = json!({"get":{"responses":{"200":{"description":"ok"}}},"delete":{"responses":{"204":{"description":"ok"}}}}); + let ops = contract::operations(&document, None).unwrap(); + let source = "const base = (id: number) => `/api/projects/${id}/things`; function read(id: number) { return fetchJson(`${base(1)}/${id}`, {method: remove ? 'DELETE' : 'GET'}); }"; + let coverage = coverage::analyze(source, false, &ops, false).unwrap(); + assert_eq!(coverage.operations.len(), 2); +} +#[test] +fn new_unmapped_call_cannot_bypass_coverage() { + let ops = contract::operations(&spec(), None).unwrap(); + assert!(coverage::analyze("fetch('/api/new-api')", false, &ops, false).is_err()); + assert!(coverage::analyze("fetch(url)", false, &ops, false).is_err()); +} +#[test] +fn dynamic_unverified_dependency_is_rejected() { + let mut doc = spec(); + doc["paths"]["/api/things"]["post"]["x-marreq-unverified"] = json!(true); + let coverage = coverage::analyze( + "fetchJson('/api/things', {method:'POST'})", + false, + &contract::operations(&doc, None).unwrap(), + false, + ) + .unwrap(); + assert!(contract::select(&doc, &coverage.operations, None, true).is_err()); +} +#[test] +fn segment_globs_do_not_capture_unrelated_children() { + assert!(!coverage::intersects( + "/api/things/*", + "/api/things/*/versions" + )); + assert!(coverage::intersects("/api/things/*.pdf", "/api/things/*")); +} + +#[test] +fn await_generic_calls_are_dependencies() { + let coverage = coverage::analyze( + "async function f(){ return await fetchJson('/api/things',{method:'POST'}); }", + false, + &contract::operations(&spec(), None).unwrap(), + false, + ) + .unwrap(); + assert_eq!(coverage.call_sites, 1); + assert_eq!(coverage.operations.len(), 1); +} +#[test] +fn browser_navigation_is_a_dependency() { + let mut document = spec(); + document["paths"]["/api/things"]["get"] = json!({"responses":{"200":{"description":"ok"}}}); + let ops = contract::operations(&document, None).unwrap(); + assert_eq!( + coverage::analyze( + "const a=Reports & exports", + true, + &ops, + false + ) + .unwrap() + .operations + .len(), + 1 + ); + assert!( + coverage::analyze( + "const a=link", + true, + &ops, + false + ) + .is_err() + ); +} +#[test] +fn unsupported_schema_semantics_fail_locally() { + let mut new = spec(); + response(&mut new)["unevaluatedProperties"] = json!(false); + assert!( + contract::compare(&spec(), &new, "server") + .unwrap_err() + .to_string() + .contains("unevaluatedProperties") + ); +} +#[test] +fn fewer_required_scopes_are_compatible() { + let mut old = spec(); + old["paths"]["/api/things"]["post"]["x-marreq-bearer-scopes"] = + json!(["things:read", "things:write"]); + let mut new = old.clone(); + new["paths"]["/api/things"]["post"]["x-marreq-bearer-scopes"] = json!(["things:read"]); + contract::compare(&old, &new, "server").unwrap(); +} + +#[test] +fn numeric_resource_ids_do_not_require_reserved_siblings() { + let mut document = spec(); + document["paths"]["/api/things/{id}"] = + json!({"get":{"responses":{"200":{"description":"ok"}}}}); + document["paths"]["/api/things/internal_stats"] = + json!({"get":{"responses":{"200":{"description":"unused"}}}}); + let coverage = coverage::analyze( + "function get(id:number){return fetchJson(`/api/things/${id}`);}", + false, + &contract::operations(&document, None).unwrap(), + false, + ) + .unwrap(); + assert_eq!(coverage.operations.len(), 1); + assert!(coverage::intersects("/api/things/#", "/api/things/123")); + assert!(!coverage::intersects( + "/api/things/#", + "/api/things/internal_stats" + )); +} + +#[test] +fn aliases_and_unrecognized_clients_cannot_hide_new_calls() { + let ops = contract::operations(&spec(), None).unwrap(); + for source in [ + "const f=fetch; f('/api/missing');", + "import {fetchJson as api} from './transport'; api('/api/missing');", + "axios.get('/api/missing');", + "axios({url:'/api/missing'});", + ] { + assert!( + coverage::analyze(source, false, &ops, false).is_err(), + "{source}" + ); + } + assert!(coverage::analyze("function f(url:string){if(true){const url='/api/things';}return fetchJson(url,{method:'POST'});}",false,&ops,false).is_err()); + assert!( + coverage::analyze( + "let path='/api/things';path=external;fetchJson(path,{method:'POST'});", + false, + &ops, + false + ) + .is_err() + ); +} + +#[test] +fn semver_build_metadata_does_not_change_policy_precedence() { + Policy { + requires_backend_min: "0.2.4".parse().unwrap(), + requires_backend_max: Some("0.2.4".parse().unwrap()), + } + .accepts(&"0.2.4+source.sha".parse().unwrap()) + .unwrap(); +} + +#[test] +fn unknown_schema_keywords_are_not_silently_accepted() { + let mut new = spec(); + response(&mut new)["customLimit"] = json!(1); + assert!( + contract::compare(&spec(), &new, "server") + .unwrap_err() + .to_string() + .contains("customLimit") + ); +} +#[test] +fn relative_plan_paths_work() { + let dir = tempfile::tempdir().unwrap(); + plan(dir.path(), "0.2.4", "0.3.0", false); + let result = Command::new(env!("CARGO_BIN_EXE_marreq-contracts")) + .current_dir(dir.path()) + .args(["verify", "--manifest", "plan.json"]) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stdout) + ); +}