diff --git a/.github/readme-source.json b/.github/readme-source.json new file mode 100644 index 0000000..8186ce4 --- /dev/null +++ b/.github/readme-source.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "sha256": "fb9ec82ff0653ff474751b88f132ecad6a49dd593e38ae24d237d8b0ed3e5522", + "history": { + "forbidden_commits": [ + "6890f718519ce148ecceb9209328236aed16efc1" + ] + } +} diff --git a/.github/scripts/check-documentation.py b/.github/scripts/check-documentation.py new file mode 100644 index 0000000..ae3dbd9 --- /dev/null +++ b/.github/scripts/check-documentation.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Check tracked documentation layout and the reviewed usage README digest.""" +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import subprocess +import sys + +MARKDOWN = {'.md', '.markdown', '.mdown', '.mkd', '.mkdn', '.mdwn', '.mdx', '.rmd', '.qmd'} +OTHER_DOCS = {'.rst', '.adoc', '.asciidoc'} + + +def check(root, include_history=True, history_required=False): + raw = subprocess.check_output(['git', 'ls-files', '--stage', '-z'], cwd=root) + entries = {} + errors = [] + for record in raw.split(b'\0'): + if not record: + continue + metadata, encoded = record.split(b'\t', 1) + mode, _oid, stage = metadata.decode().split() + name = encoded.decode('utf-8', 'surrogateescape') + if stage != '0': + errors.append('unmerged index entry: ' + name) + entries[name] = mode + path = PurePosixPath(name) + if name != 'README.md' and (path.suffix.lower() in MARKDOWN | OTHER_DOCS + or path.name.lower().startswith('readme.')): + errors.append('documentation must live in the private docs repository: ' + name) + if entries.get('README.md') != '100644' and entries.get('README.md') != '100755': + errors.append('a regular, tracked root README.md is required') + else: + readme = root / 'README.md' + if readme.is_symlink() or not readme.is_file(): + errors.append('root README.md must be a regular file') + else: + try: + body = readme.read_bytes() + if not any(line.startswith('# ') for line in body.decode('utf-8').splitlines()): + errors.append('root README.md needs a product heading') + policy_path = root / '.github/readme-source.json' + if policy_path.is_symlink(): + raise ValueError('README source policy cannot be a symlink') + policy = json.loads(policy_path.read_text()) + if policy.get('version') != 1: + raise ValueError('unsupported README source policy') + if hashlib.sha256(body).hexdigest() != policy.get('sha256'): + errors.append('README differs from its reviewed publication; update the private source first') + history = policy.get('history') + if history_required and history is None: + raise ValueError('public repositories require a history boundary') + if include_history and history is not None: + if set(history) != {'forbidden_commits'} or not isinstance(history['forbidden_commits'], list) or not history['forbidden_commits']: + raise ValueError('invalid public history boundary') + forbidden = set(history['forbidden_commits']) + if any(not isinstance(oid, str) or len(oid) != 40 or any(c not in '0123456789abcdef' for c in oid) for oid in forbidden): + raise ValueError('invalid original history identity') + shallow = subprocess.check_output(['git', 'rev-parse', '--is-shallow-repository'], cwd=root, text=True).strip() + if shallow != 'false': + raise ValueError('public history verification requires fetch-depth: 0') + ancestors = set(subprocess.check_output(['git', 'rev-list', 'HEAD'], cwd=root, text=True).splitlines()) + if ancestors & forbidden: + errors.append('original history was reintroduced; transplant changes onto the cleaned main history') + patterns = [':(icase)*'+suffix for suffix in sorted(MARKDOWN)] + past = subprocess.check_output(['git', 'log', '--format=', '--name-only', '-z', 'HEAD', '--', *patterns], cwd=root) + extra = {name.decode('utf-8', 'surrogateescape') for name in past.split(b'\0') if name and name != b'README.md'} + errors.extend('historical Markdown must remain in private docs: '+name for name in sorted(extra)) + + except (OSError, ValueError, UnicodeError) as error: + errors.append('invalid README source policy or README: ' + str(error)) + return errors + + +def main(): + root = Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()) + errors = check(root, history_required=os.environ.get('REQUIRE_CLEAN_HISTORY') == 'true') + for error in errors: + print(error, file=sys.stderr) + if errors: + return 1 + print('Documentation policy passed: one root README.md, reviewed source digest matches.') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0d1db9..0e39acf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 - name: Validate developer profile and workflow controls - uses: nicodes/tools/workflow-standards@59a948943c2f7bdd782cad48ddfdabb106c80fbd # v0.27.5 + uses: nicodes/tools/workflow-standards@66992c7abdb653dac5f7da4a4ec6b7d83737c26c # v0.27.5 - name: Build uses: ./.github/actions/build diff --git a/.github/workflows/documentation-policy.yml b/.github/workflows/documentation-policy.yml new file mode 100644 index 0000000..479ac0b --- /dev/null +++ b/.github/workflows/documentation-policy.yml @@ -0,0 +1,23 @@ +name: Documentation policy +on: + push: + pull_request: + merge_group: + workflow_dispatch: +permissions: + contents: read +jobs: + documentation-policy: + name: Documentation policy + runs-on: ubuntu-24.04 + timeout-minutes: 3 + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + fetch-depth: ${{ github.event.repository.private && 1 || 0 }} + - name: Check documentation boundary + env: + REQUIRE_CLEAN_HISTORY: ${{ !github.event.repository.private }} + run: python3 .github/scripts/check-documentation.py diff --git a/README.md b/README.md index a92a507..70257d6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ -# spritesmith-web + -For current client installation and usage, see https://github.com/aviorstudio/spritesmith-web#readme. +# spritesmith website + +Static Astro landing page for [spritesmith](https://spritesmith.ai). The application is at [app.spritesmith.ai](https://app.spritesmith.ai); the public [Go CLI](https://github.com/aviorstudio/spritesmith) connects to its generation API. + +The page introduces agent-created vector sprites and animations, reference images, chat refinement, the library, gallery, and CLI. Artwork and conversations on the page are illustrations, not captured product results. + +## Install the CLI + +With curl: + +```sh +curl -fsSL https://spritesmith.ai/install | sh +``` + +Or with mise: + +```sh +mise use --global github:aviorstudio/spritesmith@latest +``` + +An icon-only copy control sits on the right inside each install-command outline and shows a checkmark after copying. If clipboard access is unavailable, the command remains selectable for manual copying. + +Prebuilt Linux/macOS binaries are available for x86_64 and arm64. A Go compiler is not required. See [spritesmith](https://github.com/aviorstudio/spritesmith#readme) for setup and installation options.