@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.
Files changed (102) hide show
  1. package/ATTRIBUTION.md +6 -6
  2. package/CHANGELOG.md +66 -0
  3. package/NOTICE.md +5 -3
  4. package/README.md +34 -19
  5. package/SECURITY.md +5 -1
  6. package/dashboard/assurance.css +71 -0
  7. package/dashboard/assurance.js +21 -0
  8. package/dashboard/dashboard.css +6 -0
  9. package/dist/assurance/adapters.d.ts +32 -0
  10. package/dist/assurance/adapters.js +76 -0
  11. package/dist/assurance/approvals.d.ts +34 -0
  12. package/dist/assurance/approvals.js +133 -0
  13. package/dist/assurance/cli.d.ts +13 -0
  14. package/dist/assurance/cli.js +863 -0
  15. package/dist/assurance/dashboard.d.ts +18 -0
  16. package/dist/assurance/dashboard.js +267 -0
  17. package/dist/assurance/diff.d.ts +20 -0
  18. package/dist/assurance/diff.js +99 -0
  19. package/dist/assurance/evidence-log.d.ts +20 -0
  20. package/dist/assurance/evidence-log.js +100 -0
  21. package/dist/assurance/fix-packets.d.ts +12 -0
  22. package/dist/assurance/fix-packets.js +178 -0
  23. package/dist/assurance/gate.d.ts +37 -0
  24. package/dist/assurance/gate.js +324 -0
  25. package/dist/assurance/lanes.d.ts +61 -0
  26. package/dist/assurance/lanes.js +380 -0
  27. package/dist/assurance/policy.d.ts +10 -0
  28. package/dist/assurance/policy.js +85 -0
  29. package/dist/assurance/proof.d.ts +35 -0
  30. package/dist/assurance/proof.js +146 -0
  31. package/dist/assurance/register.d.ts +41 -0
  32. package/dist/assurance/register.js +480 -0
  33. package/dist/assurance/render-md.d.ts +17 -0
  34. package/dist/assurance/render-md.js +212 -0
  35. package/dist/assurance/repo-state.d.ts +15 -0
  36. package/dist/assurance/repo-state.js +79 -0
  37. package/dist/assurance/review.d.ts +52 -0
  38. package/dist/assurance/review.js +408 -0
  39. package/dist/assurance/risk.d.ts +21 -0
  40. package/dist/assurance/risk.js +184 -0
  41. package/dist/assurance/types.d.ts +573 -0
  42. package/dist/assurance/types.js +6 -0
  43. package/dist/assurance/util.d.ts +24 -0
  44. package/dist/assurance/util.js +89 -0
  45. package/dist/assurance/verification.d.ts +30 -0
  46. package/dist/assurance/verification.js +212 -0
  47. package/dist/assurance/verify.d.ts +26 -0
  48. package/dist/assurance/verify.js +304 -0
  49. package/dist/blueprint/build.d.ts +3 -1
  50. package/dist/blueprint/build.js +41 -5
  51. package/dist/cli/run.js +103 -5
  52. package/dist/contract/contract.d.ts +4 -0
  53. package/dist/contract/contract.js +16 -0
  54. package/dist/dashboard/render.d.ts +5 -0
  55. package/dist/dashboard/render.js +107 -3
  56. package/dist/discovery/pops.d.ts +2 -0
  57. package/dist/discovery/pops.js +7 -5
  58. package/dist/exec/process.d.ts +27 -0
  59. package/dist/exec/process.js +175 -0
  60. package/dist/importers/operational.d.ts +56 -0
  61. package/dist/importers/operational.js +217 -0
  62. package/dist/importers/reports.d.ts +24 -0
  63. package/dist/importers/reports.js +37 -1
  64. package/dist/inspectors/source/surface.d.ts +30 -0
  65. package/dist/inspectors/source/surface.js +529 -0
  66. package/dist/parse/yaml.d.ts +4 -0
  67. package/dist/parse/yaml.js +39 -0
  68. package/dist/reporting/markdown.d.ts +1 -0
  69. package/dist/reporting/markdown.js +18 -1
  70. package/dist/reporting/spec-markdown.d.ts +9 -0
  71. package/dist/reporting/spec-markdown.js +144 -0
  72. package/dist/run.d.ts +2 -0
  73. package/dist/run.js +52 -2
  74. package/dist/schema/registry.js +10 -0
  75. package/dist/scoring/score.js +1 -1
  76. package/dist/spec/build.d.ts +28 -0
  77. package/dist/spec/build.js +278 -0
  78. package/dist/types.d.ts +78 -2
  79. package/dist/types.js +6 -1
  80. package/dist/validation/catalog.d.ts +1 -1
  81. package/dist/validation/catalog.js +125 -2
  82. package/dist/validation/context.d.ts +6 -1
  83. package/dist/validation/import-evaluators.js +37 -3
  84. package/dist/validation/integrity.js +4 -0
  85. package/dist/validation/operational-evaluators.d.ts +3 -0
  86. package/dist/validation/operational-evaluators.js +153 -0
  87. package/dist/validation/source-evaluators.js +1 -1
  88. package/package.json +4 -1
  89. package/schemas/approval.schema.json +98 -0
  90. package/schemas/as-built-spec.schema.json +356 -0
  91. package/schemas/assurance-common.schema.json +291 -0
  92. package/schemas/assurance-policy.schema.json +185 -0
  93. package/schemas/blueprint.schema.json +3 -0
  94. package/schemas/change-review.schema.json +711 -0
  95. package/schemas/closure-register.schema.json +499 -0
  96. package/schemas/evidence-record.schema.json +94 -0
  97. package/schemas/fix-packet.schema.json +272 -0
  98. package/schemas/release-gate.schema.json +308 -0
  99. package/schemas/reviewer-findings.schema.json +49 -0
  100. package/schemas/scan-contract.schema.json +20 -0
  101. package/schemas/scorecard.schema.json +1 -1
  102. 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 { 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";
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> Optional Engineering Commons reports
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
- io.stdout(`Wrote blueprint.json, blueprint.md and ${results.blueprint.work_packets.length} work packet(s) to ${display(io.cwd, dir)}\n`);
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.1). Tiers carry forward: a tier applies its own and all earlier controls.",
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}`, "Commands: run, retest, scan, init, validate, report, blueprint, dashboard, controls, schema.");
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;
@@ -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, Assurance
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(/&amp;/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 &amp; 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 &amp; 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>
@@ -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;
@@ -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
- ? "No telemetry adapter in this release; production signals were not read."
227
- : "Not required before launch tiers (CT-T4+).",
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;