@natjswenson/devlog 0.13.0 → 0.14.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/CHANGELOG.md +30 -0
- package/README.md +230 -120
- package/SKILL.md +74 -24
- package/bin/devlog.js +108 -6
- package/config.example.json +1 -0
- package/image-style/style-guide.example.md +29 -26
- package/lib/compose_art_cover.mjs +144 -0
- package/lib/config_ops.mjs +1 -0
- package/lib/core.mjs +3 -0
- package/lib/guide_draft.mjs +134 -0
- package/lib/guide_preview.css +3 -0
- package/lib/publish_guide.mjs +124 -0
- package/package.json +4 -3
- package/references/codex-cover-art.md +119 -0
- package/references/concept-guides.md +148 -0
- package/references/cover-spec.md +40 -0
- package/references/guide-publishing.md +245 -0
- package/skill-invariants.json +34 -13
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Additive, local-only guide preparation. Never executes article code or publishes.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
7
|
+
import ReactMarkdown from 'react-markdown';
|
|
8
|
+
import remarkGfm from 'remark-gfm';
|
|
9
|
+
import sharp from 'sharp';
|
|
10
|
+
import { lintPost, parseFrontmatter } from './lint_post.mjs';
|
|
11
|
+
|
|
12
|
+
const START = '<!-- agent-handoff:start -->';
|
|
13
|
+
const END = '<!-- agent-handoff:end -->';
|
|
14
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
15
|
+
const escape = value => String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
16
|
+
const fail = (code, message, findings) => Object.assign(new Error(message), { code, ...(findings ? { findings } : {}) });
|
|
17
|
+
|
|
18
|
+
function handoff(markdown) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
const add = (rule, message) => findings.push({ rule, message });
|
|
21
|
+
if (markdown.split(START).length !== 2 || markdown.split(END).length !== 2) {
|
|
22
|
+
add('guide-handoff-markers', 'Use exactly one agent-handoff:start and one agent-handoff:end marker.');
|
|
23
|
+
return { findings, reference: markdown };
|
|
24
|
+
}
|
|
25
|
+
const start = markdown.indexOf(START);
|
|
26
|
+
const end = markdown.indexOf(END);
|
|
27
|
+
if (end <= start) {
|
|
28
|
+
add('guide-handoff-order', 'The handoff end marker must follow its start marker.');
|
|
29
|
+
return { findings, reference: markdown };
|
|
30
|
+
}
|
|
31
|
+
const { body } = parseFrontmatter(markdown);
|
|
32
|
+
if (!body.trimStart().startsWith(START)) add('guide-handoff-position', 'The handoff must be the first body content, before the introduction and Shipped.');
|
|
33
|
+
const block = markdown.slice(start + START.length, end);
|
|
34
|
+
const fences = [...block.replaceAll('\r\n', '\n').matchAll(/^```([^\n]*)$/gm)];
|
|
35
|
+
const match = /^```text\r?\n([\s\S]*?)\r?\n```\s*$/m.exec(block);
|
|
36
|
+
if (fences.length !== 2 || fences[0]?.[1] !== 'text' || fences[1]?.[1] !== '' || !match || !match[1].trim()) {
|
|
37
|
+
add('guide-handoff-prompt', 'The handoff must contain exactly one nonempty fenced text prompt.');
|
|
38
|
+
}
|
|
39
|
+
return { findings, prompt: match?.[1], reference: markdown.slice(0, start) + markdown.slice(end + END.length) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Same result shape as lintPost. Structural checks do not prove semantic quality or review.
|
|
43
|
+
export function lintGuide(markdown, { voice = false } = {}) {
|
|
44
|
+
const parsed = handoff(markdown);
|
|
45
|
+
const findings = [...lintPost(parsed.reference.replaceAll('\r\n', '\n'), { voice }).findings, ...parsed.findings];
|
|
46
|
+
return { ok: findings.length === 0, findings };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function brandCss(tokens) {
|
|
50
|
+
const fields = { paper: tokens.colors?.paper, ink: tokens.colors?.ink, dim: tokens.colors?.dim, accent: tokens.colors?.accent, hair: tokens.derived?.hair ?? tokens.colors?.ink };
|
|
51
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
52
|
+
if (typeof value !== 'string' || !/^(#[a-f\d]{6}|rgba\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3},\s*(?:0|1|0?\.\d+)\s*\))$/i.test(value)) throw fail('GUIDE_BRAND_INVALID', `Invalid PRESS color: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
for (const name of ['display', 'mono', 'serif']) {
|
|
55
|
+
const value = tokens.fonts?.[`${name}_stack`];
|
|
56
|
+
if (typeof value !== 'string' || !value.trim() || !/^[a-z\d\s,'"-]+$/i.test(value)) throw fail('GUIDE_BRAND_INVALID', `Invalid PRESS font stack: ${name}`);
|
|
57
|
+
fields[`font-${name}`] = value;
|
|
58
|
+
}
|
|
59
|
+
for (const name of ['stamp', 'name']) if (typeof tokens.identity?.[name] !== 'string' || !tokens.identity[name].trim()) throw fail('GUIDE_BRAND_INVALID', `Missing PRESS identity: ${name}`);
|
|
60
|
+
return `:root{${Object.entries(fields).map(([key, value]) => `--${key}:${value}`).join(';')}}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function prepareGuidePreview({ articlePath, outDir, brandPath, coverPath } = {}) {
|
|
64
|
+
for (const [name, value] of Object.entries({ articlePath, outDir, brandPath })) if (typeof value !== 'string' || !value.trim()) throw fail('GUIDE_ARGUMENT', `${name} is required.`);
|
|
65
|
+
const markdown = await fs.readFile(articlePath, 'utf8');
|
|
66
|
+
const lint = lintGuide(markdown);
|
|
67
|
+
if (!lint.ok) throw fail('GUIDE_LINT', 'Guide failed structural lint.', lint.findings);
|
|
68
|
+
const { prompt, reference } = handoff(markdown);
|
|
69
|
+
const payload = `${prompt}\n\n<reference-guide>\n${reference.trim()}\n</reference-guide>\n`;
|
|
70
|
+
const { data } = parseFrontmatter(markdown.replaceAll('\r\n', '\n'));
|
|
71
|
+
const { body } = parseFrontmatter(reference);
|
|
72
|
+
const brandRaw = await fs.readFile(brandPath, 'utf8');
|
|
73
|
+
let tokens;
|
|
74
|
+
try { tokens = JSON.parse(brandRaw); } catch { throw fail('GUIDE_BRAND_INVALID', 'PRESS brand file must contain JSON.'); }
|
|
75
|
+
if (!tokens || typeof tokens !== 'object') throw fail('GUIDE_BRAND_INVALID', 'PRESS brand file must contain an object.');
|
|
76
|
+
const css = brandCss(tokens);
|
|
77
|
+
let cover;
|
|
78
|
+
if (coverPath) {
|
|
79
|
+
cover = await fs.readFile(coverPath);
|
|
80
|
+
try {
|
|
81
|
+
const decoder = sharp(cover, { limitInputPixels: 40_000_000 });
|
|
82
|
+
const meta = await decoder.metadata();
|
|
83
|
+
if (meta.format !== 'png' || meta.width !== 1600 || meta.height !== 900 || (meta.pages || 1) !== 1) throw new Error('Expected one 1600 × 900 PNG.');
|
|
84
|
+
await decoder.raw().toBuffer();
|
|
85
|
+
} catch (error) { throw fail('GUIDE_COVER_INVALID', `Cover must be a decodable, single-frame 1600 × 900 PNG: ${error.message}`); }
|
|
86
|
+
}
|
|
87
|
+
const styles = await fs.readFile(new URL('./guide_preview.css', import.meta.url), 'utf8');
|
|
88
|
+
const rendered = renderToStaticMarkup(React.createElement(ReactMarkdown, {
|
|
89
|
+
remarkPlugins: [remarkGfm], skipHtml: true,
|
|
90
|
+
components: {
|
|
91
|
+
// Validate resolved Markdown nodes, including reference-style links. This
|
|
92
|
+
// draft helper copies one article, not an arbitrary companion directory.
|
|
93
|
+
a({ node: _node, href, children, ...props }) {
|
|
94
|
+
if (typeof href !== 'string' || !/^(https?:\/\/|#)/i.test(href)) throw fail('GUIDE_ASSET_UNSUPPORTED', 'Guide links must use public HTTP(S) URLs or same-page anchors. Relative companion files are not bundled; include required code inline.');
|
|
95
|
+
return React.createElement('a', { ...props, href }, children);
|
|
96
|
+
},
|
|
97
|
+
img() { throw fail('GUIDE_ASSET_UNSUPPORTED', 'Embedded Markdown images are not bundled. Supply a local PNG with coverPath, or retain the draft until companion-asset support is available.'); },
|
|
98
|
+
},
|
|
99
|
+
}, body));
|
|
100
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(data.title)}</title><style>${css}\n${styles}</style></head><body><main>
|
|
101
|
+
<header class="masthead"><div class="stamp">${escape(tokens.identity.stamp)}</div><div class="eyebrow">CONCEPT GUIDE · LOCAL DRAFT</div><div class="byline">${escape(tokens.identity.name)}</div></header>
|
|
102
|
+
<h1>${escape(data.title)}</h1><section class="agent-handoff" aria-labelledby="agent-heading"><h2 id="agent-heading">Implement this with your agent</h2><p>Copy the implementation prompt and complete guide, then paste into your coding agent in your project.</p><button type="button" id="copy-agent">Copy prompt + guide</button><p id="copy-status" role="status" aria-live="polite"></p><textarea id="copy-fallback" readonly hidden aria-label="Prompt and complete guide for manual copying"></textarea><details><summary>Read the prompt</summary><pre>${escape(prompt)}</pre></details><p><a href="agent-prompt.txt" download>Download prompt + complete guide</a></p><noscript><p>JavaScript is disabled. Open the <a href="agent-prompt.txt">complete plain-text handoff</a> and copy it into your agent.</p></noscript></section>
|
|
103
|
+
${cover ? `<figure><a href="cover.png"><img src="cover.png" width="1600" height="900" alt="Cover for ${escape(data.title)}"></a></figure>` : ''}
|
|
104
|
+
<p class="notice">Draft for review · <a href="article.md">Markdown guide</a> · Structural lint passed; implementation and independent review are separate checks.</p>${rendered}<footer class="colophon">Local draft. This helper does not publish.</footer></main>
|
|
105
|
+
<script type="application/json" id="agent-payload">${JSON.stringify(payload).replaceAll('<', '\\u003c')}</script>
|
|
106
|
+
<script>
|
|
107
|
+
const payload = JSON.parse(document.getElementById('agent-payload').textContent);
|
|
108
|
+
const button = document.getElementById('copy-agent');
|
|
109
|
+
const status = document.getElementById('copy-status');
|
|
110
|
+
const fallback = document.getElementById('copy-fallback');
|
|
111
|
+
button.addEventListener('click', async () => {
|
|
112
|
+
status.textContent = '';
|
|
113
|
+
try {
|
|
114
|
+
if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
|
|
115
|
+
await navigator.clipboard.writeText(payload);
|
|
116
|
+
fallback.hidden = true;
|
|
117
|
+
status.textContent = 'Copied prompt and complete guide. Paste into your agent.';
|
|
118
|
+
} catch {
|
|
119
|
+
fallback.hidden = false; fallback.value = payload; fallback.focus(); fallback.select();
|
|
120
|
+
status.textContent = 'Automatic copy is unavailable. The full text is selected below; press Cmd+C or Ctrl+C.';
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
</script></body></html>`;
|
|
124
|
+
const result = { schemaVersion: 1, status: 'local-draft', article: 'article.md', preview: 'index.html', agentPrompt: 'agent-prompt.txt', cover: cover ? 'cover.png' : null, articleSha256: hash(markdown), previewSha256: hash(html), stylesheetSha256: hash(styles), agentPromptSha256: hash(payload), brandSha256: hash(brandRaw), ...(cover ? { coverSha256: hash(cover) } : {}), lint, verification: { implementation: 'not-run', independentReview: 'not-run' } };
|
|
125
|
+
// mkdir without recursive is the exclusive claim. Existing directories/symlinks fail;
|
|
126
|
+
// a failed partial write retains no result.json completion marker and is never reused.
|
|
127
|
+
await fs.mkdir(outDir);
|
|
128
|
+
await fs.writeFile(path.join(outDir, 'article.md'), markdown, { flag: 'wx' });
|
|
129
|
+
await fs.writeFile(path.join(outDir, 'index.html'), html, { flag: 'wx' });
|
|
130
|
+
await fs.writeFile(path.join(outDir, 'agent-prompt.txt'), payload, { flag: 'wx' });
|
|
131
|
+
if (cover) await fs.writeFile(path.join(outDir, 'cover.png'), cover, { flag: 'wx' });
|
|
132
|
+
await fs.writeFile(path.join(outDir, 'result.json'), `${JSON.stringify(result, null, 2)}\n`, { flag: 'wx' });
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font-display);font-size:17px;line-height:1.65}main{max-width:1000px;margin:auto;padding:44px 40px 72px}header.masthead{border-top:8px solid var(--ink);padding-top:18px;display:flex;gap:20px;align-items:center;margin-bottom:42px}.stamp{font-family:var(--font-display);font-weight:900;font-size:22px;border:3px solid var(--accent);color:var(--accent);padding:7px;transform:rotate(-4deg)}.eyebrow,.byline,.colophon{font-family:var(--font-mono);font-size:12px;letter-spacing:.04em}.byline{margin-left:auto;color:var(--dim)}h1{font-size:clamp(38px,6vw,64px);line-height:1.05;letter-spacing:-.03em;font-weight:900;margin:0 0 32px;max-width:850px}h2{font-size:27px;line-height:1.2;border-top:2px solid var(--ink);padding-top:24px;margin-top:48px;letter-spacing:-.02em}p{max-width:780px}a{color:inherit;text-underline-offset:3px}pre{font-family:var(--font-mono);font-size:13px;line-height:1.6;overflow-x:auto;padding:20px 0;border-block:1px solid var(--hair)}code{font-family:var(--font-mono);font-size:.88em}pre code{font-size:inherit}img{display:block;max-width:100%;height:auto;margin:24px auto}table{border-collapse:collapse;width:100%;font-size:14px}td,th{border-bottom:1px solid var(--hair);padding:12px 16px 12px 0;text-align:left;vertical-align:top}th{border-bottom:2px solid var(--ink)}.notice{font-family:var(--font-mono);font-size:13px;color:var(--dim);padding-bottom:22px;border-bottom:1px solid var(--hair);margin-bottom:32px}.colophon{border-top:2px solid var(--ink);margin-top:48px;padding-top:18px;color:var(--dim)}@media(max-width:600px){main{padding:24px 20px}.byline{display:none}table{font-size:12px}}@media print{main{padding:0;max-width:none}pre{white-space:pre-wrap}h2{break-after:avoid}img{break-inside:avoid}}
|
|
2
|
+
.agent-handoff{border-block:2px solid var(--ink);padding:22px 0;margin:0 0 32px}.agent-handoff h2{border:0;margin:0;padding:0}.agent-handoff pre{white-space:pre-wrap;overflow-wrap:anywhere}button{font:700 14px var(--font-display);background:var(--ink);color:var(--paper);border:2px solid var(--ink);padding:12px 18px;cursor:pointer}button:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}summary{cursor:pointer;font-weight:700;margin-top:16px}#copy-status{font-family:var(--font-mono);font-size:12px;min-height:1.5em}#copy-fallback{width:100%;height:240px;margin-top:12px;background:var(--paper);color:var(--ink);font:13px var(--font-mono);border:1px solid var(--ink);padding:12px}figure{margin:32px 0}td,th{overflow-wrap:anywhere}@media print{button,#copy-status,#copy-fallback{display:none}}
|
|
3
|
+
main{overflow-wrap:anywhere;min-width:0}table{display:block;overflow-x:auto}header.masthead{flex-wrap:wrap}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Strict, additive guide publication. Receipts establish local consistency, not
|
|
2
|
+
// independent proof that commands ran or that an author reviewed the result.
|
|
3
|
+
import { readFileSync, existsSync, mkdtempSync, writeFileSync, rmSync, lstatSync } from 'node:fs';
|
|
4
|
+
import { resolve, dirname, join, isAbsolute } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import sharp from 'sharp';
|
|
8
|
+
import { lintGuide } from './guide_draft.mjs';
|
|
9
|
+
import { parseFrontmatter } from './lint_post.mjs';
|
|
10
|
+
import { publishEntry } from './publish_entry.mjs';
|
|
11
|
+
import { RE_PROJECT_KEY, RE_FINAL_RELEASE } from './core.mjs';
|
|
12
|
+
|
|
13
|
+
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
const fail = message => Object.assign(new Error(message), { code: 'GUIDE_EVIDENCE_INVALID' });
|
|
15
|
+
function requireThat(condition, message) { if (!condition) throw fail(message); }
|
|
16
|
+
function text(value) { return typeof value === 'string' && value.trim().length > 0; }
|
|
17
|
+
function local(base, value) {
|
|
18
|
+
requireThat(text(value) && (isAbsolute(value) || !/^[a-z][a-z\d+.-]*:/i.test(value)), 'Evidence paths must be local filesystem paths');
|
|
19
|
+
return resolve(base, value);
|
|
20
|
+
}
|
|
21
|
+
function parse(bytes, label) {
|
|
22
|
+
try { const data = JSON.parse(bytes.toString('utf8')); requireThat(data && typeof data === 'object' && !Array.isArray(data), `${label} must be a JSON object`); return data; }
|
|
23
|
+
catch (error) { if (error.code === 'GUIDE_EVIDENCE_INVALID') throw error; throw fail(`${label} must contain valid JSON`); }
|
|
24
|
+
}
|
|
25
|
+
function checkedFile(base, ref, label) {
|
|
26
|
+
requireThat(ref && /^[a-f\d]{64}$/.test(ref.sha256 ?? ''), `${label} requires a SHA-256`);
|
|
27
|
+
const path = local(base, ref.path);
|
|
28
|
+
const bytes = readFileSync(path);
|
|
29
|
+
requireThat(digest(bytes) === ref.sha256, `${label} hash is stale`);
|
|
30
|
+
return { path, bytes };
|
|
31
|
+
}
|
|
32
|
+
function report(base, ref, label, articleSha256, agentPromptSha256) {
|
|
33
|
+
const file = checkedFile(base, ref, label);
|
|
34
|
+
const value = parse(file.bytes, label);
|
|
35
|
+
requireThat(value.status === 'passed' && text(value.summary), `${label} must pass with observed summary`);
|
|
36
|
+
requireThat(value.articleSha256 === articleSha256 && value.agentPromptSha256 === agentPromptSha256, `${label} is bound to different article or agent payload bytes`);
|
|
37
|
+
return { value, base: dirname(file.path) };
|
|
38
|
+
}
|
|
39
|
+
function present(file) {
|
|
40
|
+
try { lstatSync(file); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
41
|
+
}
|
|
42
|
+
function assertVacant(cloneDir, project, version) {
|
|
43
|
+
requireThat(existsSync(cloneDir), 'Clone directory does not exist');
|
|
44
|
+
const projectDir = join(cloneDir, project);
|
|
45
|
+
if (present(projectDir)) requireThat(lstatSync(projectDir).isDirectory() && !lstatSync(projectDir).isSymbolicLink(), 'Project directory must be an ordinary directory');
|
|
46
|
+
for (const suffix of ['md', 'png']) {
|
|
47
|
+
requireThat(!present(join(projectDir, `${version}.${suffix}`)), `Guide identity ${project}/${version} is occupied (${suffix}); explicit editorial rewrite is separate`);
|
|
48
|
+
}
|
|
49
|
+
const manifestPath = join(projectDir, 'manifest.json');
|
|
50
|
+
if (existsSync(manifestPath)) {
|
|
51
|
+
const manifest = parse(readFileSync(manifestPath), 'Manifest');
|
|
52
|
+
requireThat(Array.isArray(manifest.entries), 'Manifest must contain entries');
|
|
53
|
+
requireThat(!manifest.entries.some(entry => entry && (entry.version === version || entry.file === `${version}.md`)), `Guide identity ${project}/${version} is occupied or tombstoned in the manifest`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Local preflight followed by existing publishEntry; never commits or pushes.
|
|
58
|
+
* All evidence paths resolve relative to evidencePath; output-log paths resolve
|
|
59
|
+
* relative to their execution report. Missing config mode does not call this API.
|
|
60
|
+
*/
|
|
61
|
+
export async function publishGuide({ cloneDir, articlePath, evidencePath, coverPath } = {}) {
|
|
62
|
+
try {
|
|
63
|
+
requireThat(text(cloneDir) && text(articlePath) && text(evidencePath), 'cloneDir, articlePath and evidencePath are required');
|
|
64
|
+
const articleBytes = readFileSync(articlePath);
|
|
65
|
+
const markdown = articleBytes.toString('utf8');
|
|
66
|
+
const lint = lintGuide(markdown);
|
|
67
|
+
requireThat(lint.ok, `Guide lint failed: ${lint.findings.map(item => item.rule).join(', ')}`);
|
|
68
|
+
const evidence = parse(readFileSync(evidencePath), 'Evidence');
|
|
69
|
+
const base = dirname(resolve(evidencePath));
|
|
70
|
+
requireThat(evidence.schema === 1, 'Evidence schema must be 1');
|
|
71
|
+
const articleSha256 = digest(articleBytes);
|
|
72
|
+
requireThat(evidence.articleSha256 === articleSha256, 'Article hash is stale');
|
|
73
|
+
const { data } = parseFrontmatter(markdown.replaceAll('\r\n', '\n'));
|
|
74
|
+
const anchor = evidence.anchor;
|
|
75
|
+
requireThat(anchor && typeof anchor.project === 'string' && RE_PROJECT_KEY.test(anchor.project) && !anchor.project.includes('..'), 'Anchor project is invalid');
|
|
76
|
+
requireThat(typeof anchor.version === 'string' && RE_FINAL_RELEASE.test(anchor.version), 'Anchor must identify a final release version');
|
|
77
|
+
requireThat(typeof anchor.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(anchor.date) && new Date(anchor.date).toISOString().slice(0, 10) === anchor.date, 'Anchor date must be a valid ISO date');
|
|
78
|
+
requireThat(data.project === anchor.project && data.version === anchor.version && String(data.date) === anchor.date, 'Article project/version/date must match the selected anchor');
|
|
79
|
+
const start = '<!-- agent-handoff:start -->', end = '<!-- agent-handoff:end -->';
|
|
80
|
+
const startIndex = markdown.indexOf(start), endIndex = markdown.indexOf(end);
|
|
81
|
+
const block = markdown.slice(startIndex + start.length, endIndex);
|
|
82
|
+
const prompt = /^```text\r?\n([\s\S]*?)\r?\n```\s*$/m.exec(block)[1];
|
|
83
|
+
const reference = markdown.slice(0, startIndex) + markdown.slice(endIndex + end.length);
|
|
84
|
+
const payload = `${prompt}\n\n<reference-guide>\n${reference.trim()}\n</reference-guide>\n`;
|
|
85
|
+
const payloadFile = checkedFile(base, evidence.agentPrompt, 'Agent prompt');
|
|
86
|
+
requireThat(payloadFile.bytes.equals(Buffer.from(payload)), 'Agent prompt does not match the exact handoff/reference payload');
|
|
87
|
+
const agentPromptSha256 = digest(payloadFile.bytes);
|
|
88
|
+
const execution = report(base, evidence.execution, 'Execution report', articleSha256, agentPromptSha256);
|
|
89
|
+
requireThat(Array.isArray(execution.value.commands) && execution.value.commands.length > 0, 'Execution report needs command observations');
|
|
90
|
+
for (const command of execution.value.commands) {
|
|
91
|
+
requireThat(command && text(command.command) && command.exitCode === 0, 'Every execution command must record successful observed completion');
|
|
92
|
+
const output = checkedFile(execution.base, command.output, 'Execution output');
|
|
93
|
+
requireThat(output.bytes.length > 0, 'Execution output cannot be empty');
|
|
94
|
+
}
|
|
95
|
+
const adaptation = report(base, evidence.adaptation, 'Adaptation report', articleSha256, agentPromptSha256).value;
|
|
96
|
+
requireThat(text(adaptation.fixture?.original) && text(adaptation.fixture?.result), 'Adaptation report must identify original and resulting fixtures');
|
|
97
|
+
requireThat(Array.isArray(adaptation.independentChecks) && adaptation.independentChecks.length > 0 && adaptation.independentChecks.every(check => check && text(check.check) && text(check.observation) && check.passed === true), 'Adaptation report needs passing independent checks with observations');
|
|
98
|
+
const review = report(base, evidence.review, 'Review report', articleSha256, agentPromptSha256).value;
|
|
99
|
+
requireThat(text(review.reviewer) && Array.isArray(review.blockingFindings) && review.blockingFindings.length === 0, 'Review report needs a reviewer and no unresolved blocking findings');
|
|
100
|
+
let coverImageBuffer;
|
|
101
|
+
if (coverPath !== undefined) {
|
|
102
|
+
requireThat(text(coverPath), 'coverPath must be a local file');
|
|
103
|
+
coverImageBuffer = readFileSync(coverPath);
|
|
104
|
+
const coverSha256 = digest(coverImageBuffer);
|
|
105
|
+
requireThat(evidence.coverSha256 === coverSha256 && review.coverSha256 === coverSha256 && text(review.coverReview), 'Cover bytes require matching evidence/review hashes and visual observations');
|
|
106
|
+
const image = sharp(coverImageBuffer, { animated: true, limitInputPixels: 40_000_000 });
|
|
107
|
+
const metadata = await image.metadata();
|
|
108
|
+
requireThat(metadata.format === 'png' && metadata.width === 1600 && metadata.height === 900 && (metadata.pages ?? 1) === 1 && !metadata.isPalette, 'Cover must be a single-frame true-color 1600x900 PNG');
|
|
109
|
+
await image.raw().toBuffer();
|
|
110
|
+
} else requireThat(evidence.coverSha256 === undefined && review.coverSha256 === undefined, 'Evidence references a cover but no coverPath was provided');
|
|
111
|
+
// Final check after async decoding, before any content write. A frozen local
|
|
112
|
+
// snapshot prevents article edits between evidence validation and copyFile.
|
|
113
|
+
assertVacant(cloneDir, anchor.project, anchor.version);
|
|
114
|
+
const staging = mkdtempSync(join(tmpdir(), 'devlog-validated-guide-'));
|
|
115
|
+
try {
|
|
116
|
+
const entryPath = join(staging, 'article.md');
|
|
117
|
+
writeFileSync(entryPath, articleBytes, { flag: 'wx' });
|
|
118
|
+
return { ...publishEntry({ cloneDir, project: anchor.project, version: anchor.version, entryPath, coverImageBuffer }), evidenceValidated: true, articleSha256, agentPromptSha256 };
|
|
119
|
+
} finally { rmSync(staging, { recursive: true, force: true }); }
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.code === 'GUIDE_EVIDENCE_INVALID') throw error;
|
|
122
|
+
throw fail(`Guide publication failed: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@natjswenson/devlog",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Release dev log generator
|
|
3
|
+
"version": "0.14.1",
|
|
4
|
+
"description": "Release dev log generator — Claude Code skill + preview app for publishing version-release dev logs, written in your voice, to your site",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nate Swenson",
|
|
7
7
|
"homepage": "https://github.com/natejswenson/devlog",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"examples/",
|
|
35
35
|
"voice/",
|
|
36
36
|
"image-style/",
|
|
37
|
+
"references/",
|
|
37
38
|
"SKILL.md",
|
|
38
39
|
"SECURITY.md",
|
|
39
40
|
"CHANGELOG.md",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"react-dom": "18.3.1",
|
|
60
61
|
"react-markdown": "9.1.0",
|
|
61
62
|
"remark-gfm": "4.0.1",
|
|
62
|
-
"sharp": "0.35.
|
|
63
|
+
"sharp": "0.35.4",
|
|
63
64
|
"vite": "8.0.16"
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# AI artwork for a local cover draft
|
|
2
|
+
|
|
3
|
+
This path is selected explicitly for an AI cover draft, by Concept guide draft mode,
|
|
4
|
+
or for the single guide selected by an opted-in normal concept Generate run.
|
|
5
|
+
It never scans releases, publishes, replaces existing cover files or changes branding
|
|
6
|
+
configuration. The legacy Generate mode's local SVG/HTML renderer and failure behavior
|
|
7
|
+
are unchanged. On Claude, use that existing local workflow for concept-guide covers. For strict
|
|
8
|
+
concept publication, expand its palette PNG into a true-color PNG with the bundled
|
|
9
|
+
Sharp dependency before final inspection and hashing. This conversion does not restore
|
|
10
|
+
any detail already lost during quantization; retain the source and inspect the result.
|
|
11
|
+
Do not apply a second palette reduction to a native AI cover.
|
|
12
|
+
|
|
13
|
+
For Claude's existing palette output only, this local conversion uses the already
|
|
14
|
+
bundled dependency and refuses to overwrite the new output path:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node --input-type=module - '<skill-root>/package.json' '<legacy-cover.png>' '<new-true-color-cover.png>' <<'JS'
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { resolve } from 'node:path';
|
|
20
|
+
import { writeFile } from 'node:fs/promises';
|
|
21
|
+
const [packageFile, input, output] = process.argv.slice(2);
|
|
22
|
+
const sharp = createRequire(resolve(packageFile))('sharp');
|
|
23
|
+
const bytes = await sharp(input).toColourspace('srgb').png({ palette: false }).toBuffer();
|
|
24
|
+
await writeFile(output, bytes, { flag: 'wx' });
|
|
25
|
+
JS
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Inspect and hash that final output, not the original palette file. A Git-installed plugin or standalone SKILL.md may have no dependencies beside it.
|
|
29
|
+
In that case use the exact-version published package runtime described in SKILL.md;
|
|
30
|
+
resolve its actual package/dependency location for this conversion instead of writing
|
|
31
|
+
into an internal plugin cache or assuming node_modules exists. If unavailable, retain
|
|
32
|
+
the draft and report the missing conversion capability rather than guessing a tool
|
|
33
|
+
path. The normal Codex native-art compositor uses the same exact-version helper
|
|
34
|
+
fallback when bundled dependencies are absent.
|
|
35
|
+
|
|
36
|
+
## Capability and brief
|
|
37
|
+
|
|
38
|
+
In Codex, inspect whether native image generation is actually available. Use the native image-generation tool for new art and native editing for raster changes
|
|
39
|
+
when present; no API key or image API client belongs in the Node helper. If
|
|
40
|
+
unavailable or failed, save the draft and art brief with the blocker. Do not silently
|
|
41
|
+
substitute SVG, claim an image exists, or activate a paid CLI/API fallback. An explicitly
|
|
42
|
+
requested fallback follows the available imagegen skill's instructions.
|
|
43
|
+
|
|
44
|
+
Finish the guide before choosing the visual. Read the reviewed outcome, mechanism,
|
|
45
|
+
failure case and exclusions, then make a short public-safe brief. Do not send the entire
|
|
46
|
+
guide handoff, private repository files, source logs or private identities to generation.
|
|
47
|
+
Inspect the most recent relevant cover if available. A reference guides craft, not a
|
|
48
|
+
copied subject; lack of a first reference is not a blocker.
|
|
49
|
+
|
|
50
|
+
Choose one concrete metaphor whose meaning matches the article. The proven direction
|
|
51
|
+
is detailed editorial engraving with controlled hatching and a clean silhouette, quiet
|
|
52
|
+
paper surrounding the subject and one small focal accent. Complexity must describe the
|
|
53
|
+
mechanism, not decorate it. A beautiful picture implying an unsupported guarantee fails.
|
|
54
|
+
|
|
55
|
+
Read palette and identity from adopted/generated brand resources, respecting custom
|
|
56
|
+
style restrictions. Do not duplicate brand values manually. Prefer an opaque paper
|
|
57
|
+
background: the pilot's transparency requests repeatedly returned painted checkerboards.
|
|
58
|
+
Use transparency only when needed, and inspect actual alpha plus the rendered composite.
|
|
59
|
+
|
|
60
|
+
## Generate, persist and compose
|
|
61
|
+
|
|
62
|
+
Write a prompt specifying subject, mechanism, medium, framing, craft, supplied palette,
|
|
63
|
+
and exclusions. Request **artwork only**, with no lettering, labels, numbers, logos or
|
|
64
|
+
fake code. Typography is rendered locally. One strong candidate is enough; no mandatory
|
|
65
|
+
batch. Use only supported tool arguments, and record only runtime details actually
|
|
66
|
+
reported. Do not invent model, seed, quality or destination controls.
|
|
67
|
+
|
|
68
|
+
After the tool returns, copy that exact returned image to a versioned source path in
|
|
69
|
+
the run directory. Never pick the newest file in a shared image folder. Save the prompt,
|
|
70
|
+
brief, reference hashes and attributable tool result. Inspect the image before any
|
|
71
|
+
native edit; preserve explicit invariants. If bytes cannot be recovered, record that
|
|
72
|
+
blocker rather than inventing a file. Retain original sources when revising.
|
|
73
|
+
|
|
74
|
+
The deterministic compositor is separate from AI generation:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
devlog compose-art-cover --spec /absolute/cover-spec.json --out /absolute/new-cover-attempt
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use the schema documented in the helper's `cover-spec` reference below. Paths resolve
|
|
81
|
+
against the spec file, not the caller's working directory. No agent-authored executable
|
|
82
|
+
HTML is accepted. The helper decodes local art, embeds it in an owned layout, renders
|
|
83
|
+
escaped typography, preserves aspect ratio, exports a 1600×900 true-color PNG and
|
|
84
|
+
320px thumbnail, and writes a result marker last. It blocks browser network requests.
|
|
85
|
+
System-font rendering is host-dependent and must be reported; supply an adopted local
|
|
86
|
+
font when exact font delivery is required. It never overwrites the source or an existing
|
|
87
|
+
attempt directory and never calls an image service or publication code.
|
|
88
|
+
|
|
89
|
+
## Review and resume
|
|
90
|
+
|
|
91
|
+
Open the actual source and final cover, both full size and thumbnail. Read every
|
|
92
|
+
overlaid word. Check meaning, coherent detail, small-size silhouette, title contrast,
|
|
93
|
+
crop, accidental generated text, brand fit and distinction from nearby covers. Show
|
|
94
|
+
the final PNG and open the preview for the user. A hash or mechanical success cannot
|
|
95
|
+
certify visual meaning, quality or approval. Local preview is not proof of the live
|
|
96
|
+
site's feed crop or image loading.
|
|
97
|
+
|
|
98
|
+
Save `cover-art.json` locally with schema version 1, brief/prompt hashes, source and
|
|
99
|
+
reference hashes, selected composition/result, and observed review findings tied to
|
|
100
|
+
the exact PNG hash. Do not copy that private receipt into manifests or the reader's
|
|
101
|
+
handoff. Measure output bytes; do not apply the legacy palette reducer to detailed art.
|
|
102
|
+
|
|
103
|
+
Fix typography locally; fix illustration defects with native editing/generation.
|
|
104
|
+
Allow at most two targeted correction cycles after the first candidate. A critical
|
|
105
|
+
unresolved failure holds the cover and retains the completed guide. Each changed
|
|
106
|
+
source/brief needs meaning review; each changed crop/font/title/palette needs final
|
|
107
|
+
image review. Unrelated prose edits call for a brief check, not automatic regeneration.
|
|
108
|
+
|
|
109
|
+
An image without a completed result marker and matching hashes is an incomplete
|
|
110
|
+
attempt. Recover from recorded inputs; uncertain generation must not trigger unattended
|
|
111
|
+
retries. No cover is promoted merely because its filename is `cover.png`. After a
|
|
112
|
+
reviewed result, pass its PNG to `prepare-guide --cover`; the agent handoff stays first.
|
|
113
|
+
Explicit draft requests stop here. For an opted-in normal concept Generate run,
|
|
114
|
+
continue through guide-publishing.md and its actual consumer checks. Existing-post
|
|
115
|
+
backfill remains separately scoped work. Older readers can continue loading the final PNG.
|
|
116
|
+
|
|
117
|
+
## Cover spec
|
|
118
|
+
|
|
119
|
+
See [cover-spec.md](cover-spec.md) for the exact schema and generated-brand input.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# One complete concept guide
|
|
2
|
+
|
|
3
|
+
Use for explicit concept-guide, agent-friendly guide, or consolidation requests. This
|
|
4
|
+
is the drafting and verification workflow on either host. Explicit draft requests
|
|
5
|
+
remain draft-only. A normal Generate request with the already selected
|
|
6
|
+
`generationMode: "concept"` uses these same quality steps, then completes
|
|
7
|
+
[guide-publishing.md](guide-publishing.md). Missing mode or `"release"` retains legacy
|
|
8
|
+
Generate. Never change the persistent preference merely because a draft was requested,
|
|
9
|
+
scan-and-publish every tag, or mark other releases covered. Existing publish-entry and
|
|
10
|
+
manifest formats remain unchanged.
|
|
11
|
+
|
|
12
|
+
## Choose one outcome
|
|
13
|
+
|
|
14
|
+
Inspect the user's selected sources and relevant existing article bodies. Titles and
|
|
15
|
+
tags are discovery aids, not evidence of duplicate concepts. Releases supply evidence;
|
|
16
|
+
they do not determine article count. Select at most one complete reader outcome:
|
|
17
|
+
|
|
18
|
+
> A reader with [prerequisites] can implement [capability] in their own project,
|
|
19
|
+
> verify it through [observable result], and handle [important failure].
|
|
20
|
+
|
|
21
|
+
Keep useful independent outcomes separate even when they share tools. For existing
|
|
22
|
+
posts recommend keep, improve, consolidation review or historical, with body evidence.
|
|
23
|
+
Consolidation means a new local draft first, not deleting sources or repurposing their
|
|
24
|
+
URLs. Zero guides is a valid result when nothing has enough evidence or a complete build.
|
|
25
|
+
|
|
26
|
+
Record a short local brief: chosen outcome, applicability limits, source revisions,
|
|
27
|
+
overlap decision, smallest complete example, meaningful failure and verification plan.
|
|
28
|
+
Verify source facts against the actual revision and external technical claims against
|
|
29
|
+
primary documentation, following Generate's ground-truth and voice rules. Private
|
|
30
|
+
source identities, paths and logs stay out of public prose, art prompts and manifests.
|
|
31
|
+
|
|
32
|
+
## Write a build a stranger can finish
|
|
33
|
+
|
|
34
|
+
Resolve voice using the entrypoint's voice rules. Keep the required `Shipped`, `Gotchas`
|
|
35
|
+
and `Sources` sections and existing flat frontmatter fields. Put a reader introduction
|
|
36
|
+
before the short Shipped origin note; distinguish the teaching example from shipped code.
|
|
37
|
+
Retain a genuine source release's project/version/date for draft metadata. For a
|
|
38
|
+
consolidation, metadata is provenance only: an occupied identity is not a destination.
|
|
39
|
+
Do not invent a release or silently change an existing date to make a guide publishable.
|
|
40
|
+
|
|
41
|
+
Provide the complete file tree, prerequisites, code, invocation and verification.
|
|
42
|
+
Essential adapters, provider calls and configuration are part of the build, not reader
|
|
43
|
+
homework. Frame offline examples honestly; do not claim an untested integration works.
|
|
44
|
+
Teach implementation in the reader's project, never installation of the author's skill
|
|
45
|
+
as the payoff. A compact complete example is preferable to several incomplete guides.
|
|
46
|
+
|
|
47
|
+
Use this visible block immediately after frontmatter and before the introduction:
|
|
48
|
+
|
|
49
|
+
````markdown
|
|
50
|
+
<!-- agent-handoff:start -->
|
|
51
|
+
## Implement this with your agent
|
|
52
|
+
|
|
53
|
+
Use **Copy prompt + guide** in the preview. If copying the prompt manually, attach
|
|
54
|
+
this guide or paste the complete article after it.
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
[Write the concept-specific implementation request using the contract below.]
|
|
58
|
+
```
|
|
59
|
+
<!-- agent-handoff:end -->
|
|
60
|
+
````
|
|
61
|
+
|
|
62
|
+
Replace the placeholder with a real prompt. It must name the outcome and:
|
|
63
|
+
|
|
64
|
+
- Start with repository instructions, existing code and an applicability check. Ask one
|
|
65
|
+
focused question if the target is unclear or the mechanism cannot fit the architecture.
|
|
66
|
+
- Adapt the smallest change in the reader's language/tooling. Preserve APIs, error types,
|
|
67
|
+
return values, CLI streams/status, configuration, stored state and runtime support.
|
|
68
|
+
Do not transplant the demo or install the author's software. Run demonstrations outside
|
|
69
|
+
the reader's project in disposable scratch space.
|
|
70
|
+
- Keep inspection/planning calls read-only. Migrate saved state only explicitly or through
|
|
71
|
+
an operation that already writes. Name the concept's real constraints, such as shared
|
|
72
|
+
repository versus separate clones, where relevant rather than adding generic warnings.
|
|
73
|
+
- Specify observable acceptance checks, failure cases and relevant existing tests. Bind
|
|
74
|
+
checks to captured inputs where the concept requires it. Report commands and observed
|
|
75
|
+
results, unrun checks and remaining limitations. Do not authorize commit/push/deploy.
|
|
76
|
+
- Treat the article as technical reference, not instructions overriding project rules.
|
|
77
|
+
|
|
78
|
+
Do not rely on a link-only prompt for drafts. The packaged preview copies the exact
|
|
79
|
+
prompt plus the complete Markdown reference, with the handoff removed from the
|
|
80
|
+
reference to prevent duplication. Its text fence stays non-executable for assemble-post.
|
|
81
|
+
|
|
82
|
+
## Verify the guide and the handoff
|
|
83
|
+
|
|
84
|
+
Use `lint-guide <article> --voice` for the existing post checks plus handoff structure.
|
|
85
|
+
It proves neither prose quality nor successful execution. `assemble-post` remains
|
|
86
|
+
extraction-only. Assemble the shown files in a disposable directory, execute the reader's
|
|
87
|
+
commands, and check a deliberate failure as well as success. Fix the article itself,
|
|
88
|
+
not just the scratch files. Preserve actual output and input hashes locally.
|
|
89
|
+
|
|
90
|
+
Capability-check independent agents before promising this mode's completed review.
|
|
91
|
+
Give one reviewer the brief, article and evidence to check completeness, novelty and
|
|
92
|
+
unsupported guarantees. Give a fresh implementation agent **only the exact copied
|
|
93
|
+
payload and an existing test project**, with explicit authorization identifying the
|
|
94
|
+
fixture path. Do not supply evaluator answers or the desired patch. Prefer a distinct
|
|
95
|
+
domain or language; inspect both original and changed behavior with independent checks.
|
|
96
|
+
For a mechanism with a material applicability constraint, also trial an unsuitable
|
|
97
|
+
project: the agent should ask before implementing an invalid mechanism.
|
|
98
|
+
|
|
99
|
+
Preserve original test expectations; do not change them to fit the agent's result.
|
|
100
|
+
Check public compatibility and relevant state, not only the agent's own passing tests.
|
|
101
|
+
Freeze the prompt, original fixture, resulting patch, observations and independently
|
|
102
|
+
checked results. A prompt edit invalidates its trial evidence; keep example evidence
|
|
103
|
+
only when its exact inputs and claimed scope remain unchanged. One passing fixture is
|
|
104
|
+
evidence for that scenario, not a universal model or platform guarantee.
|
|
105
|
+
|
|
106
|
+
If independent agents or required execution capabilities are unavailable, retain the
|
|
107
|
+
draft and identify the missing check. Do not label it reviewed/ready or switch into
|
|
108
|
+
legacy publishing. An aggregate score cannot compensate for a critical build defect.
|
|
109
|
+
|
|
110
|
+
## Prepare the local preview
|
|
111
|
+
|
|
112
|
+
Generate or obtain the user's adopted brand JSON (PRESS `tokens --format json` when
|
|
113
|
+
available). Do not write brand constants from memory or require PRESS to be installed:
|
|
114
|
+
an existing compatible adopted token file works. The helper consumes `colors`, `fonts`
|
|
115
|
+
and `identity` from that file. Keep custom branding intact. If no brand resource is
|
|
116
|
+
available, retain the Markdown draft and identify the missing preview input.
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
devlog prepare-guide --article /absolute/guide.md --brand /absolute/brand.json --out /absolute/new-preview-directory
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Add `--cover /absolute/reviewed-cover.png` after the cover is ready. A new attempt needs
|
|
123
|
+
a new output directory; these helpers refuse overwrites. Review the standalone HTML,
|
|
124
|
+
normal copy and manual fallback, mobile layout, code and images. The preview contains
|
|
125
|
+
the complete guide without JavaScript. The handoff precedes the cover. Result hashes
|
|
126
|
+
identify actual inputs and outputs; a successful render is not an editorial approval.
|
|
127
|
+
|
|
128
|
+
The initial preview helper does not bundle companion files. Include required code
|
|
129
|
+
inline; relative/local links and Markdown images are rejected before output creation.
|
|
130
|
+
Use HTTP(S) source links, same-page anchors and the explicit `--cover` PNG input.
|
|
131
|
+
Do not weaken those checks to ship a preview with missing diagrams or example archives.
|
|
132
|
+
|
|
133
|
+
For Codex artwork read `codex-cover-art.md` in this directory. On Claude use the local
|
|
134
|
+
cover path already supplied by devlog; native Codex tools are not required there.
|
|
135
|
+
Retain completed text when cover generation fails and report the art blocker separately.
|
|
136
|
+
|
|
137
|
+
Keep one local run record with source revisions, brief, chosen draft, current hashes,
|
|
138
|
+
verification/review paths and pending work. Resume from matching artifacts; do not guess
|
|
139
|
+
completion from a file's existence. Raw trial transcripts and prompts are local evidence,
|
|
140
|
+
not a new public editorial ledger.
|
|
141
|
+
|
|
142
|
+
For an explicit draft request, finish with the draft link, observed checks and specific
|
|
143
|
+
limitations; do not publish. For an opted-in normal Generate run, return to
|
|
144
|
+
[guide-publishing.md](guide-publishing.md) and complete its evidence gate, publication,
|
|
145
|
+
push and live verification. There is no automatic coverage ledger. Existing-post
|
|
146
|
+
rewrites and backfills remain separately scoped editorial work. Preserve old URLs;
|
|
147
|
+
tombstones are not redirects, and legacy writers do not understand semantic coverage
|
|
148
|
+
across merged articles.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Local raster composition schema
|
|
2
|
+
|
|
3
|
+
Pass a JSON file to `compose-art-cover --spec <file> --out <new-directory>`.
|
|
4
|
+
The output directory's parent must exist. Each attempt owns a new directory and
|
|
5
|
+
writes its result marker last. The command does not overwrite or delete an existing
|
|
6
|
+
attempt, generate artwork, call a service or publish.
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"schema": 1,
|
|
11
|
+
"source": "source-v1.png",
|
|
12
|
+
"brand": "brand.json",
|
|
13
|
+
"title": "Give your CLI an import-safe entrypoint",
|
|
14
|
+
"kicker": "ENGINEERING FIELD NOTES",
|
|
15
|
+
"stand": "Reuse the logic. Run only when invoked."
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`source`, `brand` and optional `fontPath` resolve relative to this JSON file. Use a
|
|
20
|
+
local static raster, not SVG, animated media, a URL or executable HTML. `title` is
|
|
21
|
+
required; `kicker` and `stand` are optional plain text. Text is escaped, measured and
|
|
22
|
+
rejected if it cannot fit the owned layout. The source is contained at its original
|
|
23
|
+
aspect ratio rather than cropped or stretched.
|
|
24
|
+
|
|
25
|
+
`brand` is a generated/adopted JSON token export containing `colors` (`paper`, `ink`,
|
|
26
|
+
`dim`, `accent`), `fonts` (`display_stack`, `serif_stack`, `mono_stack`) and `identity`
|
|
27
|
+
(`stamp`, `name`). Generate it using PRESS `tokens --format json` when available, or
|
|
28
|
+
use an existing compatible adopted export. Do not manually copy brand constants.
|
|
29
|
+
Optional `fontPath` supplies an adopted local display font; other font stacks remain
|
|
30
|
+
host-dependent. Inspect the result's font report rather than assuming a font loaded.
|
|
31
|
+
|
|
32
|
+
Outputs are `composition.html`, `cover.png` (1600×900 true-color sRGB PNG),
|
|
33
|
+
`thumbnail.png` (320×180) and `result.json`. Results include exact paths, hashes,
|
|
34
|
+
dimensions, bytes and mechanical findings. Successful composition is not a semantic
|
|
35
|
+
or visual review. A PNG without its matching completed result marker is incomplete.
|
|
36
|
+
|
|
37
|
+
Keep this spec, sources, prompts and receipt in local run storage. Select the exact
|
|
38
|
+
reviewed PNG for a reading preview. A separate existing-post backfill must preserve
|
|
39
|
+
article metadata, use its authorized replacement path and verify the target site's
|
|
40
|
+
actual cover/feed rendering before claiming compatibility.
|