@geraldmaron/construct 1.3.2 → 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.
- package/README.md +2 -1
- package/bin/construct +61 -15
- package/lib/a11y-audit.mjs +104 -0
- package/lib/artifact-completion-states.mjs +35 -0
- package/lib/artifact-completion.mjs +66 -0
- package/lib/artifact-gate-levels.mjs +69 -0
- package/lib/artifact-manifest.mjs +21 -0
- package/lib/artifact-release-gate.mjs +19 -1
- package/lib/artifact-workflow.mjs +16 -0
- package/lib/brand-contrast.mjs +60 -0
- package/lib/brand-tokens.mjs +14 -0
- package/lib/cli-commands.mjs +5 -3
- package/lib/codex-config.mjs +7 -1
- package/lib/deck-export-pptx.mjs +223 -18
- package/lib/diagram-quality.mjs +161 -0
- package/lib/document-export.mjs +1 -1
- package/lib/env-config.mjs +8 -8
- package/lib/export-validate.mjs +106 -0
- package/lib/gates-audit.mjs +34 -0
- package/lib/health-check.mjs +23 -9
- package/lib/hooks/session-start.mjs +2 -1
- package/lib/init-unified.mjs +5 -3
- package/lib/mcp/server.mjs +52 -2
- package/lib/mcp/tool-recovery.mjs +56 -0
- package/lib/mcp/tools/web-search.mjs +94 -0
- package/lib/mcp-manager.mjs +11 -4
- package/lib/mcp-platform-config.mjs +18 -1
- package/lib/models/provider-poll.mjs +32 -6
- package/lib/orchestration/readiness.mjs +183 -0
- package/lib/output-quality.mjs +90 -0
- package/lib/pixel-regression.mjs +172 -0
- package/lib/providers/op-run.mjs +6 -5
- package/lib/providers/secret-audit-wiring.mjs +32 -0
- package/lib/providers/secret-resolver.mjs +90 -20
- package/lib/publish.mjs +64 -1
- package/lib/render-evidence.mjs +55 -0
- package/lib/render-pipeline.mjs +136 -0
- package/lib/service-manager.mjs +31 -10
- package/lib/templates/doc-presentation.mjs +24 -0
- package/lib/tracking-surfaces.mjs +36 -0
- package/lib/visual-review.mjs +70 -0
- package/package.json +1 -1
- package/specialists/artifact-manifest.json +18 -0
- package/specialists/artifact-manifest.schema.json +46 -2
- package/templates/distribution/construct-brand.typ +1 -2
- package/templates/distribution/construct-deck.html +34 -34
- package/templates/distribution/construct-pdf.typ +1 -1
- 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
|
@@ -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)[
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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$
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
50
|
-
section.
|
|
51
|
-
section.
|
|
52
|
-
section.
|
|
53
|
-
section.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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">·</span> $date$$endif$$if(status)$ <span class="sep">·</span> $status$$endif$</p>$endif$
|
|
89
|
-
<div class="slide-foot"><span
|
|
89
|
+
<div class="slide-foot"><span>$if(classification)$<b>$classification$</b>$endif$$if(date)$$if(classification)$ · $endif$$date$$endif$</span></div>
|
|
90
90
|
</section>
|
|
91
91
|
$endif$
|
|
92
92
|
$body$
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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$
|
|
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">·</span> $docId$$endif$$if(version)$ <span class="sep">·</span> v$version$$endif$$if(classification)$ <span class="sep">·</span> $classification$$endif$</div>
|
|
82
|
-
$endif$ <
|
|
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">·</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"
|
|
87
|
+
<div class="doc-foot">$if(classification)$<b>$classification$</b>$endif$$if(date)$$if(classification)$ · $endif$$date$$endif$</div>
|
|
90
88
|
</main>
|
|
91
89
|
</body>
|
|
92
90
|
</html>
|