@enwithai/product-check 0.1.0-alpha.0 → 0.2.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 +2 -2
- package/CHANGELOG.md +45 -3
- package/NOTICE.md +5 -3
- package/README.md +8 -6
- package/dashboard/dashboard.css +6 -0
- package/dist/blueprint/build.d.ts +3 -1
- package/dist/blueprint/build.js +41 -5
- package/dist/cli/run.js +96 -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/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.js +17 -0
- 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 +1 -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 +5 -3
- package/schemas/as-built-spec.schema.json +356 -0
- package/schemas/blueprint.schema.json +3 -0
- package/schemas/scan-contract.schema.json +20 -0
- package/schemas/scorecard.schema.json +1 -1
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
|
package/CHANGELOG.md
CHANGED
|
@@ -6,11 +6,53 @@ versions may break the results schema, which carries its own `schema_version`, a
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 0.2.0 — 2026-10-28
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Commons integration and the first CT-T4 controls. Rubric `pc-rubric-0.2` (51 controls; scoring rules unchanged);
|
|
12
|
+
results schema `0.1.0`, additive. Results and baselines written by 0.1.0 still validate and load.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
- CT-T4 operational imports ([docs/operational-imports.md](docs/operational-imports.md)): `--openslo` (OpenSLO v1
|
|
15
|
+
YAML/JSON, repeatable), `--alert-rules` (Prometheus/Mimir rule files, `PrometheusRule`, `/api/v1/rules` JSON,
|
|
16
|
+
repeatable) and `--service-graph` (Jaeger `/api/dependencies`, `traces_service_graph_request_total` query result).
|
|
17
|
+
New controls PC-T-18 (SLOs complete), PC-T-19 (every SLO alerted on), PC-T-20 (declared dependencies observed at
|
|
18
|
+
runtime, with `--architecture-check`); PC-T-14 is now evaluated from these files; the observability PoP is
|
|
19
|
+
AVAILABLE when they are given.
|
|
20
|
+
- First runtime dependency: `yaml` 2.9.1 (ISC, pinned), used only through `src/parse/yaml.ts` (core schema, unique
|
|
21
|
+
keys, at most 100 aliases, 10 MiB per file).
|
|
22
|
+
- `--architecture-check <report.json>` (contract `imports.architecture_check`) imports an Architecture Check report
|
|
23
|
+
(schema 1.0 or 1.1, `analyze`/`check` output). New control **PC-T-17 "Architecture model matches the code"**
|
|
24
|
+
(technology, CT-T3, weight 1, P2): PASS with no verified drift of medium severity or above, PARTIAL with one
|
|
25
|
+
`architecture-drift` finding otherwise, NOT_TESTED without a report. Hypotheses and unknowns never count. Rules in
|
|
26
|
+
`docs/interoperability.md`.
|
|
27
|
+
- Rubric `pc-rubric-0.2` (48 controls; scoring rules unchanged). The scorecard schema accepts `pc-rubric-0.1` and
|
|
28
|
+
`pc-rubric-0.2`, and integrity recomputation keeps the stored version, so 0.1.0 results still validate and load as
|
|
29
|
+
retest baselines. Results schema stays `0.1.0`; `imports.architecture_check` is optional in the contract schema.
|
|
30
|
+
- `product-check spec`: reverse-engineers an as-built specification from a repository (pages, endpoints, data
|
|
31
|
+
entities, integrations, configuration names, auth boundaries, test titles) with evidence for every item. It writes
|
|
32
|
+
`as-built-spec.json` (new schema `as-built-spec.schema.json`) and a Spec Kit `specs/000-as-built/spec.md` that Spec
|
|
33
|
+
Check can lint. Intent that code cannot show is left as `[NEEDS CLARIFICATION]`. Source only; nothing executed.
|
|
34
|
+
- `run`/`retest` derive the as-built spec from the same snapshot. The blueprint embeds it as the new optional field
|
|
35
|
+
`as_is.as_built`, and `as-built-spec.json` plus `specs/000-as-built/spec.md` are written next to the report
|
|
36
|
+
(`blueprint --out-dir` re-renders them). Traceability now names the as-built requirement a finding touches
|
|
37
|
+
(`As-built FR-002; requirement PC-…`): pages, endpoints and entities match by file, integrations and configuration
|
|
38
|
+
only by exact line, and application URLs by path. Scores, verdicts and findings are unchanged.
|
|
39
|
+
- Schema: additive, still `0.1.0`. Existing contracts, observations and baselines remain valid. Results that carry
|
|
40
|
+
`as_is.as_built` do not validate with the schemas bundled in 0.1.0 and earlier.
|
|
41
|
+
- Dashboard: a sixth view, **As-built spec**, shows the discovered surface, the questions only a person can answer,
|
|
42
|
+
user stories, and every FR/NFR/SC/EC item with its certainty, source, evidence and linked findings (open or fixed),
|
|
43
|
+
with type, certainty and text filters. It is rendered from the results JSON like every other view.
|
|
44
|
+
- Demo: the baseline, retest and repository-only examples include the as-built spec, and
|
|
45
|
+
`examples/vibe-app-demo/output/as-built/` holds the standalone `spec` output. Both are checked against a fresh run.
|
|
46
|
+
|
|
47
|
+
## 0.1.0 — 2026-10-12
|
|
48
|
+
|
|
49
|
+
First release without a pre-release suffix, so `npm install @enwithai/product-check` and `npx @enwithai/product-check` work without a
|
|
50
|
+
tag. No functional changes from 0.1.0-alpha.0. Still pre-1.0: the report schema and command options may change in minor
|
|
51
|
+
versions.
|
|
52
|
+
|
|
53
|
+
## 0.1.0-alpha.0 — 2026-10-12 (published to npm)
|
|
54
|
+
|
|
55
|
+
First alpha. MIT licensed.
|
|
14
56
|
|
|
15
57
|
- `product-check run|retest|scan|init|validate|report|blueprint|dashboard|controls|schema`.
|
|
16
58
|
- Scan → Test → Validate → Blueprint → Dashboard → (advisory) Remediate → Retest, recorded per run.
|
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
|
@@ -16,8 +16,8 @@ Customer, Technology and Trust controls, and gives you:
|
|
|
16
16
|
|
|
17
17
|
Everything is written to one versioned JSON file. The report, blueprint and dashboard are renderings of it.
|
|
18
18
|
|
|
19
|
-
> Status: **
|
|
20
|
-
> `npm install --global @enwithai/product-check
|
|
19
|
+
> Status: **early release** (`0.2.0`, pre-1.0, results schema `0.1.0`, rubric `pc-rubric-0.2`). Install with
|
|
20
|
+
> `npm install --global @enwithai/product-check`, or build from source as in the demo below.
|
|
21
21
|
|
|
22
22
|
## What it will not pretend
|
|
23
23
|
|
|
@@ -28,7 +28,7 @@ never submits forms and never sends anything anywhere except passive GET request
|
|
|
28
28
|
|
|
29
29
|
## Two-minute demo
|
|
30
30
|
|
|
31
|
-
Requires Node.js 22, 24 or ≥ 26, and git. No account, no network,
|
|
31
|
+
Requires Node.js 22, 24 or ≥ 26, and git. No account, no network, one runtime dependency (`yaml`, for OpenSLO and alert-rule files).
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
git clone https://github.com/enwithai/product-check.git && cd product-check
|
|
@@ -64,7 +64,7 @@ Verdict: READY_WITH_CONDITIONS at CT-T2 ("CONDITIONAL")
|
|
|
64
64
|
Customer 100/100 (coverage 92%, confidence HIGH)
|
|
65
65
|
Technology 94/100 (coverage 100%, confidence MEDIUM)
|
|
66
66
|
Trust 97/100 (coverage 83%, confidence HIGH)
|
|
67
|
-
Retest vs PCR-20261014T100000Z-
|
|
67
|
+
Retest vs PCR-20261014T100000Z-D74DF2: 35 verified fixed, 0 new, 0 reopened, 0 regressed control(s)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
The two remaining P2s (no error monitoring, no AI evaluation suite) are real gaps in the fixed demo app. A
|
|
@@ -94,8 +94,9 @@ npm audit --json > npm-audit.json
|
|
|
94
94
|
product-check run --repo . --test-results junit.xml --npm-audit npm-audit.json --stage PRE_LAUNCH
|
|
95
95
|
```
|
|
96
96
|
|
|
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)
|
|
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) and, when the repository was read, an as-built spec
|
|
99
|
+
(`specs/000-as-built/spec.md`, `as-built-spec.json`) reverse-engineered from its structure. After changing things, run
|
|
99
100
|
`product-check retest --baseline .product-check/product-check-results.json …` with the same inputs.
|
|
100
101
|
|
|
101
102
|
No deployed URL you can scan? Export a HAR file from your browser's DevTools on your app and pass `--har app.har`.
|
|
@@ -112,6 +113,7 @@ authorization or tenant-isolation tests) to the controls Product Check cannot te
|
|
|
112
113
|
| `run` | Scan → Test → Validate → Blueprint; writes results and renderings |
|
|
113
114
|
| `retest --baseline <file>` | `run` plus comparison: verified fixes, reopened and new findings, regressions, trends |
|
|
114
115
|
| `scan` | Discovery only: which points of presence exist and what could be checked (no requests, no checks) |
|
|
116
|
+
| `spec` | Reverse-engineers an as-built spec (Spec Kit `spec.md` + JSON) from the repository's structure |
|
|
115
117
|
| `init` | Writes an editable scan contract |
|
|
116
118
|
| `validate <file>…` | Schema validation; for results also referential integrity and recomputation of scores and verdict |
|
|
117
119
|
| `report`, `blueprint`, `dashboard` | Re-render from a results file |
|
package/dashboard/dashboard.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { EvidenceCollector } from "../evidence/collector.js";
|
|
9
9
|
import type { ObservationSet } from "../inspectors/http/observe.js";
|
|
10
10
|
import type { RepoFacts } from "../inspectors/source/facts.js";
|
|
11
|
-
import type { Blueprint, Finding, ScanContract, Validation } from "../types.js";
|
|
11
|
+
import type { AsBuiltSpec, Blueprint, Finding, ScanContract, Validation } from "../types.js";
|
|
12
12
|
/** Inputs for blueprint construction. */
|
|
13
13
|
export interface BlueprintInput {
|
|
14
14
|
contract: ScanContract;
|
|
@@ -22,6 +22,8 @@ export interface BlueprintInput {
|
|
|
22
22
|
hasBaseline: boolean;
|
|
23
23
|
testsImported: boolean;
|
|
24
24
|
aiInScope: boolean;
|
|
25
|
+
/** The as-built spec derived from the same snapshot (null when source was not inspected). */
|
|
26
|
+
asBuilt: AsBuiltSpec | null;
|
|
25
27
|
}
|
|
26
28
|
/** Builds the blueprint and links findings to work packets (mutates finding.work_packet_id). */
|
|
27
29
|
export declare function buildBlueprint(input: BlueprintInput): Blueprint;
|
package/dist/blueprint/build.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SCHEMA_VERSION } from "../types.js";
|
|
2
2
|
import { control, CONTROLS, VERIFY_INSTRUCTION } from "../validation/catalog.js";
|
|
3
|
+
import { asBuiltBody } from "../spec/build.js";
|
|
3
4
|
import { regressionCandidate } from "./candidates.js";
|
|
4
5
|
const SEVERITY_ORDER = ["P0", "P1", "P2", "P3"];
|
|
5
6
|
const HORIZON_BY_SEVERITY = { P0: "NOW", P1: "NOW", P2: "NEXT", P3: "LATER" };
|
|
@@ -9,7 +10,7 @@ const STANDARD_CONSTRAINTS = [
|
|
|
9
10
|
"Do not disable, skip or delete tests or checks to make them pass.",
|
|
10
11
|
"Do not mark this packet done without a Product Check retest showing the control PASS with new evidence.",
|
|
11
12
|
];
|
|
12
|
-
/** As-is architecture components with evidence. */
|
|
13
|
+
/** As-is architecture components with evidence, plus the as-built spec when source was inspected. */
|
|
13
14
|
function asIs(input) {
|
|
14
15
|
const components = [];
|
|
15
16
|
const { repo, app, ev } = input;
|
|
@@ -141,7 +142,39 @@ function asIs(input) {
|
|
|
141
142
|
if (repo && repo.dbDeps.length > 0 && repo.migrations.length === 0)
|
|
142
143
|
uncertainties.push("A database is used but its schema is not versioned in the repository; the live schema was not inspected.");
|
|
143
144
|
uncertainties.push("Authenticated, multi-step and data-changing journeys were not exercised (no browser automation or credentialed access in this release).");
|
|
144
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
components: components.sort((a, b) => (a.id < b.id ? -1 : 1)),
|
|
147
|
+
uncertainties,
|
|
148
|
+
...(input.asBuilt ? { as_built: asBuiltBody(input.asBuilt) } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Maps a finding location to the as-built requirements it touches: a repository path (`file:line`) matches the
|
|
153
|
+
* surface items defined in that file; an application URL matches the page or endpoint with that path.
|
|
154
|
+
*/
|
|
155
|
+
function requirementsAt(spec, location) {
|
|
156
|
+
if (!spec)
|
|
157
|
+
return [];
|
|
158
|
+
let urlPath = null;
|
|
159
|
+
try {
|
|
160
|
+
urlPath = /^https?:\/\//.test(location) ? new URL(location).pathname : null;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
urlPath = null;
|
|
164
|
+
}
|
|
165
|
+
const line = /:(\d+)$/.exec(location)?.[1];
|
|
166
|
+
const file = location.replace(/:\d+$/, "");
|
|
167
|
+
// Pages, endpoints, entities and boundaries match by file; integrations and configuration names (which share
|
|
168
|
+
// manifests and helper files with unrelated code) only on the exact line.
|
|
169
|
+
const byFile = new Set(["page", "endpoint", "entity", "auth-boundary"]);
|
|
170
|
+
const surface = new Set(spec.surface
|
|
171
|
+
.filter((s) => urlPath !== null
|
|
172
|
+
? (s.kind === "page" || s.kind === "endpoint") && s.name === urlPath
|
|
173
|
+
: s.location.path === file &&
|
|
174
|
+
(byFile.has(s.kind) || (line !== undefined && s.location.line === Number(line))))
|
|
175
|
+
.filter((s) => s.kind !== "test")
|
|
176
|
+
.map((s) => s.id));
|
|
177
|
+
return spec.requirements.filter((r) => r.surface_ids.some((id) => surface.has(id))).map((r) => r.id);
|
|
145
178
|
}
|
|
146
179
|
/** Remediation verification state for a control's findings. */
|
|
147
180
|
function verification(findings, input, v) {
|
|
@@ -325,12 +358,15 @@ export function buildBlueprint(input) {
|
|
|
325
358
|
const journeys = input.contract.critical_journeys;
|
|
326
359
|
const traceability = input.findings.map((f) => {
|
|
327
360
|
const rem = remediations.find((r) => r.finding_ids.includes(f.id));
|
|
361
|
+
const asBuilt = requirementsAt(input.asBuilt, f.location);
|
|
328
362
|
return {
|
|
329
363
|
finding_id: f.id,
|
|
330
364
|
intent: input.contract.target.purpose,
|
|
331
|
-
journey_or_requirement:
|
|
332
|
-
? `
|
|
333
|
-
:
|
|
365
|
+
journey_or_requirement: asBuilt.length > 0
|
|
366
|
+
? `As-built ${asBuilt.join(", ")}; requirement ${f.control_id}: ${f.expected}`
|
|
367
|
+
: f.pillar === "customer" && journeys.length > 0
|
|
368
|
+
? `Journey: ${journeys[0] ?? ""}`
|
|
369
|
+
: `Requirement ${f.control_id}: ${f.expected}`,
|
|
334
370
|
observed_behavior: f.actual,
|
|
335
371
|
component: f.location,
|
|
336
372
|
evidence_ids: f.evidence_ids,
|
package/dist/cli/run.js
CHANGED
|
@@ -12,11 +12,15 @@ import { decidePermissions } from "../contract/safety.js";
|
|
|
12
12
|
import { buildPops, popCoverage } from "../discovery/pops.js";
|
|
13
13
|
import { snapshotRepo } from "../discovery/repo.js";
|
|
14
14
|
import { renderDashboard } from "../dashboard/render.js";
|
|
15
|
+
import { EvidenceCollector } from "../evidence/collector.js";
|
|
15
16
|
import { ExitCode, ProductCheckError, inputError, usageError } from "../errors.js";
|
|
16
17
|
import { deriveFacts } from "../inspectors/source/facts.js";
|
|
18
|
+
import { deriveSurface } from "../inspectors/source/surface.js";
|
|
17
19
|
import { renderBlueprint, renderPacket, renderReport } from "../reporting/markdown.js";
|
|
18
|
-
import {
|
|
20
|
+
import { renderAsBuiltSpec } from "../reporting/spec-markdown.js";
|
|
21
|
+
import { makeRunId, runAssessment } from "../run.js";
|
|
19
22
|
import { formatErrors, KIND_SCHEMA, schemaFiles, schemaText, validateAgainst } from "../schema/registry.js";
|
|
23
|
+
import { buildAsBuiltSpec, specFromResults } from "../spec/build.js";
|
|
20
24
|
import { sha256 } from "../util/hash.js";
|
|
21
25
|
import { relativize } from "../util/paths.js";
|
|
22
26
|
import { CONTROLS } from "../validation/catalog.js";
|
|
@@ -37,6 +41,9 @@ Usage:
|
|
|
37
41
|
Discovery only: what could be inspected (no requests, no checks)
|
|
38
42
|
product-check init [--repo <dir>] [--url <url>] [--stage <stage>] [--output <file>]
|
|
39
43
|
Write a scan contract to edit (default product-check.contract.json)
|
|
44
|
+
product-check spec --repo <dir> [--contract <file>] [--out-dir <dir>] [--format md|json|both]
|
|
45
|
+
Reverse-engineer an as-built spec (Spec Kit spec.md + JSON) from the
|
|
46
|
+
repository's structure; nothing is executed
|
|
40
47
|
product-check validate <file>... Validate artifacts against the schemas (+ integrity for results)
|
|
41
48
|
product-check report <results.json> Markdown report product-check blueprint <results.json>
|
|
42
49
|
product-check dashboard <results.json> [--output dashboard.html]
|
|
@@ -55,7 +62,11 @@ Inputs (run/retest):
|
|
|
55
62
|
--npm-audit <file> Output of \`npm audit --json\`
|
|
56
63
|
--har <file> Browser HAR export (offline application evidence)
|
|
57
64
|
--observations <file> Recorded observations (from --record-observations)
|
|
58
|
-
--spec-check/--impact-check/--outcome-check <file>
|
|
65
|
+
--spec-check/--impact-check/--outcome-check/--architecture-check <file>
|
|
66
|
+
Optional Engineering Commons reports
|
|
67
|
+
--openslo <file> OpenSLO v1 YAML/JSON (SLO, SLI, AlertPolicy; repeatable)
|
|
68
|
+
--alert-rules <file> Prometheus/Mimir rules, PrometheusRule, or /api/v1/rules JSON (repeatable)
|
|
69
|
+
--service-graph <file> Jaeger /api/dependencies or traces_service_graph_request_total query JSON
|
|
59
70
|
--baseline <file> Previous results to compare with (retest)
|
|
60
71
|
Output:
|
|
61
72
|
--out-dir <dir> Default .product-check --json Print results JSON to stdout, write nothing
|
|
@@ -130,6 +141,10 @@ const OPTIONS = {
|
|
|
130
141
|
"spec-check": { type: "string" },
|
|
131
142
|
"impact-check": { type: "string" },
|
|
132
143
|
"outcome-check": { type: "string" },
|
|
144
|
+
"architecture-check": { type: "string" },
|
|
145
|
+
openslo: { type: "string", multiple: true },
|
|
146
|
+
"alert-rules": { type: "string", multiple: true },
|
|
147
|
+
"service-graph": { type: "string" },
|
|
133
148
|
baseline: { type: "string" },
|
|
134
149
|
"out-dir": { type: "string" },
|
|
135
150
|
output: { type: "string" },
|
|
@@ -173,6 +188,10 @@ function buildContract(values, io) {
|
|
|
173
188
|
specCheck: values["spec-check"],
|
|
174
189
|
impactCheck: values["impact-check"],
|
|
175
190
|
outcomeCheck: values["outcome-check"],
|
|
191
|
+
architectureCheck: values["architecture-check"],
|
|
192
|
+
openslo: values.openslo,
|
|
193
|
+
alertRules: values["alert-rules"],
|
|
194
|
+
serviceGraph: values["service-graph"],
|
|
176
195
|
}, io.cwd, today);
|
|
177
196
|
if (values["authorized-by"] !== undefined && contract.authorization.attestation)
|
|
178
197
|
contract.authorization.attestation.authorized_by = values["authorized-by"];
|
|
@@ -181,6 +200,15 @@ function buildContract(values, io) {
|
|
|
181
200
|
}
|
|
182
201
|
return { contract, fromFile };
|
|
183
202
|
}
|
|
203
|
+
/** Writes the as-built spec (JSON + Spec Kit Markdown) carried by a results document, if any. */
|
|
204
|
+
function writeSpec(results, dir) {
|
|
205
|
+
const spec = specFromResults(results);
|
|
206
|
+
if (!spec)
|
|
207
|
+
return [];
|
|
208
|
+
writeFile(join(dir, "as-built-spec.json"), `${JSON.stringify(spec, null, 2)}\n`);
|
|
209
|
+
writeFile(join(dir, "specs", "000-as-built", "spec.md"), renderAsBuiltSpec(spec));
|
|
210
|
+
return ["as-built-spec.json", "specs/000-as-built/spec.md"];
|
|
211
|
+
}
|
|
184
212
|
/** Writes all renderings of a results document into a directory. */
|
|
185
213
|
function writeOutputs(results, dir, branding, dashboard) {
|
|
186
214
|
const written = [];
|
|
@@ -193,6 +221,7 @@ function writeOutputs(results, dir, branding, dashboard) {
|
|
|
193
221
|
put("blueprint.md", renderBlueprint(results, { branding }));
|
|
194
222
|
for (const p of results.blueprint.work_packets)
|
|
195
223
|
put(`work-packets/${p.id}.md`, renderPacket(results, p));
|
|
224
|
+
written.push(...writeSpec(results, dir));
|
|
196
225
|
if (dashboard)
|
|
197
226
|
put("dashboard.html", renderDashboard(results, { branding }));
|
|
198
227
|
return written;
|
|
@@ -289,6 +318,9 @@ function commandScan(values, io) {
|
|
|
289
318
|
auditImported: contract.imports.npm_audit !== null,
|
|
290
319
|
specImported: contract.imports.spec_check !== null,
|
|
291
320
|
outcomeImported: contract.imports.outcome_check !== null,
|
|
321
|
+
operationalImported: contract.imports.openslo.length > 0 ||
|
|
322
|
+
contract.imports.alert_rules.length > 0 ||
|
|
323
|
+
contract.imports.service_graph !== null,
|
|
292
324
|
aiInScope: contract.product.uses_ai === true || (repo?.aiDeps.length ?? 0) > 0,
|
|
293
325
|
tier,
|
|
294
326
|
now: (io.now?.() ?? new Date()).toISOString(),
|
|
@@ -319,6 +351,60 @@ function commandScan(values, io) {
|
|
|
319
351
|
io.stdout(lines.join("\n"));
|
|
320
352
|
return ExitCode.Ok;
|
|
321
353
|
}
|
|
354
|
+
/** spec: reverse-engineer an as-built specification from the repository (source only, no requests). */
|
|
355
|
+
function commandSpec(values, io) {
|
|
356
|
+
const { contract } = buildContract(values, io);
|
|
357
|
+
if (!contract.target.repository)
|
|
358
|
+
throw usageError("spec reads a repository: pass --repo <dir>.", "Example: product-check spec --repo .");
|
|
359
|
+
const format = values.format ?? "both";
|
|
360
|
+
if (!["md", "json", "both"].includes(format))
|
|
361
|
+
throw usageError("--format must be md, json or both.");
|
|
362
|
+
const repoPath = contract.target.repository.path;
|
|
363
|
+
const facts = deriveFacts(snapshotRepo(repoPath, contract.constraints.max_files, contract.target.name));
|
|
364
|
+
const started = io.now?.() ?? new Date();
|
|
365
|
+
const now = started.toISOString();
|
|
366
|
+
const ev = new EvidenceCollector(makeRunId(started, relativize(contract, io.cwd)), now);
|
|
367
|
+
const spec = buildAsBuiltSpec({
|
|
368
|
+
contract,
|
|
369
|
+
facts,
|
|
370
|
+
surface: deriveSurface(facts),
|
|
371
|
+
ev,
|
|
372
|
+
repository: display(io.cwd, repoPath),
|
|
373
|
+
generatedAt: now,
|
|
374
|
+
});
|
|
375
|
+
if (values.json) {
|
|
376
|
+
io.stdout(`${JSON.stringify(spec, null, 2)}\n`);
|
|
377
|
+
return ExitCode.Ok;
|
|
378
|
+
}
|
|
379
|
+
const outDir = isAbsolute(values["out-dir"] ?? "")
|
|
380
|
+
? values["out-dir"]
|
|
381
|
+
: resolve(io.cwd, values["out-dir"] ?? ".product-check");
|
|
382
|
+
const written = [];
|
|
383
|
+
if (format !== "md") {
|
|
384
|
+
writeFile(join(outDir, "as-built-spec.json"), `${JSON.stringify(spec, null, 2)}\n`);
|
|
385
|
+
written.push("as-built-spec.json");
|
|
386
|
+
}
|
|
387
|
+
if (format !== "json") {
|
|
388
|
+
writeFile(join(outDir, "specs", "000-as-built", "spec.md"), renderAsBuiltSpec(spec));
|
|
389
|
+
written.push("specs/000-as-built/spec.md");
|
|
390
|
+
}
|
|
391
|
+
if (!values.quiet) {
|
|
392
|
+
const count = (k) => spec.surface.filter((s) => s.kind === k).length;
|
|
393
|
+
io.stdout([
|
|
394
|
+
`As-built spec for ${contract.target.name} (source only; nothing executed, no requests sent)`,
|
|
395
|
+
` ${count("page")} page(s), ${count("endpoint")} endpoint(s), ${count("entity")} entit(y/ies), ${count("integration")} integration(s), ${count("config")} config name(s), ${count("test")} test title(s)`,
|
|
396
|
+
` ${spec.requirements.length} functional requirement(s), ${spec.non_functional.length} non-functional, ${spec.clarifications.length} clarification(s) for a person to answer`,
|
|
397
|
+
`Wrote ${display(io.cwd, outDir)}/: ${written.join(", ")}`,
|
|
398
|
+
format !== "json"
|
|
399
|
+
? `Next: answer the NEEDS CLARIFICATION markers, then lint it: spec-check check ${display(io.cwd, join(outDir, "specs", "000-as-built", "spec.md"))}`
|
|
400
|
+
: "",
|
|
401
|
+
"",
|
|
402
|
+
]
|
|
403
|
+
.filter((l, i, a) => l !== "" || i === a.length - 1)
|
|
404
|
+
.join("\n"));
|
|
405
|
+
}
|
|
406
|
+
return ExitCode.Ok;
|
|
407
|
+
}
|
|
322
408
|
/** init: write a contract. */
|
|
323
409
|
function commandInit(values, io) {
|
|
324
410
|
const out = isAbsolute(values.output ?? "")
|
|
@@ -424,6 +510,10 @@ async function main(argv, io) {
|
|
|
424
510
|
if (rest.length > 0)
|
|
425
511
|
throw usageError(`Unexpected argument: ${rest[0]}`);
|
|
426
512
|
return commandScan(values, io);
|
|
513
|
+
case "spec":
|
|
514
|
+
if (rest.length > 0)
|
|
515
|
+
throw usageError(`Unexpected argument: ${rest[0]}`);
|
|
516
|
+
return commandSpec(values, io);
|
|
427
517
|
case "init":
|
|
428
518
|
return commandInit(values, io);
|
|
429
519
|
case "validate":
|
|
@@ -447,7 +537,8 @@ async function main(argv, io) {
|
|
|
447
537
|
writeFile(join(dir, "blueprint.md"), renderBlueprint(results, { branding }));
|
|
448
538
|
for (const p of results.blueprint.work_packets)
|
|
449
539
|
writeFile(join(dir, "work-packets", `${p.id}.md`), renderPacket(results, p));
|
|
450
|
-
|
|
540
|
+
const spec = writeSpec(results, dir);
|
|
541
|
+
io.stdout(`Wrote blueprint.json, blueprint.md${spec.length ? ", the as-built spec" : ""} and ${results.blueprint.work_packets.length} work packet(s) to ${display(io.cwd, dir)}\n`);
|
|
451
542
|
}
|
|
452
543
|
else
|
|
453
544
|
io.stdout(values.format === "json"
|
|
@@ -468,7 +559,7 @@ async function main(argv, io) {
|
|
|
468
559
|
io.stdout(`${JSON.stringify(CONTROLS.map(({ remediation: _r, ...c }) => c), null, 2)}\n`);
|
|
469
560
|
else {
|
|
470
561
|
const lines = [
|
|
471
|
-
"Control catalog (rubric pc-rubric-0.
|
|
562
|
+
"Control catalog (rubric pc-rubric-0.2). Tiers carry forward: a tier applies its own and all earlier controls.",
|
|
472
563
|
"",
|
|
473
564
|
];
|
|
474
565
|
for (const c of CONTROLS)
|
|
@@ -490,7 +581,7 @@ async function main(argv, io) {
|
|
|
490
581
|
return ExitCode.Ok;
|
|
491
582
|
}
|
|
492
583
|
default:
|
|
493
|
-
throw usageError(`Unknown command: ${command}`, "Commands: run, retest, scan, init, validate, report, blueprint, dashboard, controls, schema.");
|
|
584
|
+
throw usageError(`Unknown command: ${command}`, "Commands: run, retest, scan, spec, init, validate, report, blueprint, dashboard, controls, schema.");
|
|
494
585
|
}
|
|
495
586
|
}
|
|
496
587
|
/** Runs the CLI and returns the process exit code. Never throws. */
|
|
@@ -27,6 +27,10 @@ export interface ContractFlags {
|
|
|
27
27
|
specCheck?: string | undefined;
|
|
28
28
|
impactCheck?: string | undefined;
|
|
29
29
|
outcomeCheck?: string | undefined;
|
|
30
|
+
architectureCheck?: string | undefined;
|
|
31
|
+
openslo?: string[] | undefined;
|
|
32
|
+
alertRules?: string[] | undefined;
|
|
33
|
+
serviceGraph?: string | undefined;
|
|
30
34
|
}
|
|
31
35
|
/** Applies CLI flags over a contract (flags win). Paths are resolved against cwd. */
|
|
32
36
|
export declare function applyFlags(base: ScanContract, flags: ContractFlags, cwd: string, today: string): ScanContract;
|
|
@@ -56,6 +56,10 @@ export function defaultContract() {
|
|
|
56
56
|
spec_check: null,
|
|
57
57
|
impact_check: null,
|
|
58
58
|
outcome_check: null,
|
|
59
|
+
architecture_check: null,
|
|
60
|
+
openslo: [],
|
|
61
|
+
alert_rules: [],
|
|
62
|
+
service_graph: null,
|
|
59
63
|
},
|
|
60
64
|
control_mappings: [],
|
|
61
65
|
risk_acceptances: [],
|
|
@@ -110,6 +114,8 @@ export function loadContract(path) {
|
|
|
110
114
|
contract.target.repository.path = abs(contract.target.repository.path);
|
|
111
115
|
const i = contract.imports;
|
|
112
116
|
i.test_results = i.test_results.map(abs);
|
|
117
|
+
i.openslo = i.openslo.map(abs);
|
|
118
|
+
i.alert_rules = i.alert_rules.map(abs);
|
|
113
119
|
for (const key of [
|
|
114
120
|
"npm_audit",
|
|
115
121
|
"har",
|
|
@@ -117,6 +123,8 @@ export function loadContract(path) {
|
|
|
117
123
|
"spec_check",
|
|
118
124
|
"impact_check",
|
|
119
125
|
"outcome_check",
|
|
126
|
+
"architecture_check",
|
|
127
|
+
"service_graph",
|
|
120
128
|
]) {
|
|
121
129
|
const value = i[key];
|
|
122
130
|
if (value !== null)
|
|
@@ -173,6 +181,14 @@ export function applyFlags(base, flags, cwd, today) {
|
|
|
173
181
|
c.imports.impact_check = abs(flags.impactCheck);
|
|
174
182
|
if (flags.outcomeCheck !== undefined)
|
|
175
183
|
c.imports.outcome_check = abs(flags.outcomeCheck);
|
|
184
|
+
if (flags.architectureCheck !== undefined)
|
|
185
|
+
c.imports.architecture_check = abs(flags.architectureCheck);
|
|
186
|
+
if (flags.openslo)
|
|
187
|
+
c.imports.openslo = [...c.imports.openslo, ...flags.openslo.map(abs)];
|
|
188
|
+
if (flags.alertRules)
|
|
189
|
+
c.imports.alert_rules = [...c.imports.alert_rules, ...flags.alertRules.map(abs)];
|
|
190
|
+
if (flags.serviceGraph !== undefined)
|
|
191
|
+
c.imports.service_graph = abs(flags.serviceGraph);
|
|
176
192
|
if (c.target.name === "unnamed product") {
|
|
177
193
|
c.target.name = c.target.repository
|
|
178
194
|
? (c.target.repository.path.split(/[\\/]/).filter(Boolean).at(-1) ?? "product")
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Results } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Converts an as-built statement (Markdown built in src/spec/build.ts: code spans for repository-derived names,
|
|
4
|
+
* backslash escapes elsewhere) to HTML. Every segment is HTML-escaped; only `<code>` is produced.
|
|
5
|
+
*/
|
|
6
|
+
export declare function statementHtml(statement: string): string;
|
|
2
7
|
/** Renders the dashboard HTML. */
|
|
3
8
|
export declare function renderDashboard(r: Results, options: {
|
|
4
9
|
branding: boolean;
|