@enwithai/product-check 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +6 -6
- package/CHANGELOG.md +66 -0
- package/NOTICE.md +5 -3
- package/README.md +34 -19
- package/SECURITY.md +5 -1
- package/dashboard/assurance.css +71 -0
- package/dashboard/assurance.js +21 -0
- package/dashboard/dashboard.css +6 -0
- package/dist/assurance/adapters.d.ts +32 -0
- package/dist/assurance/adapters.js +76 -0
- package/dist/assurance/approvals.d.ts +34 -0
- package/dist/assurance/approvals.js +133 -0
- package/dist/assurance/cli.d.ts +13 -0
- package/dist/assurance/cli.js +863 -0
- package/dist/assurance/dashboard.d.ts +18 -0
- package/dist/assurance/dashboard.js +267 -0
- package/dist/assurance/diff.d.ts +20 -0
- package/dist/assurance/diff.js +99 -0
- package/dist/assurance/evidence-log.d.ts +20 -0
- package/dist/assurance/evidence-log.js +100 -0
- package/dist/assurance/fix-packets.d.ts +12 -0
- package/dist/assurance/fix-packets.js +178 -0
- package/dist/assurance/gate.d.ts +37 -0
- package/dist/assurance/gate.js +324 -0
- package/dist/assurance/lanes.d.ts +61 -0
- package/dist/assurance/lanes.js +380 -0
- package/dist/assurance/policy.d.ts +10 -0
- package/dist/assurance/policy.js +85 -0
- package/dist/assurance/proof.d.ts +35 -0
- package/dist/assurance/proof.js +146 -0
- package/dist/assurance/register.d.ts +41 -0
- package/dist/assurance/register.js +480 -0
- package/dist/assurance/render-md.d.ts +17 -0
- package/dist/assurance/render-md.js +212 -0
- package/dist/assurance/repo-state.d.ts +15 -0
- package/dist/assurance/repo-state.js +79 -0
- package/dist/assurance/review.d.ts +52 -0
- package/dist/assurance/review.js +408 -0
- package/dist/assurance/risk.d.ts +21 -0
- package/dist/assurance/risk.js +184 -0
- package/dist/assurance/types.d.ts +573 -0
- package/dist/assurance/types.js +6 -0
- package/dist/assurance/util.d.ts +24 -0
- package/dist/assurance/util.js +89 -0
- package/dist/assurance/verification.d.ts +30 -0
- package/dist/assurance/verification.js +212 -0
- package/dist/assurance/verify.d.ts +26 -0
- package/dist/assurance/verify.js +304 -0
- package/dist/blueprint/build.d.ts +3 -1
- package/dist/blueprint/build.js +41 -5
- package/dist/cli/run.js +103 -5
- package/dist/contract/contract.d.ts +4 -0
- package/dist/contract/contract.js +16 -0
- package/dist/dashboard/render.d.ts +5 -0
- package/dist/dashboard/render.js +107 -3
- package/dist/discovery/pops.d.ts +2 -0
- package/dist/discovery/pops.js +7 -5
- package/dist/exec/process.d.ts +27 -0
- package/dist/exec/process.js +175 -0
- package/dist/importers/operational.d.ts +56 -0
- package/dist/importers/operational.js +217 -0
- package/dist/importers/reports.d.ts +24 -0
- package/dist/importers/reports.js +37 -1
- package/dist/inspectors/source/surface.d.ts +30 -0
- package/dist/inspectors/source/surface.js +529 -0
- package/dist/parse/yaml.d.ts +4 -0
- package/dist/parse/yaml.js +39 -0
- package/dist/reporting/markdown.d.ts +1 -0
- package/dist/reporting/markdown.js +18 -1
- package/dist/reporting/spec-markdown.d.ts +9 -0
- package/dist/reporting/spec-markdown.js +144 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.js +52 -2
- package/dist/schema/registry.js +10 -0
- package/dist/scoring/score.js +1 -1
- package/dist/spec/build.d.ts +28 -0
- package/dist/spec/build.js +278 -0
- package/dist/types.d.ts +78 -2
- package/dist/types.js +6 -1
- package/dist/validation/catalog.d.ts +1 -1
- package/dist/validation/catalog.js +125 -2
- package/dist/validation/context.d.ts +6 -1
- package/dist/validation/import-evaluators.js +37 -3
- package/dist/validation/integrity.js +4 -0
- package/dist/validation/operational-evaluators.d.ts +3 -0
- package/dist/validation/operational-evaluators.js +153 -0
- package/dist/validation/source-evaluators.js +1 -1
- package/package.json +4 -1
- package/schemas/approval.schema.json +98 -0
- package/schemas/as-built-spec.schema.json +356 -0
- package/schemas/assurance-common.schema.json +291 -0
- package/schemas/assurance-policy.schema.json +185 -0
- package/schemas/blueprint.schema.json +3 -0
- package/schemas/change-review.schema.json +711 -0
- package/schemas/closure-register.schema.json +499 -0
- package/schemas/evidence-record.schema.json +94 -0
- package/schemas/fix-packet.schema.json +272 -0
- package/schemas/release-gate.schema.json +308 -0
- package/schemas/reviewer-findings.schema.json +49 -0
- package/schemas/scan-contract.schema.json +20 -0
- package/schemas/scorecard.schema.json +1 -1
- package/schemas/verification-run.schema.json +199 -0
package/ATTRIBUTION.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
## Development provenance
|
|
16
16
|
|
|
17
17
|
Product Check was written for this repository with substantial AI assistance (Claude Code), directed and reviewed by
|
|
18
|
-
the project's human sponsor. Commits carry a `Co-Authored-By` trailer. It has **
|
|
19
|
-
no vendored third-party code. Test fixtures and the demo application are synthetic; credential-like strings in them
|
|
18
|
+
the project's human sponsor. Commits carry a `Co-Authored-By` trailer. It has **one runtime dependency**, `yaml` (ISC,
|
|
19
|
+
by Eemeli Aro), and contains no vendored third-party code. Test fixtures and the demo application are synthetic; credential-like strings in them
|
|
20
20
|
are generated at run time and are not real.
|
|
21
21
|
|
|
22
22
|
## Built on
|
|
@@ -33,10 +33,10 @@ references are in [REFERENCES.md](REFERENCES.md). Listing a project does not mea
|
|
|
33
33
|
|
|
34
34
|
## Frameworks
|
|
35
35
|
|
|
36
|
-
| Framework | Owner | Relationship | Note
|
|
37
|
-
| ---------------- | ---------------------- | -------------- |
|
|
38
|
-
| CLEAR (CLEAR CT) | NetworkGain Consulting | `inspired` | The design of the assessment model — separate Customer, Technology and Trust pillars, CT-T0…T6 lifecycle tiers, points of presence, evidence statuses, the rule that strong pillars never hide weak ones, and hard gates overriding scores — was inspired by NetworkGain's CLEAR CT method (v3.1, internal and unpublished). No text, catalog, prompt or scoring table from it is reproduced, and there is no structural relationship. Wording approved for publication on 2026-10-12. |
|
|
39
|
-
| TRACE | EnWithAI Technologies | `not-assessed` | Each finding carries an intent → evidence → remediation → retest chain, which may match TRACE's intent; alignment cannot be stated until the TRACE specification is available.
|
|
36
|
+
| Framework | Owner | Relationship | Note |
|
|
37
|
+
| ---------------- | ---------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| CLEAR (CLEAR CT) | NetworkGain Consulting | `inspired` | The design of the assessment model — separate Customer, Technology and Trust pillars, CT-T0…T6 lifecycle tiers, points of presence, evidence statuses, the rule that strong pillars never hide weak ones, and hard gates overriding scores — was inspired by NetworkGain's CLEAR CT method (v3.1, internal and unpublished). No text, catalog, prompt or scoring table from it is reproduced, and there is no structural relationship. Wording approved for publication on 2026-10-12. The v3.2 layer added in 0.3.0 (execution phases, finding verification states, closure state machine, release conditions) was likewise inspired by CLEAR CT v3.2 (internal and unpublished); that wording was approved for publication on 2026-11-09. |
|
|
39
|
+
| TRACE | EnWithAI Technologies | `not-assessed` | Each finding carries an intent → evidence → remediation → retest chain, which may match TRACE's intent; alignment cannot be stated until the TRACE specification is available. |
|
|
40
40
|
|
|
41
41
|
## Ecosystem
|
|
42
42
|
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,72 @@ versions may break the results schema, which carries its own `schema_version`, a
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.3.0 — 2026-11-09
|
|
10
|
+
|
|
11
|
+
Vibe-code review and verified closure, inspired by CLEAR CT v3.2 (relationship `inspired`; wording approved for publication 2026-11-09). Additive: the results schema stays `0.1.0`, the scan
|
|
12
|
+
pipeline and its outputs are unchanged (the committed demo outputs are byte-identical), and new artifacts carry their
|
|
13
|
+
own assurance schema `0.1.0`. Design: [docs/vibe-code-assurance.md](docs/vibe-code-assurance.md); status:
|
|
14
|
+
[IMPLEMENTATION_AUDIT_v3.2.md](IMPLEMENTATION_AUDIT_v3.2.md).
|
|
15
|
+
|
|
16
|
+
- New commands: `proof` (VC-P1 author-side proof), `review` (PR_DIFF / PRE_PR_LOCAL layered review), `register`,
|
|
17
|
+
`transition`, `fix`, `verify` (VC-P5 at the exact fix commit), `keygen`, `approve` (Ed25519-signed), `close`,
|
|
18
|
+
`accept-risk`, `reject`, `learn`, `fix-packets`, `gate`, `assure`; `blueprint`, `dashboard` and `gate` accept
|
|
19
|
+
`--from <results.json>`.
|
|
20
|
+
- **Safety-model change (opt-in):** `src/exec/process.ts` is the only module that starts processes: operator-declared
|
|
21
|
+
commands with `--allow-exec` (argv, no shell, reduced environment, timeout, output cap, digests) and hardened
|
|
22
|
+
read-only git. The scan pipeline still never executes anything; tests and ESLint enforce the boundary.
|
|
23
|
+
- Six deterministic review lanes, independent verification of findings and external reviewer claims
|
|
24
|
+
(`--reviewer-findings`), de-duplication, repeat detection and a review-cycle limit with escalation.
|
|
25
|
+
- Risk routing by impacted surface with protected paths, domain owners and an opt-in LOW-risk waiver; approvals bind
|
|
26
|
+
to the reviewed commit (STALE after a new commit); bots, unsigned names and implementers never count.
|
|
27
|
+
- Closure register with hash-chained transitions and invariant checks on load; signed risk acceptance with expiry;
|
|
28
|
+
false positives with secondary support for P0/P1; REGRESSED with history; regression corpus.
|
|
29
|
+
- Fix packets, `PRODUCT_CHECK_REVIEW.md`, `PRODUCT_CHECK_BLUEPRINT.md` (Mermaid as-is/to-be),
|
|
30
|
+
`PRODUCT_CHECK_RELEASE_GATE.md`, `release-gate.json` (GO / CONDITIONAL_GO / NO_GO / UNKNOWN, VC-P0…P7 progress),
|
|
31
|
+
append-only `review-evidence.jsonl`, and `dashboard/index.html` rendered from those files.
|
|
32
|
+
- Optional versioned adapters: Impact Check 2.0, Spec Check 1.0, Outcome Check 1.0.
|
|
33
|
+
- Ten new JSON Schemas (`assurance-common`, `assurance-policy`, `evidence-record`, `change-review`, `approval`,
|
|
34
|
+
`verification-run`, `fix-packet`, `closure-register`, `release-gate`, `reviewer-findings`); `validate` checks them.
|
|
35
|
+
- `examples/closure-demo/`: a scripted end-to-end run on a synthetic app.
|
|
36
|
+
|
|
37
|
+
## 0.2.0 — 2026-10-28
|
|
38
|
+
|
|
39
|
+
Commons integration and the first CT-T4 controls. Rubric `pc-rubric-0.2` (51 controls; scoring rules unchanged);
|
|
40
|
+
results schema `0.1.0`, additive. Results and baselines written by 0.1.0 still validate and load.
|
|
41
|
+
|
|
42
|
+
- CT-T4 operational imports ([docs/operational-imports.md](docs/operational-imports.md)): `--openslo` (OpenSLO v1
|
|
43
|
+
YAML/JSON, repeatable), `--alert-rules` (Prometheus/Mimir rule files, `PrometheusRule`, `/api/v1/rules` JSON,
|
|
44
|
+
repeatable) and `--service-graph` (Jaeger `/api/dependencies`, `traces_service_graph_request_total` query result).
|
|
45
|
+
New controls PC-T-18 (SLOs complete), PC-T-19 (every SLO alerted on), PC-T-20 (declared dependencies observed at
|
|
46
|
+
runtime, with `--architecture-check`); PC-T-14 is now evaluated from these files; the observability PoP is
|
|
47
|
+
AVAILABLE when they are given.
|
|
48
|
+
- First runtime dependency: `yaml` 2.9.1 (ISC, pinned), used only through `src/parse/yaml.ts` (core schema, unique
|
|
49
|
+
keys, at most 100 aliases, 10 MiB per file).
|
|
50
|
+
- `--architecture-check <report.json>` (contract `imports.architecture_check`) imports an Architecture Check report
|
|
51
|
+
(schema 1.0 or 1.1, `analyze`/`check` output). New control **PC-T-17 "Architecture model matches the code"**
|
|
52
|
+
(technology, CT-T3, weight 1, P2): PASS with no verified drift of medium severity or above, PARTIAL with one
|
|
53
|
+
`architecture-drift` finding otherwise, NOT_TESTED without a report. Hypotheses and unknowns never count. Rules in
|
|
54
|
+
`docs/interoperability.md`.
|
|
55
|
+
- Rubric `pc-rubric-0.2` (48 controls; scoring rules unchanged). The scorecard schema accepts `pc-rubric-0.1` and
|
|
56
|
+
`pc-rubric-0.2`, and integrity recomputation keeps the stored version, so 0.1.0 results still validate and load as
|
|
57
|
+
retest baselines. Results schema stays `0.1.0`; `imports.architecture_check` is optional in the contract schema.
|
|
58
|
+
- `product-check spec`: reverse-engineers an as-built specification from a repository (pages, endpoints, data
|
|
59
|
+
entities, integrations, configuration names, auth boundaries, test titles) with evidence for every item. It writes
|
|
60
|
+
`as-built-spec.json` (new schema `as-built-spec.schema.json`) and a Spec Kit `specs/000-as-built/spec.md` that Spec
|
|
61
|
+
Check can lint. Intent that code cannot show is left as `[NEEDS CLARIFICATION]`. Source only; nothing executed.
|
|
62
|
+
- `run`/`retest` derive the as-built spec from the same snapshot. The blueprint embeds it as the new optional field
|
|
63
|
+
`as_is.as_built`, and `as-built-spec.json` plus `specs/000-as-built/spec.md` are written next to the report
|
|
64
|
+
(`blueprint --out-dir` re-renders them). Traceability now names the as-built requirement a finding touches
|
|
65
|
+
(`As-built FR-002; requirement PC-…`): pages, endpoints and entities match by file, integrations and configuration
|
|
66
|
+
only by exact line, and application URLs by path. Scores, verdicts and findings are unchanged.
|
|
67
|
+
- Schema: additive, still `0.1.0`. Existing contracts, observations and baselines remain valid. Results that carry
|
|
68
|
+
`as_is.as_built` do not validate with the schemas bundled in 0.1.0 and earlier.
|
|
69
|
+
- Dashboard: a sixth view, **As-built spec**, shows the discovered surface, the questions only a person can answer,
|
|
70
|
+
user stories, and every FR/NFR/SC/EC item with its certainty, source, evidence and linked findings (open or fixed),
|
|
71
|
+
with type, certainty and text filters. It is rendered from the results JSON like every other view.
|
|
72
|
+
- Demo: the baseline, retest and repository-only examples include the as-built spec, and
|
|
73
|
+
`examples/vibe-app-demo/output/as-built/` holds the standalone `spec` output. Both are checked against a fresh run.
|
|
74
|
+
|
|
9
75
|
## 0.1.0 — 2026-10-12
|
|
10
76
|
|
|
11
77
|
First release without a pre-release suffix, so `npm install @enwithai/product-check` and `npx @enwithai/product-check` work without a
|
package/NOTICE.md
CHANGED
|
@@ -8,9 +8,11 @@ license.
|
|
|
8
8
|
|
|
9
9
|
## Third-party material
|
|
10
10
|
|
|
11
|
-
Product Check has **
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Product Check has **one runtime dependency**, [`yaml`](https://github.com/eemeli/yaml) 2.9.1 (ISC License, Copyright
|
|
12
|
+
Eemeli Aro), installed from npm and not vendored. It reads formats defined by others (JSON Schema, JUnit XML, HAR,
|
|
13
|
+
`npm audit` JSON, the report schemas of Spec Check, Impact Check, Outcome Check and Architecture Check, OpenSLO,
|
|
14
|
+
Prometheus rule files, Jaeger dependencies and the OpenTelemetry service-graph metrics); no code from those projects
|
|
15
|
+
is included.
|
|
14
16
|
|
|
15
17
|
## Trademarks
|
|
16
18
|
|
package/README.md
CHANGED
|
@@ -12,23 +12,30 @@ Customer, Technology and Trust controls, and gives you:
|
|
|
12
12
|
and that states exactly what it does and does not cover;
|
|
13
13
|
- an **engineering blueprint**: prioritized fixes, acceptance criteria, regression-test candidates and one
|
|
14
14
|
**work packet** per fix that you (or any coding agent) can act on;
|
|
15
|
-
- a **local dashboard** and a **retest** loop that shows verified fixes, regressions and trends
|
|
15
|
+
- a **local dashboard** and a **retest** loop that shows verified fixes, regressions and trends;
|
|
16
|
+
- **vibe-code review and verified closure** (new in 0.3.0; design inspired by CLEAR CT v3.2): layered change review with independent
|
|
17
|
+
verification of findings, risk-routed signed human approvals, agent-ready fix packets, a closure state machine in
|
|
18
|
+
which a finding is only `VERIFIED_FIXED` after executed proof at the exact fix commit and only `CLOSED` with
|
|
19
|
+
accountable human sign-off, and an explicit `GO / CONDITIONAL_GO / NO_GO / UNKNOWN` release gate
|
|
20
|
+
([docs/vibe-code-assurance.md](docs/vibe-code-assurance.md)).
|
|
16
21
|
|
|
17
22
|
Everything is written to one versioned JSON file. The report, blueprint and dashboard are renderings of it.
|
|
18
23
|
|
|
19
|
-
> Status: **early release** (`0.
|
|
24
|
+
> Status: **early release** (`0.3.0`, pre-1.0, results schema `0.1.0`, rubric `pc-rubric-0.2`). Install with
|
|
20
25
|
> `npm install --global @enwithai/product-check`, or build from source as in the demo below.
|
|
21
26
|
|
|
22
27
|
## What it will not pretend
|
|
23
28
|
|
|
24
29
|
Untested is never passed. Product Check reports `BLOCKED` or `NOT_TESTED` (with the reason) for anything it could not
|
|
25
30
|
check, lowers coverage accordingly, and withholds a score when evidence is thin. It is **not** a penetration test, a
|
|
26
|
-
compliance certification or a guarantee of production readiness.
|
|
27
|
-
never submits forms and never sends anything anywhere except passive GET requests to an origin you authorize.
|
|
31
|
+
compliance certification or a guarantee of production readiness. The scan never runs your repository's code, never
|
|
32
|
+
logs in, never submits forms and never sends anything anywhere except passive GET requests to an origin you authorize.
|
|
33
|
+
Only the opt-in `proof` and `verify` commands run commands, and only those you declare in a policy, with
|
|
34
|
+
`--allow-exec` on that invocation.
|
|
28
35
|
|
|
29
36
|
## Two-minute demo
|
|
30
37
|
|
|
31
|
-
Requires Node.js 22, 24 or ≥ 26, and git. No account, no network,
|
|
38
|
+
Requires Node.js 22, 24 or ≥ 26, and git. No account, no network, one runtime dependency (`yaml`, for OpenSLO and alert-rule files).
|
|
32
39
|
|
|
33
40
|
```bash
|
|
34
41
|
git clone https://github.com/enwithai/product-check.git && cd product-check
|
|
@@ -64,7 +71,7 @@ Verdict: READY_WITH_CONDITIONS at CT-T2 ("CONDITIONAL")
|
|
|
64
71
|
Customer 100/100 (coverage 92%, confidence HIGH)
|
|
65
72
|
Technology 94/100 (coverage 100%, confidence MEDIUM)
|
|
66
73
|
Trust 97/100 (coverage 83%, confidence HIGH)
|
|
67
|
-
Retest vs PCR-20261014T100000Z-
|
|
74
|
+
Retest vs PCR-20261014T100000Z-D74DF2: 35 verified fixed, 0 new, 0 reopened, 0 regressed control(s)
|
|
68
75
|
```
|
|
69
76
|
|
|
70
77
|
The two remaining P2s (no error monitoring, no AI evaluation suite) are real gaps in the fixed demo app. A
|
|
@@ -94,8 +101,9 @@ npm audit --json > npm-audit.json
|
|
|
94
101
|
product-check run --repo . --test-results junit.xml --npm-audit npm-audit.json --stage PRE_LAUNCH
|
|
95
102
|
```
|
|
96
103
|
|
|
97
|
-
Results go to `.product-check/`: `product-check-results.json`, `report.md`, `blueprint.md`, `work-packets/*.md
|
|
98
|
-
`dashboard.html` (open it directly; no server)
|
|
104
|
+
Results go to `.product-check/`: `product-check-results.json`, `report.md`, `blueprint.md`, `work-packets/*.md`,
|
|
105
|
+
`dashboard.html` (open it directly; no server) and, when the repository was read, an as-built spec
|
|
106
|
+
(`specs/000-as-built/spec.md`, `as-built-spec.json`) reverse-engineered from its structure. After changing things, run
|
|
99
107
|
`product-check retest --baseline .product-check/product-check-results.json …` with the same inputs.
|
|
100
108
|
|
|
101
109
|
No deployed URL you can scan? Export a HAR file from your browser's DevTools on your app and pass `--har app.har`.
|
|
@@ -107,15 +115,20 @@ authorization or tenant-isolation tests) to the controls Product Check cannot te
|
|
|
107
115
|
|
|
108
116
|
## Commands
|
|
109
117
|
|
|
110
|
-
| Command
|
|
111
|
-
|
|
|
112
|
-
| `run`
|
|
113
|
-
| `retest --baseline <file>`
|
|
114
|
-
| `scan`
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
118
|
+
| Command | What it does |
|
|
119
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `run` | Scan → Test → Validate → Blueprint; writes results and renderings |
|
|
121
|
+
| `retest --baseline <file>` | `run` plus comparison: verified fixes, reopened and new findings, regressions, trends |
|
|
122
|
+
| `scan` | Discovery only: which points of presence exist and what could be checked (no requests, no checks) |
|
|
123
|
+
| `spec` | Reverse-engineers an as-built spec (Spec Kit `spec.md` + JSON) from the repository's structure |
|
|
124
|
+
| `init` | Writes an editable scan contract |
|
|
125
|
+
| `validate <file>…` | Schema validation; for results also referential integrity and recomputation of scores and verdict |
|
|
126
|
+
| `report`, `blueprint`, `dashboard` | Re-render from a results file |
|
|
127
|
+
| `controls`, `schema [name]` | The control catalog and the JSON Schemas |
|
|
128
|
+
| `register`, `review`, `proof` | Closure register; layered change review (PR diff); author-side proof (runs declared checks) |
|
|
129
|
+
| `fix`, `verify`, `approve`, `close` | Record a fix; verify it at the exact commit; signed human approval; verified closure |
|
|
130
|
+
| `accept-risk`, `reject`, `learn` | Accepted risk with expiry; false positive; regression corpus |
|
|
131
|
+
| `fix-packets`, `gate`, `assure` | Agent-ready fix packets; release gate; all assurance reports and `dashboard/index.html` |
|
|
119
132
|
|
|
120
133
|
Exit codes: `0` ok · `1` internal error · `2` usage · `3` input · `4` safety refusal · `5` invalid artifact ·
|
|
121
134
|
`6` `--fail-on` matched (for CI gating, for example `--fail-on NOT_READY`). Full reference: [docs/cli.md](docs/cli.md).
|
|
@@ -134,7 +147,8 @@ Exit codes: `0` ok · `1` internal error · `2` usage · `3` input · `4` safety
|
|
|
134
147
|
|
|
135
148
|
## Privacy and security
|
|
136
149
|
|
|
137
|
-
No telemetry, account
|
|
150
|
+
No telemetry, account or model call. The scan starts no process: the repository is read, never executed (the opt-in
|
|
151
|
+
`proof`/`verify` commands are the exception, see [vibe-code assurance](docs/vibe-code-assurance.md#execution-model-a-deliberate-change-opt-in-only)); git-ignored files (such
|
|
138
152
|
as your local `.env`) are listed but never read. Evidence keeps short redacted excerpts, never file or response
|
|
139
153
|
contents; response headers are filtered to a security-relevant allowlist with cookie values removed. The dashboard is
|
|
140
154
|
a single file with a hash-pinned script and a `default-src 'none'` Content-Security-Policy. See
|
|
@@ -145,7 +159,8 @@ a single file with a hash-pinned script and a `default-src 'none'` Content-Secur
|
|
|
145
159
|
[Philosophy](docs/philosophy.md) · [Architecture](docs/architecture.md) ·
|
|
146
160
|
[Assessment model](docs/assessment-model.md) · [Scoring and gates](docs/scoring-and-gates.md) ·
|
|
147
161
|
[Lifecycle tiers](docs/lifecycle-tiers.md) · [Safety and authorization](docs/safety-and-authorization.md) ·
|
|
148
|
-
[Interoperability](docs/interoperability.md) · [
|
|
162
|
+
[Interoperability](docs/interoperability.md) · [Vibe-code assurance](docs/vibe-code-assurance.md) ·
|
|
163
|
+
[v3.2 implementation audit](IMPLEMENTATION_AUDIT_v3.2.md) · [CLI](docs/cli.md) · [Threat model](docs/threat-model.md) ·
|
|
149
164
|
[Branding and attribution](docs/branding-and-attribution.md) · [Implementation status](IMPLEMENTATION_STATUS.md) ·
|
|
150
165
|
[Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Governance](GOVERNANCE.md)
|
|
151
166
|
|
package/SECURITY.md
CHANGED
|
@@ -12,7 +12,11 @@ minimal **synthetic** reproduction. Never send real results, HAR files or reposi
|
|
|
12
12
|
|
|
13
13
|
## Security properties Product Check is designed to keep
|
|
14
14
|
|
|
15
|
-
-
|
|
15
|
+
- The scan (`run`, `retest`, `scan`, `spec`) never executes repository code or starts processes, and nothing uses
|
|
16
|
+
`eval`/dynamic imports. Only `proof` and `verify` (CT3 v3.2) can run commands: those the operator declares in a
|
|
17
|
+
policy, fix record or regression corpus, as argv without a shell, and only with `--allow-exec` on that invocation;
|
|
18
|
+
they then run with your permissions (see docs/safety-and-authorization.md). Read-only git plumbing runs with hooks,
|
|
19
|
+
fsmonitor, external diff/textconv drivers, pagers and system config disabled.
|
|
16
20
|
- It never reads git-ignored files, and stores only short redacted excerpts and checksums as evidence.
|
|
17
21
|
- It sends network requests only when explicitly authorized, only passive GETs to fixed paths on one origin, with no
|
|
18
22
|
cookies, credentials or bodies, a request budget and timeouts. It never follows redirects to other origins.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MIT */
|
|
2
|
+
/* SPDX-FileCopyrightText: 2026 EnWithAI Technologies */
|
|
3
|
+
/* Assurance dashboard additions (closure funnel, phase strip, decision hero). Inlined after dashboard.css. */
|
|
4
|
+
.funnel {
|
|
5
|
+
display: grid;
|
|
6
|
+
gap: 6px;
|
|
7
|
+
}
|
|
8
|
+
.funnel .step {
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: minmax(110px, 180px) 1fr 48px;
|
|
11
|
+
gap: 8px;
|
|
12
|
+
align-items: center;
|
|
13
|
+
}
|
|
14
|
+
.funnel .bar {
|
|
15
|
+
height: 18px;
|
|
16
|
+
border-radius: 4px;
|
|
17
|
+
background: var(--grid);
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
.funnel .bar > span {
|
|
21
|
+
display: block;
|
|
22
|
+
height: 100%;
|
|
23
|
+
background: var(--s-customer);
|
|
24
|
+
}
|
|
25
|
+
.funnel .step.done .bar > span {
|
|
26
|
+
background: var(--good);
|
|
27
|
+
}
|
|
28
|
+
.funnel .step.side .bar > span {
|
|
29
|
+
background: var(--unknown);
|
|
30
|
+
}
|
|
31
|
+
.phases {
|
|
32
|
+
display: grid;
|
|
33
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
34
|
+
gap: 8px;
|
|
35
|
+
}
|
|
36
|
+
.phases .card h4 {
|
|
37
|
+
margin: 0 0 4px;
|
|
38
|
+
font-size: 0.9rem;
|
|
39
|
+
}
|
|
40
|
+
.decision {
|
|
41
|
+
font-size: 2.2rem;
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
line-height: 1.1;
|
|
44
|
+
}
|
|
45
|
+
.insufficient {
|
|
46
|
+
border: 2px dashed var(--unknown);
|
|
47
|
+
padding: 8px 12px;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
margin: 8px 0;
|
|
50
|
+
}
|
|
51
|
+
pre.export {
|
|
52
|
+
max-height: 360px;
|
|
53
|
+
overflow: auto;
|
|
54
|
+
white-space: pre;
|
|
55
|
+
}
|
|
56
|
+
button.copy {
|
|
57
|
+
margin: 4px 0;
|
|
58
|
+
}
|
|
59
|
+
.decision.tone-good {
|
|
60
|
+
color: var(--good);
|
|
61
|
+
}
|
|
62
|
+
.decision.tone-warning {
|
|
63
|
+
color: var(--ink);
|
|
64
|
+
text-decoration: underline wavy var(--warning);
|
|
65
|
+
}
|
|
66
|
+
.decision.tone-critical {
|
|
67
|
+
color: var(--critical);
|
|
68
|
+
}
|
|
69
|
+
.decision.tone-unknown {
|
|
70
|
+
color: var(--unknown);
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 EnWithAI Technologies
|
|
3
|
+
// Progressive enhancement for the assurance dashboard: copy buttons for the agent export and Mermaid sources.
|
|
4
|
+
// Every figure is server-rendered from the JSON artifacts; without JavaScript everything is still readable.
|
|
5
|
+
(function () {
|
|
6
|
+
Array.prototype.slice.call(document.querySelectorAll("[data-copy]")).forEach(function (btn) {
|
|
7
|
+
var target = document.getElementById(btn.getAttribute("data-copy"));
|
|
8
|
+
if (!target || !window.navigator || !window.navigator.clipboard) return;
|
|
9
|
+
btn.hidden = false;
|
|
10
|
+
btn.addEventListener("click", function () {
|
|
11
|
+
window.navigator.clipboard.writeText(target.textContent || "").then(
|
|
12
|
+
function () {
|
|
13
|
+
btn.textContent = "Copied";
|
|
14
|
+
},
|
|
15
|
+
function () {
|
|
16
|
+
btn.textContent = "Copy failed";
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
})();
|
package/dashboard/dashboard.css
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChangeReview } from "./types.js";
|
|
2
|
+
/** Supported report versions, by tool. */
|
|
3
|
+
export declare const ADAPTER_VERSIONS: {
|
|
4
|
+
readonly "impact-check": readonly ["2.0"];
|
|
5
|
+
readonly "spec-check": readonly ["1.0"];
|
|
6
|
+
readonly "outcome-check": readonly ["1.0"];
|
|
7
|
+
};
|
|
8
|
+
/** Impact Check 2.0 → risk input. */
|
|
9
|
+
export declare function impactAdapter(data: unknown, source: string): NonNullable<ChangeReview["risk"]["impact_check"]> & {
|
|
10
|
+
modules: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Spec Check 1.0 → requirements and gaps. */
|
|
13
|
+
export interface SpecInput {
|
|
14
|
+
requirements: {
|
|
15
|
+
id: string;
|
|
16
|
+
path: string;
|
|
17
|
+
line: number | null;
|
|
18
|
+
}[];
|
|
19
|
+
gaps: {
|
|
20
|
+
id: string;
|
|
21
|
+
severity: string;
|
|
22
|
+
summary: string;
|
|
23
|
+
path: string | null;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
export declare function specAdapter(data: unknown, source: string): SpecInput;
|
|
27
|
+
/** Outcome Check 1.0 → delivery signal. */
|
|
28
|
+
export interface OutcomeInput {
|
|
29
|
+
ci_first_pass_rate: number | null;
|
|
30
|
+
revert_rate: number | null;
|
|
31
|
+
}
|
|
32
|
+
export declare function outcomeAdapter(data: unknown, source: string): OutcomeInput;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 EnWithAI Technologies
|
|
3
|
+
/**
|
|
4
|
+
* Optional, versioned adapters to the sibling Engineering Commons tools. Product Check reads only documented
|
|
5
|
+
* fields of their JSON reports and never runs or bundles their engines (no dependency in either direction):
|
|
6
|
+
*
|
|
7
|
+
* Impact Check 2.0 → change-risk input (high-severity impact findings raise risk; affected modules)
|
|
8
|
+
* Spec Check 1.0 → requirement IDs and acceptance gaps for the spec/outcome lane and fix packets
|
|
9
|
+
* Outcome Check 1.0 → intent-to-delivery signal for the spec/outcome lane (never a whole-product score)
|
|
10
|
+
*
|
|
11
|
+
* An unsupported version is rejected with a clear message instead of being guessed at.
|
|
12
|
+
*/
|
|
13
|
+
import { inputError } from "../errors.js";
|
|
14
|
+
const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
15
|
+
/** Supported report versions, by tool. */
|
|
16
|
+
export const ADAPTER_VERSIONS = {
|
|
17
|
+
"impact-check": ["2.0"],
|
|
18
|
+
"spec-check": ["1.0"],
|
|
19
|
+
"outcome-check": ["1.0"],
|
|
20
|
+
};
|
|
21
|
+
/** Impact Check 2.0 → risk input. */
|
|
22
|
+
export function impactAdapter(data, source) {
|
|
23
|
+
if (!isObj(data) || data["schema_version"] !== "2.0" || !Array.isArray(data["findings"]))
|
|
24
|
+
throw inputError(`${source} is not an Impact Check report (supported: ${ADAPTER_VERSIONS["impact-check"].join(", ")}).`, "Produce it with `impact-check --format json`.");
|
|
25
|
+
const findings = data["findings"].filter(isObj);
|
|
26
|
+
const modules = Array.isArray(data["affected_modules"])
|
|
27
|
+
? data["affected_modules"]
|
|
28
|
+
.filter(isObj)
|
|
29
|
+
.map((m) => String(m["path"] ?? ""))
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
: [];
|
|
32
|
+
const rev = isObj(data["repo_revision"]) ? data["repo_revision"] : {};
|
|
33
|
+
return {
|
|
34
|
+
schema_version: "2.0",
|
|
35
|
+
high: findings.filter((f) => f["severity"] === "high" && f["status"] !== "unknown").length,
|
|
36
|
+
affected_modules: modules.length,
|
|
37
|
+
commit: typeof rev["commit"] === "string" ? rev["commit"] : null,
|
|
38
|
+
modules,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function specAdapter(data, source) {
|
|
42
|
+
if (!isObj(data) ||
|
|
43
|
+
data["schema_version"] !== "1.0" ||
|
|
44
|
+
!Array.isArray(data["requirements"]) ||
|
|
45
|
+
!Array.isArray(data["findings"]))
|
|
46
|
+
throw inputError(`${source} is not a Spec Check report (supported: ${ADAPTER_VERSIONS["spec-check"].join(", ")}).`, "Produce it with `spec-check check --format json`.");
|
|
47
|
+
return {
|
|
48
|
+
requirements: data["requirements"].filter(isObj).map((r) => ({
|
|
49
|
+
id: String(r["id"] ?? ""),
|
|
50
|
+
path: String(r["path"] ?? ""),
|
|
51
|
+
line: typeof r["line_start"] === "number" ? r["line_start"] : null,
|
|
52
|
+
})),
|
|
53
|
+
gaps: data["findings"]
|
|
54
|
+
.filter(isObj)
|
|
55
|
+
.filter((f) => f["severity"] === "high" || f["severity"] === "medium")
|
|
56
|
+
.map((f) => {
|
|
57
|
+
const ev = Array.isArray(f["evidence"]) ? f["evidence"].filter(isObj)[0] : undefined;
|
|
58
|
+
return {
|
|
59
|
+
id: String(f["id"] ?? ""),
|
|
60
|
+
severity: String(f["severity"]),
|
|
61
|
+
summary: String(f["summary"] ?? ""),
|
|
62
|
+
path: ev && typeof ev["path"] === "string" ? ev["path"] : null,
|
|
63
|
+
};
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function outcomeAdapter(data, source) {
|
|
68
|
+
if (!isObj(data) || data["schema_version"] !== "1.0" || !Array.isArray(data["metrics"]))
|
|
69
|
+
throw inputError(`${source} is not an Outcome Check report (supported: ${ADAPTER_VERSIONS["outcome-check"].join(", ")}).`, "Produce it with `outcome-check report --format json`.");
|
|
70
|
+
const val = (id) => {
|
|
71
|
+
const m = data["metrics"].find((x) => isObj(x) && x["id"] === id);
|
|
72
|
+
const all = isObj(m) && isObj(m["segments"]) ? m["segments"]["all"] : null;
|
|
73
|
+
return isObj(all) && typeof all["value"] === "number" ? all["value"] : null;
|
|
74
|
+
};
|
|
75
|
+
return { ci_first_pass_rate: val("ci_first_pass_rate"), revert_rate: val("revert_rate") };
|
|
76
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Approval, type ApprovalEvaluation, type AssurancePolicy } from "./types.js";
|
|
2
|
+
/** Key ID: first 16 hex chars of SHA-256 over the DER-encoded SPKI public key. */
|
|
3
|
+
export declare function keyId(publicKeyPem: string): string;
|
|
4
|
+
/** Generates an Ed25519 key pair (PEM). */
|
|
5
|
+
export declare function generateApproverKeys(): {
|
|
6
|
+
privateKey: string;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
};
|
|
9
|
+
type Unsigned = Omit<Approval, "approval_id" | "signature">;
|
|
10
|
+
/** Creates and signs an approval with a private key (PEM). */
|
|
11
|
+
export declare function signApproval(input: Omit<Unsigned, "schema_version" | "kind">, privateKeyPem: string): Approval;
|
|
12
|
+
/** Records an unsigned claim (e.g. a name typed with --approver). It never counts as approval. */
|
|
13
|
+
export declare function unsignedClaim(input: Omit<Unsigned, "schema_version" | "kind">): Approval;
|
|
14
|
+
/** Evaluates one approval for a required scope and commit. */
|
|
15
|
+
export declare function evaluateApproval(a: Approval, policy: AssurancePolicy, want: {
|
|
16
|
+
scopeType: Approval["scope"]["type"];
|
|
17
|
+
ref: string;
|
|
18
|
+
sha: string | null;
|
|
19
|
+
excluded: string[];
|
|
20
|
+
}): ApprovalEvaluation;
|
|
21
|
+
/** Evaluates all approvals for a scope; counts distinct valid human approvers and checks domain owners. */
|
|
22
|
+
export declare function tallyApprovals(approvals: Approval[], policy: AssurancePolicy, want: {
|
|
23
|
+
scopeType: Approval["scope"]["type"];
|
|
24
|
+
ref: string;
|
|
25
|
+
sha: string | null;
|
|
26
|
+
excluded: string[];
|
|
27
|
+
}, required: number, owners?: string[]): {
|
|
28
|
+
evaluations: ApprovalEvaluation[];
|
|
29
|
+
validApprovers: string[];
|
|
30
|
+
satisfied: boolean;
|
|
31
|
+
missingOwners: string[];
|
|
32
|
+
stale: number;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 EnWithAI Technologies
|
|
3
|
+
/**
|
|
4
|
+
* Accountable human approvals. An approval is a signed statement (Ed25519 over canonical JSON) by an approver the
|
|
5
|
+
* policy lists with a public key. A name passed on the command line proves nothing: only a signature that verifies
|
|
6
|
+
* against a policy key counts. Bots can sign records but never fill a human approval slot; an approval is bound
|
|
7
|
+
* to the exact commit it reviewed and becomes STALE when the commit changes; the implementer of a fix cannot
|
|
8
|
+
* approve its closure.
|
|
9
|
+
*
|
|
10
|
+
* Limit: Product Check proves possession of a registered key, not that a person pressed the key. Key custody is the
|
|
11
|
+
* operator's control (see docs/vibe-code-assurance.md).
|
|
12
|
+
*/
|
|
13
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify, } from "node:crypto";
|
|
14
|
+
import { usageError } from "../errors.js";
|
|
15
|
+
import { stableId } from "../util/hash.js";
|
|
16
|
+
import { ASSURANCE_SCHEMA_VERSION, } from "./types.js";
|
|
17
|
+
import { canonicalJson } from "./util.js";
|
|
18
|
+
/** Key ID: first 16 hex chars of SHA-256 over the DER-encoded SPKI public key. */
|
|
19
|
+
export function keyId(publicKeyPem) {
|
|
20
|
+
const der = createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
|
|
21
|
+
return createHash("sha256").update(der).digest("hex").slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
/** Generates an Ed25519 key pair (PEM). */
|
|
24
|
+
export function generateApproverKeys() {
|
|
25
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
26
|
+
return {
|
|
27
|
+
privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
28
|
+
publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const payloadOf = (a) => canonicalJson({
|
|
32
|
+
schema_version: a.schema_version,
|
|
33
|
+
kind: a.kind,
|
|
34
|
+
approver: a.approver,
|
|
35
|
+
scope: a.scope,
|
|
36
|
+
reviewed_sha: a.reviewed_sha,
|
|
37
|
+
decision: a.decision,
|
|
38
|
+
comment: a.comment,
|
|
39
|
+
created_at: a.created_at,
|
|
40
|
+
});
|
|
41
|
+
/** Creates and signs an approval with a private key (PEM). */
|
|
42
|
+
export function signApproval(input, privateKeyPem) {
|
|
43
|
+
let key;
|
|
44
|
+
try {
|
|
45
|
+
key = createPrivateKey(privateKeyPem);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw usageError("The approver key is not a readable private key (PKCS#8 PEM).");
|
|
49
|
+
}
|
|
50
|
+
if (key.asymmetricKeyType !== "ed25519")
|
|
51
|
+
throw usageError("Approver keys must be Ed25519.");
|
|
52
|
+
const unsigned = { schema_version: ASSURANCE_SCHEMA_VERSION, kind: "approval", ...input };
|
|
53
|
+
const payload = payloadOf(unsigned);
|
|
54
|
+
const value = sign(null, Buffer.from(payload), key).toString("base64");
|
|
55
|
+
const pub = createPublicKey(key).export({ type: "spki", format: "pem" }).toString();
|
|
56
|
+
return {
|
|
57
|
+
...unsigned,
|
|
58
|
+
approval_id: stableId("AP", [payload], 12),
|
|
59
|
+
signature: { algorithm: "ed25519", key_id: keyId(pub), value },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Records an unsigned claim (e.g. a name typed with --approver). It never counts as approval. */
|
|
63
|
+
export function unsignedClaim(input) {
|
|
64
|
+
const unsigned = { schema_version: ASSURANCE_SCHEMA_VERSION, kind: "approval", ...input };
|
|
65
|
+
return { ...unsigned, approval_id: stableId("AP", [payloadOf(unsigned), "unsigned"], 12), signature: null };
|
|
66
|
+
}
|
|
67
|
+
/** Verifies the approval's signature against the policy; returns null when valid, else the failing status. */
|
|
68
|
+
function signatureStatus(a, policy) {
|
|
69
|
+
const approver = policy.approvers.find((p) => p.id === a.approver.id);
|
|
70
|
+
if (!approver)
|
|
71
|
+
return "UNKNOWN_APPROVER";
|
|
72
|
+
if (!a.signature)
|
|
73
|
+
return "UNSIGNED";
|
|
74
|
+
if (a.signature.key_id !== keyId(approver.public_key))
|
|
75
|
+
return "INVALID_SIGNATURE";
|
|
76
|
+
try {
|
|
77
|
+
const ok = verify(null, Buffer.from(payloadOf(a)), createPublicKey(approver.public_key), Buffer.from(a.signature.value, "base64"));
|
|
78
|
+
if (!ok)
|
|
79
|
+
return "INVALID_SIGNATURE";
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "INVALID_SIGNATURE";
|
|
83
|
+
}
|
|
84
|
+
if (a.approval_id !== stableId("AP", [payloadOf(a)], 12))
|
|
85
|
+
return "INVALID_SIGNATURE";
|
|
86
|
+
if (approver.type !== "human" || a.approver.type !== "human")
|
|
87
|
+
return "NOT_HUMAN";
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/** Evaluates one approval for a required scope and commit. */
|
|
91
|
+
export function evaluateApproval(a, policy, want) {
|
|
92
|
+
const res = (status, reason) => ({
|
|
93
|
+
approval_id: a.approval_id,
|
|
94
|
+
approver_id: a.approver.id,
|
|
95
|
+
status,
|
|
96
|
+
reason,
|
|
97
|
+
});
|
|
98
|
+
const sig = signatureStatus(a, policy);
|
|
99
|
+
if (sig === "UNKNOWN_APPROVER")
|
|
100
|
+
return res(sig, `${a.approver.id} is not an approver in the policy.`);
|
|
101
|
+
if (sig === "UNSIGNED")
|
|
102
|
+
return res(sig, "Unsigned claim: a typed name is not proof of approval.");
|
|
103
|
+
if (sig === "INVALID_SIGNATURE")
|
|
104
|
+
return res(sig, "The signature does not verify against the approver's policy key (edited or forged).");
|
|
105
|
+
if (sig === "NOT_HUMAN")
|
|
106
|
+
return res(sig, `${a.approver.id} is a bot; automated reviewers cannot fill a human approval slot.`);
|
|
107
|
+
if (a.scope.type !== want.scopeType || a.scope.ref !== want.ref)
|
|
108
|
+
return res("WRONG_SCOPE", `Approval covers ${a.scope.type}:${a.scope.ref}, not ${want.scopeType}:${want.ref}.`);
|
|
109
|
+
if (want.excluded.includes(a.approver.id))
|
|
110
|
+
return res("SELF_APPROVAL", `${a.approver.id} implemented this change and cannot approve it.`);
|
|
111
|
+
if (a.decision !== "approve")
|
|
112
|
+
return res("REJECTED", `${a.approver.id} rejected: ${a.comment || "(no comment)"}`);
|
|
113
|
+
if (want.sha !== null && a.reviewed_sha !== want.sha)
|
|
114
|
+
return res("STALE", `Reviewed ${a.reviewed_sha.slice(0, 12)}, but the current commit is ${want.sha.slice(0, 12)}; re-approval required.`);
|
|
115
|
+
return res("VALID", `Signed by ${a.approver.id} for ${a.reviewed_sha.slice(0, 12)}.`);
|
|
116
|
+
}
|
|
117
|
+
/** Evaluates all approvals for a scope; counts distinct valid human approvers and checks domain owners. */
|
|
118
|
+
export function tallyApprovals(approvals, policy, want, required, owners = []) {
|
|
119
|
+
const relevant = approvals.filter((a) => a.scope.type === want.scopeType && a.scope.ref === want.ref);
|
|
120
|
+
const evaluations = relevant.map((a) => evaluateApproval(a, policy, want));
|
|
121
|
+
const rejected = evaluations.some((e) => e.status === "REJECTED");
|
|
122
|
+
const validApprovers = [
|
|
123
|
+
...new Set(evaluations.filter((e) => e.status === "VALID").map((e) => e.approver_id)),
|
|
124
|
+
].sort();
|
|
125
|
+
const missingOwners = owners.filter((o) => !validApprovers.includes(o));
|
|
126
|
+
return {
|
|
127
|
+
evaluations,
|
|
128
|
+
validApprovers,
|
|
129
|
+
satisfied: !rejected && validApprovers.length >= required && missingOwners.length === 0,
|
|
130
|
+
missingOwners,
|
|
131
|
+
stale: evaluations.filter((e) => e.status === "STALE").length,
|
|
132
|
+
};
|
|
133
|
+
}
|