@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/dist/cli/run.js
CHANGED
|
@@ -12,16 +12,21 @@ 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";
|
|
23
27
|
import { checkResults } from "../validation/integrity.js";
|
|
24
28
|
import { STAGES, TIER_INFO } from "../validation/tiers.js";
|
|
29
|
+
import { ASSURANCE_COMMANDS, ASSURANCE_HELP, runAssurance } from "../assurance/cli.js";
|
|
25
30
|
import { TOOL_VERSION } from "../version.js";
|
|
26
31
|
/** Text printed by `product-check --help`. */
|
|
27
32
|
export const HELP = `Product Check ${TOOL_VERSION} — does this software work, can it be trusted, and what needs to improve?
|
|
@@ -37,6 +42,9 @@ Usage:
|
|
|
37
42
|
Discovery only: what could be inspected (no requests, no checks)
|
|
38
43
|
product-check init [--repo <dir>] [--url <url>] [--stage <stage>] [--output <file>]
|
|
39
44
|
Write a scan contract to edit (default product-check.contract.json)
|
|
45
|
+
product-check spec --repo <dir> [--contract <file>] [--out-dir <dir>] [--format md|json|both]
|
|
46
|
+
Reverse-engineer an as-built spec (Spec Kit spec.md + JSON) from the
|
|
47
|
+
repository's structure; nothing is executed
|
|
40
48
|
product-check validate <file>... Validate artifacts against the schemas (+ integrity for results)
|
|
41
49
|
product-check report <results.json> Markdown report product-check blueprint <results.json>
|
|
42
50
|
product-check dashboard <results.json> [--output dashboard.html]
|
|
@@ -55,7 +63,11 @@ Inputs (run/retest):
|
|
|
55
63
|
--npm-audit <file> Output of \`npm audit --json\`
|
|
56
64
|
--har <file> Browser HAR export (offline application evidence)
|
|
57
65
|
--observations <file> Recorded observations (from --record-observations)
|
|
58
|
-
--spec-check/--impact-check/--outcome-check <file>
|
|
66
|
+
--spec-check/--impact-check/--outcome-check/--architecture-check <file>
|
|
67
|
+
Optional Engineering Commons reports
|
|
68
|
+
--openslo <file> OpenSLO v1 YAML/JSON (SLO, SLI, AlertPolicy; repeatable)
|
|
69
|
+
--alert-rules <file> Prometheus/Mimir rules, PrometheusRule, or /api/v1/rules JSON (repeatable)
|
|
70
|
+
--service-graph <file> Jaeger /api/dependencies or traces_service_graph_request_total query JSON
|
|
59
71
|
--baseline <file> Previous results to compare with (retest)
|
|
60
72
|
Output:
|
|
61
73
|
--out-dir <dir> Default .product-check --json Print results JSON to stdout, write nothing
|
|
@@ -63,6 +75,8 @@ Output:
|
|
|
63
75
|
--fail-on <verdicts> Exit 6 when the verdict is one of these (e.g. NOT_READY,INCONCLUSIVE)
|
|
64
76
|
--no-dashboard --no-branding --quiet
|
|
65
77
|
|
|
78
|
+
${ASSURANCE_HELP}
|
|
79
|
+
|
|
66
80
|
Exit codes: 0 ok · 1 internal error · 2 usage/config · 3 input · 4 safety refusal · 5 invalid artifact · 6 --fail-on matched
|
|
67
81
|
Local-first: no telemetry, no account, no model calls. Network only to an authorized origin with passive GETs.
|
|
68
82
|
`;
|
|
@@ -130,6 +144,10 @@ const OPTIONS = {
|
|
|
130
144
|
"spec-check": { type: "string" },
|
|
131
145
|
"impact-check": { type: "string" },
|
|
132
146
|
"outcome-check": { type: "string" },
|
|
147
|
+
"architecture-check": { type: "string" },
|
|
148
|
+
openslo: { type: "string", multiple: true },
|
|
149
|
+
"alert-rules": { type: "string", multiple: true },
|
|
150
|
+
"service-graph": { type: "string" },
|
|
133
151
|
baseline: { type: "string" },
|
|
134
152
|
"out-dir": { type: "string" },
|
|
135
153
|
output: { type: "string" },
|
|
@@ -173,6 +191,10 @@ function buildContract(values, io) {
|
|
|
173
191
|
specCheck: values["spec-check"],
|
|
174
192
|
impactCheck: values["impact-check"],
|
|
175
193
|
outcomeCheck: values["outcome-check"],
|
|
194
|
+
architectureCheck: values["architecture-check"],
|
|
195
|
+
openslo: values.openslo,
|
|
196
|
+
alertRules: values["alert-rules"],
|
|
197
|
+
serviceGraph: values["service-graph"],
|
|
176
198
|
}, io.cwd, today);
|
|
177
199
|
if (values["authorized-by"] !== undefined && contract.authorization.attestation)
|
|
178
200
|
contract.authorization.attestation.authorized_by = values["authorized-by"];
|
|
@@ -181,6 +203,15 @@ function buildContract(values, io) {
|
|
|
181
203
|
}
|
|
182
204
|
return { contract, fromFile };
|
|
183
205
|
}
|
|
206
|
+
/** Writes the as-built spec (JSON + Spec Kit Markdown) carried by a results document, if any. */
|
|
207
|
+
function writeSpec(results, dir) {
|
|
208
|
+
const spec = specFromResults(results);
|
|
209
|
+
if (!spec)
|
|
210
|
+
return [];
|
|
211
|
+
writeFile(join(dir, "as-built-spec.json"), `${JSON.stringify(spec, null, 2)}\n`);
|
|
212
|
+
writeFile(join(dir, "specs", "000-as-built", "spec.md"), renderAsBuiltSpec(spec));
|
|
213
|
+
return ["as-built-spec.json", "specs/000-as-built/spec.md"];
|
|
214
|
+
}
|
|
184
215
|
/** Writes all renderings of a results document into a directory. */
|
|
185
216
|
function writeOutputs(results, dir, branding, dashboard) {
|
|
186
217
|
const written = [];
|
|
@@ -193,6 +224,7 @@ function writeOutputs(results, dir, branding, dashboard) {
|
|
|
193
224
|
put("blueprint.md", renderBlueprint(results, { branding }));
|
|
194
225
|
for (const p of results.blueprint.work_packets)
|
|
195
226
|
put(`work-packets/${p.id}.md`, renderPacket(results, p));
|
|
227
|
+
written.push(...writeSpec(results, dir));
|
|
196
228
|
if (dashboard)
|
|
197
229
|
put("dashboard.html", renderDashboard(results, { branding }));
|
|
198
230
|
return written;
|
|
@@ -289,6 +321,9 @@ function commandScan(values, io) {
|
|
|
289
321
|
auditImported: contract.imports.npm_audit !== null,
|
|
290
322
|
specImported: contract.imports.spec_check !== null,
|
|
291
323
|
outcomeImported: contract.imports.outcome_check !== null,
|
|
324
|
+
operationalImported: contract.imports.openslo.length > 0 ||
|
|
325
|
+
contract.imports.alert_rules.length > 0 ||
|
|
326
|
+
contract.imports.service_graph !== null,
|
|
292
327
|
aiInScope: contract.product.uses_ai === true || (repo?.aiDeps.length ?? 0) > 0,
|
|
293
328
|
tier,
|
|
294
329
|
now: (io.now?.() ?? new Date()).toISOString(),
|
|
@@ -319,6 +354,60 @@ function commandScan(values, io) {
|
|
|
319
354
|
io.stdout(lines.join("\n"));
|
|
320
355
|
return ExitCode.Ok;
|
|
321
356
|
}
|
|
357
|
+
/** spec: reverse-engineer an as-built specification from the repository (source only, no requests). */
|
|
358
|
+
function commandSpec(values, io) {
|
|
359
|
+
const { contract } = buildContract(values, io);
|
|
360
|
+
if (!contract.target.repository)
|
|
361
|
+
throw usageError("spec reads a repository: pass --repo <dir>.", "Example: product-check spec --repo .");
|
|
362
|
+
const format = values.format ?? "both";
|
|
363
|
+
if (!["md", "json", "both"].includes(format))
|
|
364
|
+
throw usageError("--format must be md, json or both.");
|
|
365
|
+
const repoPath = contract.target.repository.path;
|
|
366
|
+
const facts = deriveFacts(snapshotRepo(repoPath, contract.constraints.max_files, contract.target.name));
|
|
367
|
+
const started = io.now?.() ?? new Date();
|
|
368
|
+
const now = started.toISOString();
|
|
369
|
+
const ev = new EvidenceCollector(makeRunId(started, relativize(contract, io.cwd)), now);
|
|
370
|
+
const spec = buildAsBuiltSpec({
|
|
371
|
+
contract,
|
|
372
|
+
facts,
|
|
373
|
+
surface: deriveSurface(facts),
|
|
374
|
+
ev,
|
|
375
|
+
repository: display(io.cwd, repoPath),
|
|
376
|
+
generatedAt: now,
|
|
377
|
+
});
|
|
378
|
+
if (values.json) {
|
|
379
|
+
io.stdout(`${JSON.stringify(spec, null, 2)}\n`);
|
|
380
|
+
return ExitCode.Ok;
|
|
381
|
+
}
|
|
382
|
+
const outDir = isAbsolute(values["out-dir"] ?? "")
|
|
383
|
+
? values["out-dir"]
|
|
384
|
+
: resolve(io.cwd, values["out-dir"] ?? ".product-check");
|
|
385
|
+
const written = [];
|
|
386
|
+
if (format !== "md") {
|
|
387
|
+
writeFile(join(outDir, "as-built-spec.json"), `${JSON.stringify(spec, null, 2)}\n`);
|
|
388
|
+
written.push("as-built-spec.json");
|
|
389
|
+
}
|
|
390
|
+
if (format !== "json") {
|
|
391
|
+
writeFile(join(outDir, "specs", "000-as-built", "spec.md"), renderAsBuiltSpec(spec));
|
|
392
|
+
written.push("specs/000-as-built/spec.md");
|
|
393
|
+
}
|
|
394
|
+
if (!values.quiet) {
|
|
395
|
+
const count = (k) => spec.surface.filter((s) => s.kind === k).length;
|
|
396
|
+
io.stdout([
|
|
397
|
+
`As-built spec for ${contract.target.name} (source only; nothing executed, no requests sent)`,
|
|
398
|
+
` ${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)`,
|
|
399
|
+
` ${spec.requirements.length} functional requirement(s), ${spec.non_functional.length} non-functional, ${spec.clarifications.length} clarification(s) for a person to answer`,
|
|
400
|
+
`Wrote ${display(io.cwd, outDir)}/: ${written.join(", ")}`,
|
|
401
|
+
format !== "json"
|
|
402
|
+
? `Next: answer the NEEDS CLARIFICATION markers, then lint it: spec-check check ${display(io.cwd, join(outDir, "specs", "000-as-built", "spec.md"))}`
|
|
403
|
+
: "",
|
|
404
|
+
"",
|
|
405
|
+
]
|
|
406
|
+
.filter((l, i, a) => l !== "" || i === a.length - 1)
|
|
407
|
+
.join("\n"));
|
|
408
|
+
}
|
|
409
|
+
return ExitCode.Ok;
|
|
410
|
+
}
|
|
322
411
|
/** init: write a contract. */
|
|
323
412
|
function commandInit(values, io) {
|
|
324
413
|
const out = isAbsolute(values.output ?? "")
|
|
@@ -386,6 +475,10 @@ function commandValidate(files, io) {
|
|
|
386
475
|
}
|
|
387
476
|
/** Parses arguments and runs one command. */
|
|
388
477
|
async function main(argv, io) {
|
|
478
|
+
const first = argv[0] ?? "";
|
|
479
|
+
if (ASSURANCE_COMMANDS.includes(first) ||
|
|
480
|
+
((first === "dashboard" || first === "blueprint") && argv.includes("--from")))
|
|
481
|
+
return runAssurance(first, argv.slice(1), io);
|
|
389
482
|
const { values, positionals } = (() => {
|
|
390
483
|
try {
|
|
391
484
|
return parseArgs({ args: argv, allowPositionals: true, strict: true, options: OPTIONS });
|
|
@@ -424,6 +517,10 @@ async function main(argv, io) {
|
|
|
424
517
|
if (rest.length > 0)
|
|
425
518
|
throw usageError(`Unexpected argument: ${rest[0]}`);
|
|
426
519
|
return commandScan(values, io);
|
|
520
|
+
case "spec":
|
|
521
|
+
if (rest.length > 0)
|
|
522
|
+
throw usageError(`Unexpected argument: ${rest[0]}`);
|
|
523
|
+
return commandSpec(values, io);
|
|
427
524
|
case "init":
|
|
428
525
|
return commandInit(values, io);
|
|
429
526
|
case "validate":
|
|
@@ -447,7 +544,8 @@ async function main(argv, io) {
|
|
|
447
544
|
writeFile(join(dir, "blueprint.md"), renderBlueprint(results, { branding }));
|
|
448
545
|
for (const p of results.blueprint.work_packets)
|
|
449
546
|
writeFile(join(dir, "work-packets", `${p.id}.md`), renderPacket(results, p));
|
|
450
|
-
|
|
547
|
+
const spec = writeSpec(results, dir);
|
|
548
|
+
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
549
|
}
|
|
452
550
|
else
|
|
453
551
|
io.stdout(values.format === "json"
|
|
@@ -468,7 +566,7 @@ async function main(argv, io) {
|
|
|
468
566
|
io.stdout(`${JSON.stringify(CONTROLS.map(({ remediation: _r, ...c }) => c), null, 2)}\n`);
|
|
469
567
|
else {
|
|
470
568
|
const lines = [
|
|
471
|
-
"Control catalog (rubric pc-rubric-0.
|
|
569
|
+
"Control catalog (rubric pc-rubric-0.2). Tiers carry forward: a tier applies its own and all earlier controls.",
|
|
472
570
|
"",
|
|
473
571
|
];
|
|
474
572
|
for (const c of CONTROLS)
|
|
@@ -490,7 +588,7 @@ async function main(argv, io) {
|
|
|
490
588
|
return ExitCode.Ok;
|
|
491
589
|
}
|
|
492
590
|
default:
|
|
493
|
-
throw usageError(`Unknown command: ${command}`,
|
|
591
|
+
throw usageError(`Unknown command: ${command}`, `Commands: run, retest, scan, spec, init, validate, report, blueprint, dashboard, controls, schema, ${ASSURANCE_COMMANDS.join(", ")}.`);
|
|
494
592
|
}
|
|
495
593
|
}
|
|
496
594
|
/** 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;
|
package/dist/dashboard/render.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Renders the local dashboard: one self-contained HTML file generated entirely from a results document. Every
|
|
5
5
|
* number is read from (or recomputed from) the JSON; the integrity check runs first and a failure is shown as a
|
|
6
|
-
* banner. Views: Executive Readiness, Scan Explorer, Validation Workbench, Blueprint & Remediation,
|
|
7
|
-
* History. A small inline script (allowed by a hash-based CSP) adds tabs, filters and a theme toggle; without it
|
|
6
|
+
* banner. Views: Executive Readiness, Scan Explorer, Validation Workbench, Blueprint & Remediation, As-built Spec,
|
|
7
|
+
* Assurance History. A small inline script (allowed by a hash-based CSP) adds tabs, filters and a theme toggle; without it
|
|
8
8
|
* every view is still readable. No external requests.
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
@@ -176,6 +176,109 @@ function evidenceList(r, ids) {
|
|
|
176
176
|
})
|
|
177
177
|
.join("")}</ul></details>`;
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Converts an as-built statement (Markdown built in src/spec/build.ts: code spans for repository-derived names,
|
|
181
|
+
* backslash escapes elsewhere) to HTML. Every segment is HTML-escaped; only `<code>` is produced.
|
|
182
|
+
*/
|
|
183
|
+
export function statementHtml(statement) {
|
|
184
|
+
const out = [];
|
|
185
|
+
let rest = statement;
|
|
186
|
+
for (;;) {
|
|
187
|
+
const m = /(`+)( ?)([\s\S]*?)\2\1(?!`)/.exec(rest);
|
|
188
|
+
if (!m)
|
|
189
|
+
break;
|
|
190
|
+
out.push(html(unescapeMd(rest.slice(0, m.index))));
|
|
191
|
+
out.push(`<code>${html((m[3] ?? "").replace(/\\\|/g, "|"))}</code>`);
|
|
192
|
+
rest = rest.slice(m.index + m[0].length);
|
|
193
|
+
}
|
|
194
|
+
out.push(html(unescapeMd(rest)));
|
|
195
|
+
return out.join("");
|
|
196
|
+
}
|
|
197
|
+
/** Undoes the Markdown escaping of `md()` for plain-text display (the result is HTML-escaped afterwards). */
|
|
198
|
+
function unescapeMd(text) {
|
|
199
|
+
return text
|
|
200
|
+
.replace(/\\([\\`*_[\]<>|~#+=-])/g, "$1")
|
|
201
|
+
.replace(/&/g, "&")
|
|
202
|
+
.replace(/@\u200b/g, "@");
|
|
203
|
+
}
|
|
204
|
+
const CERTAINTY_TONE = { observed: "good", inferred: "warning", hypothesis: "unknown" };
|
|
205
|
+
const CERTAINTY_LABEL = {
|
|
206
|
+
observed: "observed",
|
|
207
|
+
inferred: "inferred",
|
|
208
|
+
hypothesis: "needs confirmation",
|
|
209
|
+
};
|
|
210
|
+
/** The As-built Spec view: the reverse-engineered specification carried in the blueprint. */
|
|
211
|
+
function asBuiltView(r) {
|
|
212
|
+
const head = `<section class="view" id="as-built" aria-labelledby="h-as-built"><h2 id="h-as-built">As-built spec</h2>`;
|
|
213
|
+
const built = r.blueprint.as_is.as_built;
|
|
214
|
+
if (!built)
|
|
215
|
+
return `${head}<p class="muted">No as-built spec: the repository was not inspected in this run.</p></section>`;
|
|
216
|
+
const byId = new Map(built.surface.map((s) => [s.id, s]));
|
|
217
|
+
const count = (k) => built.surface.filter((s) => s.kind === k).length;
|
|
218
|
+
const where = (s) => `<code>${html(`${s.location.path}${s.location.line ? `:${s.location.line}` : ""}`)}</code>`;
|
|
219
|
+
// Findings whose traceability names an as-built requirement ("As-built FR-001, FR-007; requirement …").
|
|
220
|
+
const findingsFor = new Map();
|
|
221
|
+
for (const t of r.blueprint.traceability) {
|
|
222
|
+
const ids = /^As-built ([^;]+);/.exec(t.journey_or_requirement)?.[1]?.split(", ") ?? [];
|
|
223
|
+
for (const id of ids)
|
|
224
|
+
findingsFor.set(id, [...(findingsFor.get(id) ?? []), t.finding_id]);
|
|
225
|
+
}
|
|
226
|
+
const openIds = new Set(r.findings.filter((f) => f.status === "OPEN" || f.status === "UNVERIFIED").map((f) => f.id));
|
|
227
|
+
const reqRow = (q, kind) => {
|
|
228
|
+
const sources = q.surface_ids.map((id) => byId.get(id)).filter((s) => s !== undefined);
|
|
229
|
+
const linked = findingsFor.get(q.id) ?? [];
|
|
230
|
+
const open = linked.filter((id) => openIds.has(id));
|
|
231
|
+
return `<tr data-kind="${kind}" data-certainty="${q.certainty}"><td class="id"><code>${html(q.id)}</code></td><td>${statementHtml(q.statement)}</td><td>${chip(CERTAINTY_LABEL[q.certainty] ?? q.certainty, CERTAINTY_TONE[q.certainty] ?? "neutral")}</td><td class="small">${sources.slice(0, 4).map(where).join("<br>") || "—"}${sources.length > 4 ? `<br><span class="muted">and ${sources.length - 4} more</span>` : ""}</td><td class="small">${linked.length ? `${open.length ? chip(`${open.length} open`, "critical") : chip("none open", "good")}<br>${linked.length > 3 ? `<details><summary>${linked.length} finding(s)</summary><code>${html(linked.join(", "))}</code></details>` : `<code>${html(linked.join(", "))}</code>`}` : "—"}</td><td class="small">${evidenceList(r, q.evidence_ids)}</td></tr>`;
|
|
232
|
+
};
|
|
233
|
+
const surfaceTable = (title, kind, cols, cells) => {
|
|
234
|
+
const rows = built.surface.filter((s) => s.kind === kind);
|
|
235
|
+
if (rows.length === 0)
|
|
236
|
+
return "";
|
|
237
|
+
return `<details class="card" style="margin-bottom:8px"><summary>${html(title)} (${rows.length})</summary><div class="scroll"><table><thead><tr>${cols.map((c) => `<th>${html(c)}</th>`).join("")}</tr></thead><tbody>${rows
|
|
238
|
+
.map((s) => `<tr>${cells(s)
|
|
239
|
+
.map((c) => `<td class="small">${c}</td>`)
|
|
240
|
+
.join("")}</tr>`)
|
|
241
|
+
.join("")}</tbody></table></div></details>`;
|
|
242
|
+
};
|
|
243
|
+
const auth = (s) => (s.auth_hint ? chip("auth helper (inferred)", "warning") : "—");
|
|
244
|
+
return `${head}
|
|
245
|
+
<p class="muted small">Reverse-engineered from the repository's structure; nothing was executed. <em>Observed</em> statements point at a file and line, <em>inferred</em> ones interpret what was found, and anything the code cannot tell us needs a person to confirm. The same spec is written as <code>specs/000-as-built/spec.md</code> (Spec Kit layout, lint it with Spec Check) and <code>as-built-spec.json</code>.</p>
|
|
246
|
+
<div class="grid kpis">${[
|
|
247
|
+
["Pages", count("page")],
|
|
248
|
+
["Endpoints", count("endpoint")],
|
|
249
|
+
["Data entities", count("entity")],
|
|
250
|
+
["Integrations", count("integration")],
|
|
251
|
+
["Config names", count("config")],
|
|
252
|
+
["Open questions", built.clarifications.length],
|
|
253
|
+
]
|
|
254
|
+
.map(([k, v]) => `<div class="card kpi"><div class="label">${html(k)}</div><div class="value">${v}</div></div>`)
|
|
255
|
+
.join("")}</div>
|
|
256
|
+
${built.clarifications.length ? `<h3>Questions only a person can answer</h3><ul>${built.clarifications.map((c) => `<li>${html(c)}</li>`).join("")}</ul>` : ""}
|
|
257
|
+
<h3>User stories</h3>${built.stories
|
|
258
|
+
.map((s) => `<details class="card" style="margin-bottom:8px"><summary>${html(s.id)} ${html(s.title)} ${s.critical_journey ? chip(`critical journey: ${s.critical_journey}`, "neutral") : ""}</summary><p class="small"><strong>Requirements:</strong> <code>${html(s.requirement_ids.join(", ") || "—")}</code></p>${s.test_behaviours.length ? `<p class="small"><strong>Behaviour described by tests</strong> (inferred)</p><ul class="small">${s.test_behaviours.map((t) => `<li>${html(t)}</li>`).join("")}</ul>` : `<p class="small muted">No test titles mention this area.</p>`}</details>`)
|
|
259
|
+
.join("") ||
|
|
260
|
+
`<p class="muted">No pages or endpoints were recognised, so no user stories could be derived.</p>`}
|
|
261
|
+
<h3>Requirements</h3>
|
|
262
|
+
<div class="filters" data-filter-table="as-built-table"><label>Type<select data-key="kind"><option value="">All</option><option value="functional">Functional</option><option value="non-functional">Non-functional</option><option value="success">Success criteria</option><option value="edge">Edge cases</option></select></label>
|
|
263
|
+
<label>Certainty<select data-key="certainty"><option value="">All</option><option value="observed">Observed</option><option value="inferred">Inferred</option><option value="hypothesis">Needs confirmation</option></select></label>
|
|
264
|
+
<label>Search<input type="search" data-key="text" placeholder="ID, route, entity, path"></label><span class="small muted" data-count style="align-self:end"></span></div>
|
|
265
|
+
<div class="scroll"><table id="as-built-table"><thead><tr><th>ID</th><th>Statement</th><th>Certainty</th><th>Source</th><th>Linked findings</th><th>Evidence</th></tr></thead><tbody>${[
|
|
266
|
+
...built.requirements.map((q) => reqRow(q, "functional")),
|
|
267
|
+
...built.non_functional.map((q) => reqRow(q, "non-functional")),
|
|
268
|
+
...built.success_criteria.map((q) => reqRow(q, "success")),
|
|
269
|
+
...built.edge_cases.map((q) => reqRow(q, "edge")),
|
|
270
|
+
].join("")}</tbody></table></div>
|
|
271
|
+
<h3>Discovered surface</h3>
|
|
272
|
+
${surfaceTable("Pages", "page", ["Route", "Kind", "Source", "Auth"], (s) => [`<code>${html(s.name)}</code>`, html(s.detail), where(s), auth(s)])}
|
|
273
|
+
${surfaceTable("Endpoints", "endpoint", ["Method", "Path", "Kind", "Source", "Auth"], (s) => [`<code>${html(s.method ?? "ANY")}</code>`, `<code>${html(s.name)}</code>`, html(s.detail), where(s), auth(s)])}
|
|
274
|
+
${surfaceTable("Data entities", "entity", ["Entity", "Detail", "Source"], (s) => [`<code>${html(s.name)}</code>`, html(s.detail), where(s)])}
|
|
275
|
+
${surfaceTable("Integrations", "integration", ["Package", "Category", "Declared in"], (s) => [`<code>${html(s.name)}</code>`, html(s.detail), where(s)])}
|
|
276
|
+
${surfaceTable("Configuration (names only; values never read)", "config", ["Variable", "Use", "First reference"], (s) => [`<code>${html(s.name)}</code>`, html(s.detail), where(s)])}
|
|
277
|
+
${surfaceTable("Authentication boundaries", "auth-boundary", ["Boundary", "Detail", "Source"], (s) => [`<code>${html(s.name)}</code>`, html(s.detail), where(s)])}
|
|
278
|
+
${surfaceTable("Test titles", "test", ["Title", "Area", "Source"], (s) => [html(s.name), html(s.area), where(s)])}
|
|
279
|
+
<h3>Uncertainties</h3><ul class="small">${built.uncertainties.map((u) => `<li>${html(u)}</li>`).join("")}</ul>
|
|
280
|
+
</section>`;
|
|
281
|
+
}
|
|
179
282
|
/** Validation table row. */
|
|
180
283
|
function validationRow(r, v) {
|
|
181
284
|
return `<tr data-pillar="${v.pillar}" data-status="${v.status}">
|
|
@@ -328,12 +431,13 @@ ${c
|
|
|
328
431
|
<div class="wrap">
|
|
329
432
|
<header class="top"><div><h1>Product Check: ${html(r.contract.target.name)}</h1><div class="meta">Run ${html(r.run.run_id)} · ${html(r.run.finished_at)} · ${html(r.run.status)}</div></div><button class="theme-toggle" type="button" hidden>Theme: auto</button></header>
|
|
330
433
|
${banners}
|
|
331
|
-
<nav class="tabs" aria-label="Dashboard views"><a href="#readiness" aria-current="page">Executive readiness</a><a href="#scan">Scan explorer</a><a href="#workbench">Validation workbench</a><a href="#blueprint">Blueprint & remediation</a><a href="#history">Assurance history</a></nav>
|
|
434
|
+
<nav class="tabs" aria-label="Dashboard views"><a href="#readiness" aria-current="page">Executive readiness</a><a href="#scan">Scan explorer</a><a href="#workbench">Validation workbench</a><a href="#blueprint">Blueprint & remediation</a><a href="#as-built">As-built spec</a><a href="#history">Assurance history</a></nav>
|
|
332
435
|
<main>
|
|
333
436
|
${exec}
|
|
334
437
|
${scan}
|
|
335
438
|
${work}
|
|
336
439
|
${bp}
|
|
440
|
+
${asBuiltView(r)}
|
|
337
441
|
${hist}
|
|
338
442
|
</main>
|
|
339
443
|
<footer>Generated from <code>product-check-results.json</code> (schema ${html(r.schema_version)}); every figure is derived from that file${integrity.errors.length === 0 ? " and its scorecard and verdict recompute from the validations" : ""}. Evidence-bounded readiness, not a penetration test, certification or guarantee.${options.branding ? " Product Check is an EnWithAI Engineering Commons tool." : ""}</footer>
|
package/dist/discovery/pops.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface PopInput {
|
|
|
16
16
|
auditImported: boolean;
|
|
17
17
|
specImported: boolean;
|
|
18
18
|
outcomeImported: boolean;
|
|
19
|
+
/** OpenSLO, alert rules or a service graph was imported (CT-T4). */
|
|
20
|
+
operationalImported: boolean;
|
|
19
21
|
aiInScope: boolean;
|
|
20
22
|
tier: Tier;
|
|
21
23
|
now: string;
|
package/dist/discovery/pops.js
CHANGED
|
@@ -217,14 +217,16 @@ export function buildPops(input) {
|
|
|
217
217
|
id: "observability",
|
|
218
218
|
category: "operations",
|
|
219
219
|
name: "Production telemetry (logs, metrics, traces, alerts)",
|
|
220
|
-
status: postLaunch ? "BLOCKED" : "NOT_APPLICABLE",
|
|
220
|
+
status: postLaunch ? (input.operationalImported ? "AVAILABLE" : "BLOCKED") : "NOT_APPLICABLE",
|
|
221
221
|
relevant: postLaunch,
|
|
222
222
|
scope: null,
|
|
223
|
-
provenance: "none",
|
|
223
|
+
provenance: postLaunch && input.operationalImported ? "imported" : "none",
|
|
224
224
|
limitations: [
|
|
225
|
-
postLaunch
|
|
226
|
-
? "
|
|
227
|
-
:
|
|
225
|
+
!postLaunch
|
|
226
|
+
? "Not required before launch tiers (CT-T4+)."
|
|
227
|
+
: input.operationalImported
|
|
228
|
+
? "Read from exported files (OpenSLO, alert rules, service graph); nothing was queried live, and a service graph covers only its time window."
|
|
229
|
+
: "No operational files were imported (--openslo, --alert-rules, --service-graph); production signals were not read.",
|
|
228
230
|
],
|
|
229
231
|
}));
|
|
230
232
|
pops.push(input.aiInScope
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CommandExecution, CommandSpec } from "../assurance/types.js";
|
|
2
|
+
/** Result of a git read. */
|
|
3
|
+
export interface GitResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
}
|
|
8
|
+
/** Runs a read-only git subcommand in `repo`. */
|
|
9
|
+
export declare function gitRead(repo: string, args: string[], maxBuffer?: number): GitResult;
|
|
10
|
+
/** Authorization for command execution, decided once per CLI invocation. */
|
|
11
|
+
export interface ExecPermission {
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
/** Inputs to one execution. */
|
|
16
|
+
export interface ExecInput {
|
|
17
|
+
spec: CommandSpec;
|
|
18
|
+
repo: string;
|
|
19
|
+
permission: ExecPermission;
|
|
20
|
+
envAllowlist: readonly string[];
|
|
21
|
+
maxOutputBytes: number;
|
|
22
|
+
environmentId: string;
|
|
23
|
+
commitSha: string | null;
|
|
24
|
+
now: () => Date;
|
|
25
|
+
}
|
|
26
|
+
/** Executes one declared command (argv, no shell). Never throws for command failures. */
|
|
27
|
+
export declare function execCommand(input: ExecInput): CommandExecution;
|