A wide 16-bit era pixel art scene of a tiny alpine railway station in a mountain valley at golden-hour sunset, in the style of a beautifully detailed retro adventure game background, crisp hard-edged pixels with no anti-aliasing and no blur, limited 48-color palette with careful dithering in the sky. In the foreground, a small wooden station building with a steep red tile roof, a round station clock, flower boxes with pink and yellow pixel flowers, a bench, two milk cans, and a hanging lantern just starting to glow. A short cream-and-forest-green steam train with three carriages waits at the platform, a soft white puff of pixel steam rising from its chimney, warm yellow light in its windows. The track curves away to the right over a stone arch viaduct across a turquoise river with tiny white rapids, then disappears into a tunnel in the mountainside. In the midground, a cluster of chalet houses with chimneys, a little church with an onion-dome steeple, pine forests in three shades of green, and a waterfall falling from a cliff. In the background, layered snow-capped peaks in lavender, peach, and rose tones catching the last light, a gradient sunset sky from deep orange at the horizon to violet at the top, a few long pixel clouds, and the first two stars. Small life details: a cat on the platform, birds as two-pixel silhouettes, smoke from chimneys. Clean parallax-style layering, cozy and nostalgic mood, readable silhouettes, consistent pixel scale across the whole image. 16:9 landscape composition. No people, no text, no user interface, no logos, no watermark.
A detailed vintage cutaway illustration of a small deep-sea research submarine, drawn in the style of a mid-century popular science magazine center spread: precise ink linework with gouache and airbrush color, slightly aged cream paper texture, and a muted palette of ochre yellow, teal, brick red, slate grey, and deep navy. The submarine is shown in a clean side view, long and rounded like a fat capsule with a yellow hull, a domed acrylic viewport at the bow, twin floodlights, a pair of jointed sampling arms holding a tiny coral sample, and a ducted propeller at the stern. A large section of the hull is cut away to reveal the interior in cross-section, compartment by compartment from bow to stern: a cramped pilot sphere with two empty padded seats, glowing dials, and control levers; a small science lab with specimen jars, a microscope, and rows of small drawers; a galley nook with a kettle and tin mugs; two folded bunks; a battery room with neat rows of cells; ballast tanks shown half-filled with blue water; and an engine compartment with pipes, valves, and the propeller shaft. Thin cutaway edges show the thick layered hull wall. The submarine floats in a deep blue ocean that darkens from teal at the top to navy at the bottom, with drifting particles, a curious lanternfish, a small squid, and a rocky seafloor with tube worms and a hydrothermal vent in the lower right. Even, clear lighting typical of technical illustration, high detail, every compartment readable, balanced horizontal 16:9 landscape composition with the submarine spanning the center. No people, no crew, no readable text, no labels, no numbers, no logos, no watermark.
--- name: dependency-license-audit description: Audits the licenses of your project's dependencies for how you actually ship (SaaS, distributed app or library, or internal tool): normalizes license names to SPDX, resolves OR and AND expressions, flags missing, AGPL, GPL, source-available, and incompatible licenses, and writes a ranked fix plan and notices list. Includes a tested stdlib Python auditor. --- # Dependency License Audit You help engineering teams find license problems in their dependencies before a release, an acquisition review, or a customer security questionnaire finds them. You turn a raw dependency list into a short, ranked list of things to fix, and you explain each one in plain language. You are a careful engineer, not a lawyer: you flag risk and say when a human with legal training must decide. ## Files in this skill - `scripts/audit_licenses.py` - normalizes and classifies every dependency license and rates it for the chosen distribution model (Python 3 standard library only) - `references/license-categories.md` - what permissive, weak copyleft, strong copyleft, network copyleft, and source-available mean in practice, with common SPDX ids - `references/remediation-playbook.md` - how to fix each kind of finding, from replacing a package to adding notices - `templates/license-audit-report.md` - the report layout - `examples/example-saas-audit.md` - a worked audit of a Node.js SaaS backend ## Workflow ### 1. Understand how the project is shipped Ask, or infer and state as an assumption: - Project license: proprietary (closed source) or an open source license (SPDX id). - Distribution: `saas` (users only reach it over a network), `distributed` (mobile or desktop apps, on-prem installs, devices, SDKs, published libraries, container images given to customers), or `internal` (only employees use it). - Any company policy: always-allowed and always-blocked licenses. The same dependency can be fine for one model and a blocker for another, so never skip this step. ### 2. Get the dependency inventory Prefer a tool's output over a hand-made list, and include transitive dependencies: - Python: `pip-licenses --format=json --with-system` (or `pip-licenses --format=csv`). - Node.js: `npx license-checker --json --production` (run again without `--production` for dev tools). - Anything else: a CSV with `name,version,license,scope` exported from an SBOM or the package manager. Mark build, test, and lint tools as `scope=dev`; they are not shipped, so their findings drop to INFO. ### 3. Run the audit ```bash python3 scripts/audit_licenses.py deps.csv --project-license proprietary --distribution saas python3 scripts/audit_licenses.py licenses.json --distribution distributed --deny AGPL-3.0-only python3 scripts/audit_licenses.py deps.csv --json ``` The script prints severity counts, a table of every package with its normalized license and category, findings with reasons, version-to-version license changes, and the licenses that need text in your third-party notices. Exit code 1 means at least one HIGH finding. If you cannot run the script, classify each license by hand with `references/license-categories.md` and say so in the report. ### 4. Verify before you conclude - For every HIGH or WARN finding, check the package's actual LICENSE file in its repository or package archive; metadata is often wrong or outdated. - For "missing" or "SEE LICENSE IN" entries, open the referenced file and classify it by its text. - For dual-licensed packages (OR), record which option you choose. - For packages whose license changed between versions, confirm which version is actually installed (lock file). ### 5. Recommend and report Use `references/remediation-playbook.md` to give one concrete action per finding: replace, isolate, pin an older permissive version, get a commercial license, comply (publish source or add notices), or accept with a recorded reason. Fill in `templates/license-audit-report.md`, as in `examples/example-saas-audit.md`. ## Rules - Never say a license "is fine" or "is illegal" as a legal conclusion. Say what the license usually requires and when to ask legal counsel. - Do not guess a license from a package name or popularity. Unknown stays unknown until someone reads the license text. - Rate risk for the user's distribution model, and note what would change if the model changes (for example SaaS today, on-prem next year). - Keep the report short: blockers first, then obligations (notices, source offers), then the clean list as a count. - Treat the audit as a snapshot; recommend running it in CI on every dependency change. FILE:references/license-categories.md # License categories in practice Severity in the script depends on the category and on how the project is shipped. This page explains the categories in plain language. It is an engineering summary, not legal advice. ## Public domain and near public domain SPDX: `0BSD`, `Unlicense`, `CC0-1.0`, `MIT-0`, `WTFPL` - No meaningful obligations. Keeping a notice is still good practice. ## Permissive SPDX: `MIT`, `ISC`, `BSD-2-Clause`, `BSD-3-Clause`, `Apache-2.0`, `Zlib`, `BSL-1.0` (Boost), `PSF-2.0`, `Python-2.0`, `CC-BY-4.0` (data such as caniuse-lite) - Use, modify, and ship in closed source products. - Keep the copyright notice and license text, usually in a third-party notices file or an "About / Licenses" screen. - Apache-2.0 also requires keeping any NOTICE file, stating significant changes to its files, and includes a patent grant that ends if you sue over patents in that code. - BSD-3-Clause forbids using the authors' names to promote your product. ## Weak copyleft (file or library level) SPDX: `LGPL-2.1-*`, `LGPL-3.0-*`, `MPL-2.0`, `EPL-1.0`, `EPL-2.0`, `CDDL-1.0`, `CDDL-1.1`, GPL with a linking exception (for example `GPL-2.0-only WITH Classpath-exception-2.0`) - You may combine them with closed source code, but changes to the library's own files must be shared under the same license when you distribute. - LGPL: users must be able to replace the library with their own version. Dynamic linking (shared library, separate jar, npm package) normally makes this easy; static linking or bundling into one binary needs care. - MPL-2.0: copyleft applies per file. Keep MPL files separate and publish changes to those files. ## Strong copyleft SPDX: `GPL-2.0-only`, `GPL-2.0-or-later`, `GPL-3.0-only`, `GPL-3.0-or-later` - If you distribute software that includes or links GPL code, the combined work must be offered under the GPL with its source code. - Running GPL software on your own servers and letting users reach it over a network is not distribution, so pure SaaS use is usually allowed. Shipping an app, an on-prem installer, a device firmware, or a container image to customers is distribution. - Version matters: `GPL-2.0-only` code cannot be combined with GPL-3.0 or Apache-2.0 code in one work. `-or-later` gives more room. ## Network copyleft SPDX: `AGPL-3.0-only`, `AGPL-3.0-or-later`, `SSPL-1.0` (not OSI approved) - Like the GPL, plus: if users interact with a modified version over a network, you must offer them its source code. - For a closed source SaaS product this is the most common blocker. Even unmodified use triggers internal reviews at most companies. - SSPL goes further: offering the software as a service can require releasing the source of your whole service stack. ## Source-available and non-commercial SPDX or names: `BUSL-1.1` (Business Source License), `Elastic-2.0`, Commons Clause add-ons, `CC-BY-NC-*`, `PolyForm-Noncommercial-1.0.0` - Source is visible, but use is restricted: often no competing hosted service, no production use above a limit, or no commercial use at all, sometimes until a change date. - Always needs a review of the exact terms. Many of these switch to an open license after a few years, so an older or newer version may differ. ## Unknown - Empty license fields, `UNKNOWN`, `NOASSERTION`, `UNLICENSED`, `SEE LICENSE IN <file>`, `LicenseRef-*`, or a name the script does not recognize. - "No license" means all rights reserved: you have no permission to use it. Find the real terms or replace the package. ## Expressions - `MIT OR Apache-2.0`: you may choose either; pick the one that suits you and record the choice. - `MIT AND BSD-3-Clause`: you must follow both; the strictest one sets the category. - `GPL-2.0-only WITH <exception>`: the exception narrows the copyleft; read it. FILE:references/remediation-playbook.md # Remediation playbook Pick one action per finding and write it down with an owner. Prefer the cheapest action that removes the risk. ## Order of work 1. Unknown and missing licenses (you may have no right to use the code at all). 2. Network copyleft and source-available packages in shipped or hosted code. 3. Strong copyleft in distributed products, and version incompatibilities in open source projects. 4. Weak copyleft obligations (linking, publishing changes). 5. Notices for everything else. ## Actions ### Find the real license - Open the package's repository and archive, read LICENSE, COPYING, or the header of the main source file. - Check whether the metadata is just stale (a common case for old packages and forks). - If there is truly no license, ask the maintainer to add one (link to choosealicense.com in the request) or replace the package. ### Replace the package - Search for a maintained alternative with a permissive license and similar API. - Estimate effort in hours and add it to the backlog with the release it blocks. - For transitive dependencies, look for the direct dependency that pulls it in; a different version or a configuration flag often drops it. ### Pin a version under the old license - When a project switched from a permissive license to source-available (or the reverse), the old versions keep their old license. - Pinning is a short-term fix: no security updates. Schedule a proper replacement. ### Isolate it - Run the copyleft tool as a separate process or service that talks to your code over a command line, HTTP, or a queue, instead of linking it into your code. - Keep it unmodified if possible. Document the boundary. - This is a common way to use GPL command line tools, but have legal counsel confirm it for AGPL and SSPL. ### Buy a commercial license - Many dual-licensed projects (open source plus commercial) sell a license that removes copyleft obligations. Note the price and renewal terms. ### Comply - Weak copyleft: keep the library as a separate, replaceable file or package; publish any changes to its own files; include its license. - GPL in a distributed product: only if the whole product can be open source under compatible terms. This is a business decision, not an engineering one. - AGPL in a hosted service: offer the corresponding source to users of the service, including your changes. ### Accept with a recorded reason - Dev-only tools (linters, test runners, build tools) that never ship are usually fine. Record "dev only, not distributed". - Internal-only use of GPL or AGPL tools is usually fine. Record who uses it and that it is not exposed to outside users. ## Third-party notices - Generate a notices file from the inventory: package, version, license, copyright line, full license text (one copy per distinct license text). - Include Apache NOTICE files as they are. - Ship it inside apps ("Settings > About > Licenses"), installers, and container images; for SaaS, link it from the footer or docs. ## Keep it clean - Run the audit in CI on every change to a lock file and fail the build on HIGH. - Keep an allow list (for example MIT, BSD, ISC, Apache-2.0) and a deny list (for example AGPL-3.0, SSPL-1.0, BUSL-1.1) agreed with legal counsel, and pass them with `--allow` and `--deny`. FILE:templates/license-audit-report.md # Dependency License Audit: <project name> Date: <YYYY-MM-DD> Project license: <proprietary | SPDX id> Distribution: <saas | distributed | internal> (<one line: how users get the software>) Inventory source: <tool and command, lock file commit> Packages audited: <n> (<runtime n>, <dev n>) ## Summary - Blockers (HIGH): <n> - <one line naming them> - Obligations (WARN): <n> - <one line> - Notes (INFO): <n> - Clean (OK): <n> - Verdict: <ready to ship | ship after fixes | blocked> because <reason>. ## Blockers | Package | Version | License (as found) | Category | Why it matters for us | Action | Owner | Due | |---|---|---|---|---|---|---|---| | | | | | | | | | ## Obligations | Package | License | What we must do | Done? | |---|---|---|---| | | | | | ## Verified by hand - <package>: metadata said <x>, LICENSE file says <y>. ## Decisions and assumptions - Dual-licensed packages and the option chosen: <package: option> - Accepted risks with reason: <package: reason, approved by> - Assumptions: <for example "the admin dashboard is internal only"> ## What changes if we change how we ship - <for example: "If we ship an on-prem version, readline-sync (GPL-3.0) becomes a blocker."> ## Third-party notices Licenses that need full text in the notices file: <list> Notices file location: <path or URL> ## Next audit Run in CI on every lock file change; full review before <next release or date>. _This report is an engineering triage, not legal advice. Blockers must be confirmed by legal counsel before relying on any exception._ FILE:examples/example-saas-audit.md # Example: license audit of a Node.js SaaS backend ## The request "We're a closed source B2B SaaS. Enterprise customers keep sending security questionnaires asking about open source licenses, and next year we may sell an on-prem version. Here is our dependency export. Anything we should worry about?" Inventory (`deps.csv`, exported from the lock file, 18 rows): ``` name,version,license,scope express,4.19.2,MIT,runtime lodash,4.17.21,MIT,runtime pg,8.12.0,MIT,runtime sharp,0.33.4,Apache-2.0,runtime dompurify,3.1.6,(MPL-2.0 OR Apache-2.0),runtime ghostscript-wrapper,2.1.0,AGPL-3.0-only,runtime chart-kit-pro,1.4.0,SEE LICENSE IN LICENSE.md,runtime readline-sync,1.4.10,GPL-3.0,runtime mariadb-connector,3.3.1,LGPL-2.1-or-later,runtime fast-csv-lite,0.9.0,,runtime redis-om,0.4.3,Business Source License 1.1,runtime jest,29.7.0,MIT,dev eslint-plugin-gpl-thing,1.0.2,GPL-2.0-only,dev argparse,2.0.1,Python-2.0,runtime tslib,2.6.3,0BSD,runtime lodash,3.10.1,MIT,runtime search-client,7.10.2,Apache-2.0,runtime search-client,8.1.0,Elastic License 2.0,runtime ``` ## Script run ``` $ python3 scripts/audit_licenses.py deps.csv --project-license proprietary --distribution saas Dependency license audit: 18 packages, project license proprietary, distribution saas HIGH 5 WARN 1 INFO 2 OK 10 ... HIGH chart-kit-pro@1.4.0 SEE LICENSE IN LICENSE.md unknown runtime HIGH fast-csv-lite@0.9.0 (missing) unknown runtime HIGH redis-om@0.4.3 BUSL-1.1 source-available runtime HIGH search-client@8.1.0 Elastic-2.0 source-available runtime HIGH ghostscript-wrapper@2.1.0 AGPL-3.0-only network-copyleft runtime WARN readline-sync@1.4.10 GPL-3.0-only strong-copyleft runtime INFO eslint-plugin-gpl-thing@1.0.2 GPL-2.0-only strong-copyleft dev INFO mariadb-connector@3.3.1 LGPL-2.1-or-later weak-copyleft runtime ... Notes: - search-client: different licenses across versions (Apache-2.0, Elastic-2.0); check which version you ship. ``` Exit code 1 (HIGH findings). ## The report (filled template, shortened) # Dependency License Audit: acme-api Project license: proprietary. Distribution: saas (customers use the web app and REST API; nothing is installed on their machines today). Packages audited: 18 (16 runtime, 2 dev). ### Summary - Blockers (HIGH): 5 - two packages with no usable license, two source-available packages, one AGPL package. - Obligations (WARN): 1 - a GPL-3.0 package that is fine for SaaS but blocks the planned on-prem version. - Verdict: ship after fixes. Nothing here is unusual, and each item has a cheap fix. ### Blockers | Package | License (as found) | Why it matters for us | Action | |---|---|---|---| | chart-kit-pro 1.4.0 | SEE LICENSE IN LICENSE.md | The LICENSE.md is a commercial license that allows use only with a paid key. We have no key on record. | Confirm with the team that bought it, or replace with a permissive charting library. | | fast-csv-lite 0.9.0 | (missing) | No license means no permission to use. | Ask the maintainer to add a license; meanwhile switch to a maintained CSV parser with MIT terms. | | redis-om 0.4.3 | BUSL-1.1 | Business Source License limits production use until its change date; terms vary per project. | Read the exact "Additional Use Grant". If it forbids our use, replace or buy a license. | | search-client 8.1.0 | Elastic-2.0 | Elastic License 2.0 forbids offering the software as a managed service. Version 7.10.2 in the same tree is Apache-2.0. | Check the lock file: which modules import 8.1.0? Pin both to the Apache-2.0 line or move to an Apache-licensed fork. | | ghostscript-wrapper 2.1.0 | AGPL-3.0-only | We render customer PDFs with it inside the API; AGPL network use requires offering source to users. | Move PDF rendering to a separate, unmodified worker service and have legal counsel confirm, or buy the commercial license. | ### Obligations | Package | License | What we must do | |---|---|---| | readline-sync 1.4.10 | GPL-3.0-only | Fine for SaaS. It becomes a blocker if the on-prem version ships with it. It is only used by an admin CLI script, so remove it from the server image. | | mariadb-connector 3.3.1 | LGPL-2.1-or-later | Keep it as a normal npm dependency (replaceable); publish changes if we ever patch it. | ### Verified by hand - dompurify: dual licensed `MPL-2.0 OR Apache-2.0`; we choose Apache-2.0. - eslint-plugin-gpl-thing: dev only, never shipped. Accepted. ### What changes if we ship on-prem - readline-sync (GPL-3.0) becomes HIGH, mariadb-connector (LGPL) needs the replaceability check, and Apache-2.0 packages need their NOTICE files in the installer. Rerun with `--distribution distributed` before that release. ### Third-party notices Licenses that need full text: Apache-2.0, LGPL-2.1-or-later, MIT, Python-2.0. Link the notices page from the app footer. _Engineering triage, not legal advice._ ## Why this is a good answer - It rates every package for this company's real distribution model, and shows what changes with the on-prem plan. - It checks the facts the metadata cannot show (the commercial LICENSE.md, the duplicate search-client versions). - Every blocker has one concrete, cheap action. FILE:scripts/audit_licenses.py #!/usr/bin/env python3 """Audit third-party dependency licenses against how a project is shipped. Usage: python3 audit_licenses.py deps.csv --project-license proprietary --distribution saas python3 audit_licenses.py licenses.json --distribution distributed [--json] cat deps.csv | python3 audit_licenses.py - [--allow LIC,LIC] [--deny LIC,LIC] Input (detected automatically): CSV with a header: name,version,license[,scope] (scope: runtime | dev; default runtime) JSON list: [{"name": "...", "version": "...", "license": "...", "scope": "dev"}] pip-licenses --format=json: [{"Name": "...", "Version": "...", "License": "..."}] license-checker --json (npm): {"pkg@1.2.3": {"licenses": "MIT"}, ...} Options: --project-license your project's license: proprietary (default) or an SPDX id such as MIT, Apache-2.0, GPL-3.0-only --distribution saas (network service, default) | distributed (apps, binaries, devices, libraries) | internal --allow / --deny comma-separated SPDX ids that your policy always allows or always blocks --json machine-readable output License names are normalized to SPDX ids, expressions with OR take the most permissive option and AND takes the most restrictive one. Each package gets a category and a severity (OK, INFO, WARN, HIGH) for the chosen distribution. Dev-only packages are not shipped, so their findings drop to INFO. Exit code: 0 no HIGH findings, 1 at least one HIGH, 2 usage or input error. This is an engineering triage aid, not legal advice. Standard library only. """ import argparse import csv import io import json import re import sys ALIASES = { # lowercased common names -> SPDX id "mit": "MIT", "mit license": "MIT", "the mit license": "MIT", "expat": "MIT", "isc": "ISC", "isc license": "ISC", "isc license (iscl)": "ISC", "apache 2.0": "Apache-2.0", "apache-2": "Apache-2.0", "apache 2": "Apache-2.0", "apache2": "Apache-2.0", "apache license 2.0": "Apache-2.0", "apache license, version 2.0": "Apache-2.0", "apache software license": "Apache-2.0", "apache license version 2.0": "Apache-2.0", "bsd": "BSD-3-Clause", "bsd license": "BSD-3-Clause", "new bsd": "BSD-3-Clause", "bsd-3": "BSD-3-Clause", "3-clause bsd": "BSD-3-Clause", "bsd 3-clause": "BSD-3-Clause", "simplified bsd": "BSD-2-Clause", "bsd-2": "BSD-2-Clause", "2-clause bsd": "BSD-2-Clause", "python software foundation license": "PSF-2.0", "psf": "PSF-2.0", "psfl": "PSF-2.0", "mozilla public license 2.0 (mpl 2.0)": "MPL-2.0", "mpl 2.0": "MPL-2.0", "mpl-2": "MPL-2.0", "mpl2": "MPL-2.0", "gplv2": "GPL-2.0-only", "gpl-2.0": "GPL-2.0-only", "gpl v2": "GPL-2.0-only", "gpl2": "GPL-2.0-only", "gplv2+": "GPL-2.0-or-later", "gpl-2.0+": "GPL-2.0-or-later", "gplv3": "GPL-3.0-only", "gpl-3.0": "GPL-3.0-only", "gpl v3": "GPL-3.0-only", "gpl3": "GPL-3.0-only", "gplv3+": "GPL-3.0-or-later", "gpl-3.0+": "GPL-3.0-or-later", "gpl": "GPL-3.0-or-later", "gnu general public license v2 (gplv2)": "GPL-2.0-only", "gnu general public license v3 (gplv3)": "GPL-3.0-only", "gnu general public license v2 or later (gplv2+)": "GPL-2.0-or-later", "gnu general public license v3 or later (gplv3+)": "GPL-3.0-or-later", "lgpl": "LGPL-2.1-or-later", "lgplv2": "LGPL-2.1-only", "lgplv2+": "LGPL-2.1-or-later", "lgpl-2.1": "LGPL-2.1-only", "lgplv3": "LGPL-3.0-only", "lgplv3+": "LGPL-3.0-or-later", "lgpl-3.0": "LGPL-3.0-only", "gnu lesser general public license v2 (lgplv2)": "LGPL-2.1-only", "gnu lesser general public license v3 (lgplv3)": "LGPL-3.0-only", "gnu lesser general public license v2 or later (lgplv2+)": "LGPL-2.1-or-later", "agpl": "AGPL-3.0-only", "agplv3": "AGPL-3.0-only", "agpl-3.0": "AGPL-3.0-only", "gnu affero general public license v3": "AGPL-3.0-only", "gnu affero general public license v3 (agplv3)": "AGPL-3.0-only", "eclipse public license 2.0": "EPL-2.0", "epl 2.0": "EPL-2.0", "epl-2": "EPL-2.0", "public domain": "Unlicense", "unlicense": "Unlicense", "the unlicense": "Unlicense", "cc0": "CC0-1.0", "boost software license 1.0": "BSL-1.0", "zlib license": "Zlib", "zlib": "Zlib", "business source license 1.1": "BUSL-1.1", "elastic license 2.0": "Elastic-2.0", "server side public license": "SSPL-1.0", "sspl": "SSPL-1.0", } CATEGORY = { # SPDX id prefix -> category "MIT": "permissive", "MIT-0": "public-domain", "ISC": "permissive", "BSD-2-Clause": "permissive", "BSD-3-Clause": "permissive", "Apache-2.0": "permissive", "PSF-2.0": "permissive", "Python-2.0": "permissive", "Zlib": "permissive", "BSL-1.0": "permissive", "X11": "permissive", "BlueOak-1.0.0": "permissive", "CC-BY-4.0": "permissive", "CC-BY-3.0": "permissive", "Unicode-DFS-2016": "permissive", "Unicode-3.0": "permissive", "0BSD": "public-domain", "Unlicense": "public-domain", "CC0-1.0": "public-domain", "WTFPL": "public-domain", "MPL-2.0": "weak-copyleft", "EPL-1.0": "weak-copyleft", "EPL-2.0": "weak-copyleft", "CDDL-1.0": "weak-copyleft", "CDDL-1.1": "weak-copyleft", "LGPL-2.1": "weak-copyleft", "LGPL-3.0": "weak-copyleft", "CC-BY-SA-4.0": "weak-copyleft", "GPL-2.0": "strong-copyleft", "GPL-3.0": "strong-copyleft", "AGPL-3.0": "network-copyleft", "SSPL-1.0": "network-copyleft", "BUSL-1.1": "source-available", "Elastic-2.0": "source-available", "Commons-Clause": "source-available", "CC-BY-NC-4.0": "source-available", "CC-BY-NC-SA-4.0": "source-available", "PolyForm-Noncommercial-1.0.0": "source-available", } RANK = {"public-domain": 0, "permissive": 1, "weak-copyleft": 2, "strong-copyleft": 3, "network-copyleft": 4, "source-available": 5, "unknown": 6} SEV_ORDER = {"HIGH": 0, "WARN": 1, "INFO": 2, "OK": 3} NOTICE_LICENSES = {"Apache-2.0"} def unwrap(text): """Remove parentheses that wrap the whole expression: '(MIT OR X)' -> 'MIT OR X'.""" t = text.strip() while t.startswith("(") and t.endswith(")"): depth = 0 for i, ch in enumerate(t): depth += ch == "(" depth -= ch == ")" if depth == 0 and i < len(t) - 1: return t t = t[1:-1].strip() return t def normalize(token): t = unwrap(token) if not t: return "" key = t.lower() if key in ALIASES: return ALIASES[key] for spdx in CATEGORY: if key == spdx.lower() or key.startswith(spdx.lower() + "-") or key == spdx.lower() + "+": return t if key.startswith(spdx.lower() + "-") else spdx return t def category_of(spdx): if spdx.upper().startswith("LICENSEREF") or spdx.upper().startswith("SEE LICENSE"): return "unknown" for prefix, cat in sorted(CATEGORY.items(), key=lambda kv: -len(kv[0])): if spdx == prefix or spdx.startswith(prefix + "-") or spdx.startswith(prefix + "+"): return cat return "unknown" def classify(expr): """Return (normalized expression, chosen license, category, exception) for a license expression.""" raw = (expr or "").strip() if not raw or raw.upper() in ("UNKNOWN", "NONE", "NOASSERTION", "UNLICENSED", "CUSTOM", "N/A"): return raw or "(missing)", raw or "(missing)", "unknown", None if raw.upper().startswith("SEE LICENSE"): return raw, raw, "unknown", None whole = unwrap(raw) if whole.lower() in ALIASES: # long names such as "... v2 or later (LGPLv2+)" are one license spdx = ALIASES[whole.lower()] return spdx, spdx, category_of(spdx), None text = re.sub(r"\s+", " ", whole.replace("/", " OR ").replace(";", " OR ")) exception = None m = re.search(r"\bWITH\s+([\w.+-]+)", text, re.I) if m: exception = m.group(1) text = text[:m.start()] + text[m.end():] if re.search(r"\sor\s", text, re.I) and not re.search(r"\sand\s", text, re.I): parts, mode = re.split(r"\s+or\s+", text, flags=re.I), "or" elif re.search(r"\sand\s", text, re.I) and not re.search(r"\sor\s", text, re.I): parts, mode = re.split(r"\s+and\s+", text, flags=re.I), "and" elif re.search(r"\s(or|and)\s", text, re.I): parts, mode = re.split(r"\s+(?:or|and)\s+", text, flags=re.I), "and" # mixed: be conservative else: parts, mode = [text], "single" lics = [normalize(p) for p in parts if normalize(p)] if not lics: return raw, raw, "unknown", exception scored = sorted(lics, key=lambda l: RANK[category_of(l)]) chosen = scored[0] if mode == "or" else scored[-1] cat = category_of(chosen) if exception and cat == "strong-copyleft" and re.search(r"classpath|linking|gcc|runtime|autoconf|bison|bootloader", exception, re.I): cat = "weak-copyleft" joiner = " OR " if mode == "or" else " AND " return joiner.join(lics) + (f" WITH {exception}" if exception else ""), chosen, cat, exception def assess(cat, chosen, distribution, project): proj_cat = "proprietary" if project.lower() == "proprietary" else category_of(normalize(project)) project_spdx = normalize(project) if cat == "unknown": return "HIGH", "License is missing or not recognized; find the real license text before shipping." if cat == "source-available": return "HIGH", "Source-available or non-commercial terms usually restrict commercial use or hosting; needs a license review." if project_spdx.startswith("GPL-2.0-only") and chosen.startswith("Apache-2.0"): return "HIGH", "Apache-2.0 is generally considered incompatible with GPL-2.0-only projects." gpl3_family = re.match(r"(A|L)?GPL-3\.0", chosen) if project_spdx.startswith("GPL-2.0-only") and gpl3_family: return "HIGH", f"{chosen} cannot be combined with a GPL-2.0-only project; the project would need GPL-3.0 terms." if re.match(r"GPL-3\.0|AGPL-3\.0", project_spdx) and chosen.startswith("GPL-2.0-only"): return "HIGH", "GPL-2.0-only code cannot be relicensed under GPL-3.0; ask upstream for 'or later' terms or replace it." if proj_cat in ("strong-copyleft", "network-copyleft") and cat in ("strong-copyleft", "network-copyleft"): if cat == "network-copyleft" and proj_cat != "network-copyleft" and distribution == "saas": return "WARN", "AGPL/SSPL in a GPL project run as a service: the whole service source must be offered to users." return "OK", "Copyleft dependency in a copyleft project; check version compatibility (for example GPL-2.0-only vs GPL-3.0)." if cat == "network-copyleft": if distribution == "internal": return "WARN", "Network copyleft: fine for purely internal use, but exposing it to outside users triggers source obligations." return "HIGH", "Network copyleft (AGPL/SSPL): offering the service or shipping it requires releasing the corresponding source." if cat == "strong-copyleft": if distribution == "distributed": return "HIGH", "GPL code distributed with your product requires the combined work to be released under the GPL." if distribution == "saas": return "WARN", "GPL is not triggered by network use alone, but any future distribution (apps, on-prem, SDKs) would be." return "INFO", "GPL for internal use only has no distribution obligations; keep it out of anything you ship." if cat == "weak-copyleft": if distribution == "distributed": return "WARN", "Weak copyleft: ship the license, keep the library replaceable (dynamic linking for LGPL), and publish changes to its files." return "INFO", "Weak copyleft: publish any changes you make to this library's own files if you distribute it later." if chosen in NOTICE_LICENSES and distribution == "distributed": return "INFO", "Apache-2.0: include the license and any NOTICE file in your distribution." return "OK", "Permissive: keep the copyright notice and license text in your third-party notices." def load(text): stripped = text.lstrip() if not stripped: raise ValueError("input is empty") if stripped[0] in "[{": data = json.loads(text) rows = [] if isinstance(data, dict): # license-checker for key, val in data.items(): name, _, ver = key.rpartition("@") if not name: name, ver = key, "" lic = val.get("licenses", "") if isinstance(val, dict) else val if isinstance(lic, list): lic = " OR ".join(lic) rows.append({"name": name, "version": ver, "license": str(lic), "scope": "runtime"}) return rows for item in data: if not isinstance(item, dict): raise ValueError("JSON list items must be objects") low = {k.lower(): v for k, v in item.items()} lic = low.get("license", low.get("licenses", "")) if isinstance(lic, list): lic = " OR ".join(lic) rows.append({"name": str(low.get("name", "")), "version": str(low.get("version", "")), "license": str(lic or ""), "scope": str(low.get("scope", "runtime") or "runtime").lower()}) return rows reader = csv.DictReader(io.StringIO(text)) fields = [f.strip().lower() for f in (reader.fieldnames or [])] if "name" not in fields or "license" not in fields: raise ValueError(f"CSV needs a header with at least 'name' and 'license' columns, got: {', '.join(fields) or '(none)'}") rows = [] for r in reader: r = {(k or "").strip().lower(): (v or "").strip() for k, v in r.items()} if not r.get("name"): continue rows.append({"name": r["name"], "version": r.get("version", ""), "license": r.get("license", ""), "scope": (r.get("scope") or "runtime").lower()}) return rows def audit(rows, args): allow = {normalize(x) for x in args.allow.split(",") if x.strip()} if args.allow else set() deny = {normalize(x) for x in args.deny.split(",") if x.strip()} if args.deny else set() results, seen = [], {} for r in rows: expr, chosen, cat, exc = classify(r["license"]) sev, why = assess(cat, chosen, args.distribution, args.project_license) if exc: why += f" License exception {exc} applies; read its exact scope." if chosen in deny: sev, why = "HIGH", f"{chosen} is on your deny list." elif chosen in allow and sev != "OK": sev, why = "OK", f"{chosen} is on your allow list ({why})" dev = r["scope"] in ("dev", "development", "test", "build") if dev and sev in ("HIGH", "WARN") and chosen not in deny: sev, why = "INFO", "Dev-only (not shipped): " + why results.append({"name": r["name"], "version": r["version"], "license_raw": r["license"], "license": expr, "effective": chosen, "category": cat, "scope": "dev" if dev else "runtime", "severity": sev, "reason": why}) seen.setdefault(r["name"].lower(), set()).add(expr) notes = [] for name, lics in seen.items(): if len(lics) > 1: notes.append(f"{name}: different licenses across versions ({', '.join(sorted(lics))}); check which version you ship.") results.sort(key=lambda x: (SEV_ORDER[x["severity"]], -RANK[x["category"]], x["name"].lower())) counts = {s: sum(1 for x in results if x["severity"] == s) for s in SEV_ORDER} cats = {} for x in results: cats[x["category"]] = cats.get(x["category"], 0) + 1 attribution = sorted({x["effective"] for x in results if x["scope"] == "runtime" and x["category"] in ("permissive", "weak-copyleft")}) return {"project_license": args.project_license, "distribution": args.distribution, "packages": len(results), "severity_counts": counts, "category_counts": cats, "attribution_licenses": attribution, "notes": notes, "results": results} def print_text(rep): print(f"Dependency license audit: {rep['packages']} packages, project license {rep['project_license']}, distribution {rep['distribution']}") c = rep["severity_counts"] print(f"HIGH {c['HIGH']} WARN {c['WARN']} INFO {c['INFO']} OK {c['OK']}") print("Categories: " + ", ".join(f"{k} {v}" for k, v in sorted(rep["category_counts"].items(), key=lambda kv: RANK[kv[0]]))) print() w = max([len(f"{x['name']}@{x['version']}") for x in rep["results"]] + [7]) print(f"{'SEV':<5} {'PACKAGE':<{w}} {'LICENSE':<28} {'CATEGORY':<17} SCOPE") for x in rep["results"]: pkg = f"{x['name']}@{x['version']}" if x["version"] else x["name"] print(f"{x['severity']:<5} {pkg:<{w}} {x['license'][:28]:<28} {x['category']:<17} {x['scope']}") findings = [x for x in rep["results"] if x["severity"] in ("HIGH", "WARN", "INFO")] if findings: print("\nFindings:") for x in findings: print(f"- [{x['severity']}] {x['name']} ({x['license_raw'] or 'no license field'}): {x['reason']}") if rep["notes"]: print("\nNotes:") for n in rep["notes"]: print(f"- {n}") if rep["attribution_licenses"]: print("\nThird-party notices must include license texts for: " + ", ".join(rep["attribution_licenses"])) print("\nTriage aid only, not legal advice.") def main(): p = argparse.ArgumentParser(description="Audit dependency licenses.", add_help=True) p.add_argument("path", help="CSV or JSON file, or - for stdin") p.add_argument("--project-license", default="proprietary") p.add_argument("--distribution", default="saas", choices=["saas", "distributed", "internal"]) p.add_argument("--allow", default="") p.add_argument("--deny", default="") p.add_argument("--json", action="store_true") args = p.parse_args() try: text = sys.stdin.read() if args.path == "-" else open(args.path, encoding="utf-8").read() rows = load(text) except FileNotFoundError: print(f"error: file not found: {args.path}", file=sys.stderr) return 2 except (ValueError, json.JSONDecodeError) as e: print(f"error: {e}", file=sys.stderr) return 2 if not rows: print("error: no dependencies found in the input", file=sys.stderr) return 2 rep = audit(rows, args) if args.json: print(json.dumps(rep, indent=2)) else: print_text(rep) return 1 if rep["severity_counts"]["HIGH"] else 0 if __name__ == "__main__": sys.exit(main())
--- name: markdown-accessibility-checker description: Reviews Markdown documentation (READMEs, docs sites, wikis, blog posts, changelogs) for accessibility - missing or poor image alt text, heading structure, vague or duplicate link text, bare URLs, empty table headers, color-only instructions, ALL CAPS, and emoji bullets - then writes good alt text for each image from context and returns a fix list or a corrected file. Use when a user asks to "check my README for accessibility", "write alt text for these images", "make our docs screen reader friendly", or wants an accessibility check for Markdown in CI. --- # Markdown Accessibility Checker You make Markdown documents work for everyone, including people who use screen readers, keyboard navigation, magnification, or reading aids. You find the problems a script can detect, then do the part a script cannot: write meaningful alt text and link text that fit the document's purpose. ## Files in this skill - `scripts/check_md_a11y.py` - scans Markdown files or folders and reports problems with line numbers and severities (Python 3 standard library only) - `references/alt-text-guide.md` - how to write alt text by image type (screenshots, diagrams, charts, photos, logos, badges, decorative images) - `references/structure-and-links.md` - headings, link text, tables, lists, emphasis, and what each rule in the script means - `templates/a11y-review.md` - the review report and the alt text worksheet - `examples/example-readme-review.md` - a worked review of a project README ## Workflow ### 1. Scope the review Ask which files or folders to check, who reads them (developers, end users, students), and whether the user wants a report, a corrected file, or both. If the documents are rendered by a site generator, ask whether it adds its own page title (then the Markdown may not need an H1). ### 2. Run the checker ```bash python3 scripts/check_md_a11y.py README.md docs/ python3 scripts/check_md_a11y.py docs/ --json python3 scripts/check_md_a11y.py docs/ --fail-on warn # for CI ``` It ignores fenced code blocks and inline code. HIGH means content is lost for some readers (missing alt, empty link, empty heading), WARN should be fixed, INFO needs a human look. If you cannot run the script, check the same rules by reading the file and say so. ### 3. Write alt text For every image flagged, and for every image with existing alt text, decide its purpose using `references/alt-text-guide.md`: - Read the text around it: what is the image for at this point in the document? - If you can see the image, describe what matters for that purpose. If you cannot, ask the user, or write a clearly marked draft from the file name and context (for example "DRAFT: check against the image"). - Decorative images get empty alt text; in Markdown prefer removing them, or use HTML `<img alt="">`. Fill in the alt text worksheet in `templates/a11y-review.md`. ### 4. Fix structure and links Apply `references/structure-and-links.md`: one H1, no skipped levels, real headings instead of bold lines, descriptive unique link text, tables with headers, instructions that do not rely on color alone. ### 5. Deliver - Report: fill in `templates/a11y-review.md`, as in `examples/example-readme-review.md`, with the most important fixes first. - Corrected file: return the full Markdown with fixes applied and a short change list. Do not change meaning, code samples, or links' destinations. - Rerun the checker on the corrected file and include the before and after counts. ## Rules - Never invent what an image shows. If you have not seen it, mark the alt text as a draft. - Keep alt text short (usually under 150 characters); put long explanations (charts, diagrams) in the text or a caption and say so in the alt. - Do not start alt text with "image of" or "picture of". - Remember that automated checks find only part of the problems; always mention what still needs a manual check (color contrast in images, reading order, video captions). - Respect the author's voice; fix accessibility, not style. FILE:references/alt-text-guide.md # Alt text guide Alt text replaces the image for people who cannot see it. Ask: "If I read this document aloud, what would I say at this point instead of showing the image?" The answer depends on why the image is there, not only on what is in it. ## Basics - Be specific and short: usually 5 to 20 words, rarely more than 150 characters. - Lead with the important part. Do not start with "image of", "picture of", or "screenshot of" unless the type itself matters ("Screenshot" can help when the reader needs to find the same screen). - End with a period so screen readers pause. - Do not repeat the caption or the sentence next to the image word for word. - Include text that appears in the image if the reader needs it (button labels, error messages). ## By image type ### Screenshots of an app or website State the screen and the one thing the reader should notice. - Weak: "screenshot" - Better: "Settings screen with the Units toggle set to Metric." ### Hero images and banners If it only sets the mood, it is decorative. If it shows the product, describe it in one sentence. - "Trailhead Weather on a phone, showing a three-day forecast for a mountain pass." ### Diagrams and flowcharts Give the main idea in the alt, and the full structure in the text or a list right after the image. - Alt: "Architecture diagram: the mobile app calls the API, which reads from a forecast cache and a weather provider. Details in the list below." ### Charts and graphs Give the type, the subject, and the key takeaway; put the data in a table if readers need numbers. - "Line chart of daily active users, January to June 2026, rising from 2,000 to 9,500 with a dip in April." ### Photos Describe what matters for the document's purpose: subject, action, setting. - "Snow-covered ridge trail with orange marker poles leading to a hut." ### Logos Use the organization or product name, plus "logo" only if it helps: "Trailhead Weather logo." When the logo is a link, describe the destination: "Trailhead Weather home." ### Badges (build status, version, license) Write the information the badge shows: "Build: passing", "License: MIT", "Version 2.4.1". ### Icons inside text or links If the icon has a function, describe the function ("Download"); if it repeats nearby text, it is decorative. ### Animated GIFs and short videos Describe what happens: "Animation: dragging a pin onto the map adds a new trailhead to the list." Videos also need captions and, for important content, a text alternative. ### Decorative images Dividers, background textures, and purely visual flourishes. In HTML use `alt=""` so screen readers skip them. In Markdown, `` is ambiguous (often a mistake), so prefer removing the image or using `<img src="divider.png" alt="">`. ## Common mistakes - File names as alt text: "IMG_2041.jpg", "hero-final-v3". - One word: "image", "logo", "chart". - Keyword stuffing for search engines. - Different alt text for the same image used in several places. - Describing colors or layout when the reader needs the information, not the look. FILE:references/structure-and-links.md # Structure, links, and what each rule means Screen reader users often jump through a document by headings or by links. Good structure is what makes that possible. ## Headings | Rule | Severity | Why | Fix | |---|---|---|---| | empty-heading | HIGH | An empty heading is announced as "heading level 2" with nothing after it. | Add text or remove the #. | | no-h1 | WARN | The page has no title in the heading outline. | Start with one `# Title`. Skip this if the site generator adds the H1 from front matter. | | multiple-h1 | WARN | Several titles confuse the outline. | Keep one H1; use `##` for sections. | | heading-skip | WARN | Jumping from H2 to H4 suggests a missing section. | Use the next level down (H3). Pick levels for structure, not for font size. | | fake-heading | INFO | A bold line looks like a heading but is not in the outline. | Use a real heading if it starts a section; keep bold if it is just emphasis. | | no-headings | INFO | Long text without headings is hard to scan. | Add section headings every few paragraphs. | ## Links | Rule | Severity | Why | Fix | |---|---|---|---| | empty-link | HIGH | The link has no accessible name. | Add descriptive text. | | vague-link | WARN | "Click here" or "read more" means nothing in a list of links. | Name the destination: "download the latest release", "setup guide". | | same-text-different-url | WARN | Two links called "docs" that go to different places are indistinguishable. | Make each text unique: "API reference", "setup guide". | | url-as-link-text | INFO | Long URLs are read character by character. | Use a short description; keep the URL only if readers must copy it. | | bare-url | INFO | Same as above, and not always clickable. | Wrap in a descriptive link. | Link text tips: put the important words first, keep it short, say when a link opens a file ("pricing sheet (PDF, 2 MB)"), and do not use "link" in the text. ## Images | Rule | Severity | Why | |---|---|---| | img-missing-alt | HIGH | `` gives no alternative; some readers announce the file name. | | img-no-alt | HIGH | `<img>` without an alt attribute; screen readers fall back to the file name. | | img-generic-alt | WARN | "image", "screenshot", "logo" alone carry no information. | | img-filename-alt | WARN | "IMG_2041.jpg" is not a description. | | img-long-alt | WARN | Very long alt text cannot be navigated; move details to the text. | | img-redundant-alt | INFO | "Image of" is redundant; screen readers already say "image". | | img-decorative | INFO | `alt=""` hides the image; confirm it is truly decorative. | See `alt-text-guide.md` for how to write the text. ## Tables - table-empty-header (WARN): every column needs a header so screen readers can announce "Pro, Forecast days, 10". Add a header such as "Feature" to the first column. - Use tables for data, not for layout. Keep them small; split very wide tables. ## Other rules - color-only (WARN): "Press the green button" fails for color-blind readers and screen reader users. Name the button: "Select Save (the green button)". - all-caps (INFO): long ALL CAPS text is harder to read, and some screen readers spell short caps words letter by letter. Use normal case and bold for emphasis. - emoji-bullet (INFO): emoji are read aloud by name ("white heavy check mark Hourly forecasts"). Use normal list markers, and keep emoji rare and meaningful. ## Not checked by the script (check by hand) - Whether alt text is accurate and useful. - Color contrast and text inside images (screenshots with tiny text, charts with low contrast). - Reading order of complex layouts, and HTML blocks embedded in Markdown. - Captions and transcripts for videos and audio. - Plain language: short sentences, defined jargon, and descriptive section names. FILE:templates/a11y-review.md # Accessibility Review: <document or folder> Date: <YYYY-MM-DD> Files checked: <n> (<list or folder>) Audience: <who reads these docs> Checker run: `python3 scripts/check_md_a11y.py <paths>` -> HIGH <n>, WARN <n>, INFO <n> ## Top fixes (most impact first) 1. <fix> (<file:line>) 2. <fix> 3. <fix> ## Alt text worksheet | File:line | Image | Purpose in the document | Current alt | New alt | Seen the image? | |---|---|---|---|---|---| | | | informative / functional / decorative / complex | | | yes / draft | ## Headings - Current outline: <H1 ... / H2 ... / H4 ...> - Proposed outline: <H1 ... / H2 ... / H3 ...> ## Links | File:line | Current text | New text | Destination | |---|---|---|---| | | | | | ## Other findings | File:line | Rule | Change | |---|---|---| | | | | ## After fixes Checker rerun: HIGH <n>, WARN <n>, INFO <n> (remaining INFO items explained below). - <INFO item: reason it is acceptable> ## Manual checks still needed - <for example: contrast of text inside screenshots, video captions> FILE:examples/example-readme-review.md # Example: accessibility review of a project README ## The request "Our open source app's README gets a lot of traffic. A user who relies on a screen reader said it is hard to follow. Can you check it and fix it? The hero image is the app on a phone with the 3-day forecast; app-main.png is the main screen with hourly temperature, wind, and snow line; the badge is our build status." ## Checker run (before) ``` $ python3 scripts/check_md_a11y.py README.md Markdown accessibility check: 1 file(s), 3 image(s) HIGH 2 WARN 8 INFO 5 README.md (3 images, 4 headings): 15 finding(s) 3 HIGH img-missing-alt Image hero.png has empty alt text; describe it, or confirm it is purely decorative. 5 INFO all-caps Long ALL CAPS text is hard to read and some screen readers spell it letter by letter. 9 WARN vague-link Link text "here" does not say where it goes; describe the destination. 9 WARN vague-link Link text "click here" does not say where it goes; describe the destination. 11 WARN img-generic-alt Alt text "screenshot" for app-main.png is generic; say what the image shows or why it is there. 13 HIGH img-no-alt <img src="docs/img/badge.svg"> has no alt attribute. 15 WARN heading-skip Heading level jumps from H2 to H4 ("Configuration"); use H3. 17 WARN color-only Instruction relies on color; add the label, shape, or position too. 19 INFO fake-heading Bold line "Features" looks like a heading; use a real # heading if it starts a section. 21 INFO emoji-bullet Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words. 22 INFO emoji-bullet Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words. 24 WARN table-empty-header Table header has 1 empty cell(s); every column needs a header. 28 WARN same-text-different-url Link text "docs" points to 2 different URLs; make each text unique. 28 INFO bare-url Bare URL https://example.org/docs; screen readers spell it out. Use [descriptive text](https://example.org/docs). 35 WARN multiple-h1 2 H1 headings (lines 1, 35); keep one title and use ## for sections. ``` The `` inside the bash code block and the inline code example on the last line were correctly ignored. ## The review (filled template, shortened) # Accessibility Review: README.md Audience: hikers and contributors, many arriving from search. Checker run: HIGH 2, WARN 8, INFO 5. ### Top fixes 1. Give the hero image and the build badge real alt text (lines 3 and 13). 2. Replace "here" and "click here" with link text that names the destination (line 9). 3. Fix the outline: "Configuration" becomes H3, "Features" becomes a real H3, and the second H1 "Contributing" becomes H2. 4. Name the Save button instead of only its color (line 17). 5. Give the first table column a header (line 24). ### Alt text worksheet | File:line | Image | Purpose | Current alt | New alt | Seen? | |---|---|---|---|---|---| | README.md:3 | hero.png | informative: shows the product | (empty) | Trailhead Weather on a phone, showing a three-day forecast for a mountain pass. | described by the user | | README.md:11 | app-main.png | informative: what the main screen shows | screenshot | Main screen with hourly temperature, wind, and snow line for the selected trailhead. | described by the user | | README.md:13 | badge.svg | informative: build status | (no alt) | Build: passing | described by the user | ### Headings - Before: H1 Trailhead Weather / H2 Install / H4 Configuration / H1 Contributing (plus a bold "Features" line). - After: H1 Trailhead Weather / H2 Install / H3 Configuration / H3 Features / H2 Contributing. ### Links | Line | Current text | New text | Destination | |---|---|---|---| | 9 | here | Download the latest release | https://example.org/releases | | 9 | click here | setup guide | docs/guides/setup.md | | 28 | docs (bare URL + "docs") | API reference | https://example.org/docs | | 28 | docs | setup guide | docs/guides/setup.md | ### Other findings | Line | Rule | Change | |---|---|---| | 5 | all-caps | Normal case inside the bold note. | | 17 | color-only | "Select Save (the green button at the bottom of Settings)". | | 21-22 | emoji-bullet | Normal "-" list items. | | 24 | table-empty-header | First column header "Feature". | ### Corrected README (excerpt) ```markdown # Trailhead Weather  Trailhead Weather shows mountain forecasts for hikers. **Note: this project is still in beta and things will break often.** ## Install [Download the latest release](https://example.org/releases), or follow the [setup guide](docs/guides/setup.md). <img src="docs/img/badge.svg" width="90" alt="Build: passing"> ### Configuration Select Save (the green button at the bottom of Settings) to save your settings. ### Features - Hourly forecasts - Avalanche bulletins | Feature | Free | Pro | |---|---|---| | Forecast days | 3 | 10 | ``` ### After fixes ``` $ python3 scripts/check_md_a11y.py README-fixed.md Markdown accessibility check: 1 file(s), 3 image(s) HIGH 0 WARN 0 INFO 0 ``` ### Manual checks still needed - The hero image has small text on the phone screen; the alt text carries the key message, so that is fine here. - If the README is ever turned into a docs site page, confirm the site does not add a second H1. ## Why this is a good answer - Alt text describes each image's purpose, using what the user said the images show instead of guessing. - Links and headings are fixed for how screen reader users navigate: by link list and by heading outline. - The fix is verified by rerunning the checker, and the remaining manual checks are listed honestly. FILE:scripts/check_md_a11y.py #!/usr/bin/env python3 """Check Markdown files for common accessibility problems. Usage: python3 check_md_a11y.py README.md docs/ # files and folders (*.md, *.markdown) python3 check_md_a11y.py docs/ --json cat page.md | python3 check_md_a11y.py - python3 check_md_a11y.py docs/ --fail-on warn # exit 1 on WARN too (for CI) Checks (code blocks and inline code are ignored): images missing alt text, file-name or generic alt ("image", "screenshot"), redundant "image of ..." openers, very long alt, HTML <img> without alt, empty alt (fine only for decorative images) headings no H1 or more than one H1, skipped levels (H2 -> H4), empty headings, bold lines used as fake headings links empty link text, vague text ("click here", "read more", "here"), bare URLs, the same text pointing to different URLs tables empty header cells other ALL CAPS sentences, emoji used as bullets, color-only instructions Severities: HIGH (blocks understanding), WARN (should fix), INFO (check by hand). Exit code: 0 no HIGH, 1 HIGH found (or WARN with --fail-on warn), 2 usage error. Standard library only. """ import argparse import json import os import re import sys GENERIC_ALT = {"image", "img", "picture", "pic", "photo", "screenshot", "screen shot", "graphic", "icon", "logo", "diagram", "chart", "figure", "untitled", "alt", "alt text", "banner", "placeholder"} VAGUE_LINK = {"click here", "here", "click", "read more", "more", "learn more", "link", "this", "this link", "this page", "go", "details", "more info", "info", "see here", "check it out"} REDUNDANT_START = re.compile(r"^(an? )?(image|picture|photo|screenshot|graphic) (of|showing)\b", re.I) FILE_ALT = re.compile(r"^[\w\-. ]+\.(png|jpe?g|gif|svg|webp|bmp|tiff?)$|^(img|image|dsc|screenshot)[_\- ]?\d+", re.I) IMG_MD = re.compile(r"!\[([^\]]*)\]\(\s*<?([^)\s>]*)>?(?:\s+\"[^\"]*\")?\s*\)") IMG_REF = re.compile(r"!\[([^\]]*)\]\[[^\]]*\]") LINK_MD = re.compile(r"(?<!!)\[([^\]]*)\]\(\s*<?([^)\s>]*)>?(?:\s+\"[^\"]*\")?\s*\)") HTML_IMG = re.compile(r"<img\b[^>]*>", re.I) BARE_URL = re.compile(r"(?<![(<\"'=\[])\bhttps?://[^\s)>\]]+") COLOR_ONLY = re.compile(r"\b(click|press|tap|select|choose|use)\b[^.]{0,30}\b(red|green|blue|yellow|orange|purple)\b(?! *\()", re.I) EMOJI_BULLET = re.compile(r"^\s*[\u2705\u274c\u2b50\u27a1\u2714\u2716\U0001F300-\U0001FAFF]\ufe0f?\s+\S") def strip_inline_code(line): return re.sub(r"`+[^`]*`+", lambda m: " " * len(m.group(0)), line) def check_text(text, name): findings = [] def add(sev, line, rule, msg): findings.append({"file": name, "line": line, "severity": sev, "rule": rule, "message": msg}) lines = text.replace("\r\n", "\n").split("\n") in_fence, fence = False, "" headings, link_targets = [], {} images = 0 prev_blank = True for no, raw in enumerate(lines, 1): stripped = raw.strip() m = re.match(r"^\s{0,3}(`{3,}|~{3,})", raw) if m: if not in_fence: in_fence, fence = True, m.group(1)[0] * 3 elif stripped.startswith(fence): in_fence = False continue if in_fence or raw.startswith(" ") and prev_blank and not stripped.startswith(("-", "*", "+")): prev_blank = not stripped if not in_fence else prev_blank continue line = strip_inline_code(raw) hm = re.match(r"^\s{0,3}(#{1,6})(\s+|$)(.*?)\s*#*\s*$", line) if hm: level, title = len(hm.group(1)), hm.group(3).strip() headings.append((no, level, title)) if not title: add("HIGH", no, "empty-heading", f"Empty H{level} heading; screen reader users navigate by headings.") elif re.fullmatch(r"\s*(\*\*|__)[^*_]{2,80}(\*\*|__):?\s*", line) and prev_blank: add("INFO", no, "fake-heading", f"Bold line \"{stripped.strip('*_:')}\" looks like a heading; use a real # heading if it starts a section.") images += len(IMG_MD.findall(line)) + len(IMG_REF.findall(line)) + len(HTML_IMG.findall(line)) for alt, src in IMG_MD.findall(line): check_alt(add, no, alt, src) for alt in IMG_REF.findall(line): check_alt(add, no, alt, "(reference)") for tag in HTML_IMG.findall(line): am = re.search(r"\balt\s*=\s*(\"([^\"]*)\"|'([^']*)'|(\S+))", tag, re.I) src = re.search(r"\bsrc\s*=\s*[\"']?([^\"'\s>]+)", tag, re.I) src = src.group(1) if src else "?" if not am: add("HIGH", no, "img-no-alt", f"<img src=\"{src}\"> has no alt attribute.") else: check_alt(add, no, am.group(2) if am.group(2) is not None else (am.group(3) if am.group(3) is not None else am.group(4)), src, html=True) no_images = IMG_MD.sub(" ", line) for text_, url in LINK_MD.findall(no_images): label = re.sub(r"[*_`]", "", text_).strip() if not label: add("HIGH", no, "empty-link", f"Link to {url or '(no url)'} has no text; screen readers will read the URL or nothing.") elif label.lower().rstrip(".!:") in VAGUE_LINK: add("WARN", no, "vague-link", f"Link text \"{label}\" does not say where it goes; describe the destination.") elif re.fullmatch(r"https?://\S+", label): add("INFO", no, "url-as-link-text", f"Link text is a raw URL ({label[:60]}); use a short description.") if label: link_targets.setdefault(label.lower(), {}).setdefault(url, no) for url in BARE_URL.findall(LINK_MD.sub(" ", no_images)): add("INFO", no, "bare-url", f"Bare URL {url[:60]}; screen readers spell it out. Use [descriptive text]({url[:40]}).") if "|" in line and re.match(r"^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$", line) and no > 1: header = lines[no - 2].strip().strip("|") cells = [c.strip() for c in header.split("|")] if any(not c for c in cells): add("WARN", no - 1, "table-empty-header", f"Table header has {sum(1 for c in cells if not c)} empty cell(s); every column needs a header.") run = longest = 0 for w in re.findall(r"[A-Za-z]+", line): run = run + 1 if w.isupper() and (len(w) > 1 or w in "AI") else 0 longest = max(longest, run) if longest >= 5 and not hm: add("INFO", no, "all-caps", "Long ALL CAPS text is hard to read and some screen readers spell it letter by letter.") if EMOJI_BULLET.match(line): add("INFO", no, "emoji-bullet", "Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words.") cm = COLOR_ONLY.search(line) if cm: between = line[cm.end(1):cm.start(2)] inside_parens = line[:cm.start(2)].count("(") > line[:cm.start(2)].count(")") names_label = re.match(r"\s+(\*\*|\"|')?[A-Z]", between) # "Select Save ..." or "Press **Save**" if inside_parens or names_label: cm = None if cm: add("WARN", no, "color-only", "Instruction relies on color; add the label, shape, or position too.") prev_blank = not stripped h1 = [h for h in headings if h[1] == 1] if headings and not h1: add("WARN", headings[0][0], "no-h1", "Document has no H1; start with one # title.") if len(h1) > 1: add("WARN", h1[1][0], "multiple-h1", f"{len(h1)} H1 headings (lines {', '.join(str(h[0]) for h in h1)}); keep one title and use ## for sections.") for (pno, plev, _), (no, lev, title) in zip(headings, headings[1:]): if lev > plev + 1: add("WARN", no, "heading-skip", f"Heading level jumps from H{plev} to H{lev} (\"{title[:50]}\"); use H{plev + 1}.") if not headings and len([l for l in lines if l.strip()]) > 15: add("INFO", 1, "no-headings", "Long document with no headings; add headings so readers can navigate.") for label, targets in link_targets.items(): if len(targets) > 1 and label not in VAGUE_LINK: first = min(targets.values()) add("WARN", first, "same-text-different-url", f"Link text \"{label}\" points to {len(targets)} different URLs; make each text unique.") order = {"HIGH": 0, "WARN": 1, "INFO": 2} findings.sort(key=lambda f: (f["line"], order[f["severity"]])) return findings, len(headings), images def check_alt(add, no, alt, src, html=False): a = alt.strip() base = os.path.basename(src) if src else "" if a == "" and html: add("INFO", no, "img-decorative", f"<img src=\"{base}\"> has alt=\"\" (marked decorative); confirm it carries no information.") return if a == "" and src: add("HIGH", no, "img-missing-alt", f"Image {base or src} has empty alt text; describe it, or confirm it is purely decorative.") return low = a.lower().rstrip(".") if low in GENERIC_ALT: add("WARN", no, "img-generic-alt", f"Alt text \"{a}\" for {base} is generic; say what the image shows or why it is there.") elif FILE_ALT.search(a) or (base and low == os.path.splitext(base)[0].lower()): add("WARN", no, "img-filename-alt", f"Alt text \"{a}\" looks like a file name; describe the image instead.") elif REDUNDANT_START.match(a): add("INFO", no, "img-redundant-alt", f"Alt text starts with \"{' '.join(a.split()[:3])}\"; screen readers already say 'image', so start with the content.") if len(a) > 150: add("WARN", no, "img-long-alt", f"Alt text is {len(a)} characters; keep it under about 150 and move details to the surrounding text or a caption.") def collect(paths): files = [] for p in paths: if p == "-": files.append("-") elif os.path.isdir(p): for root, dirs, names in os.walk(p): dirs[:] = sorted(d for d in dirs if not d.startswith(".") and d != "node_modules") files += [os.path.join(root, n) for n in sorted(names) if n.lower().endswith((".md", ".markdown"))] elif os.path.isfile(p): files.append(p) else: raise FileNotFoundError(p) return files def main(): ap = argparse.ArgumentParser(description="Markdown accessibility checker") ap.add_argument("paths", nargs="+", help="Markdown files, folders, or - for stdin") ap.add_argument("--json", action="store_true") ap.add_argument("--fail-on", choices=["high", "warn"], default="high") args = ap.parse_args() try: files = collect(args.paths) except FileNotFoundError as e: print(f"error: path not found: {e}", file=sys.stderr) return 2 if not files: print("error: no Markdown files (*.md, *.markdown) found", file=sys.stderr) return 2 all_findings, stats = [], [] for f in files: text = sys.stdin.read() if f == "-" else open(f, encoding="utf-8", errors="replace").read() name = "<stdin>" if f == "-" else f found, nhead, images = check_text(text, name) stats.append({"file": name, "images": images, "headings": nhead, "findings": len(found)}) all_findings += found counts = {s: sum(1 for x in all_findings if x["severity"] == s) for s in ("HIGH", "WARN", "INFO")} if args.json: print(json.dumps({"files": stats, "counts": counts, "findings": all_findings}, indent=2)) else: print(f"Markdown accessibility check: {len(files)} file(s), {sum(s['images'] for s in stats)} image(s)") print(f"HIGH {counts['HIGH']} WARN {counts['WARN']} INFO {counts['INFO']}") for s in stats: items = [x for x in all_findings if x["file"] == s["file"]] print(f"\n{s['file']} ({s['images']} images, {s['headings']} headings): {len(items)} finding(s)") for x in items: print(f" {x['line']:>4} {x['severity']:<4} {x['rule']:<24} {x['message']}") print("\nAutomated checks cover only part of accessibility; review alt text meaning, reading order, and contrast by hand.") bad = counts["HIGH"] + (counts["WARN"] if args.fail_on == "warn" else 0) return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())
--- name: meeting-agenda-timeboxer description: Turns a rough list of meeting topics into a timeboxed agenda with start and end times, owners, and a clear outcome for every item (inform, discuss, decide, brainstorm, review), checks that it fits the slot with a buffer, flags agenda smells such as late or rushed decisions, too many status updates, oversized items, and missing breaks, and writes a ready-to-send agenda and pre-read. Use when a user asks "help me plan this meeting", "will this agenda fit in an hour?", "make this agenda tighter", or shares a list of topics for a team meeting, workshop, offsite, or review. --- # Meeting Agenda Timeboxer You help people run meetings that end on time and produce decisions. You turn a list of topics into a realistic schedule, give every item an owner and a purpose, move status updates out of the room, and make the time math visible so nobody discovers at minute 55 that the main decision has no time left. ## Files in this skill - `scripts/timebox_agenda.py` - schedules every item, checks the fit against the slot and buffer, flags agenda smells, and can fit items to the slot in 5-minute steps (Python 3 standard library only) - `references/timeboxing-principles.md` - how long things really take, item outcomes, ordering, buffers, and breaks - `references/facilitation-moves.md` - techniques to keep each kind of item inside its timebox, and what to do when one runs over - `templates/agenda.md` - the agenda input format and the agenda to send - `examples/example-roadmap-meeting.md` - a worked fix of an overloaded one-hour planning meeting ## Workflow ### 1. Collect the facts Ask, or assume and say so: - Purpose of the meeting in one sentence: what must be true when it ends? - Start time and length (or end time), number of attendees, in person or remote. - Topics, each with an owner and what the group must do with it: inform, update, discuss, brainstorm, review, or decide. - Hard constraints: people who join late or leave early, fixed breaks. ### 2. Write the draft agenda Use the input format in `templates/agenda.md` (one line per item: `minutes | topic | owner | outcome`). Use `?` for a duration nobody knows yet. Estimate durations with `references/timeboxing-principles.md`. ### 3. Run the timeboxer ```bash python3 scripts/timebox_agenda.py agenda.txt python3 scripts/timebox_agenda.py agenda.txt --fit # scale to the slot in 5-minute steps python3 scripts/timebox_agenda.py agenda.json --json ``` It prints the schedule with clock times, the buffer, and findings: HIGH when the agenda does not fit, WARN for smells, INFO for unplanned time and the meeting cost in person-hours. `--fit` trims status items first, then scales everything else; it never reorders items, so check the order yourself afterwards. If you cannot run the script, do the clock math by hand, item by item, and say so. ### 4. Fix the agenda, not just the numbers Work through the findings in this order: 1. Remove or shrink status updates and announcements: move them into a written pre-read. 2. Put decisions early (first half), and give each one at least 10 minutes plus a written proposal sent beforehand. 3. Give every item an owner and an outcome; drop "AOB" or turn it into a 5-minute parking lot review. 4. Split items over 45 minutes; add a break every 60 to 90 minutes. 5. If it still does not fit, propose what to cut or move to another meeting or to async, and say what is lost. Rerun the script until there are no HIGH findings and every WARN is either fixed or explained. ### 5. Deliver Fill in the agenda to send from `templates/agenda.md`: purpose, desired outcomes, the timed schedule, pre-reads with owners and deadlines, and the parking lot rule. Add one or two facilitation tips per risky item from `references/facilitation-moves.md`. Follow the style of `examples/example-roadmap-meeting.md`. ## Rules - Never make the math work by shrinking a decision below what it needs; cut or move something else, or recommend a longer slot. - Keep a buffer: about 5 minutes per hour, 10 percent for workshops. - Say plainly when a meeting should not happen (no decisions, nothing to discuss) and suggest the async alternative. - Respect the user's culture and constraints; suggest, do not lecture. - Times are local to the meeting; for remote attendees in other time zones, mention it but do not convert unless asked. FILE:references/timeboxing-principles.md # Timeboxing principles ## Every item needs an outcome | Outcome | What the group does | Typical time | Notes | |---|---|---|---| | inform | Hear something, ask a clarifying question | 2 to 5 min | Better as a written pre-read. | | update | Status from people or teams | 1 to 2 min per person | Usually async; keep only blockers live. | | discuss | Explore a topic, share views, no decision yet | 10 to 20 min | Say what the discussion feeds (a decision next week, a doc). | | brainstorm | Generate options | 15 to 30 min | Needs a framed question and a method (silent writing first). | | review | Give feedback on a draft, design, or plan | 15 to 40 min | Send the material beforehand; agree what kind of feedback is wanted. | | decide | Choose between options and commit | 10 to 30 min | Needs a written proposal, the options, the decision maker, and the decision rule. | | break | Rest | 5 to 15 min | Every 60 to 90 minutes. | If an item has no clear outcome, ask: "What will be different after this item?" If the answer is "nothing", cut it. ## How long things really take - Introductions: about 1 minute per person for a new group, 0 for a regular team. - Each person speaking once on a topic: about 1.5 to 2 minutes per person. Eight people, one round, is 15 minutes. - Screen sharing and getting started: 2 to 3 minutes, more for remote meetings with new tools. - A decision with real disagreement: double the estimate, or split it into "discuss" now and "decide" next time. - Items late in the agenda run faster but worse: people rush or leave. ## Ordering 1. Start with a short framing: purpose and desired outcomes (2 to 5 min). 2. Decisions and the hardest topics in the first half, while energy and attendance are high. 3. Discussion and review in the middle. 4. Information and updates last or out of the meeting entirely. 5. End with next steps, owners, and dates (5 min), never with AOB. Group items that need the same people, so others can leave early. ## Buffers and breaks - Buffer: about 5 minutes per hour, 10 percent for workshops. The buffer is part of the slot, not extra time. - Breaks: 5 to 10 minutes every 60 to 90 minutes; 15 for longer workshops. Remote meetings need breaks sooner. - Back-to-back calendars: end 5 minutes early (25 or 50 minute meetings). ## Fit checks - More than about 8 items per hour means each gets less than 7 minutes; merge or cut. - Status and announcements above about a third of the time: move them to a pre-read. - An item over 45 minutes: split it into steps with their own outcomes (for example "brainstorm 25" then "cluster and pick 20"). ## The cost of a meeting Person-hours = attendees x length in hours. A one-hour meeting with 9 people costs 9 working hours. Ask whether everyone needs to be there for every item. FILE:references/facilitation-moves.md # Facilitation moves that keep timeboxes ## Before the meeting - Send the agenda and pre-reads 24 to 48 hours ahead, with the owner of each item. - For every decision item, send a one-page proposal: the question, options, recommendation, and who decides. - Ask people to add questions to the pre-read in writing, so the meeting starts from questions, not from a presentation. ## At the start - State the purpose and the outcomes you need by the end. - Name a timekeeper and agree on the signal (a message in the chat, a raised hand at 2 minutes left). - Say how decisions are made today: the owner decides after input, majority vote, or consent ("any strong objection?"). ## By item type - Inform: "Two minutes, then one clarifying question each." Detailed questions go to the parking lot or to a follow-up chat. - Update: Only blockers and asks live; everything else is in the pre-read. - Discuss: Frame the question first ("What worries us about option B?"). Use a quick round so everyone speaks once before anyone speaks twice. - Brainstorm: 3 to 5 minutes of silent writing first, then share, then cluster. Quantity before judgment. - Review: Agree the kind of feedback: blocking issues, questions, or polish. Collect blocking issues first. - Decide: Restate the proposal, list concerns, check for objections, then decide and say it out loud: "We decided X; owner Y; by date Z." ## When an item runs over Make the choice visible instead of silently eating the next item: 1. "We have 2 minutes left on this. Do we extend by 5 minutes and cut something else, or park it?" 2. Park it: write it in the parking lot with an owner and a date. 3. Shrink the next item, never the decision items. 4. If the meeting goal is at risk, schedule a short follow-up with only the people needed. ## Remote and hybrid - Everyone joins on their own device so remote people are equal. - Use the chat for the speaking queue and for quick polls. - Share the agenda on screen with the current item highlighted. ## At the end - Read back decisions, owners, and dates (5 minutes). - Review the parking lot: assign each item or drop it. - Send notes within 24 hours. FILE:templates/agenda.md # Agenda input (for scripts/timebox_agenda.py) ``` Meeting: <name> Start: <HH:MM> Length: <minutes> # or End: <HH:MM> Buffer: <minutes or percent, e.g. 5 or 10%> Attendees: <number> <minutes> | <topic> | <owner> | <inform | update | discuss | brainstorm | review | decide | break> ? | <topic with unknown duration> | <owner> | <outcome> ``` A Markdown table with the columns Min, Topic, Owner, Outcome works too, and so does JSON: `{"meeting": "...", "start": "14:00", "length": 60, "buffer": 5, "attendees": 6, "items": [{"minutes": 10, "topic": "...", "owner": "...", "outcome": "decide"}]}` --- # Agenda to send **<Meeting name>** <Day, date>, <start>-<end> (<length> min), <place or video link> Attendees: <names or teams> | Facilitator: <name> | Timekeeper: <name> | Notes: <name> **Purpose:** <one sentence: what must be true when we finish> **Outcomes we need** 1. <decision or result> 2. <decision or result> **Pre-read (by <day, time>)** - <document> - <owner> - <what to look at, how long it takes to read> **Schedule** | Time | Min | Item | Owner | Outcome | |---|---|---|---|---| | <HH:MM-HH:MM> | <n> | <topic> | <owner> | <outcome> | | <HH:MM-HH:MM> | <n> | Buffer and wrap-up: decisions, owners, dates | <facilitator> | | **How we decide today:** <owner decides after input | consent | vote> **Parking lot:** Off-topic points go to the parking lot and get an owner at the end. **Moved out of this meeting** - <item> -> <async update, other meeting, or a document>, owner <name> FILE:examples/example-roadmap-meeting.md # Example: fixing an overloaded one-hour planning meeting ## The request "I have one hour with 9 people on Thursday at 14:00 to plan Q4. Here is my agenda. Will it fit? The budget split must be approved in this meeting, and the top 3 initiatives too." Draft agenda (`agenda.txt`): ``` Meeting: Q4 roadmap planning Start: 14:00 Length: 60 Buffer: 5 Attendees: 9 10 | Welcome and goals | Dana | inform 15 | Team status round (each team lead) | Leads | update 10 | Customer feedback highlights | Priya | inform 20 | Q4 priorities: pick top 3 initiatives | Lee | discuss 10 | Hiring plan for Q4 | | discuss 5 | Approve Q4 budget split | Dana | decide ? | AOB | | ``` ## First run ``` $ python3 scripts/timebox_agenda.py agenda.txt Q4 roadmap planning: 14:00-15:00 (60 min, buffer 5 min, 55 min for items) Planned: 70 min 14:00-14:10 10 min Welcome and goals (Dana) [inform] 14:10-14:25 15 min Team status round (each team lead) (Leads) [update] 14:25-14:35 10 min Customer feedback highlights (Priya) [inform] 14:35-14:55 20 min Q4 priorities: pick top 3 initiatives (Lee) [discuss] 14:55-15:05 10 min Hiring plan for Q4 [discuss] 15:05-15:10 5 min Approve Q4 budget split (Dana) [decide] 15:10-15:10 ? min AOB [?] 14:55-15:00 5 min Buffer / wrap-up Findings: - [HIGH] Agenda needs 70 min but only 55 min are available after the 5-min buffer: overrun of 15 min (ends 15:10 instead of 14:55). Cut, shorten, or move items, or run with --fit. - [WARN] 1 item(s) without a duration: AOB. Remaining time for them: 0 min. - [WARN] "Hiring plan for Q4" has no owner; name who prepares and leads it. - [WARN] "Approve Q4 budget split" is a decision with only 5 min; plan at least 10, or decide by a written proposal beforehand. - [WARN] "Approve Q4 budget split" is a decision starting at 15:05, in the last quarter of the meeting; move it earlier while energy and attendance are high. - [WARN] "AOB" has no owner; name who prepares and leads it. - [WARN] "AOB" has no outcome; say whether it is to inform, discuss, decide, or brainstorm. - [WARN] Status updates and announcements take 50% of the time; send them as a pre-read and keep the meeting for discussion and decisions. - [INFO] Meeting cost: 9 people x 60 min = 9 person-hours. ``` Exit code 1: the agenda does not fit. ## What the numbers say - 70 minutes of items (plus AOB) in a 55-minute window: 15 minutes over before AOB even starts. - Half of the time is updates and announcements, while the two things that must be decided get 5 minutes (budget) and a "discuss" label (priorities), and the budget decision would start at 15:05, after the meeting should have ended. - Running `--fit` alone would squeeze everything into 55 minutes but leave the budget decision last with 5 minutes. Fitting the numbers is not enough; the order and the purpose of items have to change. ## Changes 1. Team status round (15 min) and customer feedback highlights (10 min) become a written pre-read due Wednesday 12:00; questions go in comments. 2. The budget split moves to the start with 10 minutes and a proposal sent Monday, so the meeting only confirms or adjusts it. 3. "Q4 priorities" becomes a decision with 25 minutes: the outcome is a ranked top 3. 4. The hiring plan gets an owner (Priya) and a narrower question: which roles to open. 5. AOB is replaced by a 5-minute "next steps, owners, and parking lot" close. 6. Three people only needed for the status round can skip the meeting: 6 attendees instead of 9. Revised agenda: ``` Meeting: Q4 roadmap planning Start: 14:00 Length: 60 Buffer: 5 Attendees: 6 5 | Welcome and goals for today | Dana | inform 10 | Approve Q4 budget split (proposal sent Monday) | Dana | decide 25 | Q4 priorities: pick top 3 initiatives | Lee | decide 10 | Hiring plan for Q4: agree which roles to open | Priya | discuss 5 | Next steps, owners, and parking lot | Dana | inform ``` ## Second run ``` $ python3 scripts/timebox_agenda.py agenda-revised.txt Q4 roadmap planning: 14:00-15:00 (60 min, buffer 5 min, 55 min for items) Planned: 55 min 14:00-14:05 5 min Welcome and goals for today (Dana) [inform] 14:05-14:15 10 min Approve Q4 budget split (proposal sent Monday) (Dana) [decide] 14:15-14:40 25 min Q4 priorities: pick top 3 initiatives (Lee) [decide] 14:40-14:50 10 min Hiring plan for Q4: agree which roles to open (Priya) [discuss] 14:50-14:55 5 min Next steps, owners, and parking lot (Dana) [inform] 14:55-15:00 5 min Buffer / wrap-up Findings: - [INFO] Meeting cost: 6 people x 60 min = 6 person-hours. ``` Exit code 0, no warnings, and the meeting costs 6 person-hours instead of 9. ## The agenda to send (filled template, shortened) **Q4 roadmap planning** Thursday, 14:00-15:00 (60 min), Room 3 and video link Facilitator: Dana | Timekeeper: Lee | Notes: Priya **Purpose:** Leave with an approved Q4 budget split and a ranked top 3 of initiatives. **Pre-read (by Wednesday 12:00)** - Team status one-pagers - each team lead - 5 min to read - Customer feedback highlights - Priya - 5 min - Budget split proposal - Dana - 10 min **Schedule** | Time | Min | Item | Owner | Outcome | |---|---|---|---|---| | 14:00-14:05 | 5 | Welcome and goals for today | Dana | inform | | 14:05-14:15 | 10 | Approve Q4 budget split (proposal sent Monday) | Dana | decide | | 14:15-14:40 | 25 | Q4 priorities: pick top 3 initiatives | Lee | decide | | 14:40-14:50 | 10 | Hiring plan for Q4: agree which roles to open | Priya | discuss | | 14:50-14:55 | 5 | Next steps, owners, and parking lot | Dana | inform | | 14:55-15:00 | 5 | Buffer | | | **How we decide today:** Dana decides the budget after input; the top 3 is chosen by consent (any strong objection?). **Facilitation tips** - Priorities: 3 minutes of silent dot voting on the candidate list, then discuss only the items ranked 2 to 5. - If the budget item hits 10 minutes, park the open point with an owner and a 24-hour deadline instead of eating into priorities. **Moved out of this meeting:** team status round and customer feedback (pre-read). ## Why this is a good answer - It fixes the purpose and the order, not just the minutes. - Both required decisions now happen in the first 40 minutes, with prepared proposals. - The script confirms the fit, and the people who were only there for updates get their hour back. FILE:scripts/timebox_agenda.py #!/usr/bin/env python3 """Timebox a meeting agenda: schedule every item, check the fit, and flag agenda smells. Usage: python3 timebox_agenda.py agenda.txt python3 timebox_agenda.py agenda.txt --fit # scale items to fit the slot (5-minute steps) python3 timebox_agenda.py agenda.json --json cat agenda.txt | python3 timebox_agenda.py - Text input: optional "Key: value" header lines, then one item per line as minutes | topic | owner | outcome Markdown table rows (| 10 | Topic | Owner | decide |) also work; header and separator rows are skipped. Use ? for an unknown duration. Header keys: Meeting, Start (HH:MM), Length (minutes) or End (HH:MM), Buffer (minutes or percent, e.g. 5 or 10%), Attendees (number). Outcomes: inform, update, discuss, brainstorm, review, decide, break. JSON input: {"meeting": "...", "start": "14:00", "length": 60, "buffer": 5, "attendees": 8, "items": [{"minutes": 10, "topic": "...", "owner": "...", "outcome": "decide"}]} --fit trims status items (and items with no outcome) first, then scales everything; spare time goes to decide and discuss items. Checks: overrun (HIGH), missing durations, owners, or outcomes, late or rushed decisions, oversized items, long stretches without a break, too many status updates, too many items per hour, plus the meeting's cost in person-hours. Exit code: 0 no HIGH findings, 1 HIGH found, 2 usage or input error. Standard library only. """ import argparse import json import math import re import sys OUTCOMES = {"inform", "update", "discuss", "brainstorm", "review", "decide", "break"} ALIASES = {"decision": "decide", "info": "inform", "fyi": "inform", "status": "update", "updates": "update", "discussion": "discuss", "ideas": "brainstorm", "ideation": "brainstorm", "feedback": "review", "approve": "decide", "approval": "decide", "pause": "break", "coffee": "break"} class InputError(Exception): pass def hhmm(text): m = re.fullmatch(r"\s*(\d{1,2})[:.](\d{2})\s*", str(text)) if not m or int(m.group(1)) > 23 or int(m.group(2)) > 59: raise InputError(f"time must be HH:MM, got {text!r}") return int(m.group(1)) * 60 + int(m.group(2)) def fmt(minutes): minutes = int(round(minutes)) % (24 * 60) return f"{minutes // 60:02d}:{minutes % 60:02d}" def parse_minutes(text): t = str(text).strip().lower() if t in ("", "?", "tbd", "-"): return None m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(m|min|mins|minutes?)?", t) if m: return float(m.group(1)) m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*h(?:ours?)?(?:\s*(\d+)\s*m(?:in)?)?", t) if m: return float(m.group(1)) * 60 + float(m.group(2) or 0) raise InputError(f"cannot read duration {text!r}") def norm_outcome(text): t = (text or "").strip().lower() return ALIASES.get(t, t) def parse_text(text): meta, items, errors = {}, [], [] for no, raw in enumerate(text.replace("\r\n", "\n").split("\n"), 1): line = raw.strip() if not line or line.startswith("#"): continue hm = re.fullmatch(r"([A-Za-z ]{3,20}):\s*(.+)", line) if hm and "|" not in line: meta[hm.group(1).strip().lower()] = hm.group(2).strip() continue if "|" not in line: errors.append(f"line {no}: expected 'minutes | topic | owner | outcome', got {line[:60]!r}") continue cells = [c.strip() for c in line.strip("|").split("|")] if all(re.fullmatch(r":?-{2,}:?", c) for c in cells if c): continue if cells and cells[0].lower() in ("min", "mins", "minutes", "time", "duration"): continue cells += [""] * (4 - len(cells)) try: minutes = parse_minutes(cells[0]) except InputError as e: errors.append(f"line {no}: {e}") continue if not cells[1]: errors.append(f"line {no}: item has no topic") continue items.append({"line": no, "minutes": minutes, "topic": cells[1], "owner": cells[2], "outcome": norm_outcome(cells[3])}) return meta, items, errors def parse_json(text): data = json.loads(text) if not isinstance(data, dict) or not isinstance(data.get("items"), list): raise InputError("JSON input needs an object with an 'items' list") meta = {k.lower(): str(v) for k, v in data.items() if k != "items" and v is not None} items, errors = [], [] for i, it in enumerate(data["items"], 1): try: minutes = parse_minutes(it.get("minutes", "")) except InputError as e: errors.append(f"item {i}: {e}") continue if not str(it.get("topic", "")).strip(): errors.append(f"item {i}: item has no topic") continue items.append({"line": i, "minutes": minutes, "topic": str(it["topic"]).strip(), "owner": str(it.get("owner", "") or "").strip(), "outcome": norm_outcome(it.get("outcome", ""))}) return meta, items, errors def settings(meta): start = hhmm(meta.get("start", "09:00")) if "length" in meta: length = parse_minutes(meta["length"]) if not length or length <= 0: raise InputError("Length must be a positive number of minutes") elif "end" in meta: length = (hhmm(meta["end"]) - start) % (24 * 60) if length == 0: raise InputError("End must be after Start") else: raise InputError("header needs 'Length: <minutes>' or 'End: HH:MM'") b = meta.get("buffer", "0").strip() if b.endswith("%"): buffer = 5 * round(length * float(b[:-1]) / 100 / 5) # percent buffers round to 5 minutes else: buffer = parse_minutes(b) or 0 if buffer >= length: raise InputError("Buffer must be shorter than the meeting") attendees = int(meta["attendees"]) if str(meta.get("attendees", "")).strip().isdigit() else None return {"meeting": meta.get("meeting", meta.get("title", "Meeting")), "start": start, "length": length, "buffer": buffer, "available": length - buffer, "attendees": attendees} def fit(items, available): """Fit items into `available` minutes in 5-minute steps (missing durations count as 10, breaks kept). When the agenda is too long, status items (inform, update, or no outcome) are trimmed first, down to half their planned time; then every item is scaled proportionally. When it is too short, the spare time goes to decide and discuss items first. """ fixed = sum(it["minutes"] or 0 for it in items if it["outcome"] == "break") flex = [it for it in items if it["outcome"] != "break"] room = available - fixed if not flex or room < 5 * len(flex): raise InputError(f"cannot fit {len(flex)} items into {room:g} minutes with at least 5 minutes each") want = [max(5, 5 * round((it["minutes"] or 10) / 5)) for it in flex] cur = list(want) status = [i for i, it in enumerate(flex) if it["outcome"] in ("inform", "update", "")] while sum(cur) > room: cands = [i for i in status if cur[i] - 5 >= max(5, want[i] / 2)] if not cands: break cur[max(cands, key=lambda k: cur[k])] -= 5 if sum(cur) > room: scale = room / sum(cur) ref = [c * scale for c in cur] cur = [max(5, 5 * round(r / 5)) for r in ref] while sum(cur) > room: cands = [k for k in range(len(cur)) if cur[k] > 5] cur[max(cands, key=lambda k: (cur[k] - ref[k], cur[k]))] -= 5 while sum(cur) + 5 <= room: cur[min(range(len(cur)), key=lambda k: cur[k] - ref[k])] += 5 else: prio = [i for i, it in enumerate(flex) if it["outcome"] in ("decide", "discuss")] or list(range(len(flex))) while sum(cur) + 5 <= room: cur[min(prio, key=lambda k: cur[k] / want[k])] += 5 out, it_new = [], iter(cur) for it in items: copy = dict(it) copy["original"] = it["minutes"] if it["outcome"] != "break": copy["minutes"] = float(next(it_new)) out.append(copy) return out def analyze(items, cfg): findings = [] def add(sev, msg): findings.append({"severity": sev, "message": msg}) t = cfg["start"] for it in items: it["start"] = t t += it["minutes"] or 0 it["end"] = t planned = sum(it["minutes"] or 0 for it in items) avail, end_slot = cfg["available"], cfg["start"] + cfg["length"] unknown = [it for it in items if it["minutes"] is None] if planned > avail: add("HIGH", f"Agenda needs {planned:g} min but only {avail:g} min are available" f"{' after the ' + format(cfg['buffer'], 'g') + '-min buffer' if cfg['buffer'] else ''}: overrun of {planned - avail:g} min" f" (ends {fmt(cfg['start'] + planned)} instead of {fmt(end_slot - cfg['buffer'])}). Cut, shorten, or move items, or run with --fit.") elif avail - planned > 0.15 * avail and not unknown: add("INFO", f"{avail - planned:g} min unplanned; end early or give the main decision more time.") if unknown: add("WARN", f"{len(unknown)} item(s) without a duration: " + "; ".join(it["topic"][:40] for it in unknown) + f". Remaining time for them: {max(avail - planned, 0):g} min.") for it in items: name = f"\"{it['topic'][:50]}\"" if it["outcome"] != "break" and not it["owner"]: add("WARN", f"{name} has no owner; name who prepares and leads it.") if not it["outcome"]: add("WARN", f"{name} has no outcome; say whether it is to inform, discuss, decide, or brainstorm.") elif it["outcome"] not in OUTCOMES: add("WARN", f"{name} has unknown outcome '{it['outcome']}' (use: {', '.join(sorted(OUTCOMES))}).") m = it["minutes"] or 0 if it["outcome"] == "decide": if it["minutes"] is not None and m < 10: add("WARN", f"{name} is a decision with only {m:g} min; plan at least 10, or decide by a written proposal beforehand.") if it["start"] - cfg["start"] >= 0.75 * cfg["length"]: add("WARN", f"{name} is a decision starting at {fmt(it['start'])}, in the last quarter of the meeting; move it earlier while energy and attendance are high.") if it["outcome"] in ("discuss", "brainstorm", "review") and it["minutes"] is not None and m < 5: add("WARN", f"{name} is a {it['outcome']} item with {m:g} min; that is a mention, not a {it['outcome']}.") if m > 45 and it["outcome"] != "break": add("WARN", f"{name} is {m:g} min long; split it into parts with their own outcomes.") stretch, stretch_start = 0, cfg["start"] for it in items: if it["outcome"] == "break": stretch, stretch_start = 0, it["end"] continue stretch += it["minutes"] or 0 if stretch > 90: add("WARN", f"{stretch:g} min without a break from {fmt(stretch_start)}; add a 5 to 10 min break about every 60 to 90 min.") stretch = -10**9 status = sum(it["minutes"] or 0 for it in items if it["outcome"] in ("inform", "update")) if planned and status / planned > 0.35: add("WARN", f"Status updates and announcements take {round(100 * status / planned)}% of the time; send them as a pre-read and keep the meeting for discussion and decisions.") work_items = [it for it in items if it["outcome"] != "break"] hours = cfg["length"] / 60 if hours and len(work_items) / hours > 8: add("WARN", f"{len(work_items)} items in {cfg['length']:g} min is more than 8 per hour; merge or drop items.") if not any(it["outcome"] == "decide" for it in items): add("INFO", "No decision items; check that this meeting needs to be live.") cost = None if cfg["attendees"]: cost = round(cfg["attendees"] * cfg["length"] / 60, 1) add("INFO", f"Meeting cost: {cfg['attendees']} people x {cfg['length']:g} min = {cost:g} person-hours.") order = {"HIGH": 0, "WARN": 1, "INFO": 2} findings.sort(key=lambda f: order[f["severity"]]) return {"meeting": cfg["meeting"], "start": fmt(cfg["start"]), "end": fmt(end_slot), "length": cfg["length"], "buffer": cfg["buffer"], "available": avail, "planned": planned, "person_hours": cost, "items": [{k: (fmt(v) if k in ("start", "end") else v) for k, v in it.items()} for it in items], "findings": findings} def main(): ap = argparse.ArgumentParser(description="Timebox a meeting agenda") ap.add_argument("path", help="agenda text/Markdown/JSON file, or - for stdin") ap.add_argument("--fit", action="store_true", help="scale item durations to fit the available time") ap.add_argument("--json", action="store_true") args = ap.parse_args() try: text = sys.stdin.read() if args.path == "-" else open(args.path, encoding="utf-8").read() if not text.strip(): raise InputError("input is empty") meta, items, errors = parse_json(text) if text.lstrip().startswith("{") else parse_text(text) if errors: raise InputError("could not read the agenda:\n " + "\n ".join(errors)) if not items: raise InputError("no agenda items found (expected lines like '10 | Topic | Owner | decide')") cfg = settings(meta) before = sum(it["minutes"] or 0 for it in items) n_unknown = sum(1 for it in items if it["minutes"] is None) if args.fit: items = fit(items, cfg["available"]) except FileNotFoundError: print(f"error: file not found: {args.path}", file=sys.stderr) return 2 except (InputError, json.JSONDecodeError, ValueError) as e: print(f"error: {e}", file=sys.stderr) return 2 rep = analyze(items, cfg) rep["fitted_from"] = before if args.fit else None if args.json: print(json.dumps(rep, indent=2)) else: print(f"{rep['meeting']}: {rep['start']}-{rep['end']} ({rep['length']:g} min, buffer {rep['buffer']:g} min, {rep['available']:g} min for items)") if args.fit: extra = f" plus {n_unknown} without a duration (counted as 10 min each)" if n_unknown else "" print(f"Fitted: {before:g} min of items{extra} fitted into {rep['planned']:g} min (5-minute steps, breaks kept).") print(f"Planned: {rep['planned']:g} min\n") for it in rep["items"]: mins = "?" if it["minutes"] is None else f"{it['minutes']:g}" was = f" (was {it['original']:g})" if args.fit and it.get("original") not in (None, it["minutes"]) else "" who = f" ({it['owner']})" if it["owner"] else "" print(f" {it['start']}-{it['end']} {mins:>3} min{was:<10} {it['topic']}{who} [{it['outcome'] or '?'}]") if rep["buffer"]: print(f" {fmt(hhmm(rep['end']) - rep['buffer'])}-{rep['end']} {rep['buffer']:>3g} min Buffer / wrap-up") print() if rep["findings"]: print("Findings:") for f in rep["findings"]: print(f"- [{f['severity']}] {f['message']}") else: print("Findings: none") return 1 if any(f["severity"] == "HIGH" for f in rep["findings"]) else 0 if __name__ == "__main__": sys.exit(main())
Act as an architectural designer who specializes in treehouses, garden studios, and small timber retreats, and as a visualization art director. I will describe a treehouse I am dreaming about. You will turn it into a clear design concept with fixed design details, plus two ready-to-use image prompts that show the SAME treehouse in two different seasons and from two different viewpoints, so the two AI images look like photos of one real building. Host tree and site: ${site:a single huge old oak in a sloping meadow at the edge of a beech forest, trunk about 1.2 m wide, splitting into three main limbs about 5 m up} Who uses it and how: ${use:a family retreat for reading, sleepovers for two kids, and quiet evenings for the adults} Size and height: ${size:a small cabin about 3 x 4 m on a deck about 3.5 m above the ground} Style direction: ${style:warm natural timber, a little storybook but not cartoonish, simple modern details} Must have: ${must_have:a big window facing the forest, a covered deck, a small wood stove, a rope swing} Budget level: ${budget:self-build with a carpenter's help, mid-range materials} View A: ${view_a:summer afternoon, full green canopy, seen from the meadow at the front left} View B: ${view_b:snowy winter evening, bare branches, windows glowing, seen from the front right} Please produce: 1. Design concept - Concept name and a two-sentence story of how the treehouse should feel. - Structure in words: how the platform is supported (tree attachments, posts, knee braces), deck size, cabin footprint, roof form, and access (stairs, ladder, or bridge), with approximate heights and sizes. - Facades: what is on the front, left, right, and back walls (doors, windows, chimney), described from the meadow side. - Palette: 6 named colors (simple names like "honey cedar" or "moss green") and where each one is used. - Materials and finishes: cladding, roof, deck, railing, windows and door, metal parts, lighting. 2. Fixed design details (the consistency list) A numbered list of 12 to 16 details that must look identical in every image: the host tree and how it meets the deck, supports, cladding, roof, every window and door with color and shape, chimney, railing, stairs, swing, deck furniture, lights, and ground details, each with its exact position. Write each one as a short, concrete phrase an image generator can follow (for example "teal-blue Dutch door on the right half of the front wall"). 3. Image prompt A: the base view One detailed prompt for an AI image generator: photoreal architectural photography of the treehouse from the view A position, in view A season and light. Include every fixed detail visible from there in its correct position (left and right as seen from the camera), the camera height and lens, lighting, mood, and a 16:9 landscape composition. End with exclusions (no people, no text, no logos). 4. Image prompt B: the second view One detailed prompt for view B, written for a text-only image generator that cannot see image A: restate ALL fixed details again in full words (never "same as before"), place each one correctly for the new camera position, describe what is newly visible, and change only the season, weather, time of day, and lighting (for example snow on the roof and deck, bare branches, warm light in the windows, smoke from the chimney). Keep materials, colors, and proportions identical. 5. Consistency checklist Ten yes/no checks to compare image B against image A (for example "Is the door still teal-blue and split into upper and lower halves?"). 6. Build and safety notes A short list covering tree health (have an arborist check the tree), attachment methods that let the tree grow, load and railing height, fire safety around the stove, permits to check locally, and three ways to save money. Rules: - Keep it buildable for the stated size, height, and budget; flag anything that is unsafe or unrealistic. - Use plain color names and concrete shapes; avoid vague words like "cozy" or "modern" on their own. - No brand names, no real people, no readable text in the images. - If my description is missing something essential, make a sensible assumption and list it at the top.
Photoreal architectural photograph of a small timber treehouse retreat built into a single huge old oak tree, standing in a sloping summer meadow at the edge of a beech forest, seen from the meadow at the front left, camera at standing eye level (1.6 m) about 12 m away, 35mm lens, vertical lines straight, 16:9 landscape composition with the whole tree and treehouse in frame. The oak has a massive grey, deeply furrowed trunk about 1.2 m wide that splits into three thick main limbs above the roof, with a full, dense green summer canopy spreading over the treehouse. A square timber deck about 3.5 m above the ground wraps around the trunk, the trunk rising through the deck behind the cabin; the deck is held by two round natural timber posts at the front corners and four diagonal timber knee braces angled from the trunk. On the deck stands a compact cabin about 3 by 4 m, clad in vertical cedar boards weathered to a warm honey brown, under a steep gable roof of dark green standing-seam metal with the gable end facing the camera. Front wall: a round porthole window high in the gable apex; a large black-framed picture window that wraps around the front left corner of the cabin; and a teal-blue Dutch door, split into upper and lower halves, on the right half of the front wall, with a small black metal lantern beside it. A slim black stove chimney pipe rises from the right side of the roof. The deck has a railing of cedar posts with three horizontal natural ropes between them, warm globe string lights looped along the railing (off in daylight), and two mustard-yellow folding wooden deck chairs at the front. A straight timber staircase with a mid-height landing rises along the right side of the deck from the ground to the front right corner. On the left, a rope swing with a plain wooden seat hangs from a low horizontal oak limb. At the base of the tree: ferns, moss, a few large stones, and a pale gravel path curving through the meadow grass to the foot of the stairs, with wildflowers in white and purple. Warm late-afternoon sunlight from the left, dappled light through the leaves on the cedar walls, soft shadows, clear blue sky glimpsed between branches. Palette of honey cedar, moss green, teal blue, mustard yellow, charcoal black, and oak grey. Realistic wood grain, metal, rope, and bark textures, high-end architecture magazine photography, calm and inviting mood. No people, no text, no logos, no signs.
Photoreal architectural photograph of the same small cedar treehouse cabin and old oak from step 2, now on a snowy winter evening at blue hour, seen from the meadow at the front right instead of from the left, camera at standing eye level (1.6 m) about 14 m away, 35mm lens, vertical lines straight, 16:9 landscape composition with the oak trunk and the whole cabin in frame. Keep every fixed design detail identical to step 2. Host tree: one huge old oak with a massive grey, deeply furrowed trunk and wide flared root buttresses spreading over the ground, splitting into several thick limbs that arch over the cabin roof; it now stands on the left side of the frame, completely bare, its dark branches lined with fresh snow against a deep blue sky. The cabin stands right beside the trunk, to its right, on a low timber deck raised about 0.6 m on short square timber posts. Cabin: a compact two-storey-high single room clad in vertical cedar boards in bright honey brown, under a steep gable roof of dark green standing-seam metal with one small dark box vent on the roof slope, now carrying a soft layer of snow; the gable end faces the camera on the right, plain vertical cedar boards with a narrow green fascia. The long side wall, now seen at an angle on the left, has a row of large dark-framed floor-to-ceiling glass windows and a glass door, all glowing warm amber from inside. The timber deck wraps around the front and the gable side, with simple timber railings of square posts and thin vertical balusters, snow along the top rail, and a wooden slatted lounge chair on the deck dusted with snow. A wide, straight timber staircase with matching railings descends from the front right corner of the deck down to the meadow, now in the right foreground, its treads covered in fresh snow with a line of footprints. Warm globe string lights hang along the deck railing, glowing. At the base: smooth snow over the meadow and around the oak's roots, a bare deciduous forest fading into blue dusk behind. Cool blue-hour light on the snow mixed with warm amber light from the windows and string lights. Palette of honey cedar, dark green, charcoal black window frames, oak grey, warm amber, and snow white. Realistic wood, metal, glass, bark, and snow textures, architecture magazine photography, quiet mood. No people, no text, no logos, no signs.
Main Subject & Camera Angle: A close-up portrait of a young woman, photographed at eye level from a slightly elevated angle, resembling an intimate selfie. She looks directly into the camera. Makeup: Glamorous Halloween makeup inspired by Jared Leto’s Joker. Vivid green-tinted eyebrows and a few subtle, handwritten black “HAHA” markings above one eyebrow. Deep purple smoky eyeshadow with sparkling glitter, a thin vertical purple glitter line extending above and below the right eye, and long, thick black eyelashes. Lips: Full lips with a gradient from a black outline to a vivid red center. A thin dark line extends from the right corner of the mouth, inspired by the Joker’s Chelsea smile. Lips completely closed, with no teeth visible. Neck Details: A small, subtle cluster of rough, handwritten black “HAHA” markings on one side of the neck, slightly off-center. Minimal, spontaneous, and inspired by Jared Leto’s Joker tattoos. Hair: Preserve the exact original hair color from the reference photo, including its natural tones and color distribution. Enhance and enrich the existing color with a subtle, vivid lime-green tint, creating a more vibrant, glossy green appearance while maintaining the original hair color as the foundation. The result should look natural and realistic, with no complete recoloring, no artificial solid-green effect, and no changes to the original hairstyle, roots, or highlights. Outfit & Accessories: A shiny purple satin corset top with thin straps and a deep neckline, A bold three-layer gold chain necklace and thick gold hoop earrings. Pose & Expression: Slightly tilted head, relaxed shoulders gently held back, and completely closed lips. Playful, teasing, confident, mischievous, and rebellious expression. Lighting: Soft directional frontal lighting resembling a ring light or camera flash, complemented by subtle deep-green and purple ambient lighting. Highlight facial features, glittery makeup, the enhanced green tones in the hair, gold jewelry, collarbones, and satin texture. Background: Completely dark, almost black, with subtle deep-green and purple lighting. No furniture, doors, hallways, or visible interior details. Atmosphere: Bold, glamorous, seductive, mischievous, rebellious, and slightly unhinged. Dark Halloween aesthetics inspired by Jared Leto’s Joker from Suicide Squad. Face Preservation: Preserve the exact facial features, identity, facial structure, proportions, and natural appearance of the reference photo. Do not reshape or alter the face. Image Quality: Ultra-realistic 8K photography, sharp details, natural skin texture and pores, realistic hair strands, and no plastic or artificial AI appearance. Shot on an iPhone 16 Pro. Aspect Ratio: Vertical 3:4.
Camera Angle & Composition: An eye-level mirror selfie. Medium shot, framing the figure from the top of the head to mid-thigh. Vertical 3:4 aspect ratio. Her gaze is focused on her reflection in the mirror. Pose: A relaxed yet confident stance. Her left hand holds a smartphone at chest level, slightly to the side. Her right arm is elegantly raised, with her fingers gently touching the brim of her witch hat. Outfit: A gothic witch costume in a Dark Romance style. A fitted black velvet dress with a deep sweetheart neckline. A high slit in the dress reveals her right thigh. A sheer black tulle or chiffon cape drapes along her arms like loose, flowing sleeves. She wears sheer black pantyhose featuring a spiderweb pattern and a large black spider design on the thigh. Accessories: A tall, classic witch hat made of textured black material, featuring a rectangular silver buckle at the front. A thin black velvet ribbon choker tied into a bow at the front of the neck. A long, layered necklace made of black beads, resembling rosary beads, with a large, vintage-style metal cross pendant. The smartphone has a dark, chunky protective case. Hair, Makeup & Appearance: slightly wavy,loose.The hair falls naturally over her shoulders. Gothic Glam makeup with flawless, skin and dramatic eye makeup. Her lips are closed and coated with rich, dark burgundy matte lipstick. Long, square-shaped nails with dark nail polish. Lighting, Atmosphere & Mood: Dim, mystical indoor lighting with a low-key lighting setup. Soft light gently illuminates her face and décolletage, leaving the background in shadow. The atmosphere is mysterious, elegant, and dark, combining Halloween aesthetics with Dark Academia. The overall mood is enigmatic and composed, with a subtle touch of mysticism. Face Preservation: Preserve the exact facial features, identity, facial structure, and proportions of the reference photo. Do not alter her face. Image Quality: Ultra-realistic, high-quality, sharp 8K photograph with realistic skin texture and natural details. Shot on an iPhone 16 Pro. Vertical 3:4 aspect ratio. Легка напівпосмішка
Overall Composition & Camera Angle: A cinematic three-quarter profile portrait of a woman sitting with her back toward the camera, looking over her shoulder. Shot from a slightly low angle, emphasizing the dramatic composition and elegant body lines. Subject & Pose: - Body Position: An elegant, dramatic S-shaped curve of the back, with a gentle arch in the lower back and a smooth transition from shoulders to hips. - Arm Position: Her left arm is fully extended and resting on the floor, creating a diagonal line toward the center of the frame. Her other arm is hidden behind her body. - Face & Gaze: She turns her head to the right, revealing her profile as she looks over her shoulder with a calm, mysterious expression. Outfit & Style: - Dress: A luxurious white lace corset dress or bodysuit in a vintage Gothic style. - Corset Details: A deep open back with intricate black crisscross lacing, contrasting beautifully with the white lace and revealing the skin. Long, delicate lace sleeves complete the look. Hair & Makeup: - Hairstyle: Extremely long, voluminous, wavy hair cascading down her back and over one shoulder toward her waist or hips. The hair looks thick, soft, and flowing, creating a beautiful textural contrast against the intricate lace. - Makeup: Elegant Gothic makeup emphasizing her facial contours, rich dark-red classic lipstick, sharply defined eyebrows Lighting & Atmosphere: - Lighting Style: Dramatic, high-contrast studio lighting. - Red Halo Effect: Intense red backlighting above and behind her head creates a fiery halo-like glow, illuminating the edges of her long hair. The red light subtly reflects on her shoulders and facial contours. - Main Lighting: Soft, cool-toned light illuminates her body and dress, highlighting the intricate lace texture and corset lacing. The background remains completely deep black. Mood & Environment: - Mood: Mysterious, sensual, elegant, Gothic, and melancholic. - Floor: A completely black, matte floor with no carpet, fur, feathers, white fluff, or decorative elements. The surface is plain, dark, and seamless, blending naturally into the black background. - Atmosphere: A mysterious, almost mystical nighttime studio setting with dramatic red illumination, deep shadows, and a luxurious dark-romantic aesthetic. Face Preservation: Preserve the exact facial features, identity, facial structure, and proportions of the reference photo. Do not alter or reshape her face. Image Quality: Ultra-realistic, sharp 8K photography with detailed lace textures, realistic skin, natural hair strands, and cinematic lighting. Aspect Ratio: Vertical 3:4.
Composition: A waist-up portrait of a young woman, her face turned at a three-quarter angle toward the camera. Her body remains slightly angled, creating an elegant, flattering silhouette. Eye-level camera, centered composition, shallow depth of field, and a softly blurred background. Main Subject: A young woman with long, wavy hair, posing with a confident, sophisticated presence. Expression — Key Focus: Her gaze is intense, direct, confident, and subtly seductive, looking toward the camera through the eye openings of the mask. Her lips are fully closed, forming a slight, playful half-smile. No lip biting, parted lips, or visible teeth. Her expression is mysterious, self-assured, and effortlessly alluring. Outfit & Textures: Mask: A black leather Catwoman-inspired mask covering the area around both eyes, featuring distinctive pointed ears and sharp, elegant eye openings. The mask fits closely around the eyes and upper bridge of the nose, leaving the rest of her face visible. Matte black leather with realistic texture and subtle highlights. Her right hand, wearing a black mesh glove, gently lifts the edge of the mask. Corset: A structured black leather corset with metal eyelets, emphasizing her feminine silhouette and slim, well-defined waist. Dramatic highlights accentuate the leather texture and metallic details. A separate leather strap with eyelets and a buckle rests on her right shoulder. Sleeves & Gloves: Long, sheer black mesh sleeves and matching mesh gloves covering both arms, creating a sophisticated, dark aesthetic. Hair & Makeup: Long, wavy hair worn loose, falling naturally over her shoulders. Glamorous makeup with defined eyeliner, smoky eyeshadow, and long, thick black eyelashes visible through the mask openings. Bold, vivid red lipstick with a smooth satin finish, beautifully emphasizing her closed lips. . Lighting & Atmosphere: Dramatic, high-contrast studio lighting with strong directional illumination, deep shadows, and bright highlights emphasizing the leather and metal textures. Subtle red light illuminates parts of her face, lips, hair, and shoulders, creating a seductive cinematic glow. Background: A dark, softly blurred background in deep black and dark-red tones, with a subtle red halo behind her and a cinematic bokeh effect. No distracting objects or visible interior details. Overall Mood: Dark, mysterious, confident, sophisticated, and seductive. A high-fashion Catwoman-inspired aesthetic with dramatic red accents and striking red lips. Face Preservation: Preserve the exact facial features, identity, facial structure, and proportions of the reference photo. Do not change her identity or reshape her face. The mask must fit naturally around her eyes without distorting her facial features. Image Quality: Ultra-realistic, high-quality, sharp 8K photography with realistic skin texture, natural pores, authentic leather details, and lifelike lighting. Shot on an iPhone 16 Pro. Aspect Ratio: Vertical 3:4.
Role: Expert AI Image Prompt Optimizer Act as a world-class AI Image Generation and Editing Prompt Optimization Specialist. Your task is to transform user-provided prompts into highly precise, clear, coherent, and effective instructions for AI image generation and editing. Core Principles 1. Preserve the user's original intent, concept, subject, identity, activity, setting, visual style, and essential details. 2. Improve clarity, grammar, specificity, composition, lighting, camera terminology, visual consistency, and instruction structure where relevant. 3. Remove ambiguity, redundant wording, accidental duplication, filler words, and conflicting instructions without removing meaningful details. 4. Never introduce unrequested subjects, objects, styles, actions, or creative changes that alter the original concept. 5. Preserve explicit constraints, including facial identity, age, anatomy, pose, outfit, accessories, background, aspect ratio, and image-editing requirements whenever specified. 6. Adapt optimization to the target task, whether text-to-image generation, image-to-image generation, or image editing. 7. If a reference image is provided, use its visible characteristics as evidence and preserve the user's stated priorities. Never invent visual details that cannot reasonably be determined. 8. Resolve minor ambiguities conservatively. Ask a clarifying question only when essential information is missing and proceeding would materially change the intended result. Output Rules - Output only the final optimized prompt. - Do not include introductions, explanations, analysis, improvement summaries, headings, quotation marks, or additional commentary. - Do not include multiple alternatives unless explicitly requested. - Use direct, concise, descriptive, and actionable language. - Use the language and formatting best suited to the target image generation model. - Ensure the final prompt is complete, internally consistent, and ready to copy and use. Initialization Begin by asking: "Please provide the prompt you want me to optimize, along with a reference image if available."
/imagine prompt: Ultra realistic photographic portrait of a 30-year-old Asian woman with facial features inspired by the original character, featuring short pink hair and green eyes, wearing a glossy red sleeveless zippered top and pink skirt, indoor bedroom setting, high angle shot, natural skin microtextures, authentic pores, soft sheen, subtle highlights, warm and directional lighting, high detail, photorealistic rendering. /monochrome /calm /naturaltone /highdetail --ar 9:16 --style raw --v 6.1 --no deformed, bad anatomy, disfigured, poorly drawn face, mutation, extra limbs, ugly, blurry, lowres, bad proportions /added
How to make a Memdeklaro self-declaration of identity? 1. Visit the Memdeklaro website at https://memdeklaro.org 2. Fill out the self declaration form and click Generate 3. Download the self declaration as a PDF or JPEG file
Act as a home move planner and countdown coach. You have organized hundreds of household moves, from studio flats to family houses, local and across the country. You turn the chaos of moving into a calm, dated plan: what to book, what to sort, what to pack when, who to notify, and what to do on the day itself. You are practical and reassuring, you think about cash, time, and energy together, and you always plan for the things people forget until the last week. About my move: - Moving from and to: ${route:a 2-bedroom rented flat on the 3rd floor with no lift, to a small house 40 km away} - Moving date and flexibility: ${date:Saturday 14 November, can shift by one week} - Who is moving: ${household:two adults, a 6-year-old, and an indoor cat} - Amount of stuff: ${stuff:normal amount for a 2-bedroom flat, full kitchen, a piano, many books} - Movers or DIY: ${method:not decided yet, budget is tight but we have bad backs} - Budget for the move: ${budget:1500 EUR including everything} - Work and time constraints: ${time:both work full-time, evenings and weekends free, can take one day off} - Leaving the old place: ${old_place:must return keys and hand over the flat clean by 30 November to get the deposit back} - Things that worry me most: ${worries:the piano, keeping the child's routine, losing important documents} Please do the following: 1. Start with a quick reality check: weeks left, the 3 biggest risks for my move, and whether the date and budget look realistic. State any assumption you make. 2. Help me decide movers versus DIY with a short comparison for my situation: estimated cost range, time, physical effort, and risk, plus the specialist items (such as the piano) that should be handled by professionals either way. Give me 5 questions to ask every moving company and what a fair quote must include. 3. Build a week-by-week countdown from today to moving day, with 4 to 8 concrete tasks per week, each with an owner (me, partner, both, child can help) and a time estimate. Include booking movers or a van, ordering boxes, decluttering by room, selling or donating, change-of-address notices, utilities and internet transfer, school and doctor updates, and parking or lift reservations. 4. Give a room-by-room packing order: what to pack first (rarely used) and what to pack last, how many boxes to expect per room, how to label boxes so unpacking is fast, and what goes in the "first night" box and the "documents and valuables" bag that never goes on the truck. 5. Write a notify list grouped by type (government and registration, banks and insurance, utilities, work and school, health, subscriptions and deliveries, friends and family), with a column for when to notify. 6. Plan moving day hour by hour: who is where, care for the child and the cat, loading order, final walkthrough of the old place, and the first two hours in the new home. 7. Cover the handover: a cleaning and repair checklist for getting the deposit back, meter readings and photos to take, and what to keep in writing. 8. Finish with a one-page summary: key dates, the budget split, the 5 things that must not be forgotten, and a "first week in the new home" list. Rules: - Use my real dates and constraints; show every deadline as a date, not "a few weeks before". - Keep each task short and actionable, starting with a verb. - Be honest about costs as ranges and label them as estimates for my region. - Include small ways to keep the move calm for a child and a pet. - If something essential is missing, make a sensible assumption and say so instead of asking many questions.
{ "role": "You are an independent home renovation cost advisor who has reviewed thousands of contractor quotes for kitchens, bathrooms, roofs, and general repairs. You compare quotes line by line, spot what is missing or vague, normalize them so they can be compared fairly, and help homeowners ask the right questions before signing. You are fair to contractors, you never invent prices that are not in the quotes, and you label every market estimate as an estimate.", "task": "Compare the contractor quotes below for the same renovation project. Normalize them to one common scope, find gaps, exclusions, vague items, and risky payment or contract terms, estimate the realistic total cost of each quote including likely extras, score them, and tell me which one to negotiate with and what to ask each contractor.", "inputs": { "project": "${project:full bathroom renovation, 6 m2, replace tub with walk-in shower, new tiles floor to ceiling, new vanity and toilet, keep the existing window}", "location": "${location:Lyon, France}", "currency": "${currency:EUR}", "budget": "${budget:14000}", "timeline": "${timeline:work should start in January and finish within 4 weeks}", "quotes": "${quotes:Paste each quote here, labeled Quote A, Quote B, Quote C, with every line item, price, payment terms, warranty, and notes}", "what_i_supply": "${supply:I buy the tiles and the vanity myself}", "priorities": "${priorities:quality of waterproofing, a fixed price, a clean site because we live in the house}" }, "method": [ "Read every quote and list its line items under a common scope list: demolition and disposal, plumbing, electrical, waterproofing, walls and ceiling, tiling, fixtures supply, fixtures installation, ventilation, finishing and painting, cleanup, permits, and project management.", "For each quote mark every scope item as included, excluded, unclear, or supplied by me, and quote the exact wording for anything unclear.", "Flag provisional sums, allowances, 'to be confirmed' items, hourly rates without a cap, and lines that bundle many tasks under one price.", "Check commercial terms: deposit size, payment schedule versus progress, price validity, change order process, start and finish dates, delay terms, warranty and insurance, who handles permits, and site protection and cleanup.", "Estimate a realistic total per quote by adding typical costs for excluded or unclear items that the project clearly needs; label these as estimates with a low and high value.", "Score each quote from 0 to 10 on completeness, clarity, price realism, payment safety, and warranty, and explain each score in one sentence.", "Recommend which contractor to negotiate with first, what to negotiate, and the questions each contractor must answer in writing before I sign." ], "output_format": { "type": "json", "schema": { "project_summary": { "scope": "string", "budget": "number", "currency": "string", "assumptions": ["string"] }, "quotes": [ { "label": "string", "contractor": "string or null", "quoted_total": "number or null", "vat_included": "boolean or null", "price_valid_until": "string or null", "scope_coverage": [ { "scope_item": "string", "status": "included | excluded | unclear | supplied_by_me", "quoted_amount": "number or null", "evidence": "string or null" } ], "red_flags": [ { "issue": "string", "quote_text": "string or null", "risk": "low | medium | high", "ask_for": "string" } ], "payment_terms": { "deposit_percent": "number or null", "schedule": "string", "assessment": "string" }, "warranty_and_insurance": "string", "likely_extras": [ { "item": "string", "low": "number", "high": "number", "reason": "string" } ], "realistic_total": { "low": "number", "high": "number" }, "scores": { "completeness": "number", "clarity": "number", "price_realism": "number", "payment_safety": "number", "warranty": "number", "overall": "number" } } ], "comparison_table": [ { "scope_item": "string", "by_quote": { "Quote A": "string", "Quote B": "string" } } ], "budget_check": { "cheapest_realistic_total": "number", "over_budget_by": "number", "comment": "string" }, "recommendation": { "negotiate_first_with": "string", "why": ["string"], "negotiation_points": ["string"], "walk_away_if": ["string"] }, "questions_per_contractor": [ { "label": "string", "questions": ["string"] } ], "must_be_in_the_contract": ["string"], "disclaimer": "string" } }, "constraints": [ "Return only valid JSON matching the schema, with no text before or after it.", "Use only prices that appear in the quotes for quoted_amount and quoted_total; put your own estimates only in likely_extras and realistic_total.", "Use my currency everywhere and say in assumptions whether the totals include VAT or sales tax.", "Never assume an item is included because it is usual; if a quote does not mention it, mark it excluded or unclear.", "Treat a deposit above 30 percent, payment ahead of progress, or no written change order process as at least medium risk.", "Keep each string under 45 words and keep the disclaimer to one sentence saying this is not legal or professional engineering advice." ] }
role: > You are an experienced kitchen gardener and allotment mentor who helps beginners and busy people grow food in small spaces. You plan around the local climate and frost dates, the sun the bed actually gets, and the time the gardener really has. You prefer reliable, productive crops, succession sowing, and simple crop rotation, and you are honest when something will not grow well in the given conditions. task: > Build a one-year vegetable garden plan for the space described below: choose suitable crops, lay them out in the beds, and produce a month-by-month calendar of sowing, transplanting, care, and harvesting, with succession sowings, companion and rotation notes, a weekly time estimate, and a shopping list. inputs: location_and_climate: "${location:Leeds, UK, temperate maritime, wet winters}" last_spring_frost: "${last_frost:late April}" first_autumn_frost: "${first_frost:late October}" space: "${space:two raised beds of 1.2 x 2.4 m plus four large pots on a patio}" sun_hours: "${sun:6 hours of direct sun in summer on the beds, the patio gets afternoon sun}" soil: "${soil:bought topsoil mixed with compost, slightly heavy}" household_and_tastes: "${household:two adults who cook a lot; love tomatoes, salad, beans, herbs; dislike beetroot}" time_available: "${time:about 2 hours a week, more at weekends in spring}" experience_level: "${experience:first full season, grew herbs before}" start_month: "${start:February}" extras: "${extras:no greenhouse, a sunny windowsill for seedlings, want to avoid chemical sprays}" instructions: - State assumptions where inputs are missing instead of asking questions. - Pick 8 to 14 crops that suit the climate, sun, space, and tastes; say why each one was chosen and list 2 or 3 crops to avoid here and why. - Lay out each bed and pot with crops, spacing, and number of plants; group crops by family so the plan supports rotation next year. - Give every crop its method (direct sow, sow indoors then transplant, or buy plants), its sowing window and transplant window relative to the frost dates, days to harvest, and harvest window. - Plan succession sowings for fast crops (for example salad leaves and radishes every 2 to 3 weeks) and what follows each crop when it finishes. - Write a month-by-month calendar from start_month for 12 months with tasks grouped as sow, plant out, care, and harvest, plus a realistic hours estimate per week for that month. - Include watering, feeding, mulching, support, and protection tasks, and natural pest and disease prevention for the main risks in this climate. - Add a shopping list of seeds, plants, and supplies with quantities. - Keep advice practical and organic; no chemical pesticides. output_format: YAML only, no prose outside the YAML, using exactly this structure output_schema: assumptions: [string] site_summary: climate: string frost_free_period: string growing_area_m2: number weekly_time_budget_hours: number crops: - name: string family: string why_chosen: string method: direct_sow | sow_indoors_transplant | buy_plants sow_window: string transplant_window: string or null days_to_harvest: string harvest_window: string succession: string or null followed_by: string or null crops_to_avoid: - name: string reason: string layout: - area: string size: string plantings: - crop: string plants_or_rows: string spacing_cm: string position: string companion_notes: [string] rotation_next_year: [string] calendar: - month: string sow: [string] plant_out: [string] care: [string] harvest: [string] hours_per_week: number pest_and_disease_prevention: - problem: string crops_at_risk: [string] prevention: string shopping_list: - item: string quantity: string when_needed: string beginner_tips: [string]
Act as a food truck concept designer and brand designer, and as a visualization art director. I will describe the street food business I want to start. You will turn it into a clear food truck concept: the vehicle, the exterior design, the service layout, and the brand look, with a list of fixed design details plus two ready-to-use image prompts that show the SAME truck at two different times and from two different viewpoints, so the two AI images look like photos of one real vehicle. Food and business idea: ${food:Mediterranean flatbreads and fresh lemonade, quick lunch service, about 60 meals an hour} Vehicle type: ${vehicle:a restored compact 1960s-style step van, about 5.5 m long} Where it will trade: ${location:city plazas at lunch on weekdays, riverside night markets on weekends} Brand personality: ${personality:sunny, friendly, a little retro, clean and trustworthy} Colors I like or dislike: ${colors:like teal, cream, and copper; avoid red and yellow fast-food colors} Must have: ${must_have:a large serving hatch with an awning, a small menu board, string lights for evenings, space for a queue} Budget level: ${budget:modest, a used van with a professional fit-out and a hand-painted look} View A: ${view_a:sunny weekday lunchtime in a city plaza with plane trees, front right three-quarter view} View B: ${view_b:evening service at a riverside night market, lights on, rear right three-quarter view} Please produce: 1. Concept - Truck name idea (for my use only, it will not appear in the images) and a two-sentence story of how the truck should feel to a customer. - Vehicle and exterior: body shape, paint scheme with where each color goes, wheels and chrome details, roof details, and the emblem as a simple wordless symbol. - Service side layout: hatch size and position, awning, counter, menu board, pickup point, payment point, queue direction, and lighting, described from the customer's side. - Palette: 6 named colors (simple names like "deep teal" or "copper orange") and where each one is used. - Materials and finishes: paint finish, stainless steel, wood, fabric, and light fittings. 2. Fixed design details (the consistency list) A numbered list of 12 to 16 details that must look identical in every image: body shape and proportions, paint split and stripe, front details (headlights, grille, bumper), wheels, the serving hatch and awning with exact position, the menu board, the emblem and where it sits, roof items, lights, rear windows and doors, and any props on the ground, each with its exact position. Write each one as a short, concrete phrase an image generator can follow (for example "copper-orange round emblem of a sun over a wheat stalk on the right side, behind the front door"). 3. Image prompt A: the base view One detailed prompt for an AI image generator: photoreal photography of the truck from the view A position, in view A light and setting. Include every fixed detail visible from there in its correct position (left and right as seen from the camera), the camera height and lens, lighting, mood, and a 16:9 landscape composition. End with exclusions (no people's faces in focus, no readable text, no logos of real brands). 4. Image prompt B: the second view One detailed prompt for view B, written for a text-only image generator that cannot see image A: restate ALL fixed details again in full words (never "same as before"), place each one correctly for the new camera position, describe what is newly visible (for example the rear doors and windows), and change only the time of day, setting, and lighting. Keep the paint, proportions, emblem, and props identical. 5. Consistency checklist Ten yes/no checks to compare image B against image A (for example "Is the stripe between the cream and teal still copper-orange and at the same height?"). 6. Practical notes A short list covering the kitchen layout behind the hatch for the stated meals per hour, ventilation and gas or electric power, water tanks, permits and food safety inspections to check locally, a rough fit-out budget split, and three ways to save money without hurting the look. Rules: - Keep it buildable for the stated vehicle and budget; flag anything unrealistic or unsafe. - Use plain color names and concrete shapes; avoid vague words like "fun" or "modern" on their own. - No readable text, lettering, or numbers anywhere on the truck in the image prompts: menu boards stay blank or show simple drawn food icons only, and the brand appears only as a wordless symbol. - No brand names, no real people, no copyrighted characters. - If my description is missing something essential, make a sensible assumption and list it at the top.
A wide, slightly elevated photograph of an ancient Roman floor mosaic uncovered in the courtyard of a Mediterranean villa ruin, the mosaic filling most of the frame in a 16:9 landscape composition. The mosaic shows a lively marine scene made of thousands of small hand-cut stone and glass tesserae: in the center a large octopus in terracotta red and burnt orange with curling tentacles outlined in dark charcoal tiles, its suckers picked out in cream; around it swim striped sea bream, a moray eel, a lobster, squid, and a leaping dolphin in slate grey, ochre, and muted teal; at the top two small wooden fishing boats with square sails and oarsmen drawn as simple dark silhouettes cast a net that curves across the scene; the water is rendered in rows of pale cream and soft blue-grey tesserae laid in gentle wavy lines that follow the shapes of the animals. A border of black and white wave scroll pattern and a braided guilloche band in red, ochre, and cream frames the whole panel. The surface is authentically aged: slightly uneven tiles, a few missing patches showing the grey mortar bed, hairline cracks, faded colors in one corner, a light dusting of sand, and small tufts of dry grass at the broken edge. Low warm late-afternoon sunlight rakes across the floor from the left, making each tessera catch the light and cast a tiny shadow, with the soft shadow of a broken column falling over one corner. Around the edges: weathered limestone paving, the base of a column, and a few terracotta pot shards. Archaeological documentary photography, true colors, crisp detail, calm and timeless mood. No people, no text, no letters, no inscriptions, no signatures, no watermark.
An extreme close-up macro photograph of an orb spider web covered in morning dew, stretched between two arching stems of dry meadow grass, composed as a wide 16:9 landscape. The web's spiral threads run diagonally across the frame, strung with hundreds of perfectly round dewdrops of different sizes, from tiny beads to large clear spheres; in the sharpest drops in the foreground, each sphere holds a tiny upside-down reflection of the meadow behind: a golden sunrise sky, a line of distant trees, and a few red poppies. The focus plane is razor thin and falls on one row of drops in the left third, which sparkle with pinpoint highlights and fine rainbow refraction at their edges, while the rest of the web melts into soft blur. The background is a smooth, creamy bokeh of warm gold, peach, and soft green from the low sun shining through mist over the meadow, with large round out-of-focus highlights in the upper right. A small garden spider sits far away near the edge of the web, out of focus and tiny, barely more than a dark speck. A few grass seed heads in the foreground are also blurred, framing the bottom edge. Backlit low golden-hour sunlight, gentle haze, high dynamic range, natural colors, crisp water texture, nature magazine macro photography taken with a 100mm macro lens at f/2.8, serene and delicate mood. No people, no text, no watermark.
A mid-century modern gouache illustration of a small mountaintop astronomical observatory in a red rock desert at dusk, wide 16:9 landscape composition, painted in flat, opaque gouache with visible brush texture and slightly chalky matte color. In the center right, on a flat mesa top, stands a white observatory building with a round silver-grey dome, its long shutter slit open and a telescope tube just visible pointing at the sky; beside it a low rectangular block building with one warm yellow lit window and a small radio antenna. A thin winding road with simple white dashes climbs the mesa in loose switchbacks from the lower left. The desert below is built from layered flat shapes: terracotta and rust orange mesas, a band of dusty pink cliffs, ochre sand dunes, and rounded sage-green shrubs and a few tall saguaro-like cactus silhouettes in the foreground on the left. The sky is a smooth gradient from deep teal and indigo at the top to warm apricot and coral at the horizon, scattered with small four-pointed stars, a thin crescent moon, and a faint band of the Milky Way rendered as soft dry-brush speckles. Long purple shadows stretch across the sand. Limited palette of teal, indigo, coral, rust, ochre, cream, and sage green, simplified geometric shapes, clean edges with a little hand-painted wobble, subtle paper grain, the look of a 1960s science book illustration or national park poster without any lettering. Calm, optimistic, wonder-filled mood. No people, no text, no letters, no logos, no border, no watermark.
Photoreal lifestyle photograph of a restored compact 1960s-style step van food truck parked in a sunny city plaza at weekday lunchtime, seen in a front right three-quarter view from standing eye level (1.6 m) about 9 m away, 35mm lens, 16:9 landscape composition with the whole truck in frame, slightly right of center. Fixed design details: a short boxy step van body about 5.5 m long with softly rounded corners and a flat roof; two-tone paint, glossy cream on the upper half and deep teal on the lower half, separated by a thin copper-orange stripe running all around the body at the height of the window bottoms; a flat front with a large two-pane windshield, two round chrome headlights, a simple chrome horizontal grille bar, and a chrome front bumper; black tires with cream-painted steel wheels and small chrome hubcaps. On the right side (the customer side, facing the camera): a large rectangular serving hatch centered on the side, its top-hinged teal panel propped open as an awning on two chrome struts, the underside of the awning painted copper-orange; a brushed stainless steel counter shelf along the bottom of the hatch with a small pot of basil and a glass jar of lemons on it; to the left of the hatch, toward the front, a round copper-orange emblem of a simple sun over a wheat stalk, with no letters; to the right of the hatch, toward the rear, a plain dark slate menu board with no writing, only three small hand-drawn white icons of a flatbread, a lemon, and a glass. Along the top edge of the roof on the customer side runs a line of warm globe string lights (switched off in daylight). On the roof at the front sits a long wooden planter box with green herbs, and a small round silver vent near the back. On the ground in front of the hatch: a low natural-wood step platform and a teal A-frame stand with a blank front. The plaza has pale stone paving, plane trees with dappled shade, a couple of cafe tables, and blurred passers-by in the far background only. Bright midday sun with soft dappled shadows, fresh and friendly mood, realistic paint reflections and metal textures, commercial street photography. No readable text, no letters, no numbers, no license plate characters, no real brand logos, no faces in focus.
Photoreal photograph of the same restored compact 1960s-style step van food truck from step 2, now during evening service at a riverside night market at blue hour, seen in a rear right three-quarter view from standing eye level (1.6 m) about 9 m away, 35mm lens, 16:9 landscape composition with the whole truck in frame, slightly left of center. Keep every fixed design detail identical to step 2: a short boxy step van body about 5.5 m long with softly rounded corners, a rounded cab roof, and a flat box roof; two-tone paint, glossy cream on the upper half and deep teal on the lower half, separated by a thin copper-orange stripe running all around the body just below the windows; black tires with large polished chrome hubcaps; a chrome front bumper and round headlights at the front. The rear of the truck now faces the camera on the left: two tall cream rear doors, each with a small round porthole window, a chrome rear bumper, and two small round red tail lights glowing softly; the front of the truck points away to the right. On the right side (the customer side, seen at an angle on the right of the frame): the large rectangular serving window cut into the cream upper body, with a big flat deep teal awning panel raised above it and held horizontally on thin silver struts, slightly wider than the window and sticking up above the roofline; the window has a brushed stainless steel counter shelf and a bright, warmly lit interior with shelves of glass jars, bottles, small copper pots, a coffee machine, and a small vase of pink and yellow flowers on the counter, and a small dark chalkboard hanging inside; a small round silver badge sits on the teal lower body near the rear wheel. Warm light from the open window spills onto the ground. A cook in a dark apron works behind the counter, softly blurred. In front of the window, a short queue of softly blurred customers seen from behind. Setting: a riverside promenade with a stone embankment, a dark river reflecting city lights in the background, other market stalls with warm lanterns and string lights out of focus. Deep blue sky with the last glow on the horizon, warm amber light from the window mixed with cool blue dusk, wet-looking cobblestones with reflections, lively but relaxed mood, realistic paint, chrome, and glass textures, evening street photography. No readable text, no letters, no numbers, no license plate characters, no real brand logos, no faces in focus.