@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.
Files changed (46) hide show
  1. package/ATTRIBUTION.md +2 -2
  2. package/CHANGELOG.md +45 -3
  3. package/NOTICE.md +5 -3
  4. package/README.md +8 -6
  5. package/dashboard/dashboard.css +6 -0
  6. package/dist/blueprint/build.d.ts +3 -1
  7. package/dist/blueprint/build.js +41 -5
  8. package/dist/cli/run.js +96 -5
  9. package/dist/contract/contract.d.ts +4 -0
  10. package/dist/contract/contract.js +16 -0
  11. package/dist/dashboard/render.d.ts +5 -0
  12. package/dist/dashboard/render.js +107 -3
  13. package/dist/discovery/pops.d.ts +2 -0
  14. package/dist/discovery/pops.js +7 -5
  15. package/dist/importers/operational.d.ts +56 -0
  16. package/dist/importers/operational.js +217 -0
  17. package/dist/importers/reports.d.ts +24 -0
  18. package/dist/importers/reports.js +37 -1
  19. package/dist/inspectors/source/surface.d.ts +30 -0
  20. package/dist/inspectors/source/surface.js +529 -0
  21. package/dist/parse/yaml.d.ts +4 -0
  22. package/dist/parse/yaml.js +39 -0
  23. package/dist/reporting/markdown.js +17 -0
  24. package/dist/reporting/spec-markdown.d.ts +9 -0
  25. package/dist/reporting/spec-markdown.js +144 -0
  26. package/dist/run.d.ts +2 -0
  27. package/dist/run.js +52 -2
  28. package/dist/schema/registry.js +1 -0
  29. package/dist/scoring/score.js +1 -1
  30. package/dist/spec/build.d.ts +28 -0
  31. package/dist/spec/build.js +278 -0
  32. package/dist/types.d.ts +78 -2
  33. package/dist/types.js +6 -1
  34. package/dist/validation/catalog.d.ts +1 -1
  35. package/dist/validation/catalog.js +125 -2
  36. package/dist/validation/context.d.ts +6 -1
  37. package/dist/validation/import-evaluators.js +37 -3
  38. package/dist/validation/integrity.js +4 -0
  39. package/dist/validation/operational-evaluators.d.ts +3 -0
  40. package/dist/validation/operational-evaluators.js +153 -0
  41. package/dist/validation/source-evaluators.js +1 -1
  42. package/package.json +5 -3
  43. package/schemas/as-built-spec.schema.json +356 -0
  44. package/schemas/blueprint.schema.json +3 -0
  45. package/schemas/scan-contract.schema.json +20 -0
  46. 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 **no runtime dependencies** and contains
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
- - License: MIT (same as the other Engineering Commons tools).
9
+ ## 0.2.0 — 2026-10-28
10
10
 
11
- ## 0.1.0-alpha.0 — not released
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
- First alpha candidate.
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 **no runtime dependencies** and contains no vendored source code. It reads formats defined by others
12
- (JSON Schema, JUnit XML, HAR, `npm audit` JSON, and the report schemas of Spec Check, Impact Check and Outcome Check);
13
- no code from those projects is included.
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: **alpha** (`0.1.0-alpha.0`, results schema `0.1.0`). Install with
20
- > `npm install --global @enwithai/product-check@alpha`, or build from source as in the demo below.
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, no runtime dependencies.
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-7386B1: 35 verified fixed, 0 new, 0 reopened, 0 regressed control(s)
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` and
98
- `dashboard.html` (open it directly; no server). After changing things, run
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 |
@@ -331,6 +331,12 @@ th {
331
331
  font-weight: 600;
332
332
  background: var(--surface);
333
333
  }
334
+ td.id {
335
+ white-space: nowrap;
336
+ }
337
+ td.id code {
338
+ overflow-wrap: normal;
339
+ }
334
340
  td.num,
335
341
  th.num {
336
342
  text-align: right;
@@ -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;
@@ -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 { components: components.sort((a, b) => (a.id < b.id ? -1 : 1)), uncertainties };
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: f.pillar === "customer" && journeys.length > 0
332
- ? `Journey: ${journeys[0] ?? ""}`
333
- : `Requirement ${f.control_id}: ${f.expected}`,
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 { runAssessment } from "../run.js";
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> Optional Engineering Commons reports
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
- io.stdout(`Wrote blueprint.json, blueprint.md and ${results.blueprint.work_packets.length} work packet(s) to ${display(io.cwd, dir)}\n`);
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.1). Tiers carry forward: a tier applies its own and all earlier controls.",
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;