@geraldmaron/construct 1.3.2 → 1.4.1
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/guard-bash.mjs +3 -1
- package/lib/hooks/session-start.mjs +2 -1
- package/lib/init-unified.mjs +5 -3
- package/lib/mcp/server.mjs +53 -3
- package/lib/mcp/tool-recovery.mjs +56 -0
- package/lib/mcp/tools/web-search.mjs +127 -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/orchestration-policy.mjs +36 -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/scripts/sync-specialists.mjs +107 -11
- 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
|
@@ -448,6 +448,13 @@ const standardConstructTools = [
|
|
|
448
448
|
"cx_trace",
|
|
449
449
|
"cx_score",
|
|
450
450
|
].join(",");
|
|
451
|
+
|
|
452
|
+
// The orchestrator's atomic contract is classify-then-dispatch: orchestration_policy
|
|
453
|
+
// then orchestration_run. On allowlist hosts (Claude) these must be named in the
|
|
454
|
+
// agent's tools list or the call is blocked, leaving the orchestrator unable to route.
|
|
455
|
+
// Single source of truth so every host's orchestrator grant stays in parity.
|
|
456
|
+
|
|
457
|
+
const ORCHESTRATOR_DISPATCH_TOOLS = ["orchestration_policy", "orchestration_run"];
|
|
451
458
|
const managedStart = `# BEGIN ${systemName.toUpperCase()} AGENTS`;
|
|
452
459
|
const managedEnd = `# END ${systemName.toUpperCase()} AGENTS`;
|
|
453
460
|
const mdManagedStart = `<!-- BEGIN ${systemName.toUpperCase()} AGENTS -->`;
|
|
@@ -765,7 +772,8 @@ function orchestrationMicroPrompt(platform) {
|
|
|
765
772
|
`You are the primary orchestrator. Before any non-trivial answer, call \`${policyTool}\` with the user's \`request\`. Do not guess agent names or workflow types.\n\n` +
|
|
766
773
|
`Example — the user says "add rate limiting to the API". Your first action is a tool call, not prose:\n` +
|
|
767
774
|
` call ${policyTool} { "request": "add rate limiting to the API" }\n` +
|
|
768
|
-
`If the route is focused/orchestrated specialist work, call \`${runTool}\` with the same request. If the route suggests a workflow such as \`research-synthesis\`, pass it as \`workflow_type\`. Do not narrate completed research unless \`${runTool}\` or evidence tools actually ran
|
|
775
|
+
`If the route is focused/orchestrated specialist work, call \`${runTool}\` with the same request. If the route suggests a workflow such as \`research-synthesis\`, pass it as \`workflow_type\`. Do not narrate completed research unless \`${runTool}\` or evidence tools actually ran.\n\n` +
|
|
776
|
+
`If a request needs a capability you lack — live web/network access, external data, code execution — do not refuse or tell the user to run it themselves. Route it via \`${runTool}\` to the specialist that holds the capability (web access lives with the researcher), or ask one clarifying question when the target is ambiguous.`
|
|
769
777
|
);
|
|
770
778
|
}
|
|
771
779
|
|
|
@@ -984,6 +992,13 @@ function claudeAgentMarkdown(entry, allEntries) {
|
|
|
984
992
|
...baseTools.split(",").map((t) => t.trim()),
|
|
985
993
|
...standardConstructTools.split(","),
|
|
986
994
|
]);
|
|
995
|
+
|
|
996
|
+
// Claude's tools list is a hard allowlist; the orchestrator can only route if its
|
|
997
|
+
// dispatch tools are named here.
|
|
998
|
+
|
|
999
|
+
if (entry.isOrchestrator) {
|
|
1000
|
+
for (const tool of ORCHESTRATOR_DISPATCH_TOOLS) toolSet.add(tool);
|
|
1001
|
+
}
|
|
987
1002
|
const tools = Array.from(toolSet).filter(Boolean).join(",");
|
|
988
1003
|
|
|
989
1004
|
return `---
|
|
@@ -1346,6 +1361,40 @@ When using this prompt, stay within the role above and adapt to the current repo
|
|
|
1346
1361
|
`;
|
|
1347
1362
|
}
|
|
1348
1363
|
|
|
1364
|
+
// VS Code reads custom agents (the renamed successor to chat modes) from
|
|
1365
|
+
// .github/agents/<name>.agent.md, and tool grants must use its namespaced ids:
|
|
1366
|
+
// <server>/* for an MCP server's tools, web/fetch for outbound web, search/read
|
|
1367
|
+
// for repo awareness. The Claude-format tools in .claude/agents/*.md are not
|
|
1368
|
+
// recognized here, so the orchestrator needs its own VS Code agent or it lists
|
|
1369
|
+
// in the picker with no usable tools.
|
|
1370
|
+
|
|
1371
|
+
const COPILOT_AGENT_TOOLS = [
|
|
1372
|
+
"construct-mcp/*",
|
|
1373
|
+
"web/fetch",
|
|
1374
|
+
"web/githubRepo",
|
|
1375
|
+
"search/codebase",
|
|
1376
|
+
"search/usages",
|
|
1377
|
+
"search/fileSearch",
|
|
1378
|
+
"read/problems",
|
|
1379
|
+
"edit/editFiles",
|
|
1380
|
+
];
|
|
1381
|
+
|
|
1382
|
+
function copilotAgentFile(entry, allEntries) {
|
|
1383
|
+
const name = adapterName(entry);
|
|
1384
|
+
return `---
|
|
1385
|
+
description: ${entry.description}
|
|
1386
|
+
name: ${name}
|
|
1387
|
+
tools: ${JSON.stringify(COPILOT_AGENT_TOOLS)}
|
|
1388
|
+
---
|
|
1389
|
+
|
|
1390
|
+
${generatedMarkdownNote}
|
|
1391
|
+
|
|
1392
|
+
# ${name}
|
|
1393
|
+
|
|
1394
|
+
${buildPrompt(entry, allEntries, "copilot")}
|
|
1395
|
+
`;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1349
1398
|
function syncCopilot(entries, targetDir = null, wants = true) {
|
|
1350
1399
|
const promptsDir = targetDir
|
|
1351
1400
|
? path.join(targetDir, ".github", "prompts")
|
|
@@ -1362,15 +1411,20 @@ function syncCopilot(entries, targetDir = null, wants = true) {
|
|
|
1362
1411
|
sweepLegacyPrefixedFiles(promptsDir, ".prompt.md", writeEntries.map((e) => `${adapterName(e)}.prompt.md`));
|
|
1363
1412
|
}
|
|
1364
1413
|
|
|
1365
|
-
// VS Code
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1414
|
+
// VS Code reads custom agents from `.github/agents/*.agent.md`. The Claude
|
|
1415
|
+
// tool names in `.claude/agents/*.md` are not recognized there, so the front
|
|
1416
|
+
// door ships as a VS Code agent with namespaced tool grants (construct-mcp/*,
|
|
1417
|
+
// web/fetch, search/read) — selecting it in the dropdown then scopes those
|
|
1418
|
+
// tools in. The Claude-format set stays for Claude Code.
|
|
1369
1419
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1420
|
+
const agentsDir = targetDir
|
|
1421
|
+
? path.join(targetDir, ".github", "agents")
|
|
1422
|
+
: path.join(home, ".github", "agents");
|
|
1423
|
+
if (!DRY_RUN && wants) mkdirp(agentsDir);
|
|
1424
|
+
for (const entry of writeEntries) {
|
|
1425
|
+
writeFile(path.join(agentsDir, `${adapterName(entry)}.agent.md`), copilotAgentFile(entry, entries), { stamp: false });
|
|
1373
1426
|
}
|
|
1427
|
+
if (fs.existsSync(agentsDir)) removeStaleAdapters(agentsDir, ".agent.md", writeEntries);
|
|
1374
1428
|
|
|
1375
1429
|
const instructionsPath = targetDir
|
|
1376
1430
|
? path.join(targetDir, ".github", "copilot-instructions.md")
|
|
@@ -1387,7 +1441,7 @@ function syncCopilot(entries, targetDir = null, wants = true) {
|
|
|
1387
1441
|
const list = listEntries.map((e) => `- \`${adapterName(e)}\`: use \`${promptPathPrefix}/${adapterName(e)}.prompt.md\`.`).join("\n");
|
|
1388
1442
|
const note = `## ${systemName.charAt(0).toUpperCase() + systemName.slice(1)} Agent Prompts
|
|
1389
1443
|
|
|
1390
|
-
|
|
1444
|
+
Select \`${systemName}\` from the chat mode dropdown to enter the orchestrator: describe an outcome and it classifies the request and dispatches the right specialists through the \`construct-mcp\` tools (\`orchestration_policy\` then \`orchestration_run\`). You ask for outcomes, not specialists — it routes internally. Requires the \`construct-mcp\` server (wired in \`.vscode/mcp.json\`); if its tools are unavailable, the mode cannot route and will say so rather than guess.
|
|
1391
1445
|
|
|
1392
1446
|
${list || "(no front-door prompts to surface)"}`;
|
|
1393
1447
|
|
|
@@ -1432,6 +1486,45 @@ function getVSCodeUserMcpPaths() {
|
|
|
1432
1486
|
.filter((file) => fs.existsSync(file));
|
|
1433
1487
|
}
|
|
1434
1488
|
|
|
1489
|
+
// A merged mcp.json preserves existing entries so user customizations survive a
|
|
1490
|
+
// re-sync. A construct-owned server path is a fully-resolved, non-placeholder
|
|
1491
|
+
// path, so the preserve rule keeps it even when it points at a different toolkit
|
|
1492
|
+
// root than the current one — and VS Code then launches a server that may not
|
|
1493
|
+
// exist. Treat a construct toolkit path outside the current root as stale so the
|
|
1494
|
+
// sync refreshes it; user-owned servers carry no lib/mcp toolkit path and stay.
|
|
1495
|
+
|
|
1496
|
+
export function mcpEntryPointsOutsideToolkit(entry, root) {
|
|
1497
|
+
const args = Array.isArray(entry?.args) ? entry.args : [];
|
|
1498
|
+
return args.some(
|
|
1499
|
+
(arg) => typeof arg === "string"
|
|
1500
|
+
&& /\/lib\/mcp\/[a-z0-9-]+\.mjs$/.test(arg)
|
|
1501
|
+
&& !arg.startsWith(`${root}/`),
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// VS Code scans both `.github/agents` and `.claude/agents`, so the orchestrator
|
|
1506
|
+
// would list twice — once with VS Code tools (.github/agents) and once with
|
|
1507
|
+
// Claude tool names VS Code ignores (.claude/agents). `chat.agentFilesLocations`
|
|
1508
|
+
// is the documented lever to pin the scan; its power over the built-in
|
|
1509
|
+
// `.claude/agents` compatibility scan is version-dependent, so this is a
|
|
1510
|
+
// best-effort hint, not a guarantee. Merge only into a strictly-parseable file
|
|
1511
|
+
// and never overwrite an existing choice, so a commented (JSONC) or
|
|
1512
|
+
// user-customized settings.json is left untouched.
|
|
1513
|
+
|
|
1514
|
+
export function pinVscodeAgentLocations(targetDir) {
|
|
1515
|
+
if (DRY_RUN) return;
|
|
1516
|
+
const settingsPath = path.join(targetDir, ".vscode", "settings.json");
|
|
1517
|
+
let settings = {};
|
|
1518
|
+
if (fs.existsSync(settingsPath)) {
|
|
1519
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")) || {}; }
|
|
1520
|
+
catch { return; }
|
|
1521
|
+
}
|
|
1522
|
+
if (settings["chat.agentFilesLocations"]) return;
|
|
1523
|
+
settings["chat.agentFilesLocations"] = { ".github/agents": true, ".claude/agents": false };
|
|
1524
|
+
mkdirp(path.dirname(settingsPath));
|
|
1525
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1435
1528
|
function syncVSCode(targetDir = null, wants = true) {
|
|
1436
1529
|
const registryMcp = scopedManagedMcpDefs({ projectScope: Boolean(targetDir) });
|
|
1437
1530
|
if (Object.keys(registryMcp).length === 0) return false;
|
|
@@ -1480,13 +1573,15 @@ function syncVSCode(targetDir = null, wants = true) {
|
|
|
1480
1573
|
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
1481
1574
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1482
1575
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1483
|
-
|
|
1576
|
+
const staleToolkitPath = mcpEntryPointsOutsideToolkit(existingEntry, root);
|
|
1577
|
+
if (existingEntry && !hasPlaceholder && !transportMismatch && !staleToolkitPath) continue;
|
|
1484
1578
|
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
|
|
1485
1579
|
}
|
|
1486
1580
|
if (!DRY_RUN) {
|
|
1487
1581
|
mkdirp(path.dirname(mcpPath));
|
|
1488
1582
|
fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1489
1583
|
}
|
|
1584
|
+
pinVscodeAgentLocations(targetDir);
|
|
1490
1585
|
return true;
|
|
1491
1586
|
}
|
|
1492
1587
|
|
|
@@ -1504,7 +1599,8 @@ function syncVSCode(targetDir = null, wants = true) {
|
|
|
1504
1599
|
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
1505
1600
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1506
1601
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1507
|
-
|
|
1602
|
+
const staleToolkitPath = mcpEntryPointsOutsideToolkit(existingEntry, root);
|
|
1603
|
+
if (existingEntry && !hasPlaceholder && !transportMismatch && !staleToolkitPath) continue;
|
|
1508
1604
|
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
|
|
1509
1605
|
}
|
|
1510
1606
|
if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
@@ -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>
|