@geraldmaron/construct 1.3.1 → 1.4.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 (50) hide show
  1. package/README.md +2 -1
  2. package/bin/construct +61 -15
  3. package/lib/a11y-audit.mjs +104 -0
  4. package/lib/artifact-completion-states.mjs +35 -0
  5. package/lib/artifact-completion.mjs +66 -0
  6. package/lib/artifact-gate-levels.mjs +69 -0
  7. package/lib/artifact-manifest.mjs +21 -0
  8. package/lib/artifact-release-gate.mjs +19 -1
  9. package/lib/artifact-workflow.mjs +16 -0
  10. package/lib/brand-contrast.mjs +60 -0
  11. package/lib/brand-tokens.mjs +14 -0
  12. package/lib/certification/artifact-gates.mjs +6 -1
  13. package/lib/cli-commands.mjs +5 -3
  14. package/lib/codex-config.mjs +7 -1
  15. package/lib/deck-export-pptx.mjs +223 -18
  16. package/lib/diagram-quality.mjs +161 -0
  17. package/lib/document-export.mjs +1 -1
  18. package/lib/env-config.mjs +8 -8
  19. package/lib/export-validate.mjs +106 -0
  20. package/lib/gates-audit.mjs +34 -0
  21. package/lib/health-check.mjs +23 -9
  22. package/lib/hooks/session-start.mjs +2 -1
  23. package/lib/init-unified.mjs +5 -3
  24. package/lib/mcp/server.mjs +52 -2
  25. package/lib/mcp/tool-recovery.mjs +56 -0
  26. package/lib/mcp/tools/web-search.mjs +94 -0
  27. package/lib/mcp-manager.mjs +33 -21
  28. package/lib/mcp-platform-config.mjs +44 -9
  29. package/lib/models/provider-poll.mjs +32 -6
  30. package/lib/orchestration/readiness.mjs +183 -0
  31. package/lib/output-quality.mjs +90 -0
  32. package/lib/pixel-regression.mjs +172 -0
  33. package/lib/providers/op-run.mjs +6 -5
  34. package/lib/providers/secret-audit-wiring.mjs +32 -0
  35. package/lib/providers/secret-resolver.mjs +90 -20
  36. package/lib/publish.mjs +64 -1
  37. package/lib/render-evidence.mjs +55 -0
  38. package/lib/render-pipeline.mjs +136 -0
  39. package/lib/service-manager.mjs +31 -10
  40. package/lib/templates/doc-presentation.mjs +24 -0
  41. package/lib/tracking-surfaces.mjs +36 -0
  42. package/lib/visual-review.mjs +70 -0
  43. package/package.json +1 -1
  44. package/scripts/sync-specialists.mjs +3 -3
  45. package/specialists/artifact-manifest.json +18 -0
  46. package/specialists/artifact-manifest.schema.json +46 -2
  47. package/templates/distribution/construct-brand.typ +1 -2
  48. package/templates/distribution/construct-deck.html +34 -34
  49. package/templates/distribution/construct-pdf.typ +1 -1
  50. package/templates/distribution/construct-web.html +8 -10
@@ -0,0 +1,70 @@
1
+ /**
2
+ * lib/visual-review.mjs — Rubric registry and the visually-reviewed evidence contract.
3
+ *
4
+ * A visual review is recordable only from a real captured screenshot, a named rubric, and a
5
+ * reviewer verdict — never inferred from source text. recordVisualReview throws unless it is handed
6
+ * screenshot-captured evidence with no degradation, so the visually-reviewed rung cannot be forged
7
+ * without a rendered image. The verdict rides in the proof and gates the later approved rung; the
8
+ * review itself (model or human judgment) is supplied by the caller, not fabricated here.
9
+ */
10
+ import { makeEvidence } from './artifact-completion.mjs';
11
+
12
+ export const VERDICTS = Object.freeze(['pass', 'needs-changes', 'fail']);
13
+
14
+ export const RUBRICS = Object.freeze({
15
+ 'document-v1': {
16
+ id: 'document-v1',
17
+ applies: ['pdf', 'docx', 'html'],
18
+ criteria: ['spacing and visual rhythm', 'visual hierarchy and scan-ability', 'reading order sanity'],
19
+ },
20
+ 'deck-v1': {
21
+ id: 'deck-v1',
22
+ applies: ['pptx', 'deck'],
23
+ criteria: ['slide density', 'font legibility at slide distance', 'no clipped or overflowing content'],
24
+ },
25
+ 'diagram-v1': {
26
+ id: 'diagram-v1',
27
+ applies: ['mermaid', 'd2'],
28
+ criteria: ['purpose is clear', 'labels are readable', 'density is manageable', 'non-happy path shown where relevant'],
29
+ },
30
+ });
31
+
32
+ export function makeVisualReview({ rubricId, image, verdict, reviewer, notes = '' } = {}) {
33
+ if (!RUBRICS[rubricId]) {
34
+ throw new Error(`unknown rubric: ${rubricId} (expected one of ${Object.keys(RUBRICS).join(', ')})`);
35
+ }
36
+ if (typeof image !== 'string' || image.length === 0) {
37
+ throw new Error('visual review requires a rendered image path');
38
+ }
39
+ if (!VERDICTS.includes(verdict)) {
40
+ throw new Error(`unknown verdict: ${verdict} (expected one of ${VERDICTS.join(', ')})`);
41
+ }
42
+ if (typeof reviewer !== 'string' || reviewer.length === 0) {
43
+ throw new Error('visual review requires a reviewer');
44
+ }
45
+ return Object.freeze({ rubricId, image, verdict, reviewer, notes });
46
+ }
47
+
48
+ // The review must trace to a real captured screenshot; handing in anything else (a degraded
49
+ // render, or no evidence at all) throws, so visually-reviewed is unreachable from source alone.
50
+
51
+ export function recordVisualReview({ screenshotEvidence, review, actor = 'construct-review' } = {}) {
52
+ if (!screenshotEvidence
53
+ || screenshotEvidence.state !== 'screenshot-captured'
54
+ || screenshotEvidence.degradation) {
55
+ throw new Error('visual review requires non-degraded screenshot-captured evidence (no source inference)');
56
+ }
57
+ if (!review || !RUBRICS[review.rubricId] || !VERDICTS.includes(review.verdict)) {
58
+ throw new Error('visual review requires a valid review report');
59
+ }
60
+ return makeEvidence('visually-reviewed', {
61
+ actor,
62
+ artifact: screenshotEvidence.artifact,
63
+ proof: {
64
+ rubricId: review.rubricId,
65
+ image: review.image,
66
+ verdict: review.verdict,
67
+ reviewer: review.reviewer,
68
+ },
69
+ });
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "packageManager": "npm@11.5.1",
6
6
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
@@ -1481,7 +1481,7 @@ function syncVSCode(targetDir = null, wants = true) {
1481
1481
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1482
1482
  const transportMismatch = registryWantsCommand && existingIsRemote;
1483
1483
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1484
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1484
+ config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1485
1485
  }
1486
1486
  if (!DRY_RUN) {
1487
1487
  mkdirp(path.dirname(mcpPath));
@@ -1505,7 +1505,7 @@ function syncVSCode(targetDir = null, wants = true) {
1505
1505
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1506
1506
  const transportMismatch = registryWantsCommand && existingIsRemote;
1507
1507
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1508
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1508
+ config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1509
1509
  }
1510
1510
  if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
1511
1511
  synced = true;
@@ -1575,7 +1575,7 @@ function syncCursor(targetDir = null, wants = true) {
1575
1575
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1576
1576
  const transportMismatch = registryWantsCommand && existingIsRemote;
1577
1577
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1578
- config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1578
+ config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1579
1579
  }
1580
1580
  if (!DRY_RUN) {
1581
1581
  mkdirp(path.dirname(cursorMcpPath));
@@ -9,6 +9,10 @@
9
9
  "outputs": {
10
10
  "formats": ["pdf", "docx", "doc", "deck", "pptx", "html", "rtf", "odt", "epub", "tex", "txt", "md", "mdx"],
11
11
  "branding": "construct"
12
+ },
13
+ "qualityContract": {
14
+ "gateLevel": "standard",
15
+ "requiredStates": ["exported"]
12
16
  }
13
17
  },
14
18
  "artifacts": {
@@ -31,6 +35,13 @@
31
35
  "proseMinimum": 3,
32
36
  "requiredReviewers": ["cx-devil-advocate"],
33
37
  "optionalReviewers": ["cx-ux-researcher"]
38
+ },
39
+ "qualityContract": {
40
+ "gateLevel": "render-smoke",
41
+ "requiredStates": ["exported", "file-valid", "renderable"],
42
+ "perFormat": {
43
+ "pdf": { "requiredStates": ["file-valid", "renderable"], "a11y": "wcag-2.1-aa" }
44
+ }
34
45
  }
35
46
  },
36
47
  "prd-platform": {
@@ -318,6 +329,13 @@
318
329
  "proseMinimum": 2,
319
330
  "requiredReviewers": ["cx-devil-advocate"],
320
331
  "optionalReviewers": []
332
+ },
333
+ "qualityContract": {
334
+ "gateLevel": "render-smoke",
335
+ "requiredStates": ["exported", "file-valid", "renderable"],
336
+ "perFormat": {
337
+ "pdf": { "requiredStates": ["file-valid", "renderable"] }
338
+ }
321
339
  }
322
340
  },
323
341
  "customer-profile": {
@@ -41,7 +41,8 @@
41
41
  "authorChain": { "type": "array", "items": { "type": "string" } },
42
42
  "reviewerChain": { "type": "array", "items": { "type": "string" } },
43
43
  "validation": { "$ref": "#/$defs/validation" },
44
- "outputs": { "$ref": "#/$defs/outputs" }
44
+ "outputs": { "$ref": "#/$defs/outputs" },
45
+ "qualityContract": { "$ref": "#/$defs/qualityContract" }
45
46
  }
46
47
  },
47
48
  "validation": {
@@ -58,6 +59,48 @@
58
59
  "branding": { "type": "string", "enum": ["construct", "plain"] }
59
60
  }
60
61
  },
62
+ "gateLevel": {
63
+ "type": "string",
64
+ "enum": ["fast", "standard", "render-smoke", "full-certification", "human-reviewed"]
65
+ },
66
+ "completionState": {
67
+ "type": "string",
68
+ "enum": [
69
+ "planned",
70
+ "authored",
71
+ "structurally-valid",
72
+ "source-linted",
73
+ "exported",
74
+ "file-valid",
75
+ "renderable",
76
+ "screenshot-captured",
77
+ "visually-reviewed",
78
+ "accessibility-reviewed",
79
+ "approved",
80
+ "completed"
81
+ ]
82
+ },
83
+ "qualityContract": {
84
+ "type": "object",
85
+ "additionalProperties": false,
86
+ "properties": {
87
+ "gateLevel": { "$ref": "#/$defs/gateLevel" },
88
+ "requiredStates": { "type": "array", "items": { "$ref": "#/$defs/completionState" } },
89
+ "perFormat": {
90
+ "type": "object",
91
+ "additionalProperties": {
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "gateLevel": { "$ref": "#/$defs/gateLevel" },
96
+ "requiredStates": { "type": "array", "items": { "$ref": "#/$defs/completionState" } },
97
+ "a11y": { "type": "string" },
98
+ "fontFloorPt": { "type": "number" }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ },
61
104
  "artifactEntry": {
62
105
  "type": "object",
63
106
  "required": ["template", "primaryOwners", "toneDefault", "releaseGate"],
@@ -76,7 +119,8 @@
76
119
  "authorChain": { "type": "array", "items": { "type": "string" } },
77
120
  "reviewerChain": { "type": "array", "items": { "type": "string" } },
78
121
  "validation": { "$ref": "#/$defs/validation" },
79
- "outputs": { "$ref": "#/$defs/outputs" }
122
+ "outputs": { "$ref": "#/$defs/outputs" },
123
+ "qualityContract": { "$ref": "#/$defs/qualityContract" }
80
124
  }
81
125
  }
82
126
  }
@@ -280,8 +280,7 @@
280
280
  #v(0.28em)
281
281
  #set text(font: construct-font-sans, size: fs-micro, fill: ink-faint)
282
282
  #align(center)[
283
- #text(weight: wt-bold, fill: ink, tracking: 0.04em)[Construct]
284
- #text[ #sym.dot.c #footer-label]
283
+ #text(weight: wt-bold, fill: ink, tracking: 0.04em)[#footer-label]
285
284
  #if classification != "" [ #sym.dot.c #upper(classification)]
286
285
  ]
287
286
  ]
@@ -1,15 +1,14 @@
1
1
  <!--
2
- templates/distribution/construct-deck.html Construct branded 16:9 slide deck
3
- HTML for `construct export --to=deck`. Pandoc emits section divs (--section-divs)
4
- from markdown slides separated by ---; Space Grotesk via Google Fonts; monochrome ink
5
- tokens mirror construct-brand.typ. Metadata variables fill the title slide.
2
+ Standalone 16:9 slide-deck HTML export template. Pandoc emits section divs
3
+ (--section-divs) from markdown slides separated by ---; Space Grotesk via Google
4
+ Fonts; monochrome ink tokens. Metadata variables fill the title slide.
6
5
  -->
7
6
  <!DOCTYPE html>
8
7
  <html lang="en">
9
8
  <head>
10
9
  <meta charset="utf-8">
11
10
  <meta name="viewport" content="width=device-width, initial-scale=1">
12
- <title>$if(title)$$title$$else$Construct deck$endif$</title>
11
+ <title>$if(title)$$title$$else$Slides$endif$</title>
13
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
14
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
14
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -24,57 +23,58 @@
24
23
  }
25
24
  * { box-sizing:border-box; }
26
25
  html { scroll-behavior:smooth; -webkit-text-size-adjust:100%; }
27
- body { margin:0; background:#e8e9ec; color:var(--ink-body); font-family:var(--sans); font-size:22px; line-height:1.45; -webkit-font-smoothing:antialiased; }
26
+ body { margin:0; background:#e8e9ec; color:var(--ink-body); font-family:var(--sans); font-size:17px; line-height:1.5; -webkit-font-smoothing:antialiased; }
28
27
  .deck { display:flex; flex-direction:column; align-items:center; gap:32px; padding:40px 24px 80px; }
29
- section.slide {
28
+ .deck > section {
30
29
  width:min(100%, var(--slide-w));
31
30
  aspect-ratio:16/9;
32
31
  background:var(--paper);
33
32
  border:1px solid var(--hairline);
34
33
  border-radius:8px;
35
34
  box-shadow:0 18px 48px rgba(10,12,16,.08);
36
- padding:56px 64px 48px;
35
+ padding:48px 56px 40px;
37
36
  overflow:hidden;
38
37
  display:flex;
39
38
  flex-direction:column;
40
39
  position:relative;
41
40
  }
42
- section.slide::after {
41
+ .deck > section::after {
43
42
  content:"";
44
43
  position:absolute;
45
44
  left:0; right:0; bottom:0;
46
45
  height:4px;
47
46
  background:linear-gradient(90deg, var(--ink) 46px, var(--hairline) 46px);
48
47
  }
49
- section.slide.title-slide { justify-content:center; }
50
- section.slide.title-slide h1 { font-size:52px; line-height:1.06; letter-spacing:-.024em; margin:0 0 16px; color:var(--ink); font-weight:600; }
51
- section.slide.title-slide .subtitle { font-size:26px; color:var(--muted); margin:0 0 28px; letter-spacing:-.005em; }
52
- section.slide.title-slide .meta { font-size:15px; color:var(--muted); letter-spacing:.04em; text-transform:uppercase; font-weight:600; }
53
- section.slide.title-slide .meta .sep { color:var(--faint); font-weight:400; }
54
- section.slide h1 { font-size:40px; line-height:1.08; letter-spacing:-.02em; margin:0 0 20px; color:var(--ink); font-weight:600; }
55
- section.slide h1::after { content:""; display:block; width:36px; height:3px; background:var(--ink); margin-top:14px; }
56
- section.slide h2 { font-size:28px; font-weight:600; color:var(--ink-strong); margin:28px 0 12px; letter-spacing:-.01em; }
57
- section.slide h3 { font-size:22px; font-weight:600; color:var(--ink-strong); margin:20px 0 8px; }
58
- section.slide p { margin:0 0 14px; max-width:52em; }
59
- section.slide ul, section.slide ol { margin:0 0 16px; padding-left:1.2em; }
60
- section.slide li { margin:0 0 10px; }
61
- section.slide li::marker { color:var(--ink); }
62
- section.slide strong { color:var(--ink); font-weight:600; }
63
- section.slide code { font-family:var(--mono); font-size:.84em; background:var(--surface-alt); padding:1px 6px; border-radius:4px; }
64
- section.slide pre { background:var(--surface); border:1px solid var(--hairline); border-radius:8px; padding:16px 18px; overflow:auto; font-size:17px; }
65
- section.slide pre code { background:none; padding:0; }
66
- section.slide blockquote { margin:16px 0; padding:14px 18px; background:var(--surface-alt); border-left:3px solid var(--ink); border-radius:0 4px 4px 0; }
67
- section.slide table { width:100%; border-collapse:collapse; font-size:18px; margin:12px 0; }
68
- section.slide th, section.slide td { text-align:left; padding:8px 12px; border-bottom:1px solid var(--hairline); }
69
- section.slide thead th { font-weight:700; color:var(--ink); border-bottom:2px solid var(--ink); }
70
- section.slide figure { margin:16px 0; text-align:center; }
71
- section.slide figure img { max-width:58%; max-height:42vh; width:auto; height:auto; object-fit:contain; border:1px solid var(--hairline); border-radius:6px; padding:8px; }
48
+ .deck > section.title-slide { justify-content:center; }
49
+ .deck > section.title-slide h1 { font-size:38px; line-height:1.1; letter-spacing:-.022em; margin:0 0 14px; color:var(--ink); font-weight:600; }
50
+ .deck > section.title-slide .subtitle { font-size:19px; color:var(--muted); margin:0 0 24px; letter-spacing:-.005em; }
51
+ .deck > section.title-slide .meta { font-size:13px; color:var(--muted); letter-spacing:.04em; text-transform:uppercase; font-weight:600; }
52
+ .deck > section.title-slide .meta .sep { color:var(--faint); font-weight:400; }
53
+ .deck > section h1 { font-size:28px; line-height:1.12; letter-spacing:-.018em; margin:0 0 16px; color:var(--ink); font-weight:600; }
54
+ .deck > section h1::after { content:""; display:block; width:30px; height:2px; background:var(--ink); margin-top:12px; }
55
+ .deck > section h2 { font-size:20px; font-weight:600; color:var(--ink-strong); margin:22px 0 10px; letter-spacing:-.01em; }
56
+ .deck > section h3 { font-size:16px; font-weight:600; color:var(--ink-strong); margin:16px 0 6px; }
57
+ .deck > section p { margin:0 0 12px; max-width:54em; }
58
+ .deck > section ul, .deck > section ol { margin:0 0 12px; padding-left:1.2em; }
59
+ .deck > section li { margin:0 0 7px; }
60
+ .deck > section li::marker { color:var(--ink); }
61
+ .deck > section strong { color:var(--ink); font-weight:600; }
62
+ .deck > section code { font-family:var(--mono); font-size:.84em; background:var(--surface-alt); padding:1px 6px; border-radius:4px; }
63
+ .deck > section pre { background:var(--surface); border:1px solid var(--hairline); border-radius:8px; padding:14px 16px; overflow:auto; font-size:13.5px; }
64
+ .deck > section pre code { background:none; padding:0; }
65
+ .deck > section blockquote { margin:16px 0; padding:14px 18px; background:var(--surface-alt); border-left:3px solid var(--ink); border-radius:0 4px 4px 0; }
66
+ .deck > section table { width:100%; border-collapse:collapse; font-size:15px; margin:10px 0; }
67
+ .deck > section th, .deck > section td { text-align:left; padding:8px 12px; border-bottom:1px solid var(--hairline); }
68
+ .deck > section thead th { font-weight:700; color:var(--ink); border-bottom:2px solid var(--ink); }
69
+ .deck > section figure { margin:12px 0; text-align:center; flex:1 1 auto; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; }
70
+ .deck > section figure img { max-width:80%; max-height:100%; width:auto; height:auto; object-fit:contain; border:1px solid var(--hairline); border-radius:6px; padding:8px; }
71
+ .deck > section > img { display:block; margin:12px auto; flex:1 1 auto; min-height:0; max-width:84%; max-height:100%; width:auto; height:auto; object-fit:contain; border:1px solid var(--hairline); border-radius:6px; padding:8px; }
72
72
  .slide-foot { margin-top:auto; padding-top:16px; font-size:13px; color:var(--faint); display:flex; justify-content:space-between; align-items:center; }
73
73
  .slide-foot b { color:var(--ink); font-weight:600; }
74
74
  @media print {
75
75
  body { background:var(--paper); }
76
76
  .deck { gap:0; padding:0; }
77
- section.slide { box-shadow:none; border:none; border-radius:0; page-break-after:always; width:100%; height:100vh; }
77
+ .deck > section { box-shadow:none; border:none; border-radius:0; page-break-after:always; width:100%; height:100vh; }
78
78
  }
79
79
  </style>
80
80
  </head>
@@ -86,7 +86,7 @@ $if(artifactType)$<div class="meta">$artifactType$$if(docId)$ <span class="sep">
86
86
  <h1>$title$</h1>
87
87
  $if(subtitle)$<p class="subtitle">$subtitle$</p>$endif$
88
88
  $if(owner)$<p class="meta">$owner$$if(date)$ <span class="sep">&middot;</span> $date$$endif$$if(status)$ <span class="sep">&middot;</span> $status$$endif$</p>$endif$
89
- <div class="slide-foot"><span><b>Construct</b>$if(classification)$ &middot; $classification$$endif$</span></div>
89
+ <div class="slide-foot"><span>$if(classification)$<b>$classification$</b>$endif$$if(date)$$if(classification)$ &middot; $endif$$date$$endif$</span></div>
90
90
  </section>
91
91
  $endif$
92
92
  $body$
@@ -16,7 +16,7 @@
16
16
  version: "$if(version)$$version$$endif$",
17
17
  ),
18
18
  footer: construct-running-footer(
19
- "Construct brief",
19
+ "Brief",
20
20
  classification: "$if(classification)$$classification$$endif$",
21
21
  ),
22
22
  )
@@ -1,18 +1,16 @@
1
1
  <!--
2
- templates/distribution/construct-web.html Construct branded standalone HTML
3
- template for `construct export --to=html`. Monochrome design mirroring the PDF
4
- brand (construct-brand.typ): Space Grotesk webfont, masthead from frontmatter metadata,
5
- bold-header hairline tables, callout blockquotes, framed figures. Pandoc fills
6
- $title$/$subtitle$/$status$/$owner$/$date$/$artifactType$/$version$/$docId$/
7
- $classification$ and $body$; export runs with --embed-resources so diagrams and
8
- fonts inline into a single portable file.
2
+ Standalone branded HTML export template. Monochrome design: Space Grotesk
3
+ webfont, masthead from frontmatter metadata, bold-header hairline tables, callout
4
+ blockquotes, framed figures. Pandoc fills the title, subtitle, status, owner,
5
+ date, artifactType, version, docId, classification, and document body; export
6
+ runs with --embed-resources so diagrams and fonts inline into one portable file.
9
7
  -->
10
8
  <!DOCTYPE html>
11
9
  <html lang="en">
12
10
  <head>
13
11
  <meta charset="utf-8">
14
12
  <meta name="viewport" content="width=device-width, initial-scale=1">
15
- <title>$if(title)$$title$$else$Construct document$endif$</title>
13
+ <title>$if(title)$$title$$else$Document$endif$</title>
16
14
  <link rel="preconnect" href="https://fonts.googleapis.com">
17
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
18
16
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -79,14 +77,14 @@
79
77
  $if(title)$
80
78
  <header class="masthead">
81
79
  $if(artifactType)$ <div class="chips">$artifactType$$if(docId)$ <span class="sep">&middot;</span> $docId$$endif$$if(version)$ <span class="sep">&middot;</span> v$version$$endif$$if(classification)$ <span class="sep">&middot;</span> $classification$$endif$</div>
82
- $endif$ <div class="title">$title$</div>
80
+ $endif$ <h1 class="title">$title$</h1>
83
81
  $if(subtitle)$ <p class="subtitle">$subtitle$</p>
84
82
  $endif$ <div class="byline">$if(status)$<span class="pill">$status$</span>$endif$$if(owner)$<span>$owner$</span>$endif$$if(date)$<span class="sep">&middot;</span><span>$date$</span>$endif$</div>
85
83
  </header>
86
84
  <div class="rule"></div>
87
85
  $endif$
88
86
  $body$
89
- <div class="doc-foot"><b>Construct</b>$if(classification)$ &middot; $classification$$endif$</div>
87
+ <div class="doc-foot">$if(classification)$<b>$classification$</b>$endif$$if(date)$$if(classification)$ &middot; $endif$$date$$endif$</div>
90
88
  </main>
91
89
  </body>
92
90
  </html>