④段整套同步(六节 + P/S/F/J + 模板)+ open-design 移到技能根

一、④段整套同步(用户选候选 A)
- 新增 `references/stage-proto-doc/assets/产品文档模板.html`(16 KB/247 行,从备份拷、转 LF)。
- ④段 SKILL.md 换成备份 15:28 那版,并按模板口径改齐:
  · 「产品文档框架」从「四块+一附录」改成**六节** —— 页面结构/功能清单/使用场景/操作流程/
    跨页与界面状态/待确认与偏离;与模板同构,⛔ 不从空白页重写结构。
  · 编号从 R/S/F 改成 **P 页面/S 场景/F 功能/J 二级功能**,各自唯一、按展示顺序通排。
  · 保留旧版有价值的要求:每项功能补齐 做什么/入口在哪/输入边界/异常与恢复;重点功能三处标红;
    横向流程图每步写清页面/功能/操作/完成判据。
  · 完成标准「七节」→「六节」,自查清单同步换掉「需求」那批条目,补模板与配图两条。
  · 路径 `designs/<项目>/` → `docs/pm/<项目>/ui/`(2 处)。
  · 补回本机那条「每份产出文档开头都要有一张图」的引用(指向主 skill,段内不复述)。
- 主 skill 同步:四段结构表④行、④段沿革、画图小节(④段画页面结构图 + 用户路径)。

二、open-design 移到技能根(用户选候选 A)
- `git mv product-planning/references/stage-delivery/assets/open-design → open-design/`(4178 文件)。
- `.gitignore` 白名单加 `!/open-design/` —— 否则移出去就不再纳入版本控制。
- 引用改 11 处:路径形态 `assets/open-design/...` → `<技能根>/open-design/...`;
  「随段携带的自带件」改称「同根技能」;「路径怎么找」那条改成自带件(段内相对路径)与
  同根技能(`<技能根>/<技能名>/`)两类分开写。
- open-design 自己的 ATTRIBUTION.md 两处同步(它现在与 oil-ui-pro 同层)。
- 副作用(有益):命名闸门读数由 2063 降到 225 —— 此前它把 open-design 里 919 个第三方 md
  当成规则一起扫了;现在只扫本包 23 个 md。已确认④段的 H1 与 4a/4b/4c 子步仍在检查范围内。

三、验收
- check_naming.py --ws E:/ProgramData/AIProject/contentm_agent = 通过 225 / 失败 0 / 提示 27。
- 活文件里 `assets/open-design` 残留 0;工作区 CRLF 文件 0。
This commit is contained in:
admin committed 2026-10-15 21:17:28 +08:00
1 parent fa0a9026c8
commit 5afeb6b78a
4184 files changed
+326 -70

No files matched your search

@@ -0,0 +1,174 @@
#!/usr/bin/env -S npx -y tsx
/**
* open-design-landing — SVG framework placeholder generator.
*
* When `imagery.strategy === 'placeholder'`, this script writes one
* paper-textured SVG file per slot in `assets/image-manifest.json`.
* The generated files live alongside the schema-named PNGs that the
* composer references (`hero.png`, `about.png`, `lab-1.png`, …) so
* the layout renders fully without any image budget.
*
* Each placeholder shows: slot id · ratio · pixel dimensions · the
* `prompt_section` hint copied from the manifest. Drop the real PNG
* with the same filename to swap in production imagery; no markup
* change required.
*
* Usage:
* npx tsx scripts/placeholder.ts <out-dir>
*
* Default out-dir is `./assets/`.
*/
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { resolve, dirname, isAbsolute, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
interface ManifestSlot {
id: string;
file: string;
width: number;
height: number;
ratio: string;
prompt_section: string;
required: boolean;
rekey_on_brand_change: boolean;
}
interface Manifest {
skill: string;
design_system: string;
slots: ManifestSlot[];
}
const PAPER = '#efe7d2';
const INK_FAINT = '#8b8676';
const CORAL = '#ed6f5c';
const LINE = 'rgba(21, 20, 15, 0.16)';
/** Compose a single paper-textured SVG for one slot. */
export function placeholderSvg(slot: ManifestSlot): string {
const w = slot.width;
const h = slot.height;
const cx = w / 2;
const cy = h / 2;
const isPortrait = h > w;
const titleSize = Math.round(Math.min(w, h) * (isPortrait ? 0.075 : 0.07));
const metaSize = Math.round(Math.min(w, h) * 0.028);
const dimsSize = Math.round(Math.min(w, h) * 0.024);
// Inner frame inset.
const inset = Math.round(Math.min(w, h) * 0.04);
const frame = {
x: inset,
y: inset,
w: w - inset * 2,
h: h - inset * 2,
};
// Diagonal strokes for the classic "image goes here" cross.
const cross = `
<line x1='${frame.x}' y1='${frame.y}' x2='${frame.x + frame.w}' y2='${frame.y + frame.h}' stroke='${INK_FAINT}' stroke-opacity='0.22' stroke-width='1' />
<line x1='${frame.x + frame.w}' y1='${frame.y}' x2='${frame.x}' y2='${frame.y + frame.h}' stroke='${INK_FAINT}' stroke-opacity='0.22' stroke-width='1' />
`;
const cornerLen = Math.round(Math.min(w, h) * 0.05);
const corners = `
<path d='M${frame.x} ${frame.y + cornerLen} L${frame.x} ${frame.y} L${frame.x + cornerLen} ${frame.y}' stroke='${INK_FAINT}' fill='none' stroke-width='1.5' />
<path d='M${frame.x + frame.w - cornerLen} ${frame.y} L${frame.x + frame.w} ${frame.y} L${frame.x + frame.w} ${frame.y + cornerLen}' stroke='${INK_FAINT}' fill='none' stroke-width='1.5' />
<path d='M${frame.x} ${frame.y + frame.h - cornerLen} L${frame.x} ${frame.y + frame.h} L${frame.x + cornerLen} ${frame.y + frame.h}' stroke='${INK_FAINT}' fill='none' stroke-width='1.5' />
<path d='M${frame.x + frame.w - cornerLen} ${frame.y + frame.h} L${frame.x + frame.w} ${frame.y + frame.h} L${frame.x + frame.w} ${frame.y + frame.h - cornerLen}' stroke='${INK_FAINT}' fill='none' stroke-width='1.5' />
`;
return `<?xml version='1.0' encoding='UTF-8'?>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${w} ${h}' width='${w}' height='${h}'>
<defs>
<filter id='paper'>
<feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/>
<feColorMatrix values='0 0 0 0 0.18 0 0 0 0 0.16 0 0 0 0 0.12 0 0 0 0.07 0'/>
</filter>
</defs>
<!-- paper base -->
<rect width='${w}' height='${h}' fill='${PAPER}' />
<rect width='${w}' height='${h}' filter='url(#paper)' />
<!-- frame -->
<rect x='${frame.x}' y='${frame.y}' width='${frame.w}' height='${frame.h}' fill='none' stroke='${LINE}' stroke-dasharray='6 6' />
${cross}
${corners}
<!-- coral plate index, top-left -->
<text x='${inset + 14}' y='${inset + 26}' font-family='Inter Tight, system-ui, sans-serif' font-size='${dimsSize}' font-weight='600' letter-spacing='2' fill='${CORAL}'>PLATE · ${slot.id.toUpperCase()}</text>
<!-- coordinates, top-right -->
<text x='${w - inset - 14}' y='${inset + 26}' text-anchor='end' font-family='JetBrains Mono, monospace' font-size='${dimsSize}' fill='${INK_FAINT}'>${w} × ${h} · ${slot.ratio}</text>
<!-- centered title block -->
<text x='${cx}' y='${cy - titleSize * 0.2}' text-anchor='middle' font-family='Playfair Display, serif' font-style='italic' font-weight='500' font-size='${titleSize}' fill='#15140f'>${escapeXml(slot.id)}</text>
<text x='${cx}' y='${cy + metaSize * 1.6}' text-anchor='middle' font-family='Inter Tight, system-ui, sans-serif' font-size='${metaSize}' letter-spacing='3' fill='${INK_FAINT}'>${escapeXml(slot.prompt_section.toUpperCase())}</text>
<!-- bottom slug -->
<text x='${inset + 14}' y='${h - inset - 14}' font-family='Inter Tight, system-ui, sans-serif' font-size='${dimsSize}' letter-spacing='2' fill='${INK_FAINT}'>${slot.required ? 'REQUIRED' : 'OPTIONAL'} · ${slot.rekey_on_brand_change ? 'REKEY ON BRAND' : 'STABLE'}</text>
<text x='${w - inset - 14}' y='${h - inset - 14}' text-anchor='end' font-family='Inter Tight, system-ui, sans-serif' font-size='${dimsSize}' letter-spacing='2' fill='${INK_FAINT}'>OPENDESIGN · ATELIER ZERO</text>
</svg>`;
}
function escapeXml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
async function loadManifest(): Promise<Manifest> {
const path = resolve(SKILL_ROOT, 'assets', 'image-manifest.json');
return JSON.parse(await readFile(path, 'utf8')) as Manifest;
}
/**
* Write `<out>/<slot.file>` for every slot. The composer references
* slots by .png filename; we honor that by writing `<basename>.svg`
* AND a `<basename>.png.svg` symlink-style fallback. Most static
* hosts serve SVG to <img> just fine, so the practical convention
* is: if you want placeholders, point your `imagery.assets_path` at
* a directory of `.svg` files OR rename the SVGs to `.png` (some
* browsers honor extensionless content-sniffing).
*
* For the most reliable result, write BOTH:
* - `<id>.svg` — clean, editable
* - `<file>` — same SVG content under the .png filename so the
* composer's `<img src='./assets/<id>.png'>` works
* without changing markup.
*/
export async function writePlaceholders(outDir: string): Promise<string[]> {
const manifest = await loadManifest();
await mkdir(outDir, { recursive: true });
const written: string[] = [];
for (const slot of manifest.slots) {
const svg = placeholderSvg(slot);
const svgPath = resolve(outDir, `${slot.id}.svg`);
const pngPath = resolve(outDir, slot.file);
await writeFile(svgPath, svg, 'utf8');
await writeFile(pngPath, svg, 'utf8');
written.push(svgPath, pngPath);
}
return written;
}
async function main(): Promise<void> {
const [, , outArg] = process.argv;
const out = isAbsolute(outArg ?? '')
? outArg!
: resolve(process.cwd(), outArg ?? './assets/');
const written = await writePlaceholders(out);
const pngs = written.filter((p) => p.endsWith('.png')).length;
const svgs = written.filter((p) => p.endsWith('.svg')).length;
console.log(`✓ wrote ${pngs} png-named placeholders + ${svgs} svg files into ${out}`);
console.log(` (${written.map((p) => basename(p)).join(', ')})`);
}
const isMain = import.meta.url === `file://${process.argv[1]}`;
if (isMain) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}