Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7b30d03c5 |
@@ -31,7 +31,6 @@
|
|||||||
/.github/workflows/ci.yml @igor04091968 @rachkovii68-bisness
|
/.github/workflows/ci.yml @igor04091968 @rachkovii68-bisness
|
||||||
/.github/workflows/security.yml @igor04091968 @rachkovii68-bisness
|
/.github/workflows/security.yml @igor04091968 @rachkovii68-bisness
|
||||||
/.github/workflows/coverage.yml @igor04091968 @rachkovii68-bisness
|
/.github/workflows/coverage.yml @igor04091968 @rachkovii68-bisness
|
||||||
/.github/GOVERNANCE.md @igor04091968 @rachkovii68-bisness
|
|
||||||
/.github/pull_request_template.md @igor04091968 @rachkovii68-bisness
|
/.github/pull_request_template.md @igor04091968 @rachkovii68-bisness
|
||||||
/.github/ISSUE_TEMPLATE/ @igor04091968 @rachkovii68-bisness
|
/.github/ISSUE_TEMPLATE/ @igor04091968 @rachkovii68-bisness
|
||||||
/.github/CODEOWNERS @igor04091968 @rachkovii68-bisness
|
/.github/CODEOWNERS @igor04091968 @rachkovii68-bisness
|
||||||
@@ -39,9 +38,6 @@
|
|||||||
/docs/BRANCH_PROTECTION_EVIDENCE_RU.md @igor04091968 @rachkovii68-bisness
|
/docs/BRANCH_PROTECTION_EVIDENCE_RU.md @igor04091968 @rachkovii68-bisness
|
||||||
/docs/PR_REVIEW_WORKFLOW_RU.md @igor04091968 @rachkovii68-bisness
|
/docs/PR_REVIEW_WORKFLOW_RU.md @igor04091968 @rachkovii68-bisness
|
||||||
/docs/PR_REVIEW_EVIDENCE_RU.md @igor04091968 @rachkovii68-bisness
|
/docs/PR_REVIEW_EVIDENCE_RU.md @igor04091968 @rachkovii68-bisness
|
||||||
/docs/ENTERPRISE_QUALITY_STANDARD_RU.md @igor04091968 @rachkovii68-bisness
|
|
||||||
/docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md @igor04091968 @rachkovii68-bisness
|
|
||||||
/docs/OPERATIONAL_MATURITY_RU.md @igor04091968 @rachkovii68-bisness
|
|
||||||
|
|
||||||
# Ansible deployment automation.
|
# Ansible deployment automation.
|
||||||
/ansible/ @igor04091968 @rachkovii68-bisness
|
/ansible/ @igor04091968 @rachkovii68-bisness
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# AWatch-rus Governance
|
|
||||||
|
|
||||||
GitHub is the public mirror validation surface. Primary registry release
|
|
||||||
evidence must be produced on the Russian build-runner and primary operational
|
|
||||||
context remains the private Gitea contour.
|
|
||||||
|
|
||||||
## Production-first standard
|
|
||||||
|
|
||||||
AWatch-rus is already deployed in a real company. Production stability has
|
|
||||||
absolute priority. The canonical engineering standard is:
|
|
||||||
|
|
||||||
- [Enterprise quality standard](../docs/ENTERPRISE_QUALITY_STANDARD_RU.md)
|
|
||||||
- [Review checklist](../docs/REVIEW_CHECKLIST_RU.md)
|
|
||||||
- [Operational validation runbook](../docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md)
|
|
||||||
- [Operational maturity harness](../docs/OPERATIONAL_MATURITY_RU.md)
|
|
||||||
|
|
||||||
## Required PR evidence
|
|
||||||
|
|
||||||
Every PR must state:
|
|
||||||
|
|
||||||
- Purpose.
|
|
||||||
- Operational impact.
|
|
||||||
- Risk assessment.
|
|
||||||
- Rollback strategy.
|
|
||||||
- Validation steps.
|
|
||||||
- Documentation changes.
|
|
||||||
- Acceptance criteria.
|
|
||||||
|
|
||||||
Documentation-only or governance-only PRs must explicitly state that runtime,
|
|
||||||
API and UI behavior are unchanged.
|
|
||||||
|
|
||||||
## Guardrails
|
|
||||||
|
|
||||||
- Prefer additive, backward-compatible changes.
|
|
||||||
- Do not redesign working subsystems without measured benefit.
|
|
||||||
- Do not add dependencies without justification and validation.
|
|
||||||
- Do not weaken authentication, authorization, audit logging, secret handling,
|
|
||||||
dependency hygiene or configuration validation.
|
|
||||||
- Do not enable heavy DLP, Loki or always-on Velociraptor during routine
|
|
||||||
recovery, validation or public CI.
|
|
||||||
- Keep blocking CI fast; keep heavy/load/nightly checks scheduled or advisory.
|
|
||||||
@@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
Describe what changed and why.
|
Describe what changed and why.
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
State the production-quality purpose of the change. Prefer reliability,
|
|
||||||
operational maturity, security, maintainability, reproducibility, performance
|
|
||||||
or simplicity over new functionality.
|
|
||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
- Runtime impact: `none / changed / not applicable`
|
- Runtime impact: `none / changed / not applicable`
|
||||||
@@ -17,45 +11,14 @@ or simplicity over new functionality.
|
|||||||
- Rollback impact: `none / documented / not applicable`
|
- Rollback impact: `none / documented / not applicable`
|
||||||
- Evidence impact: `none / registry docs updated / release evidence required`
|
- Evidence impact: `none / registry docs updated / release evidence required`
|
||||||
|
|
||||||
## Operational Impact
|
|
||||||
|
|
||||||
Describe deployment, upgrade, rollback, observability, diagnostics,
|
|
||||||
configuration, recovery, performance or dependency-hygiene impact.
|
|
||||||
|
|
||||||
## Risk Assessment
|
|
||||||
|
|
||||||
List production risks and why the change is backward-compatible. For
|
|
||||||
documentation-only or governance-only changes, state that runtime/API/UI behavior
|
|
||||||
is unchanged.
|
|
||||||
|
|
||||||
## Rollback Strategy
|
|
||||||
|
|
||||||
State how to revert the change. Runtime, automation, config and dependency
|
|
||||||
changes need an explicit rollback path.
|
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
List commands executed. Use `skipped: <reason>` when a check requires a live
|
List commands executed. Use `skipped: <reason>` when a check requires a live
|
||||||
stand or unavailable tool.
|
stand or unavailable tool.
|
||||||
|
|
||||||
## Documentation Changes
|
|
||||||
|
|
||||||
List README/runbook/architecture/governance updates, or state `not applicable`
|
|
||||||
with a reason.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
List concrete conditions that make the PR safe to merge.
|
|
||||||
|
|
||||||
## Review Checklist
|
## Review Checklist
|
||||||
|
|
||||||
- [ ] Linked issue is provided, or the PR explains why no issue is applicable.
|
- [ ] Linked issue is provided, or the PR explains why no issue is applicable.
|
||||||
- [ ] Purpose, operational impact, risk assessment, rollback strategy,
|
|
||||||
validation steps, documentation changes and acceptance criteria are stated.
|
|
||||||
- [ ] Change is additive/backward-compatible, or breaking impact is explicitly
|
|
||||||
blocked for this stage.
|
|
||||||
- [ ] Production stability is preserved for existing deployments.
|
|
||||||
- [ ] No working subsystem is redesigned without measured benefit.
|
|
||||||
- [ ] Runtime/API/UI impact is stated.
|
- [ ] Runtime/API/UI impact is stated.
|
||||||
- [ ] Registry claims are checked and remain conservative.
|
- [ ] Registry claims are checked and remain conservative.
|
||||||
- [ ] Secrets, PII, employee logs and customer identifiers are absent.
|
- [ ] Secrets, PII, employee logs and customer identifiers are absent.
|
||||||
@@ -70,8 +33,6 @@ List concrete conditions that make the PR safe to merge.
|
|||||||
- [ ] I checked registry claims: no completed registry submission, no
|
- [ ] I checked registry claims: no completed registry submission, no
|
||||||
FSTEC/FSB certification claim, no SIEM/DLP replacement claim.
|
FSTEC/FSB certification claim, no SIEM/DLP replacement claim.
|
||||||
- [ ] I ran relevant checks or documented why a check was skipped.
|
- [ ] I ran relevant checks or documented why a check was skipped.
|
||||||
- [ ] I checked dependency impact: no unnecessary dependency was added, and no
|
|
||||||
unused dependency remains in touched crates.
|
|
||||||
- [ ] I stated runtime/API/UI impact.
|
- [ ] I stated runtime/API/UI impact.
|
||||||
- [ ] I stated documentation impact.
|
- [ ] I stated documentation impact.
|
||||||
- [ ] I stated smoke-test result or why smoke testing is not applicable.
|
- [ ] I stated smoke-test result or why smoke testing is not applicable.
|
||||||
@@ -87,12 +48,6 @@ List concrete conditions that make the PR safe to merge.
|
|||||||
build-runner.
|
build-runner.
|
||||||
- Update `docs/registry/` when registry-readiness behavior or evidence changes.
|
- Update `docs/registry/` when registry-readiness behavior or evidence changes.
|
||||||
|
|
||||||
## Governance
|
|
||||||
|
|
||||||
- Production-first standard: `.github/GOVERNANCE.md`.
|
|
||||||
- Canonical review checklist: `docs/REVIEW_CHECKLIST_RU.md`.
|
|
||||||
- Canonical validation runbook: `docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md`.
|
|
||||||
|
|
||||||
## Safety
|
## Safety
|
||||||
|
|
||||||
- No secrets, tokens, passwords or private keys.
|
- No secrets, tokens, passwords or private keys.
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
name: Dependency hygiene
|
|
||||||
|
|
||||||
# GitHub Actions is public mirror validation only.
|
|
||||||
# Primary registry release evidence must be produced on Russian build-runner.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "17 2 * * 1"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
unused-dependencies:
|
|
||||||
name: Unused dependency check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
working-directory: adk-rust
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: false
|
|
||||||
|
|
||||||
- name: Install stable Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install cargo-machete
|
|
||||||
run: cargo install cargo-machete --locked
|
|
||||||
|
|
||||||
- name: cargo metadata
|
|
||||||
run: cargo metadata --locked --format-version 1 > /tmp/aw-rus-cargo-metadata.json
|
|
||||||
|
|
||||||
- name: cargo machete
|
|
||||||
run: cargo machete --with-metadata
|
|
||||||
|
|
||||||
- name: Ensure cargo-machete metadata did not rewrite manifests
|
|
||||||
working-directory: .
|
|
||||||
run: git diff --exit-code -- adk-rust/Cargo.lock adk-rust/Cargo.toml adk-rust/crates
|
|
||||||
|
|
||||||
- name: Require explicit justification for cargo-machete ignores
|
|
||||||
working-directory: .
|
|
||||||
run: |
|
|
||||||
python3 - <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
failures = []
|
|
||||||
for path in Path("adk-rust").rglob("Cargo.toml"):
|
|
||||||
lines = path.read_text(encoding="utf-8").splitlines()
|
|
||||||
in_machete = False
|
|
||||||
for idx, line in enumerate(lines):
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith("[") and stripped.endswith("]"):
|
|
||||||
in_machete = stripped == "[package.metadata.cargo-machete]"
|
|
||||||
continue
|
|
||||||
if not in_machete or not stripped.startswith("ignored"):
|
|
||||||
continue
|
|
||||||
same_line_comment = "#" in line and line.split("#", 1)[1].strip()
|
|
||||||
prev_comment = idx > 0 and lines[idx - 1].strip().startswith("#")
|
|
||||||
if not same_line_comment and not prev_comment:
|
|
||||||
failures.append(f"{path}:{idx + 1}")
|
|
||||||
|
|
||||||
if failures:
|
|
||||||
print("cargo-machete ignored entries require an adjacent TOML comment explaining why the dependency is intentionally kept:")
|
|
||||||
for item in failures:
|
|
||||||
print(f" {item}")
|
|
||||||
raise SystemExit(1)
|
|
||||||
PY
|
|
||||||
|
|
||||||
dependency-tree:
|
|
||||||
name: Dependency duplicate report
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
working-directory: adk-rust
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: false
|
|
||||||
|
|
||||||
- name: Install stable Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: cargo tree duplicates
|
|
||||||
run: cargo tree --duplicates --locked
|
|
||||||
|
|
||||||
dependency-security:
|
|
||||||
name: Dependency security policy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: false
|
|
||||||
|
|
||||||
- name: Install stable Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install cargo-audit
|
|
||||||
uses: taiki-e/install-action@cargo-audit
|
|
||||||
|
|
||||||
- name: Install cargo-deny
|
|
||||||
uses: taiki-e/install-action@cargo-deny
|
|
||||||
|
|
||||||
- name: cargo audit
|
|
||||||
working-directory: adk-rust
|
|
||||||
run: cargo audit --deny warnings
|
|
||||||
|
|
||||||
- name: cargo deny
|
|
||||||
run: |
|
|
||||||
cargo deny --manifest-path adk-rust/Cargo.toml check \
|
|
||||||
--config deny.toml \
|
|
||||||
--hide-inclusion-graph \
|
|
||||||
--show-stats
|
|
||||||
|
|
||||||
cargo-udeps-nightly:
|
|
||||||
name: Cargo udeps nightly advisory
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
working-directory: adk-rust
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: false
|
|
||||||
|
|
||||||
- name: Install nightly Rust
|
|
||||||
uses: dtolnay/rust-toolchain@nightly
|
|
||||||
|
|
||||||
- name: Install cargo-udeps
|
|
||||||
run: cargo install cargo-udeps --locked
|
|
||||||
|
|
||||||
- name: cargo udeps
|
|
||||||
run: cargo +nightly udeps --workspace --all-targets
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
name: Operational maturity
|
|
||||||
|
|
||||||
# GitHub Actions is public mirror validation only.
|
|
||||||
# Live DetMir production checks must be run explicitly by an operator with
|
|
||||||
# private network access and must not enable heavy DLP, Loki or always-on
|
|
||||||
# Velociraptor.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
live:
|
|
||||||
description: "Run live endpoint contract if AWATCH_OPS_LIVE_URL is configured"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
schedule:
|
|
||||||
- cron: "41 3 * * 2"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
offline-operational-maturity:
|
|
||||||
name: Offline operational maturity
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: false
|
|
||||||
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Script syntax
|
|
||||||
run: node --check scripts/operational-maturity-check.mjs
|
|
||||||
|
|
||||||
- name: Operational maturity harness
|
|
||||||
run: node scripts/operational-maturity-check.mjs --json
|
|
||||||
|
|
||||||
live-operational-contract:
|
|
||||||
name: Live operational contract
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.live == 'true'
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: false
|
|
||||||
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Run live contract when URL is available
|
|
||||||
env:
|
|
||||||
AWATCH_OPS_LIVE_URL: ${{ secrets.AWATCH_OPS_LIVE_URL }}
|
|
||||||
run: |
|
|
||||||
if [[ -z "${AWATCH_OPS_LIVE_URL}" ]]; then
|
|
||||||
echo "skipped: AWATCH_OPS_LIVE_URL secret is not configured"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
node scripts/operational-maturity-check.mjs --json --live
|
|
||||||
+1
-11
@@ -4,11 +4,6 @@ GitHub is public mirror validation only. Primary registry release evidence is
|
|||||||
produced separately on the Russian build-runner and documented under
|
produced separately on the Russian build-runner and documented under
|
||||||
`docs/registry/`.
|
`docs/registry/`.
|
||||||
|
|
||||||
All contributions must follow the production-first governance entrypoint:
|
|
||||||
`.github/GOVERNANCE.md`. AWatch-rus is already deployed in a real company, so
|
|
||||||
reliability, operational maturity, security and backward compatibility take
|
|
||||||
priority over new functionality.
|
|
||||||
|
|
||||||
## Branching
|
## Branching
|
||||||
|
|
||||||
- Работайте в feature-ветке, не пушьте напрямую в `main`.
|
- Работайте в feature-ветке, не пушьте напрямую в `main`.
|
||||||
@@ -54,11 +49,6 @@ priority over new functionality.
|
|||||||
## PR content
|
## PR content
|
||||||
|
|
||||||
- Изменения и обоснование.
|
- Изменения и обоснование.
|
||||||
- Purpose.
|
- Риск и rollback.
|
||||||
- Operational impact.
|
|
||||||
- Risk assessment.
|
|
||||||
- Rollback strategy.
|
|
||||||
- Какие команды валидации были выполнены.
|
- Какие команды валидации были выполнены.
|
||||||
- Documentation changes.
|
|
||||||
- Acceptance criteria.
|
|
||||||
- Какие проверки были пропущены и почему, если пропуск был необходим.
|
- Какие проверки были пропущены и почему, если пропуск был необходим.
|
||||||
|
|||||||
@@ -271,16 +271,8 @@ collectors.
|
|||||||
|
|
||||||
### Engineering governance and residual risks
|
### Engineering governance and residual risks
|
||||||
|
|
||||||
- Enterprise quality standard:
|
|
||||||
[docs/ENTERPRISE_QUALITY_STANDARD_RU.md](docs/ENTERPRISE_QUALITY_STANDARD_RU.md).
|
|
||||||
- GitHub governance entrypoint:
|
|
||||||
[.github/GOVERNANCE.md](.github/GOVERNANCE.md).
|
|
||||||
- Review checklist:
|
- Review checklist:
|
||||||
[docs/REVIEW_CHECKLIST_RU.md](docs/REVIEW_CHECKLIST_RU.md).
|
[docs/REVIEW_CHECKLIST_RU.md](docs/REVIEW_CHECKLIST_RU.md).
|
||||||
- Operational validation runbook:
|
|
||||||
[docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md](docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md).
|
|
||||||
- Operational maturity harness:
|
|
||||||
[docs/OPERATIONAL_MATURITY_RU.md](docs/OPERATIONAL_MATURITY_RU.md).
|
|
||||||
- Residual risks register:
|
- Residual risks register:
|
||||||
[docs/RESIDUAL_RISKS_RU.md](docs/RESIDUAL_RISKS_RU.md).
|
[docs/RESIDUAL_RISKS_RU.md](docs/RESIDUAL_RISKS_RU.md).
|
||||||
- Public issues plan:
|
- Public issues plan:
|
||||||
|
|||||||
Generated
+11
@@ -198,9 +198,12 @@ name = "aw-contour-smoke"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -247,6 +250,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -455,6 +459,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -466,6 +471,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -771,6 +777,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"detmir-state",
|
"detmir-state",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -781,6 +788,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1249,6 +1257,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1924,6 +1933,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3283,6 +3293,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
"url",
|
"url",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ publish.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ chrono.workspace = true
|
|||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ chrono.workspace = true
|
|||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ adk-rust.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
detmir-state.workspace = true
|
detmir-state.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ publish.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ regex.workspace = true
|
|||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
urlencoding.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ clap.workspace = true
|
|||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tiny_http.workspace = true
|
tiny_http.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
urlencoding.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2026-07-01.operational-maturity-v1",
|
|
||||||
"apiCompatibility": {
|
|
||||||
"openapiPath": "adk-rust/crates/detmir-portal/src/contracts/openapi.json",
|
|
||||||
"requiredOpenApiPaths": {
|
|
||||||
"/contracts": ["get"],
|
|
||||||
"/contracts/openapi.json": ["get"],
|
|
||||||
"/health": ["get"],
|
|
||||||
"/operator": ["get"],
|
|
||||||
"/reports": ["get"],
|
|
||||||
"/workforce/kpi/explain": ["get"],
|
|
||||||
"/ueba": ["get"],
|
|
||||||
"/risk/narrative": ["get"],
|
|
||||||
"/actions": ["get"],
|
|
||||||
"/readiness/latest": ["get"],
|
|
||||||
"/telemetry": ["post"]
|
|
||||||
},
|
|
||||||
"requiredSchemas": [
|
|
||||||
"ContractIndex",
|
|
||||||
"JsonObject",
|
|
||||||
"ActionCenterResponse",
|
|
||||||
"RiskNarrative",
|
|
||||||
"UebaPayload",
|
|
||||||
"WorkforceKpiExplain"
|
|
||||||
],
|
|
||||||
"runtimeEndpoints": [
|
|
||||||
"/healthz",
|
|
||||||
"/readyz",
|
|
||||||
"/version",
|
|
||||||
"/metrics"
|
|
||||||
],
|
|
||||||
"runtimeSourcePath": "adk-rust/crates/detmir-portal/src/main.rs"
|
|
||||||
},
|
|
||||||
"integrationFixtures": [
|
|
||||||
{
|
|
||||||
"path": "/healthz",
|
|
||||||
"fixture": "docs/fixtures/operational-maturity/portal-healthz.json",
|
|
||||||
"status": 200,
|
|
||||||
"contentType": "application/json",
|
|
||||||
"requiredFields": ["status", "service", "schema_version"],
|
|
||||||
"equals": { "status": "ok", "schema_version": "pilot-v1" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/readyz",
|
|
||||||
"fixture": "docs/fixtures/operational-maturity/portal-readyz.json",
|
|
||||||
"status": 200,
|
|
||||||
"contentType": "application/json",
|
|
||||||
"requiredFields": ["status", "checks.portal", "checks.dlp_runtime"],
|
|
||||||
"equals": { "status": "ready", "checks.dlp_runtime.status": "skipped" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/version",
|
|
||||||
"fixture": "docs/fixtures/operational-maturity/portal-version.json",
|
|
||||||
"status": 200,
|
|
||||||
"contentType": "application/json",
|
|
||||||
"requiredFields": ["app_version", "schema_version", "environment"],
|
|
||||||
"equals": { "schema_version": "pilot-v1" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/reports/worktime/management",
|
|
||||||
"fixture": "docs/fixtures/operational-maturity/worktime-management-ok.json",
|
|
||||||
"status": 200,
|
|
||||||
"contentType": "application/json",
|
|
||||||
"requiredFields": ["status", "stale", "report_cache_hit", "aw_query_duration_ms"],
|
|
||||||
"equals": { "status": "OK", "stale": false }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/reports/worktime/management?fault=aw_timeout",
|
|
||||||
"fixture": "docs/fixtures/operational-maturity/worktime-management-degraded.json",
|
|
||||||
"status": 200,
|
|
||||||
"contentType": "application/json",
|
|
||||||
"requiredFields": ["status", "reason", "stale", "report_stale_served", "aw_query_timeout_count"],
|
|
||||||
"equals": { "status": "DEGRADED", "reason": "report_unavailable", "stale": false }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/security/finding-inbox",
|
|
||||||
"fixture": "docs/fixtures/operational-maturity/security-finding-inbox-shadow.json",
|
|
||||||
"status": 200,
|
|
||||||
"contentType": "application/json",
|
|
||||||
"requiredFields": ["mode", "auto_apply", "workflow"],
|
|
||||||
"equals": { "mode": "shadow", "auto_apply": false }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"observability": {
|
|
||||||
"metricsFixture": "docs/fixtures/operational-maturity/portal-metrics.prom",
|
|
||||||
"requiredMetrics": [
|
|
||||||
"awatch_http_requests_total",
|
|
||||||
"awatch_http_request_duration_seconds_sum",
|
|
||||||
"awatch_http_request_duration_seconds_count",
|
|
||||||
"awatch_report_requests_total",
|
|
||||||
"awatch_report_cache_hits_total",
|
|
||||||
"awatch_report_cache_misses_total",
|
|
||||||
"awatch_report_cache_stale_hits_total",
|
|
||||||
"awatch_reports_generated_total",
|
|
||||||
"awatch_ingestion_records_total",
|
|
||||||
"awatch_ingestion_rejected_total",
|
|
||||||
"awatch_role_denied_total",
|
|
||||||
"awatch_readyz_status"
|
|
||||||
],
|
|
||||||
"sourcePath": "adk-rust/crates/detmir-portal/src/production/metrics.rs",
|
|
||||||
"diagnosticHeaders": ["x-request-id", "x-correlation-id"]
|
|
||||||
},
|
|
||||||
"configValidation": {
|
|
||||||
"jsonFiles": [
|
|
||||||
"configs/worktime-interpretation-policy.example.json",
|
|
||||||
"configs/detmir-workforce-policy.example.json",
|
|
||||||
"adk-rust/crates/detmir-portal/src/contracts/openapi.json",
|
|
||||||
"docs/fixtures/operational-maturity/portal-healthz.json",
|
|
||||||
"docs/fixtures/operational-maturity/portal-readyz.json",
|
|
||||||
"docs/fixtures/operational-maturity/portal-version.json",
|
|
||||||
"docs/fixtures/operational-maturity/worktime-management-ok.json",
|
|
||||||
"docs/fixtures/operational-maturity/worktime-management-degraded.json",
|
|
||||||
"docs/fixtures/operational-maturity/security-finding-inbox-shadow.json"
|
|
||||||
],
|
|
||||||
"systemdDirs": [
|
|
||||||
"ops/systemd",
|
|
||||||
"clickhouse-1c/ops"
|
|
||||||
],
|
|
||||||
"clickhouseInitDir": "clickhouse-1c/clickhouse/init",
|
|
||||||
"yamlFiles": [
|
|
||||||
"configs/detmir-ueba-risk-policy.example.yaml",
|
|
||||||
"clickhouse-1c/etl/config.example.yml",
|
|
||||||
"clickhouse-1c/grafana/provisioning/datasources/clickhouse.yml",
|
|
||||||
"clickhouse-1c/grafana/provisioning/dashboards/dashboards.yml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"faultInjection": {
|
|
||||||
"clientTimeoutMs": 350,
|
|
||||||
"maxFailureClassificationMs": 1200
|
|
||||||
},
|
|
||||||
"boundedLoad": {
|
|
||||||
"requests": 60,
|
|
||||||
"concurrency": 8,
|
|
||||||
"p95MaxMs": 750,
|
|
||||||
"heapGrowthMaxBytes": 20971520,
|
|
||||||
"paths": [
|
|
||||||
"/healthz",
|
|
||||||
"/readyz",
|
|
||||||
"/version",
|
|
||||||
"/reports/worktime/management"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# AWatch-rus: enterprise quality standard
|
|
||||||
|
|
||||||
Дата актуализации: 2026-07-01.
|
|
||||||
|
|
||||||
AWatch-rus уже работает в production-контуре заказчика. Основная цель
|
|
||||||
разработки - не добавление функций, а повышение надежности, эксплуатационной
|
|
||||||
зрелости, безопасности, сопровождаемости и воспроизводимости.
|
|
||||||
|
|
||||||
Приоритеты изменений, по убыванию:
|
|
||||||
|
|
||||||
1. Reliability.
|
|
||||||
2. Operational maturity.
|
|
||||||
3. Security.
|
|
||||||
4. Maintainability.
|
|
||||||
5. Reproducibility.
|
|
||||||
6. Performance.
|
|
||||||
7. Simplicity.
|
|
||||||
|
|
||||||
Новые функции не должны иметь приоритет над стабильностью production.
|
|
||||||
|
|
||||||
## Production-first правила
|
|
||||||
|
|
||||||
- Предполагать, что production deployment существует и пользователи зависят от
|
|
||||||
непрерывной работы.
|
|
||||||
- Предпочитать additive/backward-compatible изменения.
|
|
||||||
- Не перепроектировать работающие подсистемы без измеримой пользы.
|
|
||||||
- Не включать heavy DLP, Loki или always-on Velociraptor без отдельного
|
|
||||||
operator-approved решения.
|
|
||||||
- Не менять ActivityWatch logical host ids, bucket suffixes, Grafana variables
|
|
||||||
или ClickHouse workforce keys без отдельной compatibility-процедуры.
|
|
||||||
- Fail closed для security-sensitive и deployment-sensitive paths.
|
|
||||||
- Сохранять rollback path для runtime, automation, config и dependency changes.
|
|
||||||
|
|
||||||
## Порядок принятия решений
|
|
||||||
|
|
||||||
Перед реализацией любого изменения ответ должен быть положительным хотя бы на
|
|
||||||
один вопрос:
|
|
||||||
|
|
||||||
1. Улучшает ли это production stability?
|
|
||||||
2. Снижает ли это operational risk?
|
|
||||||
3. Улучшает ли это diagnostics или observability?
|
|
||||||
4. Улучшает ли это maintainability?
|
|
||||||
5. Уменьшает ли это технический долг с низким regression risk?
|
|
||||||
|
|
||||||
Если ответ отрицательный на все пять вопросов, изменение не должно попадать в
|
|
||||||
production-oriented PR.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
Перед добавлением зависимости нужно явно обосновать:
|
|
||||||
|
|
||||||
- почему стандартной библиотеки недостаточно;
|
|
||||||
- почему существующий workspace crate не решает задачу;
|
|
||||||
- operational cost зависимости;
|
|
||||||
- maintenance cost зависимости;
|
|
||||||
- license/security impact.
|
|
||||||
|
|
||||||
Неиспользуемые зависимости удаляются отдельными низкорисковыми PR после
|
|
||||||
targeted tests. Обновление `Cargo.lock` без причины не допускается.
|
|
||||||
|
|
||||||
## Required PR sections
|
|
||||||
|
|
||||||
Каждый PR обязан содержать:
|
|
||||||
|
|
||||||
- Purpose.
|
|
||||||
- Operational impact.
|
|
||||||
- Risk assessment.
|
|
||||||
- Rollback strategy.
|
|
||||||
- Validation steps.
|
|
||||||
- Documentation changes.
|
|
||||||
- Acceptance criteria.
|
|
||||||
|
|
||||||
Для documentation-only/governance-only PR нужно явно указать, что runtime, API и
|
|
||||||
UI impact отсутствуют.
|
|
||||||
|
|
||||||
## Validation baseline
|
|
||||||
|
|
||||||
Канонический набор проверок описан в
|
|
||||||
`docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md`. Минимальный baseline для
|
|
||||||
существенных Rust/runtime changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust
|
|
||||||
export CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target
|
|
||||||
|
|
||||||
cargo fmt --all --check
|
|
||||||
cargo test --workspace --all-targets --locked
|
|
||||||
cargo test --workspace --doc --locked
|
|
||||||
cargo clippy --workspace --all-targets --locked -- -D warnings
|
|
||||||
cargo audit --deny warnings
|
|
||||||
|
|
||||||
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
|
||||||
cargo deny --manifest-path adk-rust/Cargo.toml check \
|
|
||||||
--config deny.toml \
|
|
||||||
--hide-inclusion-graph \
|
|
||||||
--show-stats
|
|
||||||
python3 scripts/public_secret_pattern_check.py
|
|
||||||
node scripts/operational-maturity-check.mjs --json
|
|
||||||
```
|
|
||||||
|
|
||||||
Targeted checks are acceptable for small docs/governance or isolated changes,
|
|
||||||
but skipped checks must have a concrete reason.
|
|
||||||
|
|
||||||
## Technical debt policy
|
|
||||||
|
|
||||||
Fix technical debt only when all conditions are true:
|
|
||||||
|
|
||||||
- change is isolated;
|
|
||||||
- regression risk is low;
|
|
||||||
- tests or operational smoke cover the touched behavior;
|
|
||||||
- documentation remains accurate.
|
|
||||||
|
|
||||||
Otherwise create a follow-up task or document the residual risk instead of
|
|
||||||
mixing broad cleanup into a functional PR.
|
|
||||||
|
|
||||||
## Canonical references
|
|
||||||
|
|
||||||
- Review checklist: `docs/REVIEW_CHECKLIST_RU.md`.
|
|
||||||
- Validation runbook: `docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md`.
|
|
||||||
- Operational maturity harness: `docs/OPERATIONAL_MATURITY_RU.md`.
|
|
||||||
- PR workflow: `docs/PR_REVIEW_WORKFLOW_RU.md`.
|
|
||||||
- GitHub governance entrypoint: `.github/GOVERNANCE.md`.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# Эксплуатационная зрелость DetMir/AWatch-rus
|
|
||||||
|
|
||||||
Дата актуализации: 2026-07-01.
|
|
||||||
|
|
||||||
Этот контур добавляет автоматическую проверку эксплуатационной зрелости без
|
|
||||||
нагрузки на production. Public CI запускает только offline checks: fixtures,
|
|
||||||
локальный mock HTTP, статическую проверку конфигураций, ClickHouse DDL и
|
|
||||||
контракт наблюдаемости. Live checks запускаются только вручную оператором с
|
|
||||||
private access.
|
|
||||||
|
|
||||||
## Что проверяется
|
|
||||||
|
|
||||||
- Integration harness: локальный mock обслуживает ключевые endpoints
|
|
||||||
`/healthz`, `/readyz`, `/version`, Worktime management и Security Finding
|
|
||||||
Inbox shadow payload.
|
|
||||||
- Fault injection: клиент должен быстро классифицировать `503`, timeout и
|
|
||||||
connection reset, не зависая сверх заданного бюджета.
|
|
||||||
- API compatibility: DetMir Portal OpenAPI обязан сохранять ключевые paths,
|
|
||||||
schemas и runtime endpoints.
|
|
||||||
- Config/migration validation: JSON/YAML examples, systemd units/timers и
|
|
||||||
ClickHouse init SQL проверяются на базовую пригодность и idempotency.
|
|
||||||
- Bounded load: короткий локальный load smoke с concurrency и p95 budget,
|
|
||||||
без sizing claims.
|
|
||||||
- Observability: обязательные health/version/readiness fields, diagnostic
|
|
||||||
headers и Prometheus metric names закреплены manifest/fixtures/source check.
|
|
||||||
|
|
||||||
## Команды
|
|
||||||
|
|
||||||
Offline PR/CI gate:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
|
||||||
node --check scripts/operational-maturity-check.mjs
|
|
||||||
node scripts/operational-maturity-check.mjs --json
|
|
||||||
```
|
|
||||||
|
|
||||||
Live contract, только оператором и только если контур доступен:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
|
||||||
AWATCH_OPS_LIVE_URL=http://127.0.0.1:8720 \
|
|
||||||
node scripts/operational-maturity-check.mjs --json --live
|
|
||||||
```
|
|
||||||
|
|
||||||
Live check не включает DLP, Loki или Velociraptor. Он только читает
|
|
||||||
health/readiness/version/metrics endpoints и принимает controlled statuses
|
|
||||||
`200` или `503`.
|
|
||||||
|
|
||||||
## Governance
|
|
||||||
|
|
||||||
Контракт расположен в `configs/operational-maturity-contract.json`.
|
|
||||||
Fixture payloads лежат в `docs/fixtures/operational-maturity/`.
|
|
||||||
CI workflow: `.github/workflows/operational-maturity.yml`.
|
|
||||||
|
|
||||||
Правило изменений: если endpoint, metric, schema, systemd unit или ClickHouse
|
|
||||||
migration меняется, сначала обновляется manifest/fixture, затем код. Удаление
|
|
||||||
полей или paths считается breaking change, если нет отдельного operator-approved
|
|
||||||
major contract change.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Эксплуатационная проверка контура
|
# Эксплуатационная проверка контура
|
||||||
|
|
||||||
Дата актуализации: 2026-06-23
|
Дата актуализации: 2026-07-01
|
||||||
|
|
||||||
Документ фиксирует минимальный профессиональный контур проверки после
|
Документ фиксирует минимальный профессиональный контур проверки после
|
||||||
существенных изменений Rust-кода, сборщиков telemetry, ClickHouse workforce
|
существенных изменений Rust-кода, сборщиков telemetry, ClickHouse workforce
|
||||||
@@ -32,6 +32,41 @@ cargo deny --manifest-path adk-rust/Cargo.toml check \
|
|||||||
--show-stats
|
--show-stats
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Dependency hygiene для production/high-load Rust contour:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust
|
||||||
|
|
||||||
|
cargo metadata --locked --format-version 1 >/tmp/aw-rus-cargo-metadata.json
|
||||||
|
cargo tree --duplicates --locked > /tmp/aw-rus-cargo-tree-duplicates.txt
|
||||||
|
cargo update --locked
|
||||||
|
cargo audit --deny warnings
|
||||||
|
|
||||||
|
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
||||||
|
cargo deny --manifest-path adk-rust/Cargo.toml check \
|
||||||
|
--config deny.toml \
|
||||||
|
--hide-inclusion-graph \
|
||||||
|
--show-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo update --locked` используется как drift probe: он не должен менять
|
||||||
|
`Cargo.lock`, но может показать доступные совместимые обновления и завершиться
|
||||||
|
ошибкой из-за `--locked`. Такой результат фиксировать как сигнал для отдельного
|
||||||
|
dependency-refresh PR, а не как повод молча обновлять lockfile в функциональной
|
||||||
|
ветке.
|
||||||
|
|
||||||
|
Периодически, особенно перед dependency cleanup или production binary refresh,
|
||||||
|
проверять неиспользуемые зависимости:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust
|
||||||
|
cargo machete
|
||||||
|
```
|
||||||
|
|
||||||
|
Удалять найденные зависимости только после ручной проверки через `rg` и
|
||||||
|
целевого `cargo test -p <crate> --locked`: `cargo machete` может ошибаться на
|
||||||
|
feature-gated, generated или metadata-driven usage.
|
||||||
|
|
||||||
Windows/RDP collector дополнительно проверяется под целевой ABI:
|
Windows/RDP collector дополнительно проверяется под целевой ABI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -51,16 +86,12 @@ Repository-specific gate:
|
|||||||
```bash
|
```bash
|
||||||
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
||||||
python3 scripts/public_secret_pattern_check.py
|
python3 scripts/public_secret_pattern_check.py
|
||||||
node scripts/operational-maturity-check.mjs --json
|
|
||||||
|
|
||||||
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust
|
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust
|
||||||
export CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target
|
export CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target
|
||||||
cargo run -p quality-gate -- --root /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
cargo run -p quality-gate -- --root /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
||||||
```
|
```
|
||||||
|
|
||||||
Подробности эксплуатационного maturity harness: [эксплуатационная зрелость
|
|
||||||
DetMir/AWatch-rus](OPERATIONAL_MATURITY_RU.md).
|
|
||||||
|
|
||||||
## Browser smoke
|
## Browser smoke
|
||||||
|
|
||||||
Browser smoke не заменяет API/CLI проверки. Он подтверждает, что операторский
|
Browser smoke не заменяет API/CLI проверки. Он подтверждает, что операторский
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Дата: 2026-06-22
|
Дата: 2026-06-22
|
||||||
|
|
||||||
Статус: canonical checklist for production-first PR review.
|
Статус: advisory checklist for public review process.
|
||||||
|
|
||||||
Этот документ описывает проверочный чеклист для pull requests и внешнего
|
Этот документ описывает проверочный чеклист для pull requests и внешнего
|
||||||
инженерного review. Он не утверждает, что внешний peer review уже выполняется
|
инженерного review. Он не утверждает, что внешний peer review уже выполняется
|
||||||
@@ -13,15 +13,7 @@ release evidence должен производиться на российско
|
|||||||
|
|
||||||
## Общая безопасность изменений
|
## Общая безопасность изменений
|
||||||
|
|
||||||
- Изменение соответствует production-first стандарту:
|
|
||||||
`docs/ENTERPRISE_QUALITY_STANDARD_RU.md`.
|
|
||||||
- Цель PR относится к reliability, operational maturity, security,
|
|
||||||
maintainability, reproducibility, performance или simplicity. Если PR добавляет
|
|
||||||
функциональность, он также объясняет measurable operational benefit.
|
|
||||||
- Изменение имеет понятную цель, ограниченный scope and documented impact.
|
- Изменение имеет понятную цель, ограниченный scope and documented impact.
|
||||||
- Изменение additive/backward-compatible, если нет отдельного
|
|
||||||
operator-approved breaking-change решения.
|
|
||||||
- Не перепроектируются работающие подсистемы без измеримой пользы.
|
|
||||||
- Нет секретов, токенов, паролей, приватных ключей, recovery codes or live
|
- Нет секретов, токенов, паролей, приватных ключей, recovery codes or live
|
||||||
credentials.
|
credentials.
|
||||||
- Нет персональных данных сотрудников, реальных employee logs or customer
|
- Нет персональных данных сотрудников, реальных employee logs or customer
|
||||||
@@ -60,8 +52,6 @@ release evidence должен производиться на российско
|
|||||||
- No runtime behavior is changed by documentation/governance-only PRs.
|
- No runtime behavior is changed by documentation/governance-only PRs.
|
||||||
- No service restart, migration or production config change is implied unless
|
- No service restart, migration or production config change is implied unless
|
||||||
explicitly documented.
|
explicitly documented.
|
||||||
- Existing production deployment remains compatible, including logical host ids,
|
|
||||||
bucket suffixes, Grafana variables and ClickHouse workforce keys.
|
|
||||||
|
|
||||||
## Registry-readiness impact
|
## Registry-readiness impact
|
||||||
|
|
||||||
@@ -90,9 +80,6 @@ release evidence должен производиться на российско
|
|||||||
|
|
||||||
- README, `docs/PROJECT_STATUS_RU.md`, registry docs and operational runbooks
|
- README, `docs/PROJECT_STATUS_RU.md`, registry docs and operational runbooks
|
||||||
are updated when claims, checks, workflows or procedures change.
|
are updated when claims, checks, workflows or procedures change.
|
||||||
- Behavior changes update the relevant runbook and validation commands.
|
|
||||||
- Architecture docs are updated when an interface, data flow, deployment
|
|
||||||
boundary or security boundary changes.
|
|
||||||
- New claims are conservative and evidence-backed.
|
- New claims are conservative and evidence-backed.
|
||||||
- Pending work remains marked as planned/pending until evidence exists.
|
- Pending work remains marked as planned/pending until evidence exists.
|
||||||
- Public mirror wording remains separate from registry release evidence.
|
- Public mirror wording remains separate from registry release evidence.
|
||||||
@@ -101,8 +88,6 @@ release evidence должен производиться на российско
|
|||||||
|
|
||||||
- PR states whether deployment action is required.
|
- PR states whether deployment action is required.
|
||||||
- Rollback path is documented for runtime or automation changes.
|
- Rollback path is documented for runtime or automation changes.
|
||||||
- Dependency changes are isolated, justified and include rollback by reverting
|
|
||||||
the dependency/lockfile PR.
|
|
||||||
- Documentation-only PRs state that runtime/API/UI impact is unchanged.
|
- Documentation-only PRs state that runtime/API/UI impact is unchanged.
|
||||||
- Changes to scripts include syntax checks and a clear operator failure mode.
|
- Changes to scripts include syntax checks and a clear operator failure mode.
|
||||||
|
|
||||||
@@ -118,9 +103,6 @@ release evidence должен производиться на российско
|
|||||||
- For Rust/product changes, use
|
- For Rust/product changes, use
|
||||||
`docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md` as the default local validation
|
`docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md` as the default local validation
|
||||||
contour.
|
contour.
|
||||||
- Dependency hygiene and operational maturity checks are expected when touched
|
|
||||||
files can affect Rust dependencies, runtime contracts, configuration,
|
|
||||||
migrations, observability or CI governance.
|
|
||||||
- For operator-facing web, gateway, worktime reports or Grafana dashboards,
|
- For operator-facing web, gateway, worktime reports or Grafana dashboards,
|
||||||
browser smoke through the rendered pages is required in addition to API checks.
|
browser smoke through the rendered pages is required in addition to API checks.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"service": "detmir-portal",
|
|
||||||
"schema_version": "pilot-v1",
|
|
||||||
"timestamp": "2026-07-01T00:00:00Z"
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# HELP awatch_http_requests_total HTTP requests handled by AWatch-rus portal
|
|
||||||
# TYPE awatch_http_requests_total counter
|
|
||||||
awatch_http_requests_total{method="GET",route="/healthz",status="200",module="portal"} 1
|
|
||||||
# HELP awatch_http_request_duration_seconds HTTP request duration in seconds
|
|
||||||
# TYPE awatch_http_request_duration_seconds summary
|
|
||||||
awatch_http_request_duration_seconds_sum{method="GET",route="/healthz",status="200",module="portal"} 0.001
|
|
||||||
awatch_http_request_duration_seconds_count{method="GET",route="/healthz",status="200",module="portal"} 1
|
|
||||||
# TYPE awatch_report_requests_total counter
|
|
||||||
awatch_report_requests_total 1
|
|
||||||
# TYPE awatch_report_cache_hits_total counter
|
|
||||||
awatch_report_cache_hits_total 0
|
|
||||||
# TYPE awatch_report_cache_misses_total counter
|
|
||||||
awatch_report_cache_misses_total 1
|
|
||||||
# TYPE awatch_report_cache_stale_hits_total counter
|
|
||||||
awatch_report_cache_stale_hits_total 0
|
|
||||||
# TYPE awatch_reports_generated_total counter
|
|
||||||
awatch_reports_generated_total 1
|
|
||||||
# TYPE awatch_ingestion_records_total counter
|
|
||||||
awatch_ingestion_records_total 0
|
|
||||||
# TYPE awatch_ingestion_rejected_total counter
|
|
||||||
awatch_ingestion_rejected_total 0
|
|
||||||
# TYPE awatch_role_denied_total counter
|
|
||||||
awatch_role_denied_total 0
|
|
||||||
# HELP awatch_readyz_status Portal readiness status, 1=ready, 0=not_ready
|
|
||||||
# TYPE awatch_readyz_status gauge
|
|
||||||
awatch_readyz_status 1
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "ready",
|
|
||||||
"checks": {
|
|
||||||
"portal": {
|
|
||||||
"status": "ok",
|
|
||||||
"latency_ms": 1
|
|
||||||
},
|
|
||||||
"worktime_api": {
|
|
||||||
"status": "ok",
|
|
||||||
"stale": false
|
|
||||||
},
|
|
||||||
"clickhouse": {
|
|
||||||
"status": "ok",
|
|
||||||
"mode": "external"
|
|
||||||
},
|
|
||||||
"dlp_runtime": {
|
|
||||||
"status": "skipped",
|
|
||||||
"profile": "core_only",
|
|
||||||
"reason": "disabled_by_production_default"
|
|
||||||
},
|
|
||||||
"security_finding_inbox": {
|
|
||||||
"status": "ok",
|
|
||||||
"mode": "shadow"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"app_name": "AWatch-rus DetMir Portal",
|
|
||||||
"app_version": "0.1.0",
|
|
||||||
"schema_version": "pilot-v1",
|
|
||||||
"environment": "test",
|
|
||||||
"build": {
|
|
||||||
"profile": "ci",
|
|
||||||
"source": "fixture"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"mode": "shadow",
|
|
||||||
"auto_apply": false,
|
|
||||||
"workflow": ["decide", "plan", "approve", "apply", "verify"],
|
|
||||||
"findings": [
|
|
||||||
{
|
|
||||||
"id": "fixture-001",
|
|
||||||
"source": "hayabusa",
|
|
||||||
"severity": "medium",
|
|
||||||
"status": "new",
|
|
||||||
"containment": {
|
|
||||||
"recommended": true,
|
|
||||||
"approved": false,
|
|
||||||
"applied": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "DEGRADED",
|
|
||||||
"stale": false,
|
|
||||||
"reason": "report_unavailable",
|
|
||||||
"report_cache_hit": false,
|
|
||||||
"report_stale_served": false,
|
|
||||||
"aw_query_duration_ms": 350,
|
|
||||||
"aw_query_timeout_count": 1,
|
|
||||||
"report_build_error_count": 1,
|
|
||||||
"users": []
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "OK",
|
|
||||||
"stale": false,
|
|
||||||
"reason": null,
|
|
||||||
"report_cache_hit": false,
|
|
||||||
"report_stale_served": false,
|
|
||||||
"aw_query_duration_ms": 42,
|
|
||||||
"aw_query_timeout_count": 0,
|
|
||||||
"report_build_error_count": 0,
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"user": "demo-user",
|
|
||||||
"active_seconds": 1800,
|
|
||||||
"apps_count": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,487 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import fs from "node:fs";
|
|
||||||
import http from "node:http";
|
|
||||||
import net from "node:net";
|
|
||||||
import path from "node:path";
|
|
||||||
import process from "node:process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const manifestPath = "configs/operational-maturity-contract.json";
|
|
||||||
|
|
||||||
function readText(relativePath) {
|
|
||||||
return fs.readFileSync(path.join(root, relativePath), "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function readJson(relativePath) {
|
|
||||||
return JSON.parse(readText(relativePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
function exists(relativePath) {
|
|
||||||
return fs.existsSync(path.join(root, relativePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
function assert(condition, message) {
|
|
||||||
if (!condition) throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPath(value, dotted) {
|
|
||||||
return dotted.split(".").reduce((current, key) => {
|
|
||||||
if (current === null || current === undefined) return undefined;
|
|
||||||
return current[key];
|
|
||||||
}, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function result(name, fn) {
|
|
||||||
const started = Date.now();
|
|
||||||
try {
|
|
||||||
const details = fn() || {};
|
|
||||||
return { name, ok: true, duration_ms: Date.now() - started, ...details };
|
|
||||||
} catch (error) {
|
|
||||||
return { name, ok: false, duration_ms: Date.now() - started, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function asyncResult(name, fn) {
|
|
||||||
const started = Date.now();
|
|
||||||
try {
|
|
||||||
const details = (await fn()) || {};
|
|
||||||
return { name, ok: true, duration_ms: Date.now() - started, ...details };
|
|
||||||
} catch (error) {
|
|
||||||
return { name, ok: false, duration_ms: Date.now() - started, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function walk(dir, predicate = () => true) {
|
|
||||||
const base = path.join(root, dir);
|
|
||||||
if (!fs.existsSync(base)) return [];
|
|
||||||
const files = [];
|
|
||||||
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
||||||
const absolute = path.join(base, entry.name);
|
|
||||||
const relative = path.relative(root, absolute).replaceAll(path.sep, "/");
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
files.push(...walk(relative, predicate));
|
|
||||||
} else if (predicate(relative)) {
|
|
||||||
files.push(relative);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files.sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs() {
|
|
||||||
const args = new Set(process.argv.slice(2));
|
|
||||||
return {
|
|
||||||
json: args.has("--json"),
|
|
||||||
live: args.has("--live"),
|
|
||||||
failOnDuplicateDebt: args.has("--fail-on-duplicate-debt"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomLocalPort() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const server = net.createServer();
|
|
||||||
server.once("error", reject);
|
|
||||||
server.listen(0, "127.0.0.1", () => {
|
|
||||||
const address = server.address();
|
|
||||||
const port = typeof address === "object" && address ? address.port : 0;
|
|
||||||
server.close(() => resolve(port));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixtureResponse(fixture) {
|
|
||||||
if (fixture.contentType === "text/plain") {
|
|
||||||
return readText(fixture.fixture);
|
|
||||||
}
|
|
||||||
return JSON.stringify(readJson(fixture.fixture));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startFixtureServer(manifest) {
|
|
||||||
const fixturesByPath = new Map();
|
|
||||||
for (const fixture of manifest.integrationFixtures) {
|
|
||||||
fixturesByPath.set(fixture.path, fixture);
|
|
||||||
}
|
|
||||||
fixturesByPath.set("/metrics", {
|
|
||||||
fixture: manifest.observability.metricsFixture,
|
|
||||||
status: 200,
|
|
||||||
contentType: "text/plain",
|
|
||||||
});
|
|
||||||
|
|
||||||
const sockets = new Set();
|
|
||||||
const server = http.createServer((request, response) => {
|
|
||||||
const parsed = new URL(request.url || "/", "http://127.0.0.1");
|
|
||||||
const fixture = fixturesByPath.get(`${parsed.pathname}${parsed.search}`) || fixturesByPath.get(parsed.pathname);
|
|
||||||
if (!fixture) {
|
|
||||||
response.statusCode = 404;
|
|
||||||
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
||||||
response.end(JSON.stringify({ error: "not_found", path: parsed.pathname }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
response.statusCode = fixture.status || 200;
|
|
||||||
response.setHeader("Content-Type", fixture.contentType === "text/plain" ? "text/plain; charset=utf-8" : "application/json; charset=utf-8");
|
|
||||||
response.setHeader("X-Request-Id", request.headers["x-request-id"] || "operational-maturity-fixture");
|
|
||||||
response.setHeader("X-Correlation-Id", request.headers["x-request-id"] || "operational-maturity-fixture");
|
|
||||||
response.end(fixtureResponse(fixture));
|
|
||||||
});
|
|
||||||
server.on("connection", (socket) => {
|
|
||||||
sockets.add(socket);
|
|
||||||
socket.on("close", () => sockets.delete(socket));
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = await randomLocalPort();
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
server.once("error", reject);
|
|
||||||
server.listen(port, "127.0.0.1", resolve);
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
port,
|
|
||||||
close: () =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
for (const socket of sockets) socket.destroy();
|
|
||||||
server.close(resolve);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithTimeout(url, timeoutMs, options = {}) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
||||||
const started = Date.now();
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
||||||
const text = await response.text();
|
|
||||||
let json = null;
|
|
||||||
try {
|
|
||||||
json = text ? JSON.parse(text) : null;
|
|
||||||
} catch {
|
|
||||||
json = null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
ok: response.ok,
|
|
||||||
headers: response.headers,
|
|
||||||
text,
|
|
||||||
json,
|
|
||||||
elapsed_ms: Date.now() - started,
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkApiCompatibility(manifest) {
|
|
||||||
const spec = readJson(manifest.apiCompatibility.openapiPath);
|
|
||||||
const missing = [];
|
|
||||||
for (const [apiPath, methods] of Object.entries(manifest.apiCompatibility.requiredOpenApiPaths)) {
|
|
||||||
for (const method of methods) {
|
|
||||||
if (!spec.paths?.[apiPath]?.[method]) missing.push(`${method.toUpperCase()} ${apiPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const schema of manifest.apiCompatibility.requiredSchemas) {
|
|
||||||
if (!spec.components?.schemas?.[schema]) missing.push(`schema ${schema}`);
|
|
||||||
}
|
|
||||||
const runtimeSource = readText(manifest.apiCompatibility.runtimeSourcePath);
|
|
||||||
for (const endpoint of manifest.apiCompatibility.runtimeEndpoints) {
|
|
||||||
if (!runtimeSource.includes(`"${endpoint}"`)) missing.push(`runtime endpoint ${endpoint}`);
|
|
||||||
}
|
|
||||||
assert(missing.length === 0, `missing API compatibility anchors: ${missing.join(", ")}`);
|
|
||||||
return {
|
|
||||||
required_paths: Object.keys(manifest.apiCompatibility.requiredOpenApiPaths).length,
|
|
||||||
required_schemas: manifest.apiCompatibility.requiredSchemas.length,
|
|
||||||
runtime_endpoints: manifest.apiCompatibility.runtimeEndpoints.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateFixtureContract(manifest) {
|
|
||||||
const checked = [];
|
|
||||||
for (const fixture of manifest.integrationFixtures) {
|
|
||||||
const payload = readJson(fixture.fixture);
|
|
||||||
for (const field of fixture.requiredFields || []) {
|
|
||||||
assert(getPath(payload, field) !== undefined, `${fixture.fixture} missing field ${field}`);
|
|
||||||
}
|
|
||||||
for (const [field, expected] of Object.entries(fixture.equals || {})) {
|
|
||||||
assert(getPath(payload, field) === expected, `${fixture.fixture} expected ${field}=${JSON.stringify(expected)}`);
|
|
||||||
}
|
|
||||||
checked.push(fixture.path);
|
|
||||||
}
|
|
||||||
return { fixtures: checked.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkIntegrationHarness(manifest) {
|
|
||||||
const server = await startFixtureServer(manifest);
|
|
||||||
const checked = [];
|
|
||||||
try {
|
|
||||||
for (const fixture of manifest.integrationFixtures) {
|
|
||||||
const response = await fetchWithTimeout(`http://127.0.0.1:${server.port}${fixture.path}`, 1500, {
|
|
||||||
headers: { "X-Request-Id": "operational-maturity" },
|
|
||||||
});
|
|
||||||
assert(response.status === fixture.status, `${fixture.path} status ${response.status}, expected ${fixture.status}`);
|
|
||||||
if (fixture.contentType === "application/json") {
|
|
||||||
assert(response.json && typeof response.json === "object", `${fixture.path} did not return JSON`);
|
|
||||||
}
|
|
||||||
assert(response.headers.get("x-correlation-id") === "operational-maturity", `${fixture.path} did not echo correlation id`);
|
|
||||||
checked.push({ path: fixture.path, elapsed_ms: response.elapsed_ms });
|
|
||||||
}
|
|
||||||
return { endpoints: checked.length, max_elapsed_ms: Math.max(...checked.map((item) => item.elapsed_ms)) };
|
|
||||||
} finally {
|
|
||||||
await server.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startFaultServer() {
|
|
||||||
const sockets = new Set();
|
|
||||||
const server = http.createServer((request, response) => {
|
|
||||||
const parsed = new URL(request.url || "/", "http://127.0.0.1");
|
|
||||||
if (parsed.pathname === "/fault/503") {
|
|
||||||
response.statusCode = 503;
|
|
||||||
response.end("unavailable");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (parsed.pathname === "/fault/slow") {
|
|
||||||
setTimeout(() => {
|
|
||||||
response.statusCode = 200;
|
|
||||||
response.end("slow-ok");
|
|
||||||
}, 1200);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (parsed.pathname === "/fault/reset") {
|
|
||||||
request.socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
response.statusCode = 200;
|
|
||||||
response.end("ok");
|
|
||||||
});
|
|
||||||
server.on("connection", (socket) => {
|
|
||||||
sockets.add(socket);
|
|
||||||
socket.on("close", () => sockets.delete(socket));
|
|
||||||
});
|
|
||||||
const port = await randomLocalPort();
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
server.once("error", reject);
|
|
||||||
server.listen(port, "127.0.0.1", resolve);
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
port,
|
|
||||||
close: () =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
for (const socket of sockets) socket.destroy();
|
|
||||||
server.close(resolve);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkFaultInjection(manifest) {
|
|
||||||
const server = await startFaultServer();
|
|
||||||
const timeoutMs = manifest.faultInjection.clientTimeoutMs;
|
|
||||||
const maxClassifyMs = manifest.faultInjection.maxFailureClassificationMs;
|
|
||||||
const classifications = [];
|
|
||||||
try {
|
|
||||||
const unavailable = await fetchWithTimeout(`http://127.0.0.1:${server.port}/fault/503`, timeoutMs);
|
|
||||||
assert(unavailable.status === 503, "503 fault was not classified as HTTP 503");
|
|
||||||
classifications.push("http_503");
|
|
||||||
|
|
||||||
const slowStart = Date.now();
|
|
||||||
try {
|
|
||||||
await fetchWithTimeout(`http://127.0.0.1:${server.port}/fault/slow`, timeoutMs);
|
|
||||||
throw new Error("slow fault did not timeout");
|
|
||||||
} catch (error) {
|
|
||||||
const elapsed = Date.now() - slowStart;
|
|
||||||
assert(elapsed < maxClassifyMs, `timeout classification exceeded ${maxClassifyMs}ms`);
|
|
||||||
classifications.push("timeout");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetchWithTimeout(`http://127.0.0.1:${server.port}/fault/reset`, timeoutMs);
|
|
||||||
throw new Error("connection reset fault did not fail");
|
|
||||||
} catch {
|
|
||||||
classifications.push("connection_reset");
|
|
||||||
}
|
|
||||||
return { classifications };
|
|
||||||
} finally {
|
|
||||||
await server.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function percentile(values, pct) {
|
|
||||||
const sorted = [...values].sort((a, b) => a - b);
|
|
||||||
const index = Math.min(sorted.length - 1, Math.ceil((pct / 100) * sorted.length) - 1);
|
|
||||||
return sorted[index] || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mapLimit(items, limit, fn) {
|
|
||||||
const results = [];
|
|
||||||
let next = 0;
|
|
||||||
async function worker() {
|
|
||||||
while (next < items.length) {
|
|
||||||
const index = next;
|
|
||||||
next += 1;
|
|
||||||
results[index] = await fn(items[index], index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkBoundedLoad(manifest) {
|
|
||||||
const server = await startFixtureServer(manifest);
|
|
||||||
const cfg = manifest.boundedLoad;
|
|
||||||
const heapBefore = process.memoryUsage().heapUsed;
|
|
||||||
try {
|
|
||||||
const work = Array.from({ length: cfg.requests }, (_, index) => cfg.paths[index % cfg.paths.length]);
|
|
||||||
const responses = await mapLimit(work, cfg.concurrency, async (apiPath) => {
|
|
||||||
const response = await fetchWithTimeout(`http://127.0.0.1:${server.port}${apiPath}`, 1500);
|
|
||||||
assert(response.status >= 200 && response.status < 300, `${apiPath} failed with ${response.status}`);
|
|
||||||
return response.elapsed_ms;
|
|
||||||
});
|
|
||||||
const heapGrowth = Math.max(0, process.memoryUsage().heapUsed - heapBefore);
|
|
||||||
const p95 = percentile(responses, 95);
|
|
||||||
assert(p95 <= cfg.p95MaxMs, `bounded load p95 ${p95}ms exceeds ${cfg.p95MaxMs}ms`);
|
|
||||||
assert(heapGrowth <= cfg.heapGrowthMaxBytes, `heap growth ${heapGrowth} exceeds ${cfg.heapGrowthMaxBytes}`);
|
|
||||||
return { requests: cfg.requests, concurrency: cfg.concurrency, p95_ms: p95, heap_growth_bytes: heapGrowth };
|
|
||||||
} finally {
|
|
||||||
await server.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateJsonAndYaml(manifest) {
|
|
||||||
const checked = [];
|
|
||||||
for (const file of manifest.configValidation.jsonFiles) {
|
|
||||||
assert(exists(file), `missing JSON config/fixture ${file}`);
|
|
||||||
readJson(file);
|
|
||||||
checked.push(file);
|
|
||||||
}
|
|
||||||
for (const file of manifest.configValidation.yamlFiles) {
|
|
||||||
assert(exists(file), `missing YAML config ${file}`);
|
|
||||||
const text = readText(file);
|
|
||||||
assert(!text.includes("\t"), `${file} contains tabs`);
|
|
||||||
if (!/(\.example\.ya?ml|example\.yml)$/i.test(file)) {
|
|
||||||
assert(!/password:\s*(change-me|changeme|password)\b/i.test(text), `${file} contains unsafe password placeholder`);
|
|
||||||
}
|
|
||||||
checked.push(file);
|
|
||||||
}
|
|
||||||
return { files: checked.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateSystemdUnits(manifest) {
|
|
||||||
const files = manifest.configValidation.systemdDirs.flatMap((dir) =>
|
|
||||||
walk(dir, (file) => file.endsWith(".service") || file.endsWith(".timer")),
|
|
||||||
);
|
|
||||||
assert(files.length > 0, "no systemd files found");
|
|
||||||
const findings = [];
|
|
||||||
for (const file of files) {
|
|
||||||
const text = readText(file);
|
|
||||||
if (!text.includes("[Unit]")) findings.push(`${file}: missing [Unit]`);
|
|
||||||
if (file.endsWith(".service")) {
|
|
||||||
if (!text.includes("[Service]")) findings.push(`${file}: missing [Service]`);
|
|
||||||
if (!/^ExecStart=/m.test(text)) findings.push(`${file}: missing ExecStart`);
|
|
||||||
if (/^Type=oneshot$/m.test(text) && /^Restart=always$/m.test(text)) {
|
|
||||||
findings.push(`${file}: oneshot service must not use Restart=always`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (file.endsWith(".timer")) {
|
|
||||||
if (!text.includes("[Timer]")) findings.push(`${file}: missing [Timer]`);
|
|
||||||
if (!/^Unit=.*\.service$/m.test(text)) findings.push(`${file}: missing service Unit`);
|
|
||||||
if (!text.includes("[Install]")) findings.push(`${file}: missing [Install]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert(findings.length === 0, findings.join("; "));
|
|
||||||
return { files: files.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateClickHouseMigrations(manifest) {
|
|
||||||
const dir = manifest.configValidation.clickhouseInitDir;
|
|
||||||
const files = walk(dir, (file) => file.endsWith(".sql"));
|
|
||||||
assert(files.length > 0, "no ClickHouse init SQL files found");
|
|
||||||
const findings = [];
|
|
||||||
const order = files.map((file) => path.basename(file));
|
|
||||||
const sorted = [...order].sort();
|
|
||||||
if (order.join("|") !== sorted.join("|")) findings.push("ClickHouse init filenames are not sorted");
|
|
||||||
for (const file of files) {
|
|
||||||
const text = readText(file);
|
|
||||||
if (/\b(DROP|TRUNCATE)\s+(DATABASE|TABLE)\b/i.test(text)) {
|
|
||||||
findings.push(`${file}: destructive DDL is not allowed in init validation`);
|
|
||||||
}
|
|
||||||
const createStatements = text.match(/\bCREATE\s+(DATABASE|TABLE|VIEW|OR\s+REPLACE\s+VIEW)\b/gi) || [];
|
|
||||||
for (const create of createStatements) {
|
|
||||||
if (/CREATE\s+(DATABASE|TABLE)/i.test(create) && !/IF\s+NOT\s+EXISTS/i.test(text.slice(text.indexOf(create), text.indexOf(create) + 140))) {
|
|
||||||
findings.push(`${file}: CREATE DATABASE/TABLE must use IF NOT EXISTS`);
|
|
||||||
}
|
|
||||||
if (/CREATE\s+VIEW/i.test(create) && !/CREATE\s+(OR\s+REPLACE\s+)?VIEW/i.test(create)) {
|
|
||||||
findings.push(`${file}: CREATE VIEW must be OR REPLACE or otherwise idempotent`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert(findings.length === 0, findings.join("; "));
|
|
||||||
return { files: files.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkObservability(manifest) {
|
|
||||||
const metricsText = readText(manifest.observability.metricsFixture);
|
|
||||||
const sourceText = readText(manifest.observability.sourcePath);
|
|
||||||
const missing = [];
|
|
||||||
for (const metric of manifest.observability.requiredMetrics) {
|
|
||||||
if (!metricsText.includes(metric)) missing.push(`fixture metric ${metric}`);
|
|
||||||
if (!sourceText.includes(metric)) missing.push(`source metric ${metric}`);
|
|
||||||
}
|
|
||||||
const hardeningSmoke = readText("scripts/awatch-production-hardening-smoke.mjs");
|
|
||||||
for (const header of manifest.observability.diagnosticHeaders) {
|
|
||||||
if (!hardeningSmoke.includes(header)) missing.push(`diagnostic header ${header}`);
|
|
||||||
}
|
|
||||||
assert(missing.length === 0, `missing observability anchors: ${missing.join(", ")}`);
|
|
||||||
return { metrics: manifest.observability.requiredMetrics.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkLive(manifest) {
|
|
||||||
const baseUrl = (process.env.AWATCH_OPS_LIVE_URL || "").replace(/\/+$/, "");
|
|
||||||
assert(baseUrl, "AWATCH_OPS_LIVE_URL is required for --live");
|
|
||||||
const checked = [];
|
|
||||||
for (const endpoint of manifest.apiCompatibility.runtimeEndpoints) {
|
|
||||||
const response = await fetchWithTimeout(`${baseUrl}${endpoint}`, 3000, {
|
|
||||||
headers: { "X-Request-Id": "operational-maturity-live" },
|
|
||||||
});
|
|
||||||
assert([200, 503].includes(response.status), `${endpoint} returned uncontrolled status ${response.status}`);
|
|
||||||
checked.push({ endpoint, status: response.status, elapsed_ms: response.elapsed_ms });
|
|
||||||
}
|
|
||||||
return { baseUrl, checked };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const args = parseArgs();
|
|
||||||
const manifest = readJson(manifestPath);
|
|
||||||
const checks = [
|
|
||||||
result("api_compatibility", () => checkApiCompatibility(manifest)),
|
|
||||||
result("fixture_contracts", () => validateFixtureContract(manifest)),
|
|
||||||
await asyncResult("integration_harness", () => checkIntegrationHarness(manifest)),
|
|
||||||
await asyncResult("fault_injection", () => checkFaultInjection(manifest)),
|
|
||||||
await asyncResult("bounded_load", () => checkBoundedLoad(manifest)),
|
|
||||||
result("json_yaml_config_validation", () => validateJsonAndYaml(manifest)),
|
|
||||||
result("systemd_config_validation", () => validateSystemdUnits(manifest)),
|
|
||||||
result("clickhouse_migration_validation", () => validateClickHouseMigrations(manifest)),
|
|
||||||
result("observability_contract", () => checkObservability(manifest)),
|
|
||||||
];
|
|
||||||
if (args.live) {
|
|
||||||
checks.push(await asyncResult("live_operational_contract", () => checkLive(manifest)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const report = {
|
|
||||||
ok: checks.every((check) => check.ok),
|
|
||||||
mode: args.live ? "live" : "offline",
|
|
||||||
manifest: manifest.version,
|
|
||||||
checks,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (args.json) {
|
|
||||||
console.log(JSON.stringify(report, null, 2));
|
|
||||||
} else {
|
|
||||||
for (const check of checks) {
|
|
||||||
console.log(`${check.ok ? "ok" : "fail"} ${check.name} ${check.duration_ms}ms`);
|
|
||||||
if (!check.ok) console.log(` ${check.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!report.ok) process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(JSON.stringify({ ok: false, error: error.message }, null, 2));
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user