Spaces:
Sleeping
Sleeping
Sync from simready-oem-library-pm@c858e9dd
Browse files- Dockerfile +112 -112
- README.md +44 -44
- tools/hf_space/Dockerfile +112 -112
- tools/hf_space/README_HEADER.md +44 -44
- tools/hf_space/app.py +429 -426
- tools/hf_space/github_issues.py +324 -324
- tools/hf_space/plugin.json +11 -11
- tools/hf_space/runner.py +0 -0
- tools/hf_space/skills/deploy-hf-space/SKILL.md +257 -257
- tools/hf_space/test_discover_dgxc.py +142 -142
- tools/validation/.claude-plugin/marketplace.json +16 -16
- tools/validation/PROBLEMS.md +327 -327
- tools/validation/README.md +139 -139
- tools/validation/UPSTREAM.md +66 -66
- tools/validation/_sync.sh +98 -98
- tools/validation/bootstrap.ps1 +207 -207
- tools/validation/playbooks/foundations-deviations.md +591 -591
- tools/validation/plugins/simready-report/plugin.json +11 -11
- tools/validation/plugins/simready-report/skills/simready-package/SKILL.md +62 -62
- tools/validation/plugins/simready-report/skills/simready-package/package.py +143 -143
- tools/validation/plugins/simready-report/skills/simready-report/SKILL.md +137 -137
- tools/validation/plugins/simready-report/skills/simready-report/_kit_wrapper.py +33 -33
- tools/validation/plugins/simready-report/skills/simready-report/external_deps.py +259 -259
- tools/validation/plugins/simready-report/skills/simready-report/report.py +860 -860
- tools/validation/plugins/simready-report/skills/simready-report/validate.py +0 -0
Dockerfile
CHANGED
|
@@ -1,112 +1,112 @@
|
|
| 1 |
-
# SimReady Validator — HuggingFace Space image
|
| 2 |
-
#
|
| 3 |
-
# Base: nvcr.io/nvidia/isaac-sim:4.5.0. Ships Kit at /isaac-sim/ so
|
| 4 |
-
# runner._kit_available() auto-selects --use-kit and the validator
|
| 5 |
-
# runs PhysX / MDL rules without a code change.
|
| 6 |
-
#
|
| 7 |
-
# UID 1000 is required by HF Spaces. The Isaac Sim base already has
|
| 8 |
-
# a user at UID 1000 (varies between releases — ubuntu / kit / etc.),
|
| 9 |
-
# so we REUSE it rather than create one. /app is the workspace dir
|
| 10 |
-
# with HOME=/app so Claude Code CLI finds skills under ~/.claude/skills
|
| 11 |
-
# regardless of the existing user's name.
|
| 12 |
-
|
| 13 |
-
FROM nvcr.io/nvidia/isaac-sim:4.5.0
|
| 14 |
-
|
| 15 |
-
# Skip interactive prompts during apt-get (the deadsnakes PPA install
|
| 16 |
-
# pulls tzdata in as a dep, which otherwise blocks the build asking
|
| 17 |
-
# for a geographic area).
|
| 18 |
-
ENV DEBIAN_FRONTEND=noninteractive
|
| 19 |
-
ENV TZ=Etc/UTC
|
| 20 |
-
|
| 21 |
-
# System deps (Ubuntu 22.04). Isaac Sim base may ship apt/pip configs
|
| 22 |
-
# pointing at NVIDIA-internal mirrors (urm.nvidia.com) that HF Spaces'
|
| 23 |
-
# build infra can't reach — wipe those before any install. The base
|
| 24 |
-
# default Python is 3.10; simready-validate requires >=3.11, so we
|
| 25 |
-
# install python3.11 from the deadsnakes PPA and use it explicitly
|
| 26 |
-
# for everything below.
|
| 27 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 28 |
-
git curl ca-certificates rsync gnupg software-properties-common \
|
| 29 |
-
&& add-apt-repository -y ppa:deadsnakes/ppa \
|
| 30 |
-
&& apt-get update && apt-get install -y --no-install-recommends \
|
| 31 |
-
python3.11 python3.11-venv python3.11-distutils \
|
| 32 |
-
&& curl -fsSL https://bootstrap.pypa.io/get-pip.py | python3.11 \
|
| 33 |
-
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 34 |
-
&& apt-get install -y --no-install-recommends nodejs \
|
| 35 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 36 |
-
|
| 37 |
-
RUN npm config set registry https://registry.npmjs.org/ \
|
| 38 |
-
&& rm -f /root/.npmrc /etc/npmrc \
|
| 39 |
-
&& rm -f /root/.pip/pip.conf /etc/pip.conf
|
| 40 |
-
|
| 41 |
-
# Claude Code CLI.
|
| 42 |
-
RUN npm install -g --registry=https://registry.npmjs.org/ \
|
| 43 |
-
@anthropic-ai/claude-code@latest
|
| 44 |
-
|
| 45 |
-
# Workspace nesting matches the legacy layout (/home/appuser/app)
|
| 46 |
-
# because runner.py uses Path(__file__).resolve().parents[2] to find
|
| 47 |
-
# tools/spec_sync/state.json — needs at least 3 levels of parent.
|
| 48 |
-
# We don't create the appuser account (UID 1000 already exists in the
|
| 49 |
-
# base image with a different name); we just use the path. HOME points
|
| 50 |
-
# at the workspace's parent so Claude Code CLI finds ~/.claude/skills.
|
| 51 |
-
RUN mkdir -p /home/appuser/app /home/appuser/.claude/skills
|
| 52 |
-
WORKDIR /home/appuser/app
|
| 53 |
-
ENV HOME=/home/appuser
|
| 54 |
-
|
| 55 |
-
# Python deps (system Python, public PyPI). --break-system-packages
|
| 56 |
-
# because Ubuntu 22.04 marks system Python as PEP-668 externally-
|
| 57 |
-
# managed; on a build-once container image that's the right trade-off.
|
| 58 |
-
COPY tools/hf_space/requirements.txt ./requirements.txt
|
| 59 |
-
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 60 |
-
--index-url https://pypi.org/simple/ \
|
| 61 |
-
-r requirements.txt
|
| 62 |
-
|
| 63 |
-
# Foundation specs.
|
| 64 |
-
ENV SIMREADY_FOUNDATIONS_PATH=/opt/simready_foundations
|
| 65 |
-
ENV SIMREADY_FOUNDATIONS_COMMIT=805d2c50179a9878c89b0f41baaa0ecafe47c3d7
|
| 66 |
-
RUN git clone https://github.com/NVIDIA/simready-foundation \
|
| 67 |
-
${SIMREADY_FOUNDATIONS_PATH} \
|
| 68 |
-
&& cd ${SIMREADY_FOUNDATIONS_PATH} \
|
| 69 |
-
&& git checkout ${SIMREADY_FOUNDATIONS_COMMIT}
|
| 70 |
-
|
| 71 |
-
# Vendored simready-foundation-core wheel.
|
| 72 |
-
COPY tools/hf_space/wheels/simready_foundation_core-0.2.0a1-py3-none-any.whl /tmp/
|
| 73 |
-
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 74 |
-
--index-url https://pypi.org/simple/ \
|
| 75 |
-
/tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl \
|
| 76 |
-
&& rm /tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl
|
| 77 |
-
|
| 78 |
-
# Validator + Space code.
|
| 79 |
-
COPY tools/validation /home/appuser/app/tools/validation
|
| 80 |
-
COPY tools/hf_space/app.py ./app.py
|
| 81 |
-
COPY tools/hf_space/runner.py ./runner.py
|
| 82 |
-
COPY tools/hf_space/github_issues.py ./github_issues.py
|
| 83 |
-
COPY tools/hf_space/agentic_issues.py ./agentic_issues.py
|
| 84 |
-
COPY README.md ./README.md
|
| 85 |
-
COPY VALIDATE.md ./VALIDATE.md
|
| 86 |
-
|
| 87 |
-
# Claude Code skills (HOME=/home/appuser → ~/.claude/skills).
|
| 88 |
-
COPY tools/hf_space/skills/simready-validator-agent /home/appuser/.claude/skills/simready-validator-agent
|
| 89 |
-
COPY tools/validation/plugins/simready-report/skills/simready-report /home/appuser/.claude/skills/simready-report
|
| 90 |
-
|
| 91 |
-
# UID 1000 owns the workspace + foundation clone.
|
| 92 |
-
RUN chown -R 1000:1000 /home/appuser ${SIMREADY_FOUNDATIONS_PATH}
|
| 93 |
-
|
| 94 |
-
# runner._kit_available() probes /isaac-sim/python.sh — present in the
|
| 95 |
-
# base image — so the validator auto-selects --use-kit at runtime.
|
| 96 |
-
# The validator's --use-kit path needs to know WHERE Kit's python lives;
|
| 97 |
-
# wire it via SIMREADY_KIT_PYTHON so the auto-detect is fully self-
|
| 98 |
-
# configuring (no per-call --kit-python flag needed).
|
| 99 |
-
ENV SIMREADY_KIT_PYTHON=/isaac-sim/python.sh
|
| 100 |
-
|
| 101 |
-
USER 1000:1000
|
| 102 |
-
|
| 103 |
-
ENV GRADIO_SERVER_NAME=0.0.0.0
|
| 104 |
-
ENV GRADIO_SERVER_PORT=7860
|
| 105 |
-
EXPOSE 7860
|
| 106 |
-
|
| 107 |
-
# Clear the Isaac Sim base's ENTRYPOINT (it runs /isaac-sim/runheadless.sh
|
| 108 |
-
# and treats CMD as its args — fails with 'Permission denied' under
|
| 109 |
-
# UID 1000). We invoke the Python app directly; Kit is invoked
|
| 110 |
-
# explicitly by the validator via /isaac-sim/python.sh when needed.
|
| 111 |
-
ENTRYPOINT []
|
| 112 |
-
CMD ["python3.11", "app.py"]
|
|
|
|
| 1 |
+
# SimReady Validator — HuggingFace Space image
|
| 2 |
+
#
|
| 3 |
+
# Base: nvcr.io/nvidia/isaac-sim:4.5.0. Ships Kit at /isaac-sim/ so
|
| 4 |
+
# runner._kit_available() auto-selects --use-kit and the validator
|
| 5 |
+
# runs PhysX / MDL rules without a code change.
|
| 6 |
+
#
|
| 7 |
+
# UID 1000 is required by HF Spaces. The Isaac Sim base already has
|
| 8 |
+
# a user at UID 1000 (varies between releases — ubuntu / kit / etc.),
|
| 9 |
+
# so we REUSE it rather than create one. /app is the workspace dir
|
| 10 |
+
# with HOME=/app so Claude Code CLI finds skills under ~/.claude/skills
|
| 11 |
+
# regardless of the existing user's name.
|
| 12 |
+
|
| 13 |
+
FROM nvcr.io/nvidia/isaac-sim:4.5.0
|
| 14 |
+
|
| 15 |
+
# Skip interactive prompts during apt-get (the deadsnakes PPA install
|
| 16 |
+
# pulls tzdata in as a dep, which otherwise blocks the build asking
|
| 17 |
+
# for a geographic area).
|
| 18 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 19 |
+
ENV TZ=Etc/UTC
|
| 20 |
+
|
| 21 |
+
# System deps (Ubuntu 22.04). Isaac Sim base may ship apt/pip configs
|
| 22 |
+
# pointing at NVIDIA-internal mirrors (urm.nvidia.com) that HF Spaces'
|
| 23 |
+
# build infra can't reach — wipe those before any install. The base
|
| 24 |
+
# default Python is 3.10; simready-validate requires >=3.11, so we
|
| 25 |
+
# install python3.11 from the deadsnakes PPA and use it explicitly
|
| 26 |
+
# for everything below.
|
| 27 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 28 |
+
git curl ca-certificates rsync gnupg software-properties-common \
|
| 29 |
+
&& add-apt-repository -y ppa:deadsnakes/ppa \
|
| 30 |
+
&& apt-get update && apt-get install -y --no-install-recommends \
|
| 31 |
+
python3.11 python3.11-venv python3.11-distutils \
|
| 32 |
+
&& curl -fsSL https://bootstrap.pypa.io/get-pip.py | python3.11 \
|
| 33 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 34 |
+
&& apt-get install -y --no-install-recommends nodejs \
|
| 35 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 36 |
+
|
| 37 |
+
RUN npm config set registry https://registry.npmjs.org/ \
|
| 38 |
+
&& rm -f /root/.npmrc /etc/npmrc \
|
| 39 |
+
&& rm -f /root/.pip/pip.conf /etc/pip.conf
|
| 40 |
+
|
| 41 |
+
# Claude Code CLI.
|
| 42 |
+
RUN npm install -g --registry=https://registry.npmjs.org/ \
|
| 43 |
+
@anthropic-ai/claude-code@latest
|
| 44 |
+
|
| 45 |
+
# Workspace nesting matches the legacy layout (/home/appuser/app)
|
| 46 |
+
# because runner.py uses Path(__file__).resolve().parents[2] to find
|
| 47 |
+
# tools/spec_sync/state.json — needs at least 3 levels of parent.
|
| 48 |
+
# We don't create the appuser account (UID 1000 already exists in the
|
| 49 |
+
# base image with a different name); we just use the path. HOME points
|
| 50 |
+
# at the workspace's parent so Claude Code CLI finds ~/.claude/skills.
|
| 51 |
+
RUN mkdir -p /home/appuser/app /home/appuser/.claude/skills
|
| 52 |
+
WORKDIR /home/appuser/app
|
| 53 |
+
ENV HOME=/home/appuser
|
| 54 |
+
|
| 55 |
+
# Python deps (system Python, public PyPI). --break-system-packages
|
| 56 |
+
# because Ubuntu 22.04 marks system Python as PEP-668 externally-
|
| 57 |
+
# managed; on a build-once container image that's the right trade-off.
|
| 58 |
+
COPY tools/hf_space/requirements.txt ./requirements.txt
|
| 59 |
+
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 60 |
+
--index-url https://pypi.org/simple/ \
|
| 61 |
+
-r requirements.txt
|
| 62 |
+
|
| 63 |
+
# Foundation specs.
|
| 64 |
+
ENV SIMREADY_FOUNDATIONS_PATH=/opt/simready_foundations
|
| 65 |
+
ENV SIMREADY_FOUNDATIONS_COMMIT=805d2c50179a9878c89b0f41baaa0ecafe47c3d7
|
| 66 |
+
RUN git clone https://github.com/NVIDIA/simready-foundation \
|
| 67 |
+
${SIMREADY_FOUNDATIONS_PATH} \
|
| 68 |
+
&& cd ${SIMREADY_FOUNDATIONS_PATH} \
|
| 69 |
+
&& git checkout ${SIMREADY_FOUNDATIONS_COMMIT}
|
| 70 |
+
|
| 71 |
+
# Vendored simready-foundation-core wheel.
|
| 72 |
+
COPY tools/hf_space/wheels/simready_foundation_core-0.2.0a1-py3-none-any.whl /tmp/
|
| 73 |
+
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 74 |
+
--index-url https://pypi.org/simple/ \
|
| 75 |
+
/tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl \
|
| 76 |
+
&& rm /tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl
|
| 77 |
+
|
| 78 |
+
# Validator + Space code.
|
| 79 |
+
COPY tools/validation /home/appuser/app/tools/validation
|
| 80 |
+
COPY tools/hf_space/app.py ./app.py
|
| 81 |
+
COPY tools/hf_space/runner.py ./runner.py
|
| 82 |
+
COPY tools/hf_space/github_issues.py ./github_issues.py
|
| 83 |
+
COPY tools/hf_space/agentic_issues.py ./agentic_issues.py
|
| 84 |
+
COPY README.md ./README.md
|
| 85 |
+
COPY VALIDATE.md ./VALIDATE.md
|
| 86 |
+
|
| 87 |
+
# Claude Code skills (HOME=/home/appuser → ~/.claude/skills).
|
| 88 |
+
COPY tools/hf_space/skills/simready-validator-agent /home/appuser/.claude/skills/simready-validator-agent
|
| 89 |
+
COPY tools/validation/plugins/simready-report/skills/simready-report /home/appuser/.claude/skills/simready-report
|
| 90 |
+
|
| 91 |
+
# UID 1000 owns the workspace + foundation clone.
|
| 92 |
+
RUN chown -R 1000:1000 /home/appuser ${SIMREADY_FOUNDATIONS_PATH}
|
| 93 |
+
|
| 94 |
+
# runner._kit_available() probes /isaac-sim/python.sh — present in the
|
| 95 |
+
# base image — so the validator auto-selects --use-kit at runtime.
|
| 96 |
+
# The validator's --use-kit path needs to know WHERE Kit's python lives;
|
| 97 |
+
# wire it via SIMREADY_KIT_PYTHON so the auto-detect is fully self-
|
| 98 |
+
# configuring (no per-call --kit-python flag needed).
|
| 99 |
+
ENV SIMREADY_KIT_PYTHON=/isaac-sim/python.sh
|
| 100 |
+
|
| 101 |
+
USER 1000:1000
|
| 102 |
+
|
| 103 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0
|
| 104 |
+
ENV GRADIO_SERVER_PORT=7860
|
| 105 |
+
EXPOSE 7860
|
| 106 |
+
|
| 107 |
+
# Clear the Isaac Sim base's ENTRYPOINT (it runs /isaac-sim/runheadless.sh
|
| 108 |
+
# and treats CMD as its args — fails with 'Permission denied' under
|
| 109 |
+
# UID 1000). We invoke the Python app directly; Kit is invoked
|
| 110 |
+
# explicitly by the validator via /isaac-sim/python.sh when needed.
|
| 111 |
+
ENTRYPOINT []
|
| 112 |
+
CMD ["python3.11", "app.py"]
|
README.md
CHANGED
|
@@ -1,44 +1,44 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Simready Validator
|
| 3 |
-
emoji: 🏢
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
# SimReady Validator
|
| 11 |
-
|
| 12 |
-
A hosted validator for [SimReady](https://github.com/NVIDIA/simready-foundation)
|
| 13 |
-
asset packages. Submit a HuggingFace dataset, pick a profile, and the Space
|
| 14 |
-
runs the SimReady foundation rules against it and reports back what passed,
|
| 15 |
-
what failed, and why — without copying your assets off HuggingFace.
|
| 16 |
-
|
| 17 |
-
## Using the validator
|
| 18 |
-
|
| 19 |
-
Click the **App** tab above to open the validator UI. In the form:
|
| 20 |
-
|
| 21 |
-
| Field | What to enter |
|
| 22 |
-
|---|---|
|
| 23 |
-
| **Dataset** | The HuggingFace dataset slug you want to validate (e.g. `your-org/your-asset-bundle`). The dataset must be readable by this Space's `HF_TOKEN`. |
|
| 24 |
-
| **Profile** | The SimReady profile that matches your asset type — props vs. robots, neutral vs. PhysX vs. Isaac. See the validation guide for the full list. |
|
| 25 |
-
| **Version** | Profile version. Leave at `1.0.0` unless your profile is pinned to a different release. |
|
| 26 |
-
| **Open PR on dataset with verdict** | When ticked, the verdict is written back to the dataset as a `validation/results.json` pull request. Leave off for ad-hoc smoke tests. |
|
| 27 |
-
|
| 28 |
-
Click **Validate** and the log streams in real time. When it finishes the
|
| 29 |
-
results.json appears in the **HTML report** download.
|
| 30 |
-
|
| 31 |
-
## Going deeper
|
| 32 |
-
|
| 33 |
-
- **[Partner walkthrough → VALIDATE.md](./blob/main/VALIDATE.md)** — the full nine-step guide from cloning the foundation repo to submitting your assets. Read this first if you're packaging assets for SimReady for the first time.
|
| 34 |
-
- **[NVIDIA/simready-foundation](https://github.com/NVIDIA/simready-foundation)** — public spec repo with profile authoring guides and sample assets.
|
| 35 |
-
- **[WRAPP on NGC](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/omniverse/collections/wrapp)** — packaging tool referenced in the walkthrough.
|
| 36 |
-
|
| 37 |
-
## About this Space
|
| 38 |
-
|
| 39 |
-
The validator reads the requested HuggingFace dataset in place via
|
| 40 |
-
`huggingface_hub.snapshot_download` — assets stay on HuggingFace, no
|
| 41 |
-
transfer onto NVIDIA infrastructure. Running on a `t4-medium` GPU
|
| 42 |
-
tier. PhysX / MDL rules that require an Isaac Sim runtime are not
|
| 43 |
-
covered here yet (Isaac Sim isn't in the Space image — that's a
|
| 44 |
-
separate workstream).
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Simready Validator
|
| 3 |
+
emoji: 🏢
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# SimReady Validator
|
| 11 |
+
|
| 12 |
+
A hosted validator for [SimReady](https://github.com/NVIDIA/simready-foundation)
|
| 13 |
+
asset packages. Submit a HuggingFace dataset, pick a profile, and the Space
|
| 14 |
+
runs the SimReady foundation rules against it and reports back what passed,
|
| 15 |
+
what failed, and why — without copying your assets off HuggingFace.
|
| 16 |
+
|
| 17 |
+
## Using the validator
|
| 18 |
+
|
| 19 |
+
Click the **App** tab above to open the validator UI. In the form:
|
| 20 |
+
|
| 21 |
+
| Field | What to enter |
|
| 22 |
+
|---|---|
|
| 23 |
+
| **Dataset** | The HuggingFace dataset slug you want to validate (e.g. `your-org/your-asset-bundle`). The dataset must be readable by this Space's `HF_TOKEN`. |
|
| 24 |
+
| **Profile** | The SimReady profile that matches your asset type — props vs. robots, neutral vs. PhysX vs. Isaac. See the validation guide for the full list. |
|
| 25 |
+
| **Version** | Profile version. Leave at `1.0.0` unless your profile is pinned to a different release. |
|
| 26 |
+
| **Open PR on dataset with verdict** | When ticked, the verdict is written back to the dataset as a `validation/results.json` pull request. Leave off for ad-hoc smoke tests. |
|
| 27 |
+
|
| 28 |
+
Click **Validate** and the log streams in real time. When it finishes the
|
| 29 |
+
results.json appears in the **HTML report** download.
|
| 30 |
+
|
| 31 |
+
## Going deeper
|
| 32 |
+
|
| 33 |
+
- **[Partner walkthrough → VALIDATE.md](./blob/main/VALIDATE.md)** — the full nine-step guide from cloning the foundation repo to submitting your assets. Read this first if you're packaging assets for SimReady for the first time.
|
| 34 |
+
- **[NVIDIA/simready-foundation](https://github.com/NVIDIA/simready-foundation)** — public spec repo with profile authoring guides and sample assets.
|
| 35 |
+
- **[WRAPP on NGC](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/omniverse/collections/wrapp)** — packaging tool referenced in the walkthrough.
|
| 36 |
+
|
| 37 |
+
## About this Space
|
| 38 |
+
|
| 39 |
+
The validator reads the requested HuggingFace dataset in place via
|
| 40 |
+
`huggingface_hub.snapshot_download` — assets stay on HuggingFace, no
|
| 41 |
+
transfer onto NVIDIA infrastructure. Running on a `t4-medium` GPU
|
| 42 |
+
tier. PhysX / MDL rules that require an Isaac Sim runtime are not
|
| 43 |
+
covered here yet (Isaac Sim isn't in the Space image — that's a
|
| 44 |
+
separate workstream).
|
tools/hf_space/Dockerfile
CHANGED
|
@@ -1,112 +1,112 @@
|
|
| 1 |
-
# SimReady Validator — HuggingFace Space image
|
| 2 |
-
#
|
| 3 |
-
# Base: nvcr.io/nvidia/isaac-sim:4.5.0. Ships Kit at /isaac-sim/ so
|
| 4 |
-
# runner._kit_available() auto-selects --use-kit and the validator
|
| 5 |
-
# runs PhysX / MDL rules without a code change.
|
| 6 |
-
#
|
| 7 |
-
# UID 1000 is required by HF Spaces. The Isaac Sim base already has
|
| 8 |
-
# a user at UID 1000 (varies between releases — ubuntu / kit / etc.),
|
| 9 |
-
# so we REUSE it rather than create one. /app is the workspace dir
|
| 10 |
-
# with HOME=/app so Claude Code CLI finds skills under ~/.claude/skills
|
| 11 |
-
# regardless of the existing user's name.
|
| 12 |
-
|
| 13 |
-
FROM nvcr.io/nvidia/isaac-sim:4.5.0
|
| 14 |
-
|
| 15 |
-
# Skip interactive prompts during apt-get (the deadsnakes PPA install
|
| 16 |
-
# pulls tzdata in as a dep, which otherwise blocks the build asking
|
| 17 |
-
# for a geographic area).
|
| 18 |
-
ENV DEBIAN_FRONTEND=noninteractive
|
| 19 |
-
ENV TZ=Etc/UTC
|
| 20 |
-
|
| 21 |
-
# System deps (Ubuntu 22.04). Isaac Sim base may ship apt/pip configs
|
| 22 |
-
# pointing at NVIDIA-internal mirrors (urm.nvidia.com) that HF Spaces'
|
| 23 |
-
# build infra can't reach — wipe those before any install. The base
|
| 24 |
-
# default Python is 3.10; simready-validate requires >=3.11, so we
|
| 25 |
-
# install python3.11 from the deadsnakes PPA and use it explicitly
|
| 26 |
-
# for everything below.
|
| 27 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 28 |
-
git curl ca-certificates rsync gnupg software-properties-common \
|
| 29 |
-
&& add-apt-repository -y ppa:deadsnakes/ppa \
|
| 30 |
-
&& apt-get update && apt-get install -y --no-install-recommends \
|
| 31 |
-
python3.11 python3.11-venv python3.11-distutils \
|
| 32 |
-
&& curl -fsSL https://bootstrap.pypa.io/get-pip.py | python3.11 \
|
| 33 |
-
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 34 |
-
&& apt-get install -y --no-install-recommends nodejs \
|
| 35 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 36 |
-
|
| 37 |
-
RUN npm config set registry https://registry.npmjs.org/ \
|
| 38 |
-
&& rm -f /root/.npmrc /etc/npmrc \
|
| 39 |
-
&& rm -f /root/.pip/pip.conf /etc/pip.conf
|
| 40 |
-
|
| 41 |
-
# Claude Code CLI.
|
| 42 |
-
RUN npm install -g --registry=https://registry.npmjs.org/ \
|
| 43 |
-
@anthropic-ai/claude-code@latest
|
| 44 |
-
|
| 45 |
-
# Workspace nesting matches the legacy layout (/home/appuser/app)
|
| 46 |
-
# because runner.py uses Path(__file__).resolve().parents[2] to find
|
| 47 |
-
# tools/spec_sync/state.json — needs at least 3 levels of parent.
|
| 48 |
-
# We don't create the appuser account (UID 1000 already exists in the
|
| 49 |
-
# base image with a different name); we just use the path. HOME points
|
| 50 |
-
# at the workspace's parent so Claude Code CLI finds ~/.claude/skills.
|
| 51 |
-
RUN mkdir -p /home/appuser/app /home/appuser/.claude/skills
|
| 52 |
-
WORKDIR /home/appuser/app
|
| 53 |
-
ENV HOME=/home/appuser
|
| 54 |
-
|
| 55 |
-
# Python deps (system Python, public PyPI). --break-system-packages
|
| 56 |
-
# because Ubuntu 22.04 marks system Python as PEP-668 externally-
|
| 57 |
-
# managed; on a build-once container image that's the right trade-off.
|
| 58 |
-
COPY tools/hf_space/requirements.txt ./requirements.txt
|
| 59 |
-
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 60 |
-
--index-url https://pypi.org/simple/ \
|
| 61 |
-
-r requirements.txt
|
| 62 |
-
|
| 63 |
-
# Foundation specs.
|
| 64 |
-
ENV SIMREADY_FOUNDATIONS_PATH=/opt/simready_foundations
|
| 65 |
-
ENV SIMREADY_FOUNDATIONS_COMMIT=805d2c50179a9878c89b0f41baaa0ecafe47c3d7
|
| 66 |
-
RUN git clone https://github.com/NVIDIA/simready-foundation \
|
| 67 |
-
${SIMREADY_FOUNDATIONS_PATH} \
|
| 68 |
-
&& cd ${SIMREADY_FOUNDATIONS_PATH} \
|
| 69 |
-
&& git checkout ${SIMREADY_FOUNDATIONS_COMMIT}
|
| 70 |
-
|
| 71 |
-
# Vendored simready-foundation-core wheel.
|
| 72 |
-
COPY tools/hf_space/wheels/simready_foundation_core-0.2.0a1-py3-none-any.whl /tmp/
|
| 73 |
-
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 74 |
-
--index-url https://pypi.org/simple/ \
|
| 75 |
-
/tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl \
|
| 76 |
-
&& rm /tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl
|
| 77 |
-
|
| 78 |
-
# Validator + Space code.
|
| 79 |
-
COPY tools/validation /home/appuser/app/tools/validation
|
| 80 |
-
COPY tools/hf_space/app.py ./app.py
|
| 81 |
-
COPY tools/hf_space/runner.py ./runner.py
|
| 82 |
-
COPY tools/hf_space/github_issues.py ./github_issues.py
|
| 83 |
-
COPY tools/hf_space/agentic_issues.py ./agentic_issues.py
|
| 84 |
-
COPY README.md ./README.md
|
| 85 |
-
COPY VALIDATE.md ./VALIDATE.md
|
| 86 |
-
|
| 87 |
-
# Claude Code skills (HOME=/home/appuser → ~/.claude/skills).
|
| 88 |
-
COPY tools/hf_space/skills/simready-validator-agent /home/appuser/.claude/skills/simready-validator-agent
|
| 89 |
-
COPY tools/validation/plugins/simready-report/skills/simready-report /home/appuser/.claude/skills/simready-report
|
| 90 |
-
|
| 91 |
-
# UID 1000 owns the workspace + foundation clone.
|
| 92 |
-
RUN chown -R 1000:1000 /home/appuser ${SIMREADY_FOUNDATIONS_PATH}
|
| 93 |
-
|
| 94 |
-
# runner._kit_available() probes /isaac-sim/python.sh — present in the
|
| 95 |
-
# base image — so the validator auto-selects --use-kit at runtime.
|
| 96 |
-
# The validator's --use-kit path needs to know WHERE Kit's python lives;
|
| 97 |
-
# wire it via SIMREADY_KIT_PYTHON so the auto-detect is fully self-
|
| 98 |
-
# configuring (no per-call --kit-python flag needed).
|
| 99 |
-
ENV SIMREADY_KIT_PYTHON=/isaac-sim/python.sh
|
| 100 |
-
|
| 101 |
-
USER 1000:1000
|
| 102 |
-
|
| 103 |
-
ENV GRADIO_SERVER_NAME=0.0.0.0
|
| 104 |
-
ENV GRADIO_SERVER_PORT=7860
|
| 105 |
-
EXPOSE 7860
|
| 106 |
-
|
| 107 |
-
# Clear the Isaac Sim base's ENTRYPOINT (it runs /isaac-sim/runheadless.sh
|
| 108 |
-
# and treats CMD as its args — fails with 'Permission denied' under
|
| 109 |
-
# UID 1000). We invoke the Python app directly; Kit is invoked
|
| 110 |
-
# explicitly by the validator via /isaac-sim/python.sh when needed.
|
| 111 |
-
ENTRYPOINT []
|
| 112 |
-
CMD ["python3.11", "app.py"]
|
|
|
|
| 1 |
+
# SimReady Validator — HuggingFace Space image
|
| 2 |
+
#
|
| 3 |
+
# Base: nvcr.io/nvidia/isaac-sim:4.5.0. Ships Kit at /isaac-sim/ so
|
| 4 |
+
# runner._kit_available() auto-selects --use-kit and the validator
|
| 5 |
+
# runs PhysX / MDL rules without a code change.
|
| 6 |
+
#
|
| 7 |
+
# UID 1000 is required by HF Spaces. The Isaac Sim base already has
|
| 8 |
+
# a user at UID 1000 (varies between releases — ubuntu / kit / etc.),
|
| 9 |
+
# so we REUSE it rather than create one. /app is the workspace dir
|
| 10 |
+
# with HOME=/app so Claude Code CLI finds skills under ~/.claude/skills
|
| 11 |
+
# regardless of the existing user's name.
|
| 12 |
+
|
| 13 |
+
FROM nvcr.io/nvidia/isaac-sim:4.5.0
|
| 14 |
+
|
| 15 |
+
# Skip interactive prompts during apt-get (the deadsnakes PPA install
|
| 16 |
+
# pulls tzdata in as a dep, which otherwise blocks the build asking
|
| 17 |
+
# for a geographic area).
|
| 18 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 19 |
+
ENV TZ=Etc/UTC
|
| 20 |
+
|
| 21 |
+
# System deps (Ubuntu 22.04). Isaac Sim base may ship apt/pip configs
|
| 22 |
+
# pointing at NVIDIA-internal mirrors (urm.nvidia.com) that HF Spaces'
|
| 23 |
+
# build infra can't reach — wipe those before any install. The base
|
| 24 |
+
# default Python is 3.10; simready-validate requires >=3.11, so we
|
| 25 |
+
# install python3.11 from the deadsnakes PPA and use it explicitly
|
| 26 |
+
# for everything below.
|
| 27 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 28 |
+
git curl ca-certificates rsync gnupg software-properties-common \
|
| 29 |
+
&& add-apt-repository -y ppa:deadsnakes/ppa \
|
| 30 |
+
&& apt-get update && apt-get install -y --no-install-recommends \
|
| 31 |
+
python3.11 python3.11-venv python3.11-distutils \
|
| 32 |
+
&& curl -fsSL https://bootstrap.pypa.io/get-pip.py | python3.11 \
|
| 33 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 34 |
+
&& apt-get install -y --no-install-recommends nodejs \
|
| 35 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 36 |
+
|
| 37 |
+
RUN npm config set registry https://registry.npmjs.org/ \
|
| 38 |
+
&& rm -f /root/.npmrc /etc/npmrc \
|
| 39 |
+
&& rm -f /root/.pip/pip.conf /etc/pip.conf
|
| 40 |
+
|
| 41 |
+
# Claude Code CLI.
|
| 42 |
+
RUN npm install -g --registry=https://registry.npmjs.org/ \
|
| 43 |
+
@anthropic-ai/claude-code@latest
|
| 44 |
+
|
| 45 |
+
# Workspace nesting matches the legacy layout (/home/appuser/app)
|
| 46 |
+
# because runner.py uses Path(__file__).resolve().parents[2] to find
|
| 47 |
+
# tools/spec_sync/state.json — needs at least 3 levels of parent.
|
| 48 |
+
# We don't create the appuser account (UID 1000 already exists in the
|
| 49 |
+
# base image with a different name); we just use the path. HOME points
|
| 50 |
+
# at the workspace's parent so Claude Code CLI finds ~/.claude/skills.
|
| 51 |
+
RUN mkdir -p /home/appuser/app /home/appuser/.claude/skills
|
| 52 |
+
WORKDIR /home/appuser/app
|
| 53 |
+
ENV HOME=/home/appuser
|
| 54 |
+
|
| 55 |
+
# Python deps (system Python, public PyPI). --break-system-packages
|
| 56 |
+
# because Ubuntu 22.04 marks system Python as PEP-668 externally-
|
| 57 |
+
# managed; on a build-once container image that's the right trade-off.
|
| 58 |
+
COPY tools/hf_space/requirements.txt ./requirements.txt
|
| 59 |
+
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 60 |
+
--index-url https://pypi.org/simple/ \
|
| 61 |
+
-r requirements.txt
|
| 62 |
+
|
| 63 |
+
# Foundation specs.
|
| 64 |
+
ENV SIMREADY_FOUNDATIONS_PATH=/opt/simready_foundations
|
| 65 |
+
ENV SIMREADY_FOUNDATIONS_COMMIT=805d2c50179a9878c89b0f41baaa0ecafe47c3d7
|
| 66 |
+
RUN git clone https://github.com/NVIDIA/simready-foundation \
|
| 67 |
+
${SIMREADY_FOUNDATIONS_PATH} \
|
| 68 |
+
&& cd ${SIMREADY_FOUNDATIONS_PATH} \
|
| 69 |
+
&& git checkout ${SIMREADY_FOUNDATIONS_COMMIT}
|
| 70 |
+
|
| 71 |
+
# Vendored simready-foundation-core wheel.
|
| 72 |
+
COPY tools/hf_space/wheels/simready_foundation_core-0.2.0a1-py3-none-any.whl /tmp/
|
| 73 |
+
RUN python3.11 -m pip install --no-cache-dir --break-system-packages \
|
| 74 |
+
--index-url https://pypi.org/simple/ \
|
| 75 |
+
/tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl \
|
| 76 |
+
&& rm /tmp/simready_foundation_core-0.2.0a1-py3-none-any.whl
|
| 77 |
+
|
| 78 |
+
# Validator + Space code.
|
| 79 |
+
COPY tools/validation /home/appuser/app/tools/validation
|
| 80 |
+
COPY tools/hf_space/app.py ./app.py
|
| 81 |
+
COPY tools/hf_space/runner.py ./runner.py
|
| 82 |
+
COPY tools/hf_space/github_issues.py ./github_issues.py
|
| 83 |
+
COPY tools/hf_space/agentic_issues.py ./agentic_issues.py
|
| 84 |
+
COPY README.md ./README.md
|
| 85 |
+
COPY VALIDATE.md ./VALIDATE.md
|
| 86 |
+
|
| 87 |
+
# Claude Code skills (HOME=/home/appuser → ~/.claude/skills).
|
| 88 |
+
COPY tools/hf_space/skills/simready-validator-agent /home/appuser/.claude/skills/simready-validator-agent
|
| 89 |
+
COPY tools/validation/plugins/simready-report/skills/simready-report /home/appuser/.claude/skills/simready-report
|
| 90 |
+
|
| 91 |
+
# UID 1000 owns the workspace + foundation clone.
|
| 92 |
+
RUN chown -R 1000:1000 /home/appuser ${SIMREADY_FOUNDATIONS_PATH}
|
| 93 |
+
|
| 94 |
+
# runner._kit_available() probes /isaac-sim/python.sh — present in the
|
| 95 |
+
# base image — so the validator auto-selects --use-kit at runtime.
|
| 96 |
+
# The validator's --use-kit path needs to know WHERE Kit's python lives;
|
| 97 |
+
# wire it via SIMREADY_KIT_PYTHON so the auto-detect is fully self-
|
| 98 |
+
# configuring (no per-call --kit-python flag needed).
|
| 99 |
+
ENV SIMREADY_KIT_PYTHON=/isaac-sim/python.sh
|
| 100 |
+
|
| 101 |
+
USER 1000:1000
|
| 102 |
+
|
| 103 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0
|
| 104 |
+
ENV GRADIO_SERVER_PORT=7860
|
| 105 |
+
EXPOSE 7860
|
| 106 |
+
|
| 107 |
+
# Clear the Isaac Sim base's ENTRYPOINT (it runs /isaac-sim/runheadless.sh
|
| 108 |
+
# and treats CMD as its args — fails with 'Permission denied' under
|
| 109 |
+
# UID 1000). We invoke the Python app directly; Kit is invoked
|
| 110 |
+
# explicitly by the validator via /isaac-sim/python.sh when needed.
|
| 111 |
+
ENTRYPOINT []
|
| 112 |
+
CMD ["python3.11", "app.py"]
|
tools/hf_space/README_HEADER.md
CHANGED
|
@@ -1,44 +1,44 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Simready Validator
|
| 3 |
-
emoji: 🏢
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
# SimReady Validator
|
| 11 |
-
|
| 12 |
-
A hosted validator for [SimReady](https://github.com/NVIDIA/simready-foundation)
|
| 13 |
-
asset packages. Submit a HuggingFace dataset, pick a profile, and the Space
|
| 14 |
-
runs the SimReady foundation rules against it and reports back what passed,
|
| 15 |
-
what failed, and why — without copying your assets off HuggingFace.
|
| 16 |
-
|
| 17 |
-
## Using the validator
|
| 18 |
-
|
| 19 |
-
Click the **App** tab above to open the validator UI. In the form:
|
| 20 |
-
|
| 21 |
-
| Field | What to enter |
|
| 22 |
-
|---|---|
|
| 23 |
-
| **Dataset** | The HuggingFace dataset slug you want to validate (e.g. `your-org/your-asset-bundle`). The dataset must be readable by this Space's `HF_TOKEN`. |
|
| 24 |
-
| **Profile** | The SimReady profile that matches your asset type — props vs. robots, neutral vs. PhysX vs. Isaac. See the validation guide for the full list. |
|
| 25 |
-
| **Version** | Profile version. Leave at `1.0.0` unless your profile is pinned to a different release. |
|
| 26 |
-
| **Open PR on dataset with verdict** | When ticked, the verdict is written back to the dataset as a `validation/results.json` pull request. Leave off for ad-hoc smoke tests. |
|
| 27 |
-
|
| 28 |
-
Click **Validate** and the log streams in real time. When it finishes the
|
| 29 |
-
results.json appears in the **HTML report** download.
|
| 30 |
-
|
| 31 |
-
## Going deeper
|
| 32 |
-
|
| 33 |
-
- **[Partner walkthrough → VALIDATE.md](./blob/main/VALIDATE.md)** — the full nine-step guide from cloning the foundation repo to submitting your assets. Read this first if you're packaging assets for SimReady for the first time.
|
| 34 |
-
- **[NVIDIA/simready-foundation](https://github.com/NVIDIA/simready-foundation)** — public spec repo with profile authoring guides and sample assets.
|
| 35 |
-
- **[WRAPP on NGC](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/omniverse/collections/wrapp)** — packaging tool referenced in the walkthrough.
|
| 36 |
-
|
| 37 |
-
## About this Space
|
| 38 |
-
|
| 39 |
-
The validator reads the requested HuggingFace dataset in place via
|
| 40 |
-
`huggingface_hub.snapshot_download` — assets stay on HuggingFace, no
|
| 41 |
-
transfer onto NVIDIA infrastructure. Running on a `t4-medium` GPU
|
| 42 |
-
tier. PhysX / MDL rules that require an Isaac Sim runtime are not
|
| 43 |
-
covered here yet (Isaac Sim isn't in the Space image — that's a
|
| 44 |
-
separate workstream).
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Simready Validator
|
| 3 |
+
emoji: 🏢
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# SimReady Validator
|
| 11 |
+
|
| 12 |
+
A hosted validator for [SimReady](https://github.com/NVIDIA/simready-foundation)
|
| 13 |
+
asset packages. Submit a HuggingFace dataset, pick a profile, and the Space
|
| 14 |
+
runs the SimReady foundation rules against it and reports back what passed,
|
| 15 |
+
what failed, and why — without copying your assets off HuggingFace.
|
| 16 |
+
|
| 17 |
+
## Using the validator
|
| 18 |
+
|
| 19 |
+
Click the **App** tab above to open the validator UI. In the form:
|
| 20 |
+
|
| 21 |
+
| Field | What to enter |
|
| 22 |
+
|---|---|
|
| 23 |
+
| **Dataset** | The HuggingFace dataset slug you want to validate (e.g. `your-org/your-asset-bundle`). The dataset must be readable by this Space's `HF_TOKEN`. |
|
| 24 |
+
| **Profile** | The SimReady profile that matches your asset type — props vs. robots, neutral vs. PhysX vs. Isaac. See the validation guide for the full list. |
|
| 25 |
+
| **Version** | Profile version. Leave at `1.0.0` unless your profile is pinned to a different release. |
|
| 26 |
+
| **Open PR on dataset with verdict** | When ticked, the verdict is written back to the dataset as a `validation/results.json` pull request. Leave off for ad-hoc smoke tests. |
|
| 27 |
+
|
| 28 |
+
Click **Validate** and the log streams in real time. When it finishes the
|
| 29 |
+
results.json appears in the **HTML report** download.
|
| 30 |
+
|
| 31 |
+
## Going deeper
|
| 32 |
+
|
| 33 |
+
- **[Partner walkthrough → VALIDATE.md](./blob/main/VALIDATE.md)** — the full nine-step guide from cloning the foundation repo to submitting your assets. Read this first if you're packaging assets for SimReady for the first time.
|
| 34 |
+
- **[NVIDIA/simready-foundation](https://github.com/NVIDIA/simready-foundation)** — public spec repo with profile authoring guides and sample assets.
|
| 35 |
+
- **[WRAPP on NGC](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/omniverse/collections/wrapp)** — packaging tool referenced in the walkthrough.
|
| 36 |
+
|
| 37 |
+
## About this Space
|
| 38 |
+
|
| 39 |
+
The validator reads the requested HuggingFace dataset in place via
|
| 40 |
+
`huggingface_hub.snapshot_download` — assets stay on HuggingFace, no
|
| 41 |
+
transfer onto NVIDIA infrastructure. Running on a `t4-medium` GPU
|
| 42 |
+
tier. PhysX / MDL rules that require an Isaac Sim runtime are not
|
| 43 |
+
covered here yet (Isaac Sim isn't in the Space image — that's a
|
| 44 |
+
separate workstream).
|
tools/hf_space/app.py
CHANGED
|
@@ -1,426 +1,429 @@
|
|
| 1 |
-
"""SimReady Validator — Gradio UI for the HuggingFace Space.
|
| 2 |
-
|
| 3 |
-
Two surfaces, same engine:
|
| 4 |
-
|
| 5 |
-
- **/run** (the on-screen button) — streams log lines to the UI for
|
| 6 |
-
interactive use by an operator in the browser.
|
| 7 |
-
- **/run_api** (hidden, programmatic) — returns the full RunResult as
|
| 8 |
-
a JSON-serializable dict. This is what `tools/hf_watch/call_hf_space.py`
|
| 9 |
-
hits from the GitHub Actions runner so the workflow can patch
|
| 10 |
-
status.json and asset-status.json without scraping the UI's text.
|
| 11 |
-
|
| 12 |
-
Both go through `runner.run()`. The split is purely about output
|
| 13 |
-
shape (streaming text vs. one-shot dict).
|
| 14 |
-
|
| 15 |
-
The Space is internal-pilot scope: HF_TOKEN comes from the Space's
|
| 16 |
-
secrets, NOT from the requester. When a customer's dataset PR triggers
|
| 17 |
-
this (next milestone), the webhook payload identifies the dataset and
|
| 18 |
-
the Space's own token opens the verdict PR.
|
| 19 |
-
"""
|
| 20 |
-
from __future__ import annotations
|
| 21 |
-
|
| 22 |
-
import json
|
| 23 |
-
import os
|
| 24 |
-
from pathlib import Path
|
| 25 |
-
|
| 26 |
-
import gradio as gr
|
| 27 |
-
|
| 28 |
-
from runner import (run as run_validator, progress_path_for, cancel_path_for,
|
| 29 |
-
run_token_path_for, CANCEL_DIR)
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
PROFILE_CHOICES = [
|
| 33 |
-
"Prop-Robotics-Neutral",
|
| 34 |
-
"Prop-Robotics-Physx",
|
| 35 |
-
"Prop-Robotics-Isaac",
|
| 36 |
-
"Robot-Body-Neutral",
|
| 37 |
-
"Robot-Body-Runnable",
|
| 38 |
-
"Robot-Body-Isaac",
|
| 39 |
-
"Package",
|
| 40 |
-
"Package-Candidate",
|
| 41 |
-
]
|
| 42 |
-
DEFAULT_PROFILE = "Prop-Robotics-Neutral"
|
| 43 |
-
DEFAULT_VERSION = "1.0.0"
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def _run_api(dataset: str, profile: str, version: str, open_pr: bool,
|
| 47 |
-
submission_id: str = "", force: bool = False,
|
| 48 |
-
preliminary: bool = False) -> dict:
|
| 49 |
-
"""Programmatic endpoint. Returns the RunResult as a JSON dict.
|
| 50 |
-
|
| 51 |
-
Caller is typically `tools/hf_watch/call_hf_space.py` running from
|
| 52 |
-
a GitHub Actions ubuntu-latest runner. Output shape must stay
|
| 53 |
-
stable — bump `schema_version` if you change it. The receiver
|
| 54 |
-
pattern-matches on the same field names `tools/hf_watch/validate.py`
|
| 55 |
-
produces, so status.json patching is identical regardless of which
|
| 56 |
-
backend ran the validation.
|
| 57 |
-
|
| 58 |
-
`submission_id` is optional — when set, the validator writes
|
| 59 |
-
per-asset progress to /tmp/sr-progress/<id>.json, which the
|
| 60 |
-
get_progress endpoint serves to the dashboard.
|
| 61 |
-
|
| 62 |
-
`preliminary` switches the runner to a structure-only sweep:
|
| 63 |
-
zip-bundled datasets are scanned (instead of failing
|
| 64 |
-
PKG.NO-ARCHIVES at the listing stage) and per-asset validation is
|
| 65 |
-
sliced to the first asset only. Used by the dashboard's
|
| 66 |
-
Preliminary scan tab.
|
| 67 |
-
"""
|
| 68 |
-
print(f"[run_api] preliminary={preliminary!r} force={force!r} "
|
| 69 |
-
f"submission_id={submission_id!r}", flush=True)
|
| 70 |
-
# Untrusted callers can hit /run_api directly — profile/version flow
|
| 71 |
-
# into the validator's argv, so validate them before use. Empty
|
| 72 |
-
# falls back to the defaults (existing behavior).
|
| 73 |
-
import re
|
| 74 |
-
profile = profile or DEFAULT_PROFILE
|
| 75 |
-
if profile not in PROFILE_CHOICES and profile.lower() != "auto":
|
| 76 |
-
raise ValueError(f"invalid profile: {profile!r}")
|
| 77 |
-
version = (version or DEFAULT_VERSION).strip()
|
| 78 |
-
if not re.fullmatch(r"[\w.\-]+", version):
|
| 79 |
-
raise ValueError(f"invalid version: {version!r}")
|
| 80 |
-
result = run_validator(
|
| 81 |
-
dataset=(dataset or "").strip(),
|
| 82 |
-
profile=profile,
|
| 83 |
-
version=version,
|
| 84 |
-
open_pr=bool(open_pr),
|
| 85 |
-
submission_id=(submission_id or "").strip(),
|
| 86 |
-
force=bool(force),
|
| 87 |
-
preliminary=bool(preliminary),
|
| 88 |
-
)
|
| 89 |
-
return {
|
| 90 |
-
"schema_version": 1,
|
| 91 |
-
"dataset": result.dataset,
|
| 92 |
-
"profile": result.profile,
|
| 93 |
-
"version": result.version,
|
| 94 |
-
"status": result.status,
|
| 95 |
-
"summary": result.summary,
|
| 96 |
-
"results_json": _sanitize_results_json(result.results_json),
|
| 97 |
-
"pr_url": result.pr_url,
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def _list_profiles() -> dict:
|
| 102 |
-
"""Return the set of profiles that actually load on this Space's
|
| 103 |
-
foundation+validator combination. The dashboard polls this to
|
| 104 |
-
populate its dropdown so operators can't pick a profile that
|
| 105 |
-
would fatally fail at registration time.
|
| 106 |
-
|
| 107 |
-
Uses --use-plugin since the default CLI loader has known
|
| 108 |
-
registration mismatches against the current foundation pin; the
|
| 109 |
-
plugin path is what runner.py's streaming-zip flow falls back
|
| 110 |
-
to and is the source of truth for "actually usable" here.
|
| 111 |
-
|
| 112 |
-
Output format from validate.py is `PROFILE: <id> v<version>`
|
| 113 |
-
per profile, one per line.
|
| 114 |
-
"""
|
| 115 |
-
import subprocess, sys
|
| 116 |
-
from runner import VALIDATOR
|
| 117 |
-
try:
|
| 118 |
-
proc = subprocess.run(
|
| 119 |
-
#
|
| 120 |
-
#
|
| 121 |
-
#
|
| 122 |
-
#
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
if
|
| 174 |
-
return {"state": "no_id"}
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
#
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
return
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
if
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
dataset
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
gr.
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
#
|
| 360 |
-
#
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
#
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
#
|
| 396 |
-
#
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
#
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SimReady Validator — Gradio UI for the HuggingFace Space.
|
| 2 |
+
|
| 3 |
+
Two surfaces, same engine:
|
| 4 |
+
|
| 5 |
+
- **/run** (the on-screen button) — streams log lines to the UI for
|
| 6 |
+
interactive use by an operator in the browser.
|
| 7 |
+
- **/run_api** (hidden, programmatic) — returns the full RunResult as
|
| 8 |
+
a JSON-serializable dict. This is what `tools/hf_watch/call_hf_space.py`
|
| 9 |
+
hits from the GitHub Actions runner so the workflow can patch
|
| 10 |
+
status.json and asset-status.json without scraping the UI's text.
|
| 11 |
+
|
| 12 |
+
Both go through `runner.run()`. The split is purely about output
|
| 13 |
+
shape (streaming text vs. one-shot dict).
|
| 14 |
+
|
| 15 |
+
The Space is internal-pilot scope: HF_TOKEN comes from the Space's
|
| 16 |
+
secrets, NOT from the requester. When a customer's dataset PR triggers
|
| 17 |
+
this (next milestone), the webhook payload identifies the dataset and
|
| 18 |
+
the Space's own token opens the verdict PR.
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
import gradio as gr
|
| 27 |
+
|
| 28 |
+
from runner import (run as run_validator, progress_path_for, cancel_path_for,
|
| 29 |
+
run_token_path_for, CANCEL_DIR)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
PROFILE_CHOICES = [
|
| 33 |
+
"Prop-Robotics-Neutral",
|
| 34 |
+
"Prop-Robotics-Physx",
|
| 35 |
+
"Prop-Robotics-Isaac",
|
| 36 |
+
"Robot-Body-Neutral",
|
| 37 |
+
"Robot-Body-Runnable",
|
| 38 |
+
"Robot-Body-Isaac",
|
| 39 |
+
"Package",
|
| 40 |
+
"Package-Candidate",
|
| 41 |
+
]
|
| 42 |
+
DEFAULT_PROFILE = "Prop-Robotics-Neutral"
|
| 43 |
+
DEFAULT_VERSION = "1.0.0"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _run_api(dataset: str, profile: str, version: str, open_pr: bool,
|
| 47 |
+
submission_id: str = "", force: bool = False,
|
| 48 |
+
preliminary: bool = False) -> dict:
|
| 49 |
+
"""Programmatic endpoint. Returns the RunResult as a JSON dict.
|
| 50 |
+
|
| 51 |
+
Caller is typically `tools/hf_watch/call_hf_space.py` running from
|
| 52 |
+
a GitHub Actions ubuntu-latest runner. Output shape must stay
|
| 53 |
+
stable — bump `schema_version` if you change it. The receiver
|
| 54 |
+
pattern-matches on the same field names `tools/hf_watch/validate.py`
|
| 55 |
+
produces, so status.json patching is identical regardless of which
|
| 56 |
+
backend ran the validation.
|
| 57 |
+
|
| 58 |
+
`submission_id` is optional — when set, the validator writes
|
| 59 |
+
per-asset progress to /tmp/sr-progress/<id>.json, which the
|
| 60 |
+
get_progress endpoint serves to the dashboard.
|
| 61 |
+
|
| 62 |
+
`preliminary` switches the runner to a structure-only sweep:
|
| 63 |
+
zip-bundled datasets are scanned (instead of failing
|
| 64 |
+
PKG.NO-ARCHIVES at the listing stage) and per-asset validation is
|
| 65 |
+
sliced to the first asset only. Used by the dashboard's
|
| 66 |
+
Preliminary scan tab.
|
| 67 |
+
"""
|
| 68 |
+
print(f"[run_api] preliminary={preliminary!r} force={force!r} "
|
| 69 |
+
f"submission_id={submission_id!r}", flush=True)
|
| 70 |
+
# Untrusted callers can hit /run_api directly — profile/version flow
|
| 71 |
+
# into the validator's argv, so validate them before use. Empty
|
| 72 |
+
# falls back to the defaults (existing behavior).
|
| 73 |
+
import re
|
| 74 |
+
profile = profile or DEFAULT_PROFILE
|
| 75 |
+
if profile not in PROFILE_CHOICES and profile.lower() != "auto":
|
| 76 |
+
raise ValueError(f"invalid profile: {profile!r}")
|
| 77 |
+
version = (version or DEFAULT_VERSION).strip()
|
| 78 |
+
if not re.fullmatch(r"[\w.\-]+", version):
|
| 79 |
+
raise ValueError(f"invalid version: {version!r}")
|
| 80 |
+
result = run_validator(
|
| 81 |
+
dataset=(dataset or "").strip(),
|
| 82 |
+
profile=profile,
|
| 83 |
+
version=version,
|
| 84 |
+
open_pr=bool(open_pr),
|
| 85 |
+
submission_id=(submission_id or "").strip(),
|
| 86 |
+
force=bool(force),
|
| 87 |
+
preliminary=bool(preliminary),
|
| 88 |
+
)
|
| 89 |
+
return {
|
| 90 |
+
"schema_version": 1,
|
| 91 |
+
"dataset": result.dataset,
|
| 92 |
+
"profile": result.profile,
|
| 93 |
+
"version": result.version,
|
| 94 |
+
"status": result.status,
|
| 95 |
+
"summary": result.summary,
|
| 96 |
+
"results_json": _sanitize_results_json(result.results_json),
|
| 97 |
+
"pr_url": result.pr_url,
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _list_profiles() -> dict:
|
| 102 |
+
"""Return the set of profiles that actually load on this Space's
|
| 103 |
+
foundation+validator combination. The dashboard polls this to
|
| 104 |
+
populate its dropdown so operators can't pick a profile that
|
| 105 |
+
would fatally fail at registration time.
|
| 106 |
+
|
| 107 |
+
Uses --use-plugin since the default CLI loader has known
|
| 108 |
+
registration mismatches against the current foundation pin; the
|
| 109 |
+
plugin path is what runner.py's streaming-zip flow falls back
|
| 110 |
+
to and is the source of truth for "actually usable" here.
|
| 111 |
+
|
| 112 |
+
Output format from validate.py is `PROFILE: <id> v<version>`
|
| 113 |
+
per profile, one per line.
|
| 114 |
+
"""
|
| 115 |
+
import subprocess, sys
|
| 116 |
+
from runner import VALIDATOR
|
| 117 |
+
try:
|
| 118 |
+
proc = subprocess.run(
|
| 119 |
+
# --list-profiles only ENUMERATES registered profiles from the
|
| 120 |
+
# spec/plugin registry (--use-plugin) — it runs no validation
|
| 121 |
+
# rules, so it never needs Kit. Force --no-use-kit: on a
|
| 122 |
+
# Kit-enabled image the validator auto-enables --use-kit for the
|
| 123 |
+
# PhysX-bearing default profile and boots the full Isaac Sim
|
| 124 |
+
# runtime (~5 min) just to print the list, blowing the 300s
|
| 125 |
+
# timeout below. Actual validation (runner.py) still uses Kit.
|
| 126 |
+
[sys.executable, str(VALIDATOR), "--list-profiles", "--use-plugin", "--no-use-kit"],
|
| 127 |
+
capture_output=True, text=True, timeout=300,
|
| 128 |
+
)
|
| 129 |
+
names: list[str] = []
|
| 130 |
+
for line in (proc.stdout or "").splitlines():
|
| 131 |
+
s = line.strip()
|
| 132 |
+
# Validator emits "PROFILE: <id> v<version>" — that's our
|
| 133 |
+
# only authoritative shape. Anything else is noise.
|
| 134 |
+
if s.startswith("PROFILE:"):
|
| 135 |
+
rest = s[len("PROFILE:"):].strip()
|
| 136 |
+
pid = rest.split()[0] if rest else ""
|
| 137 |
+
if pid:
|
| 138 |
+
names.append(pid)
|
| 139 |
+
# Dedupe while preserving order.
|
| 140 |
+
seen = set()
|
| 141 |
+
unique = []
|
| 142 |
+
for n in names:
|
| 143 |
+
if n not in seen:
|
| 144 |
+
seen.add(n)
|
| 145 |
+
unique.append(n)
|
| 146 |
+
result: dict = {"profiles": unique, "schema_version": 1, "rc": proc.returncode}
|
| 147 |
+
if not unique:
|
| 148 |
+
# No profiles registered AND no parse hits — surface why so
|
| 149 |
+
# the dashboard can show something useful. Truncate so the
|
| 150 |
+
# JSON response stays small.
|
| 151 |
+
stderr_tail = "\n".join((proc.stderr or "").splitlines()[-20:])[:2000]
|
| 152 |
+
stdout_tail = "\n".join((proc.stdout or "").splitlines()[-20:])[:2000]
|
| 153 |
+
result["stderr_tail"] = stderr_tail
|
| 154 |
+
result["stdout_tail"] = stdout_tail
|
| 155 |
+
return result
|
| 156 |
+
except subprocess.TimeoutExpired:
|
| 157 |
+
return {"profiles": [], "error": "timeout after 300s (spec load >5 min)"}
|
| 158 |
+
except Exception as e:
|
| 159 |
+
return {"profiles": [], "error": f"{type(e).__name__}: {e}"}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _cancel_run(submission_id: str, run_token: str = "") -> dict:
|
| 163 |
+
"""Write the cancel-signal file for a given submission. The
|
| 164 |
+
streaming-zip loop in runner.py checks for this file between zips
|
| 165 |
+
and aborts when present. Idempotent — calling multiple times has no
|
| 166 |
+
extra effect; consuming runner.py deletes it.
|
| 167 |
+
|
| 168 |
+
`run_token` is the per-run token the dashboard read from get_progress.
|
| 169 |
+
It becomes the flag's content so runner._is_cancelled only honors it
|
| 170 |
+
for the exact run it was issued against — a flag left over from a
|
| 171 |
+
prior run of this submission can never abort a fresh one."""
|
| 172 |
+
sid = (submission_id or "").strip()
|
| 173 |
+
if not sid:
|
| 174 |
+
return {"state": "no_id"}
|
| 175 |
+
path = cancel_path_for(sid)
|
| 176 |
+
if path is None:
|
| 177 |
+
return {"state": "no_id"}
|
| 178 |
+
try:
|
| 179 |
+
CANCEL_DIR.mkdir(parents=True, exist_ok=True)
|
| 180 |
+
path.write_text((run_token or "").strip(), encoding="utf-8")
|
| 181 |
+
return {"state": "signaled", "path": str(path)}
|
| 182 |
+
except OSError as e:
|
| 183 |
+
return {"state": "error", "error": f"{type(e).__name__}: {e}"}
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _get_progress(submission_id: str) -> dict:
|
| 187 |
+
"""Read the validator's per-asset progress file for this submission.
|
| 188 |
+
|
| 189 |
+
Polled by the dashboard ~every 3 s while a Validate-now click is
|
| 190 |
+
in-flight, so the "Validate now" button can fill up as the
|
| 191 |
+
validator works through the asset list.
|
| 192 |
+
|
| 193 |
+
Returns one of three shapes:
|
| 194 |
+
- {"state": "not_found"} — no progress file (Space restarted, or
|
| 195 |
+
the dashboard is polling a Space-run that never happened).
|
| 196 |
+
- {"state": "starting"} — file seeded by runner.py before the
|
| 197 |
+
validator started its loop. processed/total are 0.
|
| 198 |
+
- {processed, total, current, started_at, updated_at} — live
|
| 199 |
+
per-asset progress written by validate.py._emit_progress.
|
| 200 |
+
|
| 201 |
+
Every shape also carries `run_token` (the current run's cancel
|
| 202 |
+
token, from the sidecar file) when one exists, so the dashboard can
|
| 203 |
+
echo it back to cancel_run and target the exact run.
|
| 204 |
+
|
| 205 |
+
Caller treats anything with total > 0 as "show the fill bar".
|
| 206 |
+
"""
|
| 207 |
+
sid = (submission_id or "").strip()
|
| 208 |
+
if not sid:
|
| 209 |
+
return {"state": "no_id"}
|
| 210 |
+
# Per-run cancel token (sidecar; see runner.run_token_path_for).
|
| 211 |
+
# Surfaced on every shape so the dashboard can echo it back to
|
| 212 |
+
# cancel_run — a cancel then only aborts the run it was issued
|
| 213 |
+
# against, never a later one that reused the submission_id.
|
| 214 |
+
run_token = ""
|
| 215 |
+
tok_path = run_token_path_for(sid)
|
| 216 |
+
if tok_path and tok_path.is_file():
|
| 217 |
+
try:
|
| 218 |
+
run_token = tok_path.read_text(encoding="utf-8").strip()
|
| 219 |
+
except OSError:
|
| 220 |
+
pass
|
| 221 |
+
path = progress_path_for(sid)
|
| 222 |
+
if path is None or not path.is_file():
|
| 223 |
+
return {"state": "not_found", "run_token": run_token}
|
| 224 |
+
try:
|
| 225 |
+
data = json.loads(path.read_text(encoding="utf-8"))
|
| 226 |
+
if isinstance(data, dict) and run_token:
|
| 227 |
+
data["run_token"] = run_token
|
| 228 |
+
return data
|
| 229 |
+
except (OSError, json.JSONDecodeError):
|
| 230 |
+
# Mid-write — caller will poll again in a few seconds.
|
| 231 |
+
return {"state": "transient", "run_token": run_token}
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _sanitize_results_json(raw: dict) -> dict:
|
| 235 |
+
"""Strip absolute filesystem paths from results_json before returning.
|
| 236 |
+
|
| 237 |
+
Gradio's JSON serializer treats string fields that resolve to files
|
| 238 |
+
on the Space's filesystem as downloadable references and tries to
|
| 239 |
+
serve them through `/gradio_api/file=...`. The validator's
|
| 240 |
+
results.json contains absolute paths (target dir + per-asset
|
| 241 |
+
`path`) which point into the Space's ephemeral tempdir and are
|
| 242 |
+
NOT exposed through gradio's allowed_paths — gradio_client then
|
| 243 |
+
fails with 403 trying to auto-fetch them after a successful run.
|
| 244 |
+
|
| 245 |
+
Callers don't need filesystem paths anyway — only `rel_path`
|
| 246 |
+
(dataset-relative), `passed`, and `issues` are used downstream.
|
| 247 |
+
Keep the rest of the report intact (profile_coverage, summary,
|
| 248 |
+
layout_findings, etc.).
|
| 249 |
+
"""
|
| 250 |
+
if not isinstance(raw, dict):
|
| 251 |
+
return raw
|
| 252 |
+
sanitized = {k: v for k, v in raw.items() if k != "target"}
|
| 253 |
+
if "results" in sanitized and isinstance(sanitized["results"], list):
|
| 254 |
+
sanitized["results"] = [
|
| 255 |
+
{k: v for k, v in asset.items() if k != "path"}
|
| 256 |
+
for asset in sanitized["results"]
|
| 257 |
+
if isinstance(asset, dict)
|
| 258 |
+
]
|
| 259 |
+
# Specs/dashboard dir paths are local to the Space, useless to caller.
|
| 260 |
+
for k in ("specs_docs_dir", "dashboard_docs_dir"):
|
| 261 |
+
sanitized.pop(k, None)
|
| 262 |
+
return sanitized
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def _run_streaming(dataset: str, profile: str, version: str, open_pr: bool):
|
| 266 |
+
"""Generator that yields incremental log output to the UI as the
|
| 267 |
+
validator runs. Gradio streams each yielded tuple to the connected
|
| 268 |
+
outputs."""
|
| 269 |
+
lines: list[str] = []
|
| 270 |
+
|
| 271 |
+
def log(line: str) -> None:
|
| 272 |
+
lines.append(line)
|
| 273 |
+
|
| 274 |
+
yield "\n".join(lines), "", "(running…)", None
|
| 275 |
+
|
| 276 |
+
try:
|
| 277 |
+
result = run_validator(
|
| 278 |
+
dataset=dataset.strip(),
|
| 279 |
+
profile=profile,
|
| 280 |
+
version=version.strip() or DEFAULT_VERSION,
|
| 281 |
+
open_pr=open_pr,
|
| 282 |
+
log=log,
|
| 283 |
+
)
|
| 284 |
+
except Exception as e:
|
| 285 |
+
lines.append(f"\nERROR: {type(e).__name__}: {e}")
|
| 286 |
+
yield "\n".join(lines), "", f"error: {e}", None
|
| 287 |
+
return
|
| 288 |
+
|
| 289 |
+
status_badge = f"**{result.status.upper()}** — {result.summary}"
|
| 290 |
+
if result.pr_url:
|
| 291 |
+
status_badge += f"\n\nPR: {result.pr_url}"
|
| 292 |
+
|
| 293 |
+
report_index = result.report_path / "index.html"
|
| 294 |
+
report_url = str(report_index) if report_index.is_file() else None
|
| 295 |
+
|
| 296 |
+
yield (
|
| 297 |
+
"\n".join(lines),
|
| 298 |
+
status_badge,
|
| 299 |
+
result.summary,
|
| 300 |
+
report_url,
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _read_md(name: str) -> str:
|
| 305 |
+
"""Return the contents of name (relative to this file's dir),
|
| 306 |
+
stripping a leading YAML frontmatter block if present. Falls back
|
| 307 |
+
to a friendly stub when the file is missing — keeps the Space
|
| 308 |
+
bootable even before the space-deploy workflow has synced the
|
| 309 |
+
assembled docs into the container."""
|
| 310 |
+
from pathlib import Path
|
| 311 |
+
p = Path(__file__).resolve().parent / name
|
| 312 |
+
try:
|
| 313 |
+
src = p.read_text(encoding="utf-8")
|
| 314 |
+
except FileNotFoundError:
|
| 315 |
+
return f"_{name} not yet synced into this Space — check back after the next deploy._"
|
| 316 |
+
if src.startswith("---"):
|
| 317 |
+
end = src.find("\n---\n", 4)
|
| 318 |
+
if end > 0:
|
| 319 |
+
src = src[end + len("\n---\n"):].lstrip()
|
| 320 |
+
return src
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
with gr.Blocks(title="SimReady Validator") as demo:
|
| 324 |
+
with gr.Tabs():
|
| 325 |
+
with gr.Tab("Overview"):
|
| 326 |
+
gr.Markdown(_read_md("README.md"))
|
| 327 |
+
with gr.Tab("Validator"):
|
| 328 |
+
gr.Markdown(
|
| 329 |
+
"Submit a HuggingFace dataset to validate against a SimReady "
|
| 330 |
+
"profile. With **Open PR** enabled, the verdict is uploaded "
|
| 331 |
+
"back to the dataset as a `validation/` pull request."
|
| 332 |
+
)
|
| 333 |
+
with gr.Row():
|
| 334 |
+
dataset = gr.Textbox(
|
| 335 |
+
label="Dataset",
|
| 336 |
+
placeholder="org/dataset (e.g. imagineio/PhysicalAI-SimReady-Kitchens-v1)",
|
| 337 |
+
)
|
| 338 |
+
with gr.Row():
|
| 339 |
+
profile = gr.Dropdown(
|
| 340 |
+
choices=PROFILE_CHOICES, value=DEFAULT_PROFILE, label="Profile",
|
| 341 |
+
)
|
| 342 |
+
version = gr.Textbox(label="Version", value=DEFAULT_VERSION)
|
| 343 |
+
open_pr = gr.Checkbox(label="Open PR on dataset with verdict", value=False)
|
| 344 |
+
run_btn = gr.Button("Validate", variant="primary")
|
| 345 |
+
status_md = gr.Markdown(label="Verdict")
|
| 346 |
+
summary_box = gr.Textbox(label="Summary", interactive=False)
|
| 347 |
+
log_box = gr.Textbox(label="Log", lines=20, interactive=False)
|
| 348 |
+
report_link = gr.File(label="HTML report (download)", interactive=False)
|
| 349 |
+
with gr.Tab("Partner walkthrough"):
|
| 350 |
+
gr.Markdown(_read_md("VALIDATE.md"))
|
| 351 |
+
|
| 352 |
+
run_btn.click(
|
| 353 |
+
fn=_run_streaming,
|
| 354 |
+
inputs=[dataset, profile, version, open_pr],
|
| 355 |
+
outputs=[log_box, status_md, summary_box, report_link],
|
| 356 |
+
api_name="run",
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# Programmatic endpoint — bound to invisible components so the UI
|
| 360 |
+
# doesn't render anything extra, but the Gradio queue still exposes
|
| 361 |
+
# an `/api/predict/run_api` route the gradio_client can hit. The
|
| 362 |
+
# outputs[0] is the JSON return; api_name turns it into a stable
|
| 363 |
+
# path the GitHub Actions caller depends on.
|
| 364 |
+
api_dataset = gr.Textbox(visible=False)
|
| 365 |
+
api_profile = gr.Textbox(visible=False)
|
| 366 |
+
api_version = gr.Textbox(visible=False)
|
| 367 |
+
api_open_pr = gr.Checkbox(visible=False)
|
| 368 |
+
api_submission_id = gr.Textbox(visible=False)
|
| 369 |
+
api_force = gr.Checkbox(visible=False)
|
| 370 |
+
api_preliminary = gr.Checkbox(visible=False)
|
| 371 |
+
api_output = gr.JSON(visible=False)
|
| 372 |
+
api_button = gr.Button(visible=False)
|
| 373 |
+
api_button.click(
|
| 374 |
+
fn=_run_api,
|
| 375 |
+
inputs=[api_dataset, api_profile, api_version, api_open_pr,
|
| 376 |
+
api_submission_id, api_force, api_preliminary],
|
| 377 |
+
outputs=api_output,
|
| 378 |
+
api_name="run_api",
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
# Progress endpoint — polled by the dashboard while a row is
|
| 382 |
+
# validating. CORS is open on /gradio_api/* by default, so the
|
| 383 |
+
# browser can fetch this from github.io directly without any
|
| 384 |
+
# GitHub-Actions side polling/commit churn.
|
| 385 |
+
prog_in = gr.Textbox(visible=False)
|
| 386 |
+
prog_out = gr.JSON(visible=False)
|
| 387 |
+
prog_button = gr.Button(visible=False)
|
| 388 |
+
prog_button.click(
|
| 389 |
+
fn=_get_progress,
|
| 390 |
+
inputs=[prog_in],
|
| 391 |
+
outputs=prog_out,
|
| 392 |
+
api_name="get_progress",
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
# Profile-listing endpoint — polled by the dashboard at startup
|
| 396 |
+
# so its dropdown reflects what's actually loadable on this Space
|
| 397 |
+
# right now (foundation+validator pin determines which profiles
|
| 398 |
+
# register). Stops the operator from picking something that
|
| 399 |
+
# would fatal at runtime.
|
| 400 |
+
profiles_out = gr.JSON(visible=False)
|
| 401 |
+
profiles_button = gr.Button(visible=False)
|
| 402 |
+
profiles_button.click(
|
| 403 |
+
fn=_list_profiles,
|
| 404 |
+
inputs=None,
|
| 405 |
+
outputs=profiles_out,
|
| 406 |
+
api_name="list_profiles",
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
# Cancel endpoint — the dashboard's Cancel button calls this AFTER
|
| 410 |
+
# cancelling the GH Action so the in-flight server-side gradio call
|
| 411 |
+
# actually stops (cancelling the Action alone only kills the
|
| 412 |
+
# gradio_client wrapper, the Space's loop keeps going).
|
| 413 |
+
cancel_in = gr.Textbox(visible=False)
|
| 414 |
+
cancel_token = gr.Textbox(visible=False)
|
| 415 |
+
cancel_out = gr.JSON(visible=False)
|
| 416 |
+
cancel_button = gr.Button(visible=False)
|
| 417 |
+
cancel_button.click(
|
| 418 |
+
fn=_cancel_run,
|
| 419 |
+
inputs=[cancel_in, cancel_token],
|
| 420 |
+
outputs=cancel_out,
|
| 421 |
+
api_name="cancel_run",
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
if __name__ == "__main__":
|
| 426 |
+
demo.queue().launch(
|
| 427 |
+
server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 428 |
+
server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
|
| 429 |
+
)
|
tools/hf_space/github_issues.py
CHANGED
|
@@ -1,324 +1,324 @@
|
|
| 1 |
-
"""Auto-file GitHub issues for validator-internal bugs.
|
| 2 |
-
|
| 3 |
-
Mirror of `tools/hf_watch/validate.py::_ensure_validator_internal_issues`
|
| 4 |
-
moved to the HF Space side so the Space can self-report tooling
|
| 5 |
-
failures without depending on the GH Actions wrapper to do it.
|
| 6 |
-
|
| 7 |
-
Policy reminder (from CLAUDE.md / project conventions):
|
| 8 |
-
- GitHub Issues track NVIDIA-internal *tooling* problems only.
|
| 9 |
-
- Customer-asset findings (real spec violations) stay on the dashboard;
|
| 10 |
-
they do NOT become issues.
|
| 11 |
-
- Distinguishing the two is `is_validator_internal_issue`'s job.
|
| 12 |
-
|
| 13 |
-
Token: GH_VALIDATOR_TOKEN (or GITHUB_TOKEN) — set as a Space secret.
|
| 14 |
-
A fine-grained PAT with `issues: read+write` on
|
| 15 |
-
`NVIDIA-dev/simready-oem-library-pm` is enough; no other scope needed.
|
| 16 |
-
"""
|
| 17 |
-
from __future__ import annotations
|
| 18 |
-
|
| 19 |
-
import json
|
| 20 |
-
import os
|
| 21 |
-
import re
|
| 22 |
-
import urllib.parse
|
| 23 |
-
import urllib.request
|
| 24 |
-
from typing import Any
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
GH_REPO = "NVIDIA-dev/simready-oem-library-pm"
|
| 28 |
-
|
| 29 |
-
# Redact token-shaped strings before they land in a (public-ish) issue.
|
| 30 |
-
# Covers GitHub PATs (gh[ps]_…), HF tokens (hf_…), and long base64-ish
|
| 31 |
-
# secrets. Validator log text passes through here on its way into bodies.
|
| 32 |
-
_SECRET_RE = re.compile(r"gh[ps]_\w+|hf_\w+|[A-Za-z0-9+/]{40,}={0,2}")
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def scrub_secrets(text: str) -> str:
|
| 36 |
-
return _SECRET_RE.sub("[REDACTED]", text or "")
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def _gh_token() -> str | None:
|
| 40 |
-
return os.environ.get("GH_VALIDATOR_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def is_validator_internal_issue(iss: dict) -> bool:
|
| 44 |
-
"""Heuristic: distinguish validator-package crashes from real
|
| 45 |
-
asset findings."""
|
| 46 |
-
code = iss.get("code") or ""
|
| 47 |
-
msg = iss.get("msg") or ""
|
| 48 |
-
if code == "UNKNOWN" or code.startswith("SDK."):
|
| 49 |
-
return True
|
| 50 |
-
if "Uncaught error" in msg:
|
| 51 |
-
return True
|
| 52 |
-
if "is not registered to requirement" in msg:
|
| 53 |
-
return True
|
| 54 |
-
return False
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def _gh_request(method: str, path: str, payload: dict | None = None) -> dict | list:
|
| 58 |
-
token = _gh_token()
|
| 59 |
-
if not token:
|
| 60 |
-
raise RuntimeError("no GitHub token in env (GH_VALIDATOR_TOKEN or GITHUB_TOKEN)")
|
| 61 |
-
url = f"https://api.github.com/repos/{GH_REPO}{path}"
|
| 62 |
-
headers = {
|
| 63 |
-
"Accept": "application/vnd.github+json",
|
| 64 |
-
"Authorization": f"Bearer {token}",
|
| 65 |
-
"X-GitHub-Api-Version": "2022-11-28",
|
| 66 |
-
"User-Agent": "simready-validator-space/0.1",
|
| 67 |
-
}
|
| 68 |
-
body = None
|
| 69 |
-
if payload is not None:
|
| 70 |
-
body = json.dumps(payload).encode("utf-8")
|
| 71 |
-
headers["Content-Type"] = "application/json"
|
| 72 |
-
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
| 73 |
-
with urllib.request.urlopen(req, timeout=30) as r:
|
| 74 |
-
return json.loads(r.read() or "null")
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def _find_issue(title: str) -> dict | None:
|
| 78 |
-
# Strip `"` — title is untrusted and a quote would break the
|
| 79 |
-
# quoted-phrase search query (and could inject extra qualifiers).
|
| 80 |
-
safe_title = title.replace('"', "")
|
| 81 |
-
q = urllib.parse.quote(f'repo:{GH_REPO} in:title "{safe_title}" is:issue')
|
| 82 |
-
result = _gh_request("GET", f"/../../search/issues?q={q}")
|
| 83 |
-
items = (result or {}).get("items") or []
|
| 84 |
-
for it in items:
|
| 85 |
-
if it.get("title") == title:
|
| 86 |
-
return it
|
| 87 |
-
return None
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def _create_issue(title: str, body: str, labels: list[str]) -> int:
|
| 91 |
-
result = _gh_request("POST", "/issues",
|
| 92 |
-
{"title": title, "body": body, "labels": labels})
|
| 93 |
-
return result.get("number", 0)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def _add_comment(issue_num: int, body: str) -> None:
|
| 97 |
-
_gh_request("POST", f"/issues/{issue_num}/comments", {"body": body})
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def _build_dataset_issue_body(by_pair: dict, dataset: str, profile: str,
|
| 101 |
-
total: int) -> str:
|
| 102 |
-
rows = "\n".join(
|
| 103 |
-
f"| `{rule}` | `{code}` | {g['severity'] or '?'} | {g['count']} | `{g['sample_msg']}` |"
|
| 104 |
-
for (rule, code), g in sorted(by_pair.items(), key=lambda kv: -kv[1]["count"])
|
| 105 |
-
)
|
| 106 |
-
return (
|
| 107 |
-
f"**Validator-internal bugs on a single dataset** — surfaced during "
|
| 108 |
-
f"automatic SimReady validation. NOT a customer-asset finding; the "
|
| 109 |
-
f"validator's own rule registration / spec loading is misbehaving "
|
| 110 |
-
f"on this dataset and emitting errors that don't map to any real "
|
| 111 |
-
f"spec violation.\n\n"
|
| 112 |
-
f"| Field | Value |\n|---|---|\n"
|
| 113 |
-
f"| Dataset | `{dataset}` |\n"
|
| 114 |
-
f"| Profile (first run) | `{profile}` |\n"
|
| 115 |
-
f"| Total internal occurrences (first run) | {total} |\n"
|
| 116 |
-
f"| Distinct (rule, code) pairs (first run) | {len(by_pair)} |\n\n"
|
| 117 |
-
f"**Breakdown** (sorted by occurrence count, descending):\n\n"
|
| 118 |
-
f"| Rule | Code | Severity | Count | Sample message |\n"
|
| 119 |
-
f"|---|---|---|---|---|\n{rows}\n\n"
|
| 120 |
-
f"---\n"
|
| 121 |
-
f"_Filed automatically by the HF Space (`tools/hf_space/github_issues.py`). "
|
| 122 |
-
f"One issue per dataset — re-validating the same dataset comments "
|
| 123 |
-
f"here with the new counts instead of opening a duplicate._"
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def _build_dataset_recurrence_comment(by_pair: dict, dataset: str, profile: str,
|
| 128 |
-
total: int) -> str:
|
| 129 |
-
rows = "\n".join(
|
| 130 |
-
f"| `{rule}` | `{code}` | {g['count']} |"
|
| 131 |
-
for (rule, code), g in sorted(by_pair.items(), key=lambda kv: -kv[1]["count"])
|
| 132 |
-
)
|
| 133 |
-
return (
|
| 134 |
-
f"Re-hit during validation of `{dataset}` (profile `{profile}`).\n"
|
| 135 |
-
f"This run: **{total}** internal occurrences across **{len(by_pair)}** "
|
| 136 |
-
f"distinct (rule, code) pairs.\n\n"
|
| 137 |
-
f"| Rule | Code | Count this run |\n|---|---|---|\n{rows}"
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
def _list_existing_internal_issues() -> list[dict]:
|
| 142 |
-
"""Pull every validator-internal issue (open + recently closed) so
|
| 143 |
-
the agent can dedupe semantically against them. Closed issues are
|
| 144 |
-
included because the same bug can come back after a fix is
|
| 145 |
-
reverted or after a regression."""
|
| 146 |
-
q = urllib.parse.quote(
|
| 147 |
-
f'repo:{GH_REPO} label:"validator-internal" is:issue')
|
| 148 |
-
try:
|
| 149 |
-
result = _gh_request("GET", f"/../../search/issues?q={q}&per_page=100")
|
| 150 |
-
except Exception:
|
| 151 |
-
return []
|
| 152 |
-
return (result or {}).get("items") or []
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def _execute_decisions(decisions: list[dict], by_pair: dict, dataset: str,
|
| 156 |
-
profile: str, total: int, log_fn) -> dict:
|
| 157 |
-
"""Carry out the agent's decisions. Returns counters."""
|
| 158 |
-
out = log_fn
|
| 159 |
-
created = updated = skipped = 0
|
| 160 |
-
aborted = False
|
| 161 |
-
# Map negative placeholder numbers → real issue numbers as `create`
|
| 162 |
-
# decisions resolve. The agent uses negatives to cross-reference
|
| 163 |
-
# decisions that share a parent issue when several groups roll up
|
| 164 |
-
# into one new bug.
|
| 165 |
-
placeholder_to_real: dict[int, int] = {}
|
| 166 |
-
# Two passes: creates first (so their numbers are known), then
|
| 167 |
-
# comments (so cross-references resolve). Skips are free.
|
| 168 |
-
creates = [d for d in decisions if d.get("action") == "create"]
|
| 169 |
-
comments = [d for d in decisions if d.get("action") == "comment"]
|
| 170 |
-
skips = [d for d in decisions if d.get("action") == "skip"]
|
| 171 |
-
for d in creates:
|
| 172 |
-
if aborted: break
|
| 173 |
-
title = d.get("title") or f"[validator-internal] {dataset} :: {d.get('rule')} / {d.get('code')}"
|
| 174 |
-
body = d.get("body") or "(agent did not provide a body)"
|
| 175 |
-
try:
|
| 176 |
-
existing = _find_issue(title)
|
| 177 |
-
if existing:
|
| 178 |
-
_add_comment(existing["number"],
|
| 179 |
-
f"_Re-hit during validation of `{dataset}` "
|
| 180 |
-
f"(profile `{profile}`)._\n\n{body}")
|
| 181 |
-
updated += 1
|
| 182 |
-
out(f" internal-issue #{existing['number']}: comment added "
|
| 183 |
-
f"(agent: create→existing match by title)")
|
| 184 |
-
placeholder_to_real.setdefault(-(creates.index(d) + 1), existing["number"])
|
| 185 |
-
else:
|
| 186 |
-
num = _create_issue(title, body, ["validator-internal", "process", "agent-reviewed"])
|
| 187 |
-
created += 1
|
| 188 |
-
out(f" internal-issue #{num}: opened ({title!r}) — agent reasoning: "
|
| 189 |
-
f"{d.get('reasoning', '')[:160]}")
|
| 190 |
-
placeholder_to_real[-(creates.index(d) + 1)] = num
|
| 191 |
-
except Exception as e:
|
| 192 |
-
msg = f"{type(e).__name__}: {e}"
|
| 193 |
-
if "404" in msg:
|
| 194 |
-
out(f" ! internal-issue tracking aborted (404 — token lacks issues:write on {GH_REPO})")
|
| 195 |
-
aborted = True
|
| 196 |
-
else:
|
| 197 |
-
out(f" ! create failed for {title!r}: {msg}")
|
| 198 |
-
for d in comments:
|
| 199 |
-
if aborted: break
|
| 200 |
-
target = d.get("target_issue")
|
| 201 |
-
if target is None:
|
| 202 |
-
out(f" ! comment decision has no target_issue; skipping ({d.get('reasoning', '')[:100]})")
|
| 203 |
-
continue
|
| 204 |
-
if target < 0:
|
| 205 |
-
target = placeholder_to_real.get(target)
|
| 206 |
-
if target is None:
|
| 207 |
-
out(f" ! comment decision cross-references an unresolved placeholder; skipping")
|
| 208 |
-
continue
|
| 209 |
-
body = d.get("body") or (
|
| 210 |
-
f"Re-hit during validation of `{dataset}` (profile `{profile}`). "
|
| 211 |
-
f"Same underlying bug as this issue — see agent reasoning: "
|
| 212 |
-
f"{d.get('reasoning', '')}"
|
| 213 |
-
)
|
| 214 |
-
try:
|
| 215 |
-
_add_comment(target, body)
|
| 216 |
-
updated += 1
|
| 217 |
-
out(f" internal-issue #{target}: comment added (agent: comment) — {d.get('reasoning', '')[:120]}")
|
| 218 |
-
except Exception as e:
|
| 219 |
-
msg = f"{type(e).__name__}: {e}"
|
| 220 |
-
if "404" in msg:
|
| 221 |
-
out(f" ! comment tracking aborted (404)")
|
| 222 |
-
aborted = True
|
| 223 |
-
else:
|
| 224 |
-
out(f" ! comment failed for #{target}: {msg}")
|
| 225 |
-
for d in skips:
|
| 226 |
-
skipped += 1
|
| 227 |
-
out(f" internal-issue {d.get('rule')}/{d.get('code')}: skipped — {d.get('reasoning', '')[:160]}")
|
| 228 |
-
return {"created": created, "updated": updated, "skipped": skipped,
|
| 229 |
-
"aborted_404": aborted}
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
def _ensure_internal_issues_simple(by_pair: dict, dataset: str, profile: str,
|
| 233 |
-
total: int, log_fn) -> dict:
|
| 234 |
-
"""Fallback (no agentic review): one issue per dataset, dedup by
|
| 235 |
-
exact title match. This is what we used before the agent was
|
| 236 |
-
wired up; kept as a backstop for when ANTHROPIC_API_KEY is unset,
|
| 237 |
-
the SDK is missing, or the Claude call fails."""
|
| 238 |
-
out = log_fn
|
| 239 |
-
title = f"[validator-internal] {dataset}"
|
| 240 |
-
try:
|
| 241 |
-
existing = _find_issue(title)
|
| 242 |
-
if existing:
|
| 243 |
-
_add_comment(existing["number"],
|
| 244 |
-
_build_dataset_recurrence_comment(by_pair, dataset, profile, total))
|
| 245 |
-
out(f" internal-issue #{existing['number']}: comment added for dataset "
|
| 246 |
-
f"{dataset} ({total} occurrences, {len(by_pair)} pairs) — fallback policy")
|
| 247 |
-
return {"created": 0, "updated": 1, "pairs": len(by_pair), "total": total,
|
| 248 |
-
"fallback": True}
|
| 249 |
-
num = _create_issue(title,
|
| 250 |
-
_build_dataset_issue_body(by_pair, dataset, profile, total),
|
| 251 |
-
["validator-internal", "process"])
|
| 252 |
-
out(f" internal-issue #{num}: opened for dataset {dataset} "
|
| 253 |
-
f"({total} occurrences, {len(by_pair)} pairs) — fallback policy")
|
| 254 |
-
return {"created": 1, "updated": 0, "pairs": len(by_pair), "total": total,
|
| 255 |
-
"fallback": True}
|
| 256 |
-
except Exception as e:
|
| 257 |
-
msg = f"{type(e).__name__}: {e}"
|
| 258 |
-
if "404" in msg:
|
| 259 |
-
out(f" ! internal-issue tracking aborted (404 — token lacks issues:write on {GH_REPO})")
|
| 260 |
-
return {"created": 0, "updated": 0, "aborted_404": True, "fallback": True}
|
| 261 |
-
out(f" ! internal-issue tracking for dataset {dataset} failed: {msg}")
|
| 262 |
-
return {"created": 0, "updated": 0, "error": msg, "fallback": True}
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
def ensure_internal_issues(results_json: dict, dataset: str, profile: str,
|
| 266 |
-
log_fn=None) -> dict:
|
| 267 |
-
"""Scan results.json for validator-internal bugs and route them to
|
| 268 |
-
GitHub issues via an agentic review pass (Claude) that classifies,
|
| 269 |
-
dedupes against existing issues, and writes plain-language
|
| 270 |
-
explanations. Falls back to a simple one-issue-per-dataset policy
|
| 271 |
-
if the agent is unavailable.
|
| 272 |
-
|
| 273 |
-
Best-effort — swallowed exceptions return {"error": ...} so the
|
| 274 |
-
validator's verdict is never blocked on GitHub being flaky."""
|
| 275 |
-
out = log_fn or (lambda s: print(s, flush=True))
|
| 276 |
-
if not _gh_token():
|
| 277 |
-
out(" (skipping internal-issue tracking: no GH token)")
|
| 278 |
-
return {"skipped": True, "reason": "no_token"}
|
| 279 |
-
|
| 280 |
-
# Group across the whole dataset: (rule, code) → {count, sample, severity}
|
| 281 |
-
by_pair: dict[tuple[str, str], dict[str, Any]] = {}
|
| 282 |
-
total = 0
|
| 283 |
-
for asset in results_json.get("results", []):
|
| 284 |
-
for iss in (asset.get("issues") or []):
|
| 285 |
-
if not is_validator_internal_issue(iss):
|
| 286 |
-
continue
|
| 287 |
-
rule = iss.get("rule") or "?"
|
| 288 |
-
code = iss.get("code") or "UNKNOWN"
|
| 289 |
-
key = (rule, code)
|
| 290 |
-
g = by_pair.setdefault(key, {
|
| 291 |
-
"count": 0,
|
| 292 |
-
"sample_msg": scrub_secrets((iss.get("msg") or "")[:200]),
|
| 293 |
-
"severity": (iss.get("severity") or "").lower(),
|
| 294 |
-
})
|
| 295 |
-
g["count"] += 1
|
| 296 |
-
total += 1
|
| 297 |
-
|
| 298 |
-
if not by_pair:
|
| 299 |
-
return {"created": 0, "updated": 0}
|
| 300 |
-
|
| 301 |
-
# Agentic path: Claude classifies + writes the issue body.
|
| 302 |
-
try:
|
| 303 |
-
from agentic_issues import is_available as _agent_available, review_and_decide
|
| 304 |
-
except Exception as e:
|
| 305 |
-
out(f" (agentic_issues import failed: {type(e).__name__}: {str(e)[:120]}); "
|
| 306 |
-
f"using fallback policy")
|
| 307 |
-
return _ensure_internal_issues_simple(by_pair, dataset, profile, total, out)
|
| 308 |
-
|
| 309 |
-
if not _agent_available():
|
| 310 |
-
out(" (agentic review unavailable; using fallback policy)")
|
| 311 |
-
return _ensure_internal_issues_simple(by_pair, dataset, profile, total, out)
|
| 312 |
-
|
| 313 |
-
existing = _list_existing_internal_issues()
|
| 314 |
-
out(f" agentic review: {len(by_pair)} group(s) vs {len(existing)} existing "
|
| 315 |
-
f"validator-internal issue(s)")
|
| 316 |
-
review = review_and_decide(by_pair, dataset, profile, total, existing, log_fn=out)
|
| 317 |
-
if review is None or not review.get("decisions"):
|
| 318 |
-
return _ensure_internal_issues_simple(by_pair, dataset, profile, total, out)
|
| 319 |
-
|
| 320 |
-
if review.get("summary"):
|
| 321 |
-
out(f" agent summary: {review['summary'][:300]}")
|
| 322 |
-
result = _execute_decisions(review["decisions"], by_pair, dataset, profile, total, out)
|
| 323 |
-
result.update({"pairs": len(by_pair), "total": total, "agentic": True})
|
| 324 |
-
return result
|
|
|
|
| 1 |
+
"""Auto-file GitHub issues for validator-internal bugs.
|
| 2 |
+
|
| 3 |
+
Mirror of `tools/hf_watch/validate.py::_ensure_validator_internal_issues`
|
| 4 |
+
moved to the HF Space side so the Space can self-report tooling
|
| 5 |
+
failures without depending on the GH Actions wrapper to do it.
|
| 6 |
+
|
| 7 |
+
Policy reminder (from CLAUDE.md / project conventions):
|
| 8 |
+
- GitHub Issues track NVIDIA-internal *tooling* problems only.
|
| 9 |
+
- Customer-asset findings (real spec violations) stay on the dashboard;
|
| 10 |
+
they do NOT become issues.
|
| 11 |
+
- Distinguishing the two is `is_validator_internal_issue`'s job.
|
| 12 |
+
|
| 13 |
+
Token: GH_VALIDATOR_TOKEN (or GITHUB_TOKEN) — set as a Space secret.
|
| 14 |
+
A fine-grained PAT with `issues: read+write` on
|
| 15 |
+
`NVIDIA-dev/simready-oem-library-pm` is enough; no other scope needed.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import json
|
| 20 |
+
import os
|
| 21 |
+
import re
|
| 22 |
+
import urllib.parse
|
| 23 |
+
import urllib.request
|
| 24 |
+
from typing import Any
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
GH_REPO = "NVIDIA-dev/simready-oem-library-pm"
|
| 28 |
+
|
| 29 |
+
# Redact token-shaped strings before they land in a (public-ish) issue.
|
| 30 |
+
# Covers GitHub PATs (gh[ps]_…), HF tokens (hf_…), and long base64-ish
|
| 31 |
+
# secrets. Validator log text passes through here on its way into bodies.
|
| 32 |
+
_SECRET_RE = re.compile(r"gh[ps]_\w+|hf_\w+|[A-Za-z0-9+/]{40,}={0,2}")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def scrub_secrets(text: str) -> str:
|
| 36 |
+
return _SECRET_RE.sub("[REDACTED]", text or "")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _gh_token() -> str | None:
|
| 40 |
+
return os.environ.get("GH_VALIDATOR_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def is_validator_internal_issue(iss: dict) -> bool:
|
| 44 |
+
"""Heuristic: distinguish validator-package crashes from real
|
| 45 |
+
asset findings."""
|
| 46 |
+
code = iss.get("code") or ""
|
| 47 |
+
msg = iss.get("msg") or ""
|
| 48 |
+
if code == "UNKNOWN" or code.startswith("SDK."):
|
| 49 |
+
return True
|
| 50 |
+
if "Uncaught error" in msg:
|
| 51 |
+
return True
|
| 52 |
+
if "is not registered to requirement" in msg:
|
| 53 |
+
return True
|
| 54 |
+
return False
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _gh_request(method: str, path: str, payload: dict | None = None) -> dict | list:
|
| 58 |
+
token = _gh_token()
|
| 59 |
+
if not token:
|
| 60 |
+
raise RuntimeError("no GitHub token in env (GH_VALIDATOR_TOKEN or GITHUB_TOKEN)")
|
| 61 |
+
url = f"https://api.github.com/repos/{GH_REPO}{path}"
|
| 62 |
+
headers = {
|
| 63 |
+
"Accept": "application/vnd.github+json",
|
| 64 |
+
"Authorization": f"Bearer {token}",
|
| 65 |
+
"X-GitHub-Api-Version": "2022-11-28",
|
| 66 |
+
"User-Agent": "simready-validator-space/0.1",
|
| 67 |
+
}
|
| 68 |
+
body = None
|
| 69 |
+
if payload is not None:
|
| 70 |
+
body = json.dumps(payload).encode("utf-8")
|
| 71 |
+
headers["Content-Type"] = "application/json"
|
| 72 |
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
| 73 |
+
with urllib.request.urlopen(req, timeout=30) as r:
|
| 74 |
+
return json.loads(r.read() or "null")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _find_issue(title: str) -> dict | None:
|
| 78 |
+
# Strip `"` — title is untrusted and a quote would break the
|
| 79 |
+
# quoted-phrase search query (and could inject extra qualifiers).
|
| 80 |
+
safe_title = title.replace('"', "")
|
| 81 |
+
q = urllib.parse.quote(f'repo:{GH_REPO} in:title "{safe_title}" is:issue')
|
| 82 |
+
result = _gh_request("GET", f"/../../search/issues?q={q}")
|
| 83 |
+
items = (result or {}).get("items") or []
|
| 84 |
+
for it in items:
|
| 85 |
+
if it.get("title") == title:
|
| 86 |
+
return it
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _create_issue(title: str, body: str, labels: list[str]) -> int:
|
| 91 |
+
result = _gh_request("POST", "/issues",
|
| 92 |
+
{"title": title, "body": body, "labels": labels})
|
| 93 |
+
return result.get("number", 0)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _add_comment(issue_num: int, body: str) -> None:
|
| 97 |
+
_gh_request("POST", f"/issues/{issue_num}/comments", {"body": body})
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _build_dataset_issue_body(by_pair: dict, dataset: str, profile: str,
|
| 101 |
+
total: int) -> str:
|
| 102 |
+
rows = "\n".join(
|
| 103 |
+
f"| `{rule}` | `{code}` | {g['severity'] or '?'} | {g['count']} | `{g['sample_msg']}` |"
|
| 104 |
+
for (rule, code), g in sorted(by_pair.items(), key=lambda kv: -kv[1]["count"])
|
| 105 |
+
)
|
| 106 |
+
return (
|
| 107 |
+
f"**Validator-internal bugs on a single dataset** — surfaced during "
|
| 108 |
+
f"automatic SimReady validation. NOT a customer-asset finding; the "
|
| 109 |
+
f"validator's own rule registration / spec loading is misbehaving "
|
| 110 |
+
f"on this dataset and emitting errors that don't map to any real "
|
| 111 |
+
f"spec violation.\n\n"
|
| 112 |
+
f"| Field | Value |\n|---|---|\n"
|
| 113 |
+
f"| Dataset | `{dataset}` |\n"
|
| 114 |
+
f"| Profile (first run) | `{profile}` |\n"
|
| 115 |
+
f"| Total internal occurrences (first run) | {total} |\n"
|
| 116 |
+
f"| Distinct (rule, code) pairs (first run) | {len(by_pair)} |\n\n"
|
| 117 |
+
f"**Breakdown** (sorted by occurrence count, descending):\n\n"
|
| 118 |
+
f"| Rule | Code | Severity | Count | Sample message |\n"
|
| 119 |
+
f"|---|---|---|---|---|\n{rows}\n\n"
|
| 120 |
+
f"---\n"
|
| 121 |
+
f"_Filed automatically by the HF Space (`tools/hf_space/github_issues.py`). "
|
| 122 |
+
f"One issue per dataset — re-validating the same dataset comments "
|
| 123 |
+
f"here with the new counts instead of opening a duplicate._"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _build_dataset_recurrence_comment(by_pair: dict, dataset: str, profile: str,
|
| 128 |
+
total: int) -> str:
|
| 129 |
+
rows = "\n".join(
|
| 130 |
+
f"| `{rule}` | `{code}` | {g['count']} |"
|
| 131 |
+
for (rule, code), g in sorted(by_pair.items(), key=lambda kv: -kv[1]["count"])
|
| 132 |
+
)
|
| 133 |
+
return (
|
| 134 |
+
f"Re-hit during validation of `{dataset}` (profile `{profile}`).\n"
|
| 135 |
+
f"This run: **{total}** internal occurrences across **{len(by_pair)}** "
|
| 136 |
+
f"distinct (rule, code) pairs.\n\n"
|
| 137 |
+
f"| Rule | Code | Count this run |\n|---|---|---|\n{rows}"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _list_existing_internal_issues() -> list[dict]:
|
| 142 |
+
"""Pull every validator-internal issue (open + recently closed) so
|
| 143 |
+
the agent can dedupe semantically against them. Closed issues are
|
| 144 |
+
included because the same bug can come back after a fix is
|
| 145 |
+
reverted or after a regression."""
|
| 146 |
+
q = urllib.parse.quote(
|
| 147 |
+
f'repo:{GH_REPO} label:"validator-internal" is:issue')
|
| 148 |
+
try:
|
| 149 |
+
result = _gh_request("GET", f"/../../search/issues?q={q}&per_page=100")
|
| 150 |
+
except Exception:
|
| 151 |
+
return []
|
| 152 |
+
return (result or {}).get("items") or []
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _execute_decisions(decisions: list[dict], by_pair: dict, dataset: str,
|
| 156 |
+
profile: str, total: int, log_fn) -> dict:
|
| 157 |
+
"""Carry out the agent's decisions. Returns counters."""
|
| 158 |
+
out = log_fn
|
| 159 |
+
created = updated = skipped = 0
|
| 160 |
+
aborted = False
|
| 161 |
+
# Map negative placeholder numbers → real issue numbers as `create`
|
| 162 |
+
# decisions resolve. The agent uses negatives to cross-reference
|
| 163 |
+
# decisions that share a parent issue when several groups roll up
|
| 164 |
+
# into one new bug.
|
| 165 |
+
placeholder_to_real: dict[int, int] = {}
|
| 166 |
+
# Two passes: creates first (so their numbers are known), then
|
| 167 |
+
# comments (so cross-references resolve). Skips are free.
|
| 168 |
+
creates = [d for d in decisions if d.get("action") == "create"]
|
| 169 |
+
comments = [d for d in decisions if d.get("action") == "comment"]
|
| 170 |
+
skips = [d for d in decisions if d.get("action") == "skip"]
|
| 171 |
+
for d in creates:
|
| 172 |
+
if aborted: break
|
| 173 |
+
title = d.get("title") or f"[validator-internal] {dataset} :: {d.get('rule')} / {d.get('code')}"
|
| 174 |
+
body = d.get("body") or "(agent did not provide a body)"
|
| 175 |
+
try:
|
| 176 |
+
existing = _find_issue(title)
|
| 177 |
+
if existing:
|
| 178 |
+
_add_comment(existing["number"],
|
| 179 |
+
f"_Re-hit during validation of `{dataset}` "
|
| 180 |
+
f"(profile `{profile}`)._\n\n{body}")
|
| 181 |
+
updated += 1
|
| 182 |
+
out(f" internal-issue #{existing['number']}: comment added "
|
| 183 |
+
f"(agent: create→existing match by title)")
|
| 184 |
+
placeholder_to_real.setdefault(-(creates.index(d) + 1), existing["number"])
|
| 185 |
+
else:
|
| 186 |
+
num = _create_issue(title, body, ["validator-internal", "process", "agent-reviewed"])
|
| 187 |
+
created += 1
|
| 188 |
+
out(f" internal-issue #{num}: opened ({title!r}) — agent reasoning: "
|
| 189 |
+
f"{d.get('reasoning', '')[:160]}")
|
| 190 |
+
placeholder_to_real[-(creates.index(d) + 1)] = num
|
| 191 |
+
except Exception as e:
|
| 192 |
+
msg = f"{type(e).__name__}: {e}"
|
| 193 |
+
if "404" in msg:
|
| 194 |
+
out(f" ! internal-issue tracking aborted (404 — token lacks issues:write on {GH_REPO})")
|
| 195 |
+
aborted = True
|
| 196 |
+
else:
|
| 197 |
+
out(f" ! create failed for {title!r}: {msg}")
|
| 198 |
+
for d in comments:
|
| 199 |
+
if aborted: break
|
| 200 |
+
target = d.get("target_issue")
|
| 201 |
+
if target is None:
|
| 202 |
+
out(f" ! comment decision has no target_issue; skipping ({d.get('reasoning', '')[:100]})")
|
| 203 |
+
continue
|
| 204 |
+
if target < 0:
|
| 205 |
+
target = placeholder_to_real.get(target)
|
| 206 |
+
if target is None:
|
| 207 |
+
out(f" ! comment decision cross-references an unresolved placeholder; skipping")
|
| 208 |
+
continue
|
| 209 |
+
body = d.get("body") or (
|
| 210 |
+
f"Re-hit during validation of `{dataset}` (profile `{profile}`). "
|
| 211 |
+
f"Same underlying bug as this issue — see agent reasoning: "
|
| 212 |
+
f"{d.get('reasoning', '')}"
|
| 213 |
+
)
|
| 214 |
+
try:
|
| 215 |
+
_add_comment(target, body)
|
| 216 |
+
updated += 1
|
| 217 |
+
out(f" internal-issue #{target}: comment added (agent: comment) — {d.get('reasoning', '')[:120]}")
|
| 218 |
+
except Exception as e:
|
| 219 |
+
msg = f"{type(e).__name__}: {e}"
|
| 220 |
+
if "404" in msg:
|
| 221 |
+
out(f" ! comment tracking aborted (404)")
|
| 222 |
+
aborted = True
|
| 223 |
+
else:
|
| 224 |
+
out(f" ! comment failed for #{target}: {msg}")
|
| 225 |
+
for d in skips:
|
| 226 |
+
skipped += 1
|
| 227 |
+
out(f" internal-issue {d.get('rule')}/{d.get('code')}: skipped — {d.get('reasoning', '')[:160]}")
|
| 228 |
+
return {"created": created, "updated": updated, "skipped": skipped,
|
| 229 |
+
"aborted_404": aborted}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _ensure_internal_issues_simple(by_pair: dict, dataset: str, profile: str,
|
| 233 |
+
total: int, log_fn) -> dict:
|
| 234 |
+
"""Fallback (no agentic review): one issue per dataset, dedup by
|
| 235 |
+
exact title match. This is what we used before the agent was
|
| 236 |
+
wired up; kept as a backstop for when ANTHROPIC_API_KEY is unset,
|
| 237 |
+
the SDK is missing, or the Claude call fails."""
|
| 238 |
+
out = log_fn
|
| 239 |
+
title = f"[validator-internal] {dataset}"
|
| 240 |
+
try:
|
| 241 |
+
existing = _find_issue(title)
|
| 242 |
+
if existing:
|
| 243 |
+
_add_comment(existing["number"],
|
| 244 |
+
_build_dataset_recurrence_comment(by_pair, dataset, profile, total))
|
| 245 |
+
out(f" internal-issue #{existing['number']}: comment added for dataset "
|
| 246 |
+
f"{dataset} ({total} occurrences, {len(by_pair)} pairs) — fallback policy")
|
| 247 |
+
return {"created": 0, "updated": 1, "pairs": len(by_pair), "total": total,
|
| 248 |
+
"fallback": True}
|
| 249 |
+
num = _create_issue(title,
|
| 250 |
+
_build_dataset_issue_body(by_pair, dataset, profile, total),
|
| 251 |
+
["validator-internal", "process"])
|
| 252 |
+
out(f" internal-issue #{num}: opened for dataset {dataset} "
|
| 253 |
+
f"({total} occurrences, {len(by_pair)} pairs) — fallback policy")
|
| 254 |
+
return {"created": 1, "updated": 0, "pairs": len(by_pair), "total": total,
|
| 255 |
+
"fallback": True}
|
| 256 |
+
except Exception as e:
|
| 257 |
+
msg = f"{type(e).__name__}: {e}"
|
| 258 |
+
if "404" in msg:
|
| 259 |
+
out(f" ! internal-issue tracking aborted (404 — token lacks issues:write on {GH_REPO})")
|
| 260 |
+
return {"created": 0, "updated": 0, "aborted_404": True, "fallback": True}
|
| 261 |
+
out(f" ! internal-issue tracking for dataset {dataset} failed: {msg}")
|
| 262 |
+
return {"created": 0, "updated": 0, "error": msg, "fallback": True}
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def ensure_internal_issues(results_json: dict, dataset: str, profile: str,
|
| 266 |
+
log_fn=None) -> dict:
|
| 267 |
+
"""Scan results.json for validator-internal bugs and route them to
|
| 268 |
+
GitHub issues via an agentic review pass (Claude) that classifies,
|
| 269 |
+
dedupes against existing issues, and writes plain-language
|
| 270 |
+
explanations. Falls back to a simple one-issue-per-dataset policy
|
| 271 |
+
if the agent is unavailable.
|
| 272 |
+
|
| 273 |
+
Best-effort — swallowed exceptions return {"error": ...} so the
|
| 274 |
+
validator's verdict is never blocked on GitHub being flaky."""
|
| 275 |
+
out = log_fn or (lambda s: print(s, flush=True))
|
| 276 |
+
if not _gh_token():
|
| 277 |
+
out(" (skipping internal-issue tracking: no GH token)")
|
| 278 |
+
return {"skipped": True, "reason": "no_token"}
|
| 279 |
+
|
| 280 |
+
# Group across the whole dataset: (rule, code) → {count, sample, severity}
|
| 281 |
+
by_pair: dict[tuple[str, str], dict[str, Any]] = {}
|
| 282 |
+
total = 0
|
| 283 |
+
for asset in results_json.get("results", []):
|
| 284 |
+
for iss in (asset.get("issues") or []):
|
| 285 |
+
if not is_validator_internal_issue(iss):
|
| 286 |
+
continue
|
| 287 |
+
rule = iss.get("rule") or "?"
|
| 288 |
+
code = iss.get("code") or "UNKNOWN"
|
| 289 |
+
key = (rule, code)
|
| 290 |
+
g = by_pair.setdefault(key, {
|
| 291 |
+
"count": 0,
|
| 292 |
+
"sample_msg": scrub_secrets((iss.get("msg") or "")[:200]),
|
| 293 |
+
"severity": (iss.get("severity") or "").lower(),
|
| 294 |
+
})
|
| 295 |
+
g["count"] += 1
|
| 296 |
+
total += 1
|
| 297 |
+
|
| 298 |
+
if not by_pair:
|
| 299 |
+
return {"created": 0, "updated": 0}
|
| 300 |
+
|
| 301 |
+
# Agentic path: Claude classifies + writes the issue body.
|
| 302 |
+
try:
|
| 303 |
+
from agentic_issues import is_available as _agent_available, review_and_decide
|
| 304 |
+
except Exception as e:
|
| 305 |
+
out(f" (agentic_issues import failed: {type(e).__name__}: {str(e)[:120]}); "
|
| 306 |
+
f"using fallback policy")
|
| 307 |
+
return _ensure_internal_issues_simple(by_pair, dataset, profile, total, out)
|
| 308 |
+
|
| 309 |
+
if not _agent_available():
|
| 310 |
+
out(" (agentic review unavailable; using fallback policy)")
|
| 311 |
+
return _ensure_internal_issues_simple(by_pair, dataset, profile, total, out)
|
| 312 |
+
|
| 313 |
+
existing = _list_existing_internal_issues()
|
| 314 |
+
out(f" agentic review: {len(by_pair)} group(s) vs {len(existing)} existing "
|
| 315 |
+
f"validator-internal issue(s)")
|
| 316 |
+
review = review_and_decide(by_pair, dataset, profile, total, existing, log_fn=out)
|
| 317 |
+
if review is None or not review.get("decisions"):
|
| 318 |
+
return _ensure_internal_issues_simple(by_pair, dataset, profile, total, out)
|
| 319 |
+
|
| 320 |
+
if review.get("summary"):
|
| 321 |
+
out(f" agent summary: {review['summary'][:300]}")
|
| 322 |
+
result = _execute_decisions(review["decisions"], by_pair, dataset, profile, total, out)
|
| 323 |
+
result.update({"pairs": len(by_pair), "total": total, "agentic": True})
|
| 324 |
+
return result
|
tools/hf_space/plugin.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
{
|
| 2 |
-
"$schema": "https://json.schemastore.org/claude-code-plugin",
|
| 3 |
-
"name": "deploy-hf-space",
|
| 4 |
-
"version": "0.1.0",
|
| 5 |
-
"description": "Deploy the SimReady validator HuggingFace Space (phase-3 of the PRD). Ships one slash command: /deploy-hf-space — clones the Space repo, vendors in the code from this checkout, pushes, and confirms the build went green.",
|
| 6 |
-
"author": {
|
| 7 |
-
"name": "loginowskid",
|
| 8 |
-
"email": "dloginowski@nvidia.com"
|
| 9 |
-
},
|
| 10 |
-
"homepage": "https://github.com/NVIDIA-dev/simready-oem-library-pm"
|
| 11 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json.schemastore.org/claude-code-plugin",
|
| 3 |
+
"name": "deploy-hf-space",
|
| 4 |
+
"version": "0.1.0",
|
| 5 |
+
"description": "Deploy the SimReady validator HuggingFace Space (phase-3 of the PRD). Ships one slash command: /deploy-hf-space — clones the Space repo, vendors in the code from this checkout, pushes, and confirms the build went green.",
|
| 6 |
+
"author": {
|
| 7 |
+
"name": "loginowskid",
|
| 8 |
+
"email": "dloginowski@nvidia.com"
|
| 9 |
+
},
|
| 10 |
+
"homepage": "https://github.com/NVIDIA-dev/simready-oem-library-pm"
|
| 11 |
+
}
|
tools/hf_space/runner.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tools/hf_space/skills/deploy-hf-space/SKILL.md
CHANGED
|
@@ -1,257 +1,257 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: deploy-hf-space
|
| 3 |
-
description: Stand up or update the SimReady validator HuggingFace Space (nvidia/simready-validator or wherever the operator points). Handles the cp-dance the Dockerfile's COPY paths require, the hf CLI launcher gotcha, secret/hardware setup, and post-push build verification. Invoke with /deploy-hf-space [<space-slug>].
|
| 4 |
-
---
|
| 5 |
-
|
| 6 |
-
# /deploy-hf-space
|
| 7 |
-
|
| 8 |
-
Deploy or update the HuggingFace Space that runs the SimReady validator
|
| 9 |
-
on HF-hosted datasets (PRD phase-3). Use this when the operator wants
|
| 10 |
-
the dashboard validator running on HF infrastructure instead of (or in
|
| 11 |
-
addition to) the DGXC self-hosted runner.
|
| 12 |
-
|
| 13 |
-
## When to use
|
| 14 |
-
|
| 15 |
-
- First-time stand-up of a new SimReady-validator Space
|
| 16 |
-
- Pushing updated `tools/hf_space/runner.py`, `tools/hf_space/app.py`,
|
| 17 |
-
or `tools/validation/` after fixing something in the validator engine
|
| 18 |
-
- Diagnosing a green-build-but-broken-runtime situation (the skill's
|
| 19 |
-
verification step pulls the run logs)
|
| 20 |
-
|
| 21 |
-
## When NOT to use
|
| 22 |
-
|
| 23 |
-
- The Space repo lives at `https://huggingface.co/spaces/<owner>/<name>`
|
| 24 |
-
— that's a separate git repo from this monorepo. This skill clones
|
| 25 |
-
it, vendors files in, and pushes. It does **not** make any changes
|
| 26 |
-
to this repo. If you're changing validator code, commit it here
|
| 27 |
-
first; this skill just sync-deploys the snapshot.
|
| 28 |
-
- Customer-facing per-tenant deployments — the current Dockerfile
|
| 29 |
-
uses the Space's own `HF_TOKEN` to open verdict PRs. For
|
| 30 |
-
multi-tenant deployment (one Space per partner), additional work is
|
| 31 |
-
needed (see Open issues below).
|
| 32 |
-
|
| 33 |
-
## Pre-flight
|
| 34 |
-
|
| 35 |
-
The operator needs:
|
| 36 |
-
|
| 37 |
-
1. **`huggingface_hub` Python package installed.** Verify with:
|
| 38 |
-
```
|
| 39 |
-
<py> -c "import huggingface_hub; print(huggingface_hub.__version__)"
|
| 40 |
-
```
|
| 41 |
-
where `<py>` is whichever python actually owns it. On Windows the
|
| 42 |
-
`hf.exe` launcher often points at a non-existent `C:\Python314\`
|
| 43 |
-
path — use the python directly instead of trusting `hf.exe`.
|
| 44 |
-
|
| 45 |
-
2. **HF auth.** The `hf auth whoami` (or `<py> -m huggingface_hub.cli.hf
|
| 46 |
-
auth whoami`) should return the user + orgs. If not, log in:
|
| 47 |
-
```
|
| 48 |
-
<py> -m huggingface_hub.cli.hf auth login
|
| 49 |
-
```
|
| 50 |
-
Paste a **write-scoped** PAT from https://huggingface.co/settings/tokens
|
| 51 |
-
at the interactive prompt. **Never echo a PAT into the chat input
|
| 52 |
-
itself** — that puts it in the transcript. If the operator does, tell
|
| 53 |
-
them to immediately rotate it.
|
| 54 |
-
|
| 55 |
-
3. **A Space already exists at the target slug.** This skill expects
|
| 56 |
-
the Space to be created via the HF UI first:
|
| 57 |
-
- https://huggingface.co/new-space
|
| 58 |
-
- **SDK**: Docker (required — the validator's deps don't fit
|
| 59 |
-
Gradio's default Python SDK)
|
| 60 |
-
- **Hardware**: `cpu-basic` (free) is enough for the spike; the
|
| 61 |
-
validator runs without Kit and the work is CPU-bound USD parsing
|
| 62 |
-
- **Visibility**: Private while internal-pilot
|
| 63 |
-
|
| 64 |
-
4. **The `HF_TOKEN` secret is set on the Space.** Settings → Variables
|
| 65 |
-
and secrets → New secret. Required for `Open PR` mode to commit
|
| 66 |
-
verdicts back to customer datasets.
|
| 67 |
-
|
| 68 |
-
If pre-flight 3 or 4 isn't done, ask the operator to do them in the
|
| 69 |
-
browser, then resume. Don't attempt to create the Space via the API —
|
| 70 |
-
the manual flow is what the team is set up for.
|
| 71 |
-
|
| 72 |
-
## Steps for Claude when `/deploy-hf-space [<slug>]` is invoked
|
| 73 |
-
|
| 74 |
-
The skill operates on the slug `<owner>/<name>` (e.g.
|
| 75 |
-
`nvidia/simready-validator`). If the operator omits the slug,
|
| 76 |
-
default to `nvidia/simready-validator` for the current spike.
|
| 77 |
-
|
| 78 |
-
1. **Verify the Space exists and runtime is queryable.**
|
| 79 |
-
```
|
| 80 |
-
<py> -c "
|
| 81 |
-
from huggingface_hub import HfApi
|
| 82 |
-
api = HfApi()
|
| 83 |
-
info = api.space_info('<slug>')
|
| 84 |
-
print(info.runtime, info.sdk)
|
| 85 |
-
"
|
| 86 |
-
```
|
| 87 |
-
`sdk` must be `docker`. If it's `gradio` or `static`, refuse to
|
| 88 |
-
continue and tell the operator to recreate as Docker.
|
| 89 |
-
|
| 90 |
-
2. **Stage the Space repo at `/tmp/space-stage` (or equivalent).**
|
| 91 |
-
```
|
| 92 |
-
git clone https://huggingface.co/spaces/<slug> /tmp/space-stage
|
| 93 |
-
```
|
| 94 |
-
The HF git-credential helper that `hf auth login --add-to-git-credential`
|
| 95 |
-
sets up handles auth. If clone prompts for a password, the operator's
|
| 96 |
-
git creds aren't wired — back to pre-flight.
|
| 97 |
-
|
| 98 |
-
3. **Vendor in the deployable files.** The Dockerfile inside
|
| 99 |
-
`tools/hf_space/Dockerfile` COPYs from these paths in the build
|
| 100 |
-
context, so the Space repo root needs them mirrored:
|
| 101 |
-
|
| 102 |
-
- `tools/hf_space/Dockerfile` → Space `Dockerfile` (root)
|
| 103 |
-
- `tools/hf_space/` → Space `tools/hf_space/`
|
| 104 |
-
- `tools/validation/` → Space `tools/validation/`
|
| 105 |
-
|
| 106 |
-
In Bash:
|
| 107 |
-
```
|
| 108 |
-
cp tools/hf_space/Dockerfile /tmp/space-stage/Dockerfile
|
| 109 |
-
mkdir -p /tmp/space-stage/tools
|
| 110 |
-
cp -r tools/hf_space /tmp/space-stage/tools/
|
| 111 |
-
cp -r tools/validation /tmp/space-stage/tools/
|
| 112 |
-
```
|
| 113 |
-
|
| 114 |
-
**DO NOT** drop a wholesale copy of this monorepo into the Space —
|
| 115 |
-
it'd include unrelated subtrees (`docs/`, `goals/`, `clients.yaml`)
|
| 116 |
-
that don't belong in a Space and may contain secrets-shaped content.
|
| 117 |
-
Keep the COPY surface minimal.
|
| 118 |
-
|
| 119 |
-
4. **Preserve the HF-side README frontmatter.** HF auto-generates a
|
| 120 |
-
`README.md` with the Space's title/emoji/sdk YAML frontmatter on
|
| 121 |
-
creation. **Keep that frontmatter intact** — rewriting the README
|
| 122 |
-
body is fine, but blowing away the frontmatter de-registers the
|
| 123 |
-
Space card. The shipped README in `tools/hf_space/README.md` is the
|
| 124 |
-
internal-docs version, NOT what should land at the Space root.
|
| 125 |
-
Build a fresh `/tmp/space-stage/README.md` that keeps HF's
|
| 126 |
-
frontmatter and adds a short body pointing back at this repo as the
|
| 127 |
-
source of truth.
|
| 128 |
-
|
| 129 |
-
5. **Commit + push.**
|
| 130 |
-
```
|
| 131 |
-
cd /tmp/space-stage
|
| 132 |
-
git add -A
|
| 133 |
-
git -c user.email=<op-email> -c user.name=<op-name> commit -m \\
|
| 134 |
-
"Update from <sha> in simready-oem-library-pm"
|
| 135 |
-
git push
|
| 136 |
-
```
|
| 137 |
-
Use the **operator's** identity, not a bot — Spaces are usually
|
| 138 |
-
personal accounts in this team. The commit message should reference
|
| 139 |
-
the source-repo sha so the Space's history maps back.
|
| 140 |
-
|
| 141 |
-
6. **Verify the build.**
|
| 142 |
-
```
|
| 143 |
-
<py> -c "
|
| 144 |
-
from huggingface_hub import HfApi
|
| 145 |
-
info = HfApi().space_info('<slug>')
|
| 146 |
-
print(f'stage: {info.runtime.stage}')
|
| 147 |
-
print(f'sha: {info.sha}')
|
| 148 |
-
"
|
| 149 |
-
```
|
| 150 |
-
Stages of interest:
|
| 151 |
-
- `BUILDING` / `RUNNING_BUILDING` — wait. First build ~5 min;
|
| 152 |
-
incremental builds (when only `runner.py` / `app.py` changed,
|
| 153 |
-
not `requirements.txt` or `Dockerfile`) finish in ~1 min thanks
|
| 154 |
-
to layer cache.
|
| 155 |
-
- `RUNNING` — done. Open `https://huggingface.co/spaces/<slug>`
|
| 156 |
-
in the browser to interact.
|
| 157 |
-
- `BUILD_ERROR` / `RUNTIME_ERROR` — pull the build/run logs (see
|
| 158 |
-
step 7) and report the failing lines to the operator.
|
| 159 |
-
|
| 160 |
-
7. **Pull logs if needed.** The HF API exposes the logs at:
|
| 161 |
-
```
|
| 162 |
-
GET https://huggingface.co/api/spaces/<slug>/logs/{build,run}
|
| 163 |
-
```
|
| 164 |
-
This is a Server-Sent Events stream — not a static endpoint. With
|
| 165 |
-
the SDK >= 1.16 use `api.fetch_logs(...)` if available; otherwise
|
| 166 |
-
open the URL via `urllib.request` with an `Authorization: Bearer
|
| 167 |
-
<token>` header and read with a short idle timeout (the stream
|
| 168 |
-
blocks indefinitely once it catches up to head).
|
| 169 |
-
|
| 170 |
-
For a one-shot dump, this Python snippet works on Windows:
|
| 171 |
-
```python
|
| 172 |
-
import http.client, socket, json
|
| 173 |
-
from huggingface_hub import get_token
|
| 174 |
-
conn = http.client.HTTPSConnection("huggingface.co", timeout=15)
|
| 175 |
-
conn.request("GET", "/api/spaces/<slug>/logs/run",
|
| 176 |
-
headers={"Authorization": f"Bearer {get_token()}"})
|
| 177 |
-
resp = conn.getresponse()
|
| 178 |
-
buf = b""
|
| 179 |
-
try:
|
| 180 |
-
while True:
|
| 181 |
-
chunk = resp.read(2048)
|
| 182 |
-
if not chunk: break
|
| 183 |
-
buf += chunk
|
| 184 |
-
except (socket.timeout, TimeoutError):
|
| 185 |
-
pass
|
| 186 |
-
# parse SSE: each `data: {...}` line carries one log line.
|
| 187 |
-
```
|
| 188 |
-
|
| 189 |
-
## Verification of the deploy
|
| 190 |
-
|
| 191 |
-
After the push lands and runtime flips to `RUNNING`:
|
| 192 |
-
|
| 193 |
-
1. Open `https://huggingface.co/spaces/<slug>` in a browser.
|
| 194 |
-
2. Enter a USD-native (not zip-bundled) dataset in the **Dataset** box.
|
| 195 |
-
Good smoke-test candidates:
|
| 196 |
-
- `nvidia/PhysicalAI-SimReady-Warehouse-01`
|
| 197 |
-
- Anything from the `nvidia/PhysicalAI-SimReady-*` family that ships
|
| 198 |
-
`.usd` files directly (not in archives)
|
| 199 |
-
3. Leave **Profile = Robot-Body-Runnable**, **Version = 1.0.0**,
|
| 200 |
-
**Open PR unchecked** for the first run.
|
| 201 |
-
4. Click **Validate**. Watch the **Log** box. Expected progression:
|
| 202 |
-
```
|
| 203 |
-
[...] validating dataset=... profile=Robot-Body-Runnable v1.0.0
|
| 204 |
-
workdir: /tmp/hfsp-<safe-name>-XXXX
|
| 205 |
-
$ snapshot_download <repo> ...
|
| 206 |
-
validator target: /tmp/hfsp-<safe-name>-XXXX/raw
|
| 207 |
-
$ python /home/appuser/app/tools/validation/.../validate.py ...
|
| 208 |
-
[validator spec loading — ~243 lines of "Registering feature ..."]
|
| 209 |
-
Discovered N USD asset(s) under ...
|
| 210 |
-
layout: M asset(s) deviate from SimReady packaging spec ... (if any)
|
| 211 |
-
[per-asset "validate: ..." lines]
|
| 212 |
-
PASS: K/N assets passed
|
| 213 |
-
```
|
| 214 |
-
5. If the verdict matches DGXC's verdict on the same dataset
|
| 215 |
-
(byte-for-byte on `results.json`), the cutover criterion in
|
| 216 |
-
`tools/hf_space/README.md` is met for that dataset.
|
| 217 |
-
|
| 218 |
-
## Known limitations
|
| 219 |
-
|
| 220 |
-
These are real constraints that bit the spike — flag them to the
|
| 221 |
-
operator if the dataset they're trying to validate hits one:
|
| 222 |
-
|
| 223 |
-
- **Zip-bundled datasets fail with "no USD assets".** `HF_DOWNLOAD_EXCLUDES`
|
| 224 |
-
in `runner.py` skips `.zip`, and even if we kept them, 800 × 600 MB
|
| 225 |
-
doesn't fit on `cpu-basic`'s ephemeral disk (~50 GiB). The
|
| 226 |
-
imagineio Kitchens-v1 dataset is the canonical example. Fix is to
|
| 227 |
-
add zip-streaming validation (download one zip, extract, validate,
|
| 228 |
-
delete, next) — a separate workstream.
|
| 229 |
-
- **PhysX-bearing profiles are partially validated.** `--no-use-kit`
|
| 230 |
-
is hard-coded because Spaces don't have Isaac Sim. The validator's
|
| 231 |
-
P2 patch drops the resulting `physxschema_unavailable` /
|
| 232 |
-
`omnipbr_unresolved` issues so the verdict isn't artificially
|
| 233 |
-
failing, but PhysX/MDL rules that need Kit are silently not
|
| 234 |
-
exercised. To restore them, bake an `a10g-small` tier with Isaac
|
| 235 |
-
Sim wheels in the Dockerfile — different workstream.
|
| 236 |
-
- **One Space, one HF_TOKEN.** Every customer's verdict PR is opened
|
| 237 |
-
by the Space's own token. Fine for internal pilot, not safe for
|
| 238 |
-
customer-facing deployment where customers shouldn't see each
|
| 239 |
-
other's commit identities. Refactor `runner.run()` to accept a
|
| 240 |
-
caller-provided token before exposing the Space externally.
|
| 241 |
-
- **Validator subprocess is a single Claude-less Python invocation.**
|
| 242 |
-
The DGXC path runs the validator inside a Claude Code subprocess
|
| 243 |
-
(`tools/hf_watch/validate.py` does this) so the simready-report
|
| 244 |
-
skill's agentic decisions (profile selection, error recovery) are
|
| 245 |
-
exercised. The Space bypasses Claude entirely — calls
|
| 246 |
-
`validate.py` directly. If/when we want the agentic decisions on
|
| 247 |
-
HF too, bundle Claude Code into the Dockerfile + add an
|
| 248 |
-
`ANTHROPIC_API_KEY` Space secret.
|
| 249 |
-
|
| 250 |
-
## Session memory hooks
|
| 251 |
-
|
| 252 |
-
This skill is the source of truth for the deployment workflow. If the
|
| 253 |
-
operator hits a step that doesn't match what's documented here (e.g.
|
| 254 |
-
HF changes their Space API, hf CLI launcher works again, a new
|
| 255 |
-
hardware tier appears), update **this file** rather than just patching
|
| 256 |
-
the conversation. The next session that invokes `/deploy-hf-space`
|
| 257 |
-
reads this file, not the old transcript.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: deploy-hf-space
|
| 3 |
+
description: Stand up or update the SimReady validator HuggingFace Space (nvidia/simready-validator or wherever the operator points). Handles the cp-dance the Dockerfile's COPY paths require, the hf CLI launcher gotcha, secret/hardware setup, and post-push build verification. Invoke with /deploy-hf-space [<space-slug>].
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# /deploy-hf-space
|
| 7 |
+
|
| 8 |
+
Deploy or update the HuggingFace Space that runs the SimReady validator
|
| 9 |
+
on HF-hosted datasets (PRD phase-3). Use this when the operator wants
|
| 10 |
+
the dashboard validator running on HF infrastructure instead of (or in
|
| 11 |
+
addition to) the DGXC self-hosted runner.
|
| 12 |
+
|
| 13 |
+
## When to use
|
| 14 |
+
|
| 15 |
+
- First-time stand-up of a new SimReady-validator Space
|
| 16 |
+
- Pushing updated `tools/hf_space/runner.py`, `tools/hf_space/app.py`,
|
| 17 |
+
or `tools/validation/` after fixing something in the validator engine
|
| 18 |
+
- Diagnosing a green-build-but-broken-runtime situation (the skill's
|
| 19 |
+
verification step pulls the run logs)
|
| 20 |
+
|
| 21 |
+
## When NOT to use
|
| 22 |
+
|
| 23 |
+
- The Space repo lives at `https://huggingface.co/spaces/<owner>/<name>`
|
| 24 |
+
— that's a separate git repo from this monorepo. This skill clones
|
| 25 |
+
it, vendors files in, and pushes. It does **not** make any changes
|
| 26 |
+
to this repo. If you're changing validator code, commit it here
|
| 27 |
+
first; this skill just sync-deploys the snapshot.
|
| 28 |
+
- Customer-facing per-tenant deployments — the current Dockerfile
|
| 29 |
+
uses the Space's own `HF_TOKEN` to open verdict PRs. For
|
| 30 |
+
multi-tenant deployment (one Space per partner), additional work is
|
| 31 |
+
needed (see Open issues below).
|
| 32 |
+
|
| 33 |
+
## Pre-flight
|
| 34 |
+
|
| 35 |
+
The operator needs:
|
| 36 |
+
|
| 37 |
+
1. **`huggingface_hub` Python package installed.** Verify with:
|
| 38 |
+
```
|
| 39 |
+
<py> -c "import huggingface_hub; print(huggingface_hub.__version__)"
|
| 40 |
+
```
|
| 41 |
+
where `<py>` is whichever python actually owns it. On Windows the
|
| 42 |
+
`hf.exe` launcher often points at a non-existent `C:\Python314\`
|
| 43 |
+
path — use the python directly instead of trusting `hf.exe`.
|
| 44 |
+
|
| 45 |
+
2. **HF auth.** The `hf auth whoami` (or `<py> -m huggingface_hub.cli.hf
|
| 46 |
+
auth whoami`) should return the user + orgs. If not, log in:
|
| 47 |
+
```
|
| 48 |
+
<py> -m huggingface_hub.cli.hf auth login
|
| 49 |
+
```
|
| 50 |
+
Paste a **write-scoped** PAT from https://huggingface.co/settings/tokens
|
| 51 |
+
at the interactive prompt. **Never echo a PAT into the chat input
|
| 52 |
+
itself** — that puts it in the transcript. If the operator does, tell
|
| 53 |
+
them to immediately rotate it.
|
| 54 |
+
|
| 55 |
+
3. **A Space already exists at the target slug.** This skill expects
|
| 56 |
+
the Space to be created via the HF UI first:
|
| 57 |
+
- https://huggingface.co/new-space
|
| 58 |
+
- **SDK**: Docker (required — the validator's deps don't fit
|
| 59 |
+
Gradio's default Python SDK)
|
| 60 |
+
- **Hardware**: `cpu-basic` (free) is enough for the spike; the
|
| 61 |
+
validator runs without Kit and the work is CPU-bound USD parsing
|
| 62 |
+
- **Visibility**: Private while internal-pilot
|
| 63 |
+
|
| 64 |
+
4. **The `HF_TOKEN` secret is set on the Space.** Settings → Variables
|
| 65 |
+
and secrets → New secret. Required for `Open PR` mode to commit
|
| 66 |
+
verdicts back to customer datasets.
|
| 67 |
+
|
| 68 |
+
If pre-flight 3 or 4 isn't done, ask the operator to do them in the
|
| 69 |
+
browser, then resume. Don't attempt to create the Space via the API —
|
| 70 |
+
the manual flow is what the team is set up for.
|
| 71 |
+
|
| 72 |
+
## Steps for Claude when `/deploy-hf-space [<slug>]` is invoked
|
| 73 |
+
|
| 74 |
+
The skill operates on the slug `<owner>/<name>` (e.g.
|
| 75 |
+
`nvidia/simready-validator`). If the operator omits the slug,
|
| 76 |
+
default to `nvidia/simready-validator` for the current spike.
|
| 77 |
+
|
| 78 |
+
1. **Verify the Space exists and runtime is queryable.**
|
| 79 |
+
```
|
| 80 |
+
<py> -c "
|
| 81 |
+
from huggingface_hub import HfApi
|
| 82 |
+
api = HfApi()
|
| 83 |
+
info = api.space_info('<slug>')
|
| 84 |
+
print(info.runtime, info.sdk)
|
| 85 |
+
"
|
| 86 |
+
```
|
| 87 |
+
`sdk` must be `docker`. If it's `gradio` or `static`, refuse to
|
| 88 |
+
continue and tell the operator to recreate as Docker.
|
| 89 |
+
|
| 90 |
+
2. **Stage the Space repo at `/tmp/space-stage` (or equivalent).**
|
| 91 |
+
```
|
| 92 |
+
git clone https://huggingface.co/spaces/<slug> /tmp/space-stage
|
| 93 |
+
```
|
| 94 |
+
The HF git-credential helper that `hf auth login --add-to-git-credential`
|
| 95 |
+
sets up handles auth. If clone prompts for a password, the operator's
|
| 96 |
+
git creds aren't wired — back to pre-flight.
|
| 97 |
+
|
| 98 |
+
3. **Vendor in the deployable files.** The Dockerfile inside
|
| 99 |
+
`tools/hf_space/Dockerfile` COPYs from these paths in the build
|
| 100 |
+
context, so the Space repo root needs them mirrored:
|
| 101 |
+
|
| 102 |
+
- `tools/hf_space/Dockerfile` → Space `Dockerfile` (root)
|
| 103 |
+
- `tools/hf_space/` → Space `tools/hf_space/`
|
| 104 |
+
- `tools/validation/` → Space `tools/validation/`
|
| 105 |
+
|
| 106 |
+
In Bash:
|
| 107 |
+
```
|
| 108 |
+
cp tools/hf_space/Dockerfile /tmp/space-stage/Dockerfile
|
| 109 |
+
mkdir -p /tmp/space-stage/tools
|
| 110 |
+
cp -r tools/hf_space /tmp/space-stage/tools/
|
| 111 |
+
cp -r tools/validation /tmp/space-stage/tools/
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
**DO NOT** drop a wholesale copy of this monorepo into the Space —
|
| 115 |
+
it'd include unrelated subtrees (`docs/`, `goals/`, `clients.yaml`)
|
| 116 |
+
that don't belong in a Space and may contain secrets-shaped content.
|
| 117 |
+
Keep the COPY surface minimal.
|
| 118 |
+
|
| 119 |
+
4. **Preserve the HF-side README frontmatter.** HF auto-generates a
|
| 120 |
+
`README.md` with the Space's title/emoji/sdk YAML frontmatter on
|
| 121 |
+
creation. **Keep that frontmatter intact** — rewriting the README
|
| 122 |
+
body is fine, but blowing away the frontmatter de-registers the
|
| 123 |
+
Space card. The shipped README in `tools/hf_space/README.md` is the
|
| 124 |
+
internal-docs version, NOT what should land at the Space root.
|
| 125 |
+
Build a fresh `/tmp/space-stage/README.md` that keeps HF's
|
| 126 |
+
frontmatter and adds a short body pointing back at this repo as the
|
| 127 |
+
source of truth.
|
| 128 |
+
|
| 129 |
+
5. **Commit + push.**
|
| 130 |
+
```
|
| 131 |
+
cd /tmp/space-stage
|
| 132 |
+
git add -A
|
| 133 |
+
git -c user.email=<op-email> -c user.name=<op-name> commit -m \\
|
| 134 |
+
"Update from <sha> in simready-oem-library-pm"
|
| 135 |
+
git push
|
| 136 |
+
```
|
| 137 |
+
Use the **operator's** identity, not a bot — Spaces are usually
|
| 138 |
+
personal accounts in this team. The commit message should reference
|
| 139 |
+
the source-repo sha so the Space's history maps back.
|
| 140 |
+
|
| 141 |
+
6. **Verify the build.**
|
| 142 |
+
```
|
| 143 |
+
<py> -c "
|
| 144 |
+
from huggingface_hub import HfApi
|
| 145 |
+
info = HfApi().space_info('<slug>')
|
| 146 |
+
print(f'stage: {info.runtime.stage}')
|
| 147 |
+
print(f'sha: {info.sha}')
|
| 148 |
+
"
|
| 149 |
+
```
|
| 150 |
+
Stages of interest:
|
| 151 |
+
- `BUILDING` / `RUNNING_BUILDING` — wait. First build ~5 min;
|
| 152 |
+
incremental builds (when only `runner.py` / `app.py` changed,
|
| 153 |
+
not `requirements.txt` or `Dockerfile`) finish in ~1 min thanks
|
| 154 |
+
to layer cache.
|
| 155 |
+
- `RUNNING` — done. Open `https://huggingface.co/spaces/<slug>`
|
| 156 |
+
in the browser to interact.
|
| 157 |
+
- `BUILD_ERROR` / `RUNTIME_ERROR` — pull the build/run logs (see
|
| 158 |
+
step 7) and report the failing lines to the operator.
|
| 159 |
+
|
| 160 |
+
7. **Pull logs if needed.** The HF API exposes the logs at:
|
| 161 |
+
```
|
| 162 |
+
GET https://huggingface.co/api/spaces/<slug>/logs/{build,run}
|
| 163 |
+
```
|
| 164 |
+
This is a Server-Sent Events stream — not a static endpoint. With
|
| 165 |
+
the SDK >= 1.16 use `api.fetch_logs(...)` if available; otherwise
|
| 166 |
+
open the URL via `urllib.request` with an `Authorization: Bearer
|
| 167 |
+
<token>` header and read with a short idle timeout (the stream
|
| 168 |
+
blocks indefinitely once it catches up to head).
|
| 169 |
+
|
| 170 |
+
For a one-shot dump, this Python snippet works on Windows:
|
| 171 |
+
```python
|
| 172 |
+
import http.client, socket, json
|
| 173 |
+
from huggingface_hub import get_token
|
| 174 |
+
conn = http.client.HTTPSConnection("huggingface.co", timeout=15)
|
| 175 |
+
conn.request("GET", "/api/spaces/<slug>/logs/run",
|
| 176 |
+
headers={"Authorization": f"Bearer {get_token()}"})
|
| 177 |
+
resp = conn.getresponse()
|
| 178 |
+
buf = b""
|
| 179 |
+
try:
|
| 180 |
+
while True:
|
| 181 |
+
chunk = resp.read(2048)
|
| 182 |
+
if not chunk: break
|
| 183 |
+
buf += chunk
|
| 184 |
+
except (socket.timeout, TimeoutError):
|
| 185 |
+
pass
|
| 186 |
+
# parse SSE: each `data: {...}` line carries one log line.
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
## Verification of the deploy
|
| 190 |
+
|
| 191 |
+
After the push lands and runtime flips to `RUNNING`:
|
| 192 |
+
|
| 193 |
+
1. Open `https://huggingface.co/spaces/<slug>` in a browser.
|
| 194 |
+
2. Enter a USD-native (not zip-bundled) dataset in the **Dataset** box.
|
| 195 |
+
Good smoke-test candidates:
|
| 196 |
+
- `nvidia/PhysicalAI-SimReady-Warehouse-01`
|
| 197 |
+
- Anything from the `nvidia/PhysicalAI-SimReady-*` family that ships
|
| 198 |
+
`.usd` files directly (not in archives)
|
| 199 |
+
3. Leave **Profile = Robot-Body-Runnable**, **Version = 1.0.0**,
|
| 200 |
+
**Open PR unchecked** for the first run.
|
| 201 |
+
4. Click **Validate**. Watch the **Log** box. Expected progression:
|
| 202 |
+
```
|
| 203 |
+
[...] validating dataset=... profile=Robot-Body-Runnable v1.0.0
|
| 204 |
+
workdir: /tmp/hfsp-<safe-name>-XXXX
|
| 205 |
+
$ snapshot_download <repo> ...
|
| 206 |
+
validator target: /tmp/hfsp-<safe-name>-XXXX/raw
|
| 207 |
+
$ python /home/appuser/app/tools/validation/.../validate.py ...
|
| 208 |
+
[validator spec loading — ~243 lines of "Registering feature ..."]
|
| 209 |
+
Discovered N USD asset(s) under ...
|
| 210 |
+
layout: M asset(s) deviate from SimReady packaging spec ... (if any)
|
| 211 |
+
[per-asset "validate: ..." lines]
|
| 212 |
+
PASS: K/N assets passed
|
| 213 |
+
```
|
| 214 |
+
5. If the verdict matches DGXC's verdict on the same dataset
|
| 215 |
+
(byte-for-byte on `results.json`), the cutover criterion in
|
| 216 |
+
`tools/hf_space/README.md` is met for that dataset.
|
| 217 |
+
|
| 218 |
+
## Known limitations
|
| 219 |
+
|
| 220 |
+
These are real constraints that bit the spike — flag them to the
|
| 221 |
+
operator if the dataset they're trying to validate hits one:
|
| 222 |
+
|
| 223 |
+
- **Zip-bundled datasets fail with "no USD assets".** `HF_DOWNLOAD_EXCLUDES`
|
| 224 |
+
in `runner.py` skips `.zip`, and even if we kept them, 800 × 600 MB
|
| 225 |
+
doesn't fit on `cpu-basic`'s ephemeral disk (~50 GiB). The
|
| 226 |
+
imagineio Kitchens-v1 dataset is the canonical example. Fix is to
|
| 227 |
+
add zip-streaming validation (download one zip, extract, validate,
|
| 228 |
+
delete, next) — a separate workstream.
|
| 229 |
+
- **PhysX-bearing profiles are partially validated.** `--no-use-kit`
|
| 230 |
+
is hard-coded because Spaces don't have Isaac Sim. The validator's
|
| 231 |
+
P2 patch drops the resulting `physxschema_unavailable` /
|
| 232 |
+
`omnipbr_unresolved` issues so the verdict isn't artificially
|
| 233 |
+
failing, but PhysX/MDL rules that need Kit are silently not
|
| 234 |
+
exercised. To restore them, bake an `a10g-small` tier with Isaac
|
| 235 |
+
Sim wheels in the Dockerfile — different workstream.
|
| 236 |
+
- **One Space, one HF_TOKEN.** Every customer's verdict PR is opened
|
| 237 |
+
by the Space's own token. Fine for internal pilot, not safe for
|
| 238 |
+
customer-facing deployment where customers shouldn't see each
|
| 239 |
+
other's commit identities. Refactor `runner.run()` to accept a
|
| 240 |
+
caller-provided token before exposing the Space externally.
|
| 241 |
+
- **Validator subprocess is a single Claude-less Python invocation.**
|
| 242 |
+
The DGXC path runs the validator inside a Claude Code subprocess
|
| 243 |
+
(`tools/hf_watch/validate.py` does this) so the simready-report
|
| 244 |
+
skill's agentic decisions (profile selection, error recovery) are
|
| 245 |
+
exercised. The Space bypasses Claude entirely — calls
|
| 246 |
+
`validate.py` directly. If/when we want the agentic decisions on
|
| 247 |
+
HF too, bundle Claude Code into the Dockerfile + add an
|
| 248 |
+
`ANTHROPIC_API_KEY` Space secret.
|
| 249 |
+
|
| 250 |
+
## Session memory hooks
|
| 251 |
+
|
| 252 |
+
This skill is the source of truth for the deployment workflow. If the
|
| 253 |
+
operator hits a step that doesn't match what's documented here (e.g.
|
| 254 |
+
HF changes their Space API, hf CLI launcher works again, a new
|
| 255 |
+
hardware tier appears), update **this file** rather than just patching
|
| 256 |
+
the conversation. The next session that invokes `/deploy-hf-space`
|
| 257 |
+
reads this file, not the old transcript.
|
tools/hf_space/test_discover_dgxc.py
CHANGED
|
@@ -1,142 +1,142 @@
|
|
| 1 |
-
"""DGXC discovery verifier — exercise the new recursive discover_assets
|
| 2 |
-
against four synthetic layouts. Self-contained: imports validate.py with
|
| 3 |
-
SIMREADY_INSIDE_KIT + SIMREADY_FOUNDATIONS_PATH already set so the bootstrap
|
| 4 |
-
gate clears.
|
| 5 |
-
|
| 6 |
-
Usage on the pod:
|
| 7 |
-
export SIMREADY_FOUNDATIONS_PATH=/home/horde/.simready/simready_foundations
|
| 8 |
-
/home/horde/.simready/venv/bin/python3 test_discover_dgxc.py
|
| 9 |
-
"""
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import os
|
| 13 |
-
import sys
|
| 14 |
-
import tempfile
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
|
| 17 |
-
# Skip the Kit re-exec gate (we're testing pure-Python discovery, no Kit).
|
| 18 |
-
os.environ.setdefault("SIMREADY_INSIDE_KIT", "1")
|
| 19 |
-
|
| 20 |
-
# Resolve the repo: env var first (DGXC ad-hoc run), then assume the
|
| 21 |
-
# script lives inside a checkout (workflow / local).
|
| 22 |
-
_env_repo = os.environ.get("SIMREADY_REPO")
|
| 23 |
-
if _env_repo:
|
| 24 |
-
REPO = Path(_env_repo)
|
| 25 |
-
else:
|
| 26 |
-
REPO = Path(__file__).resolve().parents[2]
|
| 27 |
-
SKILL = REPO / "tools" / "validation" / "plugins" / "simready-report" / "skills" / "simready-report"
|
| 28 |
-
if not SKILL.is_dir():
|
| 29 |
-
raise SystemExit(
|
| 30 |
-
f"Validator skill dir not found at {SKILL}. "
|
| 31 |
-
f"Set SIMREADY_REPO to the simready-oem-library-pm checkout root."
|
| 32 |
-
)
|
| 33 |
-
sys.path.insert(0, str(SKILL))
|
| 34 |
-
|
| 35 |
-
import validate as v # noqa: E402
|
| 36 |
-
|
| 37 |
-
CASES = [
|
| 38 |
-
# (label, build_tree, expected_assets, expected_finding_codes)
|
| 39 |
-
("well_shaped",
|
| 40 |
-
lambda root: [
|
| 41 |
-
(root / "well_shaped" / "well_shaped.usda", "interface"),
|
| 42 |
-
(root / "well_shaped" / "Geometry" / "mesh.usda", "sublayer (Geometry)"),
|
| 43 |
-
(root / "well_shaped" / "Materials" / "mat.usda", "sublayer (Materials)"),
|
| 44 |
-
],
|
| 45 |
-
# Old behavior preserved: only the bundle interface is returned.
|
| 46 |
-
["well_shaped/well_shaped.usda"],
|
| 47 |
-
[]),
|
| 48 |
-
("flat",
|
| 49 |
-
lambda root: [
|
| 50 |
-
(root / "flat" / "scene_a.usda", "root-level USD, no bundle"),
|
| 51 |
-
(root / "flat" / "scene_b.usda", "root-level USD, no bundle"),
|
| 52 |
-
],
|
| 53 |
-
# New behavior: both found, both tagged as non-standard.
|
| 54 |
-
["flat/scene_a.usda", "flat/scene_b.usda"],
|
| 55 |
-
["LAYOUT.NON_STANDARD_BUNDLE", "LAYOUT.NON_STANDARD_BUNDLE"]),
|
| 56 |
-
("deep",
|
| 57 |
-
lambda root: [
|
| 58 |
-
(root / "deep" / "team1" / "v1" / "scenes" / "render_target.usda",
|
| 59 |
-
"deeply-nested USD, no bundle wrapper"),
|
| 60 |
-
],
|
| 61 |
-
# New behavior: found at depth, flagged.
|
| 62 |
-
["deep/team1/v1/scenes/render_target.usda"],
|
| 63 |
-
["LAYOUT.NON_STANDARD_BUNDLE"]),
|
| 64 |
-
("mixed",
|
| 65 |
-
lambda root: [
|
| 66 |
-
(root / "mixed" / "proper_bundle" / "proper_bundle.usda", "interface"),
|
| 67 |
-
(root / "mixed" / "proper_bundle" / "Geometry" / "g.usda", "sublayer"),
|
| 68 |
-
(root / "mixed" / "orphan.usda", "standalone — not a bundle"),
|
| 69 |
-
],
|
| 70 |
-
# Bundle interface and the orphan validate; sublayer excluded; orphan flagged.
|
| 71 |
-
["mixed/orphan.usda", "mixed/proper_bundle/proper_bundle.usda"],
|
| 72 |
-
["LAYOUT.NON_STANDARD_BUNDLE"]),
|
| 73 |
-
# Real-world layout from Aperdata's Kitchen-01 dataset:
|
| 74 |
-
# `<top_dir>/<asset>.usd` for the top-level interface plus a
|
| 75 |
-
# `<top_dir>/SubUSDs/<many>.usd` sublayer tree. The interface
|
| 76 |
-
# stem (`Indoor`) doesn't match the dir name (`0_Kitchen_Indoor`),
|
| 77 |
-
# so older logic missed the SubUSDs filter and counted them as
|
| 78 |
-
# standalone assets — Aperdata went from 23 expected to 933
|
| 79 |
-
# validated. New logic excludes anything under a SUBLAYER_DIR
|
| 80 |
-
# ancestor structurally, regardless of bundle naming.
|
| 81 |
-
("aperdata_kitchen",
|
| 82 |
-
lambda root: [
|
| 83 |
-
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "Indoor.usda", "interface (stem != parent dir)"),
|
| 84 |
-
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "SubUSDs" / "CL06.usda", "sublayer (under SubUSDs)"),
|
| 85 |
-
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "SubUSDs" / "CL06_1.usda", "sublayer (under SubUSDs)"),
|
| 86 |
-
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "SubUSDs" / "DEC_large_leaf.usda", "sublayer (under SubUSDs)"),
|
| 87 |
-
(root / "aperdata_kitchen" / "mug_handheld" / "Collected_dining_mug_handheld" / "dining_mug_handheld.usda",
|
| 88 |
-
"interface in nested Collected_* dir"),
|
| 89 |
-
],
|
| 90 |
-
# Only the two interface USDs validate. SubUSDs/* are excluded
|
| 91 |
-
# structurally. The deep mug_handheld interface IS the only
|
| 92 |
-
# candidate in its top-level dir, so it counts as a bundle
|
| 93 |
-
# interface (no layout warning). Indoor.usd is also the only
|
| 94 |
-
# candidate under 0_Kitchen_Indoor → no warning.
|
| 95 |
-
[
|
| 96 |
-
"aperdata_kitchen/0_Kitchen_Indoor/Indoor.usda",
|
| 97 |
-
"aperdata_kitchen/mug_handheld/Collected_dining_mug_handheld/dining_mug_handheld.usda",
|
| 98 |
-
],
|
| 99 |
-
[]),
|
| 100 |
-
]
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
def _normalize(paths: list[Path], root: Path) -> list[str]:
|
| 104 |
-
return sorted(str(p.relative_to(root)).replace("\\", "/") for p in paths)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def main() -> int:
|
| 108 |
-
failures = 0
|
| 109 |
-
with tempfile.TemporaryDirectory(prefix="sr-discover-test-") as td:
|
| 110 |
-
root = Path(td)
|
| 111 |
-
for label, build, expected_assets, expected_codes in CASES:
|
| 112 |
-
case_dir = root / label
|
| 113 |
-
case_dir.mkdir(parents=True, exist_ok=True)
|
| 114 |
-
for usd_path, _why in build(root):
|
| 115 |
-
usd_path.parent.mkdir(parents=True, exist_ok=True)
|
| 116 |
-
usd_path.write_text("()") # empty stage — discover_assets is pure FS walk
|
| 117 |
-
assets, findings = v.discover_assets(case_dir)
|
| 118 |
-
got_assets = _normalize(assets, root)
|
| 119 |
-
got_codes = sorted([f["code"] for f in findings])
|
| 120 |
-
exp_assets = sorted(expected_assets)
|
| 121 |
-
exp_codes = sorted(expected_codes)
|
| 122 |
-
ok = (got_assets == exp_assets and got_codes == exp_codes)
|
| 123 |
-
mark = "PASS" if ok else "FAIL"
|
| 124 |
-
print(f" [{mark}] {label}")
|
| 125 |
-
print(f" expected assets: {exp_assets}")
|
| 126 |
-
print(f" got assets: {got_assets}")
|
| 127 |
-
print(f" expected findings: {exp_codes}")
|
| 128 |
-
print(f" got findings: {got_codes}")
|
| 129 |
-
if not ok:
|
| 130 |
-
failures += 1
|
| 131 |
-
for f in findings:
|
| 132 |
-
print(f" finding detail: {f}")
|
| 133 |
-
print()
|
| 134 |
-
if failures:
|
| 135 |
-
print(f"{failures} case(s) FAILED")
|
| 136 |
-
return 1
|
| 137 |
-
print("all cases passed — recursive discovery + layout findings working end-to-end on DGXC")
|
| 138 |
-
return 0
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
if __name__ == "__main__":
|
| 142 |
-
raise SystemExit(main())
|
|
|
|
| 1 |
+
"""DGXC discovery verifier — exercise the new recursive discover_assets
|
| 2 |
+
against four synthetic layouts. Self-contained: imports validate.py with
|
| 3 |
+
SIMREADY_INSIDE_KIT + SIMREADY_FOUNDATIONS_PATH already set so the bootstrap
|
| 4 |
+
gate clears.
|
| 5 |
+
|
| 6 |
+
Usage on the pod:
|
| 7 |
+
export SIMREADY_FOUNDATIONS_PATH=/home/horde/.simready/simready_foundations
|
| 8 |
+
/home/horde/.simready/venv/bin/python3 test_discover_dgxc.py
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import sys
|
| 14 |
+
import tempfile
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
# Skip the Kit re-exec gate (we're testing pure-Python discovery, no Kit).
|
| 18 |
+
os.environ.setdefault("SIMREADY_INSIDE_KIT", "1")
|
| 19 |
+
|
| 20 |
+
# Resolve the repo: env var first (DGXC ad-hoc run), then assume the
|
| 21 |
+
# script lives inside a checkout (workflow / local).
|
| 22 |
+
_env_repo = os.environ.get("SIMREADY_REPO")
|
| 23 |
+
if _env_repo:
|
| 24 |
+
REPO = Path(_env_repo)
|
| 25 |
+
else:
|
| 26 |
+
REPO = Path(__file__).resolve().parents[2]
|
| 27 |
+
SKILL = REPO / "tools" / "validation" / "plugins" / "simready-report" / "skills" / "simready-report"
|
| 28 |
+
if not SKILL.is_dir():
|
| 29 |
+
raise SystemExit(
|
| 30 |
+
f"Validator skill dir not found at {SKILL}. "
|
| 31 |
+
f"Set SIMREADY_REPO to the simready-oem-library-pm checkout root."
|
| 32 |
+
)
|
| 33 |
+
sys.path.insert(0, str(SKILL))
|
| 34 |
+
|
| 35 |
+
import validate as v # noqa: E402
|
| 36 |
+
|
| 37 |
+
CASES = [
|
| 38 |
+
# (label, build_tree, expected_assets, expected_finding_codes)
|
| 39 |
+
("well_shaped",
|
| 40 |
+
lambda root: [
|
| 41 |
+
(root / "well_shaped" / "well_shaped.usda", "interface"),
|
| 42 |
+
(root / "well_shaped" / "Geometry" / "mesh.usda", "sublayer (Geometry)"),
|
| 43 |
+
(root / "well_shaped" / "Materials" / "mat.usda", "sublayer (Materials)"),
|
| 44 |
+
],
|
| 45 |
+
# Old behavior preserved: only the bundle interface is returned.
|
| 46 |
+
["well_shaped/well_shaped.usda"],
|
| 47 |
+
[]),
|
| 48 |
+
("flat",
|
| 49 |
+
lambda root: [
|
| 50 |
+
(root / "flat" / "scene_a.usda", "root-level USD, no bundle"),
|
| 51 |
+
(root / "flat" / "scene_b.usda", "root-level USD, no bundle"),
|
| 52 |
+
],
|
| 53 |
+
# New behavior: both found, both tagged as non-standard.
|
| 54 |
+
["flat/scene_a.usda", "flat/scene_b.usda"],
|
| 55 |
+
["LAYOUT.NON_STANDARD_BUNDLE", "LAYOUT.NON_STANDARD_BUNDLE"]),
|
| 56 |
+
("deep",
|
| 57 |
+
lambda root: [
|
| 58 |
+
(root / "deep" / "team1" / "v1" / "scenes" / "render_target.usda",
|
| 59 |
+
"deeply-nested USD, no bundle wrapper"),
|
| 60 |
+
],
|
| 61 |
+
# New behavior: found at depth, flagged.
|
| 62 |
+
["deep/team1/v1/scenes/render_target.usda"],
|
| 63 |
+
["LAYOUT.NON_STANDARD_BUNDLE"]),
|
| 64 |
+
("mixed",
|
| 65 |
+
lambda root: [
|
| 66 |
+
(root / "mixed" / "proper_bundle" / "proper_bundle.usda", "interface"),
|
| 67 |
+
(root / "mixed" / "proper_bundle" / "Geometry" / "g.usda", "sublayer"),
|
| 68 |
+
(root / "mixed" / "orphan.usda", "standalone — not a bundle"),
|
| 69 |
+
],
|
| 70 |
+
# Bundle interface and the orphan validate; sublayer excluded; orphan flagged.
|
| 71 |
+
["mixed/orphan.usda", "mixed/proper_bundle/proper_bundle.usda"],
|
| 72 |
+
["LAYOUT.NON_STANDARD_BUNDLE"]),
|
| 73 |
+
# Real-world layout from Aperdata's Kitchen-01 dataset:
|
| 74 |
+
# `<top_dir>/<asset>.usd` for the top-level interface plus a
|
| 75 |
+
# `<top_dir>/SubUSDs/<many>.usd` sublayer tree. The interface
|
| 76 |
+
# stem (`Indoor`) doesn't match the dir name (`0_Kitchen_Indoor`),
|
| 77 |
+
# so older logic missed the SubUSDs filter and counted them as
|
| 78 |
+
# standalone assets — Aperdata went from 23 expected to 933
|
| 79 |
+
# validated. New logic excludes anything under a SUBLAYER_DIR
|
| 80 |
+
# ancestor structurally, regardless of bundle naming.
|
| 81 |
+
("aperdata_kitchen",
|
| 82 |
+
lambda root: [
|
| 83 |
+
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "Indoor.usda", "interface (stem != parent dir)"),
|
| 84 |
+
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "SubUSDs" / "CL06.usda", "sublayer (under SubUSDs)"),
|
| 85 |
+
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "SubUSDs" / "CL06_1.usda", "sublayer (under SubUSDs)"),
|
| 86 |
+
(root / "aperdata_kitchen" / "0_Kitchen_Indoor" / "SubUSDs" / "DEC_large_leaf.usda", "sublayer (under SubUSDs)"),
|
| 87 |
+
(root / "aperdata_kitchen" / "mug_handheld" / "Collected_dining_mug_handheld" / "dining_mug_handheld.usda",
|
| 88 |
+
"interface in nested Collected_* dir"),
|
| 89 |
+
],
|
| 90 |
+
# Only the two interface USDs validate. SubUSDs/* are excluded
|
| 91 |
+
# structurally. The deep mug_handheld interface IS the only
|
| 92 |
+
# candidate in its top-level dir, so it counts as a bundle
|
| 93 |
+
# interface (no layout warning). Indoor.usd is also the only
|
| 94 |
+
# candidate under 0_Kitchen_Indoor → no warning.
|
| 95 |
+
[
|
| 96 |
+
"aperdata_kitchen/0_Kitchen_Indoor/Indoor.usda",
|
| 97 |
+
"aperdata_kitchen/mug_handheld/Collected_dining_mug_handheld/dining_mug_handheld.usda",
|
| 98 |
+
],
|
| 99 |
+
[]),
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _normalize(paths: list[Path], root: Path) -> list[str]:
|
| 104 |
+
return sorted(str(p.relative_to(root)).replace("\\", "/") for p in paths)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def main() -> int:
|
| 108 |
+
failures = 0
|
| 109 |
+
with tempfile.TemporaryDirectory(prefix="sr-discover-test-") as td:
|
| 110 |
+
root = Path(td)
|
| 111 |
+
for label, build, expected_assets, expected_codes in CASES:
|
| 112 |
+
case_dir = root / label
|
| 113 |
+
case_dir.mkdir(parents=True, exist_ok=True)
|
| 114 |
+
for usd_path, _why in build(root):
|
| 115 |
+
usd_path.parent.mkdir(parents=True, exist_ok=True)
|
| 116 |
+
usd_path.write_text("()") # empty stage — discover_assets is pure FS walk
|
| 117 |
+
assets, findings = v.discover_assets(case_dir)
|
| 118 |
+
got_assets = _normalize(assets, root)
|
| 119 |
+
got_codes = sorted([f["code"] for f in findings])
|
| 120 |
+
exp_assets = sorted(expected_assets)
|
| 121 |
+
exp_codes = sorted(expected_codes)
|
| 122 |
+
ok = (got_assets == exp_assets and got_codes == exp_codes)
|
| 123 |
+
mark = "PASS" if ok else "FAIL"
|
| 124 |
+
print(f" [{mark}] {label}")
|
| 125 |
+
print(f" expected assets: {exp_assets}")
|
| 126 |
+
print(f" got assets: {got_assets}")
|
| 127 |
+
print(f" expected findings: {exp_codes}")
|
| 128 |
+
print(f" got findings: {got_codes}")
|
| 129 |
+
if not ok:
|
| 130 |
+
failures += 1
|
| 131 |
+
for f in findings:
|
| 132 |
+
print(f" finding detail: {f}")
|
| 133 |
+
print()
|
| 134 |
+
if failures:
|
| 135 |
+
print(f"{failures} case(s) FAILED")
|
| 136 |
+
return 1
|
| 137 |
+
print("all cases passed — recursive discovery + layout findings working end-to-end on DGXC")
|
| 138 |
+
return 0
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
raise SystemExit(main())
|
tools/validation/.claude-plugin/marketplace.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
-
{
|
| 2 |
-
"$schema": "https://json.schemastore.org/claude-code-marketplace",
|
| 3 |
-
"name": "simready-playbook",
|
| 4 |
-
"description": "SimReady asset pipeline plugins for Claude Code.",
|
| 5 |
-
"owner": {
|
| 6 |
-
"name": "dloginowski",
|
| 7 |
-
"email": "dloginowski@nvidia.com"
|
| 8 |
-
},
|
| 9 |
-
"plugins": [
|
| 10 |
-
{
|
| 11 |
-
"name": "simready-report",
|
| 12 |
-
"source": "./plugins/simready-report",
|
| 13 |
-
"description": "SimReady asset pipeline tooling. Provides /simready-report (validation + HTML dashboard) and /simready-package (standalone simready ingest packaging)."
|
| 14 |
-
}
|
| 15 |
-
]
|
| 16 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json.schemastore.org/claude-code-marketplace",
|
| 3 |
+
"name": "simready-playbook",
|
| 4 |
+
"description": "SimReady asset pipeline plugins for Claude Code.",
|
| 5 |
+
"owner": {
|
| 6 |
+
"name": "dloginowski",
|
| 7 |
+
"email": "dloginowski@nvidia.com"
|
| 8 |
+
},
|
| 9 |
+
"plugins": [
|
| 10 |
+
{
|
| 11 |
+
"name": "simready-report",
|
| 12 |
+
"source": "./plugins/simready-report",
|
| 13 |
+
"description": "SimReady asset pipeline tooling. Provides /simready-report (validation + HTML dashboard) and /simready-package (standalone simready ingest packaging)."
|
| 14 |
+
}
|
| 15 |
+
]
|
| 16 |
+
}
|
tools/validation/PROBLEMS.md
CHANGED
|
@@ -1,327 +1,327 @@
|
|
| 1 |
-
# Known problems
|
| 2 |
-
|
| 3 |
-
Issues encountered with the validation pipeline, the foundation specs, or the
|
| 4 |
-
SimReady SDK that aren't bugs in this repo's code but affect the report
|
| 5 |
-
output. Each entry: **symptom → root cause → workaround / how to address →
|
| 6 |
-
status**.
|
| 7 |
-
|
| 8 |
-
When you hit a new issue, add it here. Keep the list short — fold resolved
|
| 9 |
-
items into git history once they're fixed.
|
| 10 |
-
|
| 11 |
-
---
|
| 12 |
-
|
| 13 |
-
## P1 — Profiles silently drop features that aren't registered
|
| 14 |
-
|
| 15 |
-
**Symptom.** A profile loads with far fewer features than its
|
| 16 |
-
`profiles.toml` declares. Concretely on the current foundations checkout:
|
| 17 |
-
|
| 18 |
-
| Profile | Declared in `profiles.toml` | Loaded by validator | Dropped feature IDs |
|
| 19 |
-
|---|---|---|---|
|
| 20 |
-
| `Robot-Body-Runnable` v1.0.0 | 5 | **1** | `FET004_ROBOT_PHYSX` v0.2.0, `FET021_ROBOT_CORE_RUNNABLE` v0.2.0, `FET022_DRIVEN_JOINTS_PHYSX` v0.1.0, `FET024_BASE_ARTICULATION_PHYSX` v0.1.0 |
|
| 21 |
-
| `Robot-Body-Isaac` v1.0.0 | 6 | 2 | `FET004_ROBOT_PHYSX` v0.2.0, `FET021_ROBOT_CORE_ISAAC` v0.2.0, `FET022_DRIVEN_JOINTS_ISAAC` v0.1.0, `FET024_BASE_ARTICULATION_PHYSX` v0.1.0 |
|
| 22 |
-
| `Robot-Body-Neutral` v1.0.0 | 5 | 3 | `FET022_DRIVEN_JOINTS_NEUTRAL` v0.1.0, `FET024_BASE_ARTICULATION_NEUTRAL` v0.1.0 |
|
| 23 |
-
| `Prop-Robotics-Neutral` v2.0.0 | 6 | 5 | `FET006_BASE_MDL` v0.1.0 |
|
| 24 |
-
|
| 25 |
-
What's actually registered for the FET02x family (after `repo
|
| 26 |
-
usd_profiles_codegen`):
|
| 27 |
-
|
| 28 |
-
- `fet_021_robot_core` v0.2.0 (no variant suffix)
|
| 29 |
-
- `fet_022_driven_joints` v0.1.0
|
| 30 |
-
- `fet_023_robot_materials` v0.1.0
|
| 31 |
-
- `fet_024_base_articulation` v0.1.0
|
| 32 |
-
|
| 33 |
-
And for FET004/FET006 the only `_BASE_NEUTRAL` / `_BASE_USDPREVIEW`
|
| 34 |
-
forms are registered, not the `_ROBOT_PHYSX` / `_BASE_MDL` variants the
|
| 35 |
-
profiles ask for.
|
| 36 |
-
|
| 37 |
-
**Root cause.** Profile entries in
|
| 38 |
-
`<foundations>/nv_core/sr_specs/docs/profiles/profiles.toml` reference
|
| 39 |
-
variant feature IDs that the local foundations registry doesn't ship — e.g.
|
| 40 |
-
profiles ask for `FET021_ROBOT_CORE_RUNNABLE`, `FET022_DRIVEN_JOINTS_PHYSX`,
|
| 41 |
-
`FET004_ROBOT_PHYSX`, `FET024_BASE_ARTICULATION_NEUTRAL`. What is actually
|
| 42 |
-
registered (after `repo usd_profiles_codegen` runs against our checkout) is
|
| 43 |
-
only the unsuffixed `_BASE_NEUTRAL` / `_BASE_USDPREVIEW` / `_CORE` /
|
| 44 |
-
`_BASE_ISAACSIM` variants. The variant features the profiles ask for are
|
| 45 |
-
silently absent.
|
| 46 |
-
|
| 47 |
-
`omni.asset_validator.ProfileRegistry` resolves features by ID + version and
|
| 48 |
-
silently drops any it can't find. There's no warning emitted at profile
|
| 49 |
-
load time.
|
| 50 |
-
|
| 51 |
-
**Verified the gap is local, not universal.** Sample reports from another
|
| 52 |
-
SimReady environment (Fanuc content, sr9iar.usd.usd) show all five
|
| 53 |
-
declared `Robot-Body-Runnable` features actually loaded —
|
| 54 |
-
`FET001_BASE_NEUTRAL` (failed), plus `FET004_ROBOT_PHYSX`,
|
| 55 |
-
`FET021_ROBOT_CORE_RUNNABLE`, `FET022_DRIVEN_JOINTS_PHYSX`,
|
| 56 |
-
`FET024_BASE_ARTICULATION_PHYSX`, `FET024_BASE_ARTICULATION_NEUTRAL`
|
| 57 |
-
(passed). So the variants *are* registrable; our local codegen output
|
| 58 |
-
just doesn't include them.
|
| 59 |
-
|
| 60 |
-
**Where the variants live.** The foundation source ships JSON variant
|
| 61 |
-
definitions next to each feature's markdown. For FET004:
|
| 62 |
-
|
| 63 |
-
```
|
| 64 |
-
docs/features/FET_004-simulate_multi_body_physics.md (md, _BASE_NEUTRAL only)
|
| 65 |
-
docs/features/FET_004_base_neutral-0.1.0-...json
|
| 66 |
-
docs/features/FET_004_base_physx-0.1.0-...json
|
| 67 |
-
docs/features/FET_004_base_physx-0.2.0-...json
|
| 68 |
-
docs/features/FET_004_robot_physx-0.1.0-...json <- this is the one profiles ask for
|
| 69 |
-
docs/features/FET_004_robot_physx-0.2.0-...json
|
| 70 |
-
```
|
| 71 |
-
|
| 72 |
-
Each JSON declares an `id` (e.g. `FET004_ROBOT_PHYSX`), `version`, `path`,
|
| 73 |
-
and a `requirements` list. **Our `repo usd_profiles_codegen` run only
|
| 74 |
-
parses the `.md` files** — the generated
|
| 75 |
-
`_build/python/omni/capabilities/_features.py` contains zero
|
| 76 |
-
`_ROBOT_PHYSX` / `_BASE_PHYSX` / `_RUNNABLE` entries even though the JSON
|
| 77 |
-
sources are right there.
|
| 78 |
-
|
| 79 |
-
**How to address.** Three paths, in priority order:
|
| 80 |
-
|
| 81 |
-
1. **Codegen — pick up the JSON variant files.** The foundations
|
| 82 |
-
`repo usd_profiles_codegen` tool needs to also enumerate
|
| 83 |
-
`FET_*_*-*-...json` files in the features directory and emit Feature
|
| 84 |
-
entries for each, not just the `Internal ID` declared in the `.md`.
|
| 85 |
-
This closes the gap without changing source data — the variants
|
| 86 |
-
already exist.
|
| 87 |
-
2. **Validator — emit a warning when a profile entry doesn't resolve.**
|
| 88 |
-
`omni.asset_validator.ProfileRegistry` silently drops missing
|
| 89 |
-
features. A `WARNING:` log at profile-load time would have made this
|
| 90 |
-
bug visible from day one.
|
| 91 |
-
3. **Documentation** — once #1 lands, `repo.toml`'s codegen description
|
| 92 |
-
should call out that JSON variants are part of the source of truth so
|
| 93 |
-
downstream tooling doesn't repeat the mistake.
|
| 94 |
-
|
| 95 |
-
Open this with the SimReady foundations team
|
| 96 |
-
(`#omni-simready` / `#simready-next-support`); cite the discrepancy
|
| 97 |
-
between our local codegen output and the working Fanuc environment
|
| 98 |
-
sample.
|
| 99 |
-
|
| 100 |
-
**In-repo workaround.** The dashboard now surfaces the coverage gap
|
| 101 |
-
explicitly: when a profile declares more features than the validator
|
| 102 |
-
loaded, a banner at the top lists the missing IDs and tells the user which
|
| 103 |
-
ones got silently dropped. The validation result the report shows is
|
| 104 |
-
genuinely against the *loaded* feature set — running with
|
| 105 |
-
`Robot-Body-Runnable` does *not* check the four declared-but-missing
|
| 106 |
-
features.
|
| 107 |
-
|
| 108 |
-
**Status.** Local patch applied — see "Local patch applied" below.
|
| 109 |
-
Upstream codegen fix still needed. Banner workaround still in place
|
| 110 |
-
for environments without the patch.
|
| 111 |
-
|
| 112 |
-
### Local patch applied
|
| 113 |
-
|
| 114 |
-
A patch lives in
|
| 115 |
-
`plugins/simready-report/skills/simready-report/validate.py` —
|
| 116 |
-
function `_patch_register_json_variant_features()`. It runs at module
|
| 117 |
-
load (so worker processes pick it up too) right after
|
| 118 |
-
`import omni.asset_validator as oav`. It is loud about itself: every
|
| 119 |
-
run prints a `[PATCH P1] FeatureRegistry: +N JSON-variant feature(s)
|
| 120 |
-
... ProfileRegistry: rebuilt feature lists for M profile(s) ...`
|
| 121 |
-
line so it's never invisible.
|
| 122 |
-
|
| 123 |
-
**Concretely on the Yaskawa-local checkout** the patch lifts:
|
| 124 |
-
- FeatureRegistry: 11 features → 30 features (+19 from JSON variants).
|
| 125 |
-
- Robot-Body-Runnable v1.0.0: 1 feature / 8 requirements → **5 / 41**.
|
| 126 |
-
- 11 profiles got their `.features` lists rebuilt; +24 feature
|
| 127 |
-
references appended in total.
|
| 128 |
-
|
| 129 |
-
**Exactly what the patch does**, step by step:
|
| 130 |
-
|
| 131 |
-
1. **Discover variant JSON files.** Globs
|
| 132 |
-
`<foundations>/nv_core/sr_specs/docs/features/FET_*.json`. The
|
| 133 |
-
filename styles in this repo are inconsistent
|
| 134 |
-
(`FET_021-robot_core_isaac-0.1.0.json`,
|
| 135 |
-
`FET_021_robot_core_runnable_0.2.0.json`,
|
| 136 |
-
`FET_004_base_physx-0.2.0-simulate_multi_body_phyics.json`), so we
|
| 137 |
-
ignore the filename and read the `id` + `version` fields from the
|
| 138 |
-
JSON content.
|
| 139 |
-
|
| 140 |
-
2. **Build a Requirement code lookup.** `r.code -> r` for every
|
| 141 |
-
`Requirement` in `omni.asset_validator.RequirementsRegistry()`.
|
| 142 |
-
The JSON variants list dotted code strings like `"RB.COL.003"`,
|
| 143 |
-
`"DJ.001"`, `"BA.002"` — those are looked up here.
|
| 144 |
-
|
| 145 |
-
3. **Construct a Feature-protocol object per JSON variant.**
|
| 146 |
-
`omni.asset_validator._features.Feature` is a `typing.Protocol`
|
| 147 |
-
declaring `id: str, version: str, path: str, requirements: list`.
|
| 148 |
-
We use a frozen dataclass that satisfies the protocol structurally:
|
| 149 |
-
|
| 150 |
-
```python
|
| 151 |
-
@dataclass(frozen=True)
|
| 152 |
-
class _PatchedFeature:
|
| 153 |
-
id: str
|
| 154 |
-
version: str
|
| 155 |
-
path: str
|
| 156 |
-
requirements: list
|
| 157 |
-
```
|
| 158 |
-
|
| 159 |
-
4. **Skip duplicates and unresolvable variants.** If
|
| 160 |
-
`FeatureRegistry.find(fid, fver)` already returns something, skip
|
| 161 |
-
(don't double-register). If any of the JSON's requirement code
|
| 162 |
-
strings doesn't resolve in the code lookup, skip the whole feature
|
| 163 |
-
and record it in `unresolved_codes`. (Currently zero unresolved on
|
| 164 |
-
our checkout — every JSON variant's required codes exist.)
|
| 165 |
-
|
| 166 |
-
5. **Register via `FeatureRegistry().add(feat)`.** The internal
|
| 167 |
-
`create_key` uses `.id` + `.version` so our dataclass satisfies it.
|
| 168 |
-
|
| 169 |
-
6. **Re-resolve every profile's feature list — the critical step.**
|
| 170 |
-
`Profile` is a frozen dataclass, so we can't reassign
|
| 171 |
-
`profile.features`. But the field is a *mutable list*, and the
|
| 172 |
-
freeze only prevents field reassignment, not mutation of held
|
| 173 |
-
collections. So for every entry in `profiles.toml`'s declared
|
| 174 |
-
features that isn't already in the live profile, we look it up in
|
| 175 |
-
the now-fuller `FeatureRegistry` and `profile.features.append(...)`
|
| 176 |
-
in place. **Without this step the patch is invisible** — adding to
|
| 177 |
-
`FeatureRegistry` after profiles are already built doesn't
|
| 178 |
-
retroactively join existing Profile objects.
|
| 179 |
-
|
| 180 |
-
**What the patch does NOT touch**: foundation source files, the
|
| 181 |
-
codegen tool, generated `_features.py`, profile definitions in
|
| 182 |
-
`profiles.toml`, or any rule code. The local patch is purely in our
|
| 183 |
-
validation runner.
|
| 184 |
-
|
| 185 |
-
**Remove the patch when**: the foundation `repo usd_profiles_codegen`
|
| 186 |
-
tool is updated to enumerate JSON variant files alongside the
|
| 187 |
-
markdown. Once `_features.py` carries every variant on its own,
|
| 188 |
-
delete `_patch_register_json_variant_features()` and its call site.
|
| 189 |
-
|
| 190 |
-
---
|
| 191 |
-
|
| 192 |
-
## P2 — Public ur10 sample asset fails current foundation specs
|
| 193 |
-
|
| 194 |
-
**Symptom.** Running `/simready-report` against the public ur10 sample
|
| 195 |
-
in
|
| 196 |
-
`simready_foundations/sample_content/common_assets/robots_general/ur10/`
|
| 197 |
-
(release `2026.04.0`, commit `805d2c5`) reports failures on every
|
| 198 |
-
variant when validated against its declared profile (or against
|
| 199 |
-
Robot-Body-Runnable cross-profile). Concretely, against each interface
|
| 200 |
-
USD's declared profile, with `--use-kit` so PhysX rules actually run:
|
| 201 |
-
|
| 202 |
-
| Variant interface | Declared profile | Result | Failing requirements |
|
| 203 |
-
|---|---|---|---|
|
| 204 |
-
| `simready_usd/ur10.usda` | Robot-Body-Neutral v1.0.0 | **PASS** | — |
|
| 205 |
-
| `simready_physx_usd/ur10.usda` | Robot-Body-Physx v1.0.0 | **FAIL** | (profile evaluates 0 features against the asset; `features_summary` empty in JSON output) |
|
| 206 |
-
| `simready_isaac_usd/ur10.usda` | Robot-Body-Isaac v1.0.0 | **FAIL** | `RC.005` (`VerifyRobotPhysicsAttributesSourceLayer`), `RC.008` (`robot-type`), `RC.009` (`root-joint-pinned`), `DJ.010`, `ISA.001` |
|
| 207 |
-
|
| 208 |
-
Under the cross-profile `Robot-Body-Runnable` run, Neutral and PhysX
|
| 209 |
-
variants additionally light up with `RC.007` (`robot-schema` — no
|
| 210 |
-
`IsaacRobotAPI` on the default prim) because neither of those variants
|
| 211 |
-
applies `IsaacRobotAPI`, only the Isaac variant does.
|
| 212 |
-
|
| 213 |
-
**Root cause.** Asset-side metadata gaps and layer-organization issues
|
| 214 |
-
in the shipped public sample, *not* validator or skill code. The asset
|
| 215 |
-
file is tracked by Git LFS at the same OID as the release (`b30f9c1b…`
|
| 216 |
-
for `simready_isaac_usd/payloads/base.usda`), so no in-tree edit
|
| 217 |
-
caused this. Each asset's `customLayerData.SimReady_Metadata.validation`
|
| 218 |
-
records `validated_features` against a snapshot dated **2026-01-09**;
|
| 219 |
-
between then and the current `805d2c5` foundation specs the rules in
|
| 220 |
-
question were tightened (or added), so the asset that passed in
|
| 221 |
-
January no longer passes today. Specifically:
|
| 222 |
-
|
| 223 |
-
- **`RC.008` (`robot-type`)** — `simready_isaac_usd/payloads/base.usda`
|
| 224 |
-
lines 81-83 declare `token isaac:robotType` with **no value**. The
|
| 225 |
-
current rule requires a valid schema-defined token (e.g.
|
| 226 |
-
`"Manipulator"`, `"End Effector"`); the placeholder `"Default"` and
|
| 227 |
-
the absent-value case both fail.
|
| 228 |
-
- **`RC.009` (`root-joint-pinned`)** — depends on `isaac:robotType`
|
| 229 |
-
to decide whether the root joint must be pinned. The asset's
|
| 230 |
-
`root_joint` *is* a `PhysicsFixedJoint` with empty `physics:body0`
|
| 231 |
-
(pinned to world, correct for a Manipulator), but without a valid
|
| 232 |
-
`robotType` value the rule can't evaluate and fails.
|
| 233 |
-
- **`RC.005` (`VerifyRobotPhysicsAttributesSourceLayer`)** — physics
|
| 234 |
-
attributes (`physics:axis`, `physics:mass`, `physics:centerOfMass`,
|
| 235 |
-
`physics:diagonalInertia`, `physics:principalAxes`) on each robot
|
| 236 |
-
link are authored in *both* `payloads/base.usda` and
|
| 237 |
-
`payloads/Physics/physics.usda`. The current rule wants them in the
|
| 238 |
-
physics payload only.
|
| 239 |
-
- **PhysX variant empty `features_summary`** — Robot-Body-Physx v1.0.0
|
| 240 |
-
evaluates zero features against the asset, suggesting the profile's
|
| 241 |
-
declared feature set no longer matches what the asset has stamped
|
| 242 |
-
(related to P1, but for a different feature family).
|
| 243 |
-
|
| 244 |
-
**How to address.** Asset fixes belong in
|
| 245 |
-
`github.com/NVIDIA/simready-foundation`, not here:
|
| 246 |
-
|
| 247 |
-
1. **`RC.008`/`RC.009`** — *verified one-liner*. In
|
| 248 |
-
`simready_isaac_usd/payloads/base.usda` line 81, change
|
| 249 |
-
`token isaac:robotType (…)` to
|
| 250 |
-
`token isaac:robotType = "Manipulator" (…)`. Verified locally with
|
| 251 |
-
`--use-kit`:
|
| 252 |
-
- Against `Robot-Body-Isaac`, `FET021_ROBOT_CORE_ISAAC` failing
|
| 253 |
-
codes went from `[RC.005, RC.008, RC.009]` →
|
| 254 |
-
**`[RC.003, RC.005]`** (RC.008 and RC.009 cleared; RC.003 was
|
| 255 |
-
previously cascade-suppressed and now surfaces).
|
| 256 |
-
- Against `Robot-Body-Runnable` cross-profile, the **Isaac variant
|
| 257 |
-
now PASSES** (`FET021_ROBOT_CORE_RUNNABLE` only requires
|
| 258 |
-
RC.007/008/009 — all three OK once `isaac:robotType` is set,
|
| 259 |
-
because IsaacRobotAPI is already applied with the correct
|
| 260 |
-
joints/links relationships and the root joint is pinned via
|
| 261 |
-
empty `physics:body0`). Cross-profile summary moved from
|
| 262 |
-
`0 passed / 3 failed` → `1 passed / 2 failed` (Isaac passes,
|
| 263 |
-
Neutral and PhysX still fail RC.007/008/009 because they don't
|
| 264 |
-
apply IsaacRobotAPI at all).
|
| 265 |
-
2. **`RC.005`**: move every `physics:*` attribute currently authored
|
| 266 |
-
on links in `payloads/base.usda` into `payloads/Physics/physics.usda`
|
| 267 |
-
only (or refactor so `base.usda` carries no physics attributes at
|
| 268 |
-
all). This is a meaningful layer-organization change, not a
|
| 269 |
-
one-liner. Still failing after fix 1 (88 RC.005 issues remain).
|
| 270 |
-
3. **`RC.003` (`RobotNaming`)** — surfaced *only after* fix 1. The
|
| 271 |
-
rule compares the **immediate folder name** (`simready_isaac_usd`)
|
| 272 |
-
to the **interface USD stem** (`ur10`). Under the standard SimReady
|
| 273 |
-
packaging convention these never match — every Isaac variant of
|
| 274 |
-
every SimReady asset fails RC.003. Likely a rule bug (should
|
| 275 |
-
probably look at the *bundle* parent, e.g. `ur10/`, not the
|
| 276 |
-
variant folder) or a misalignment between rule and packaging spec.
|
| 277 |
-
Worth raising upstream.
|
| 278 |
-
4. **`RC.007` (cross-profile only)**: if the intent is that the
|
| 279 |
-
Neutral and PhysX variants should also satisfy Robot-Body-Runnable,
|
| 280 |
-
apply `IsaacRobotAPI` to their default prims with the same
|
| 281 |
-
`isaac:physics:robotJoints`/`robotLinks` relationships used in the
|
| 282 |
-
Isaac variant. If the intent is that each variant only ever
|
| 283 |
-
validates against its own declared profile, this is not needed.
|
| 284 |
-
5. **PhysX empty features**: align the variant USDs and Robot-Body-Physx
|
| 285 |
-
profile's expected feature set — likely needs the assets to carry
|
| 286 |
-
the variant feature IDs that the profile declares (intersect with
|
| 287 |
-
P1's missing-feature list once that's resolved). Also: `validate.py`
|
| 288 |
-
currently emits an empty `.reports/<…>/` dir when zero features
|
| 289 |
-
match (no `index.html` / `results.json` written). Tracked
|
| 290 |
-
separately as a `validate.py` robustness issue.
|
| 291 |
-
|
| 292 |
-
**In-repo workaround.** None — this is downstream of the validator. The
|
| 293 |
-
`simready-report` skill faithfully reports what the rules say. The
|
| 294 |
-
dashboard's "Failing requirements" panel surfaces the offending codes
|
| 295 |
-
so users can map each failure back to its requirement doc.
|
| 296 |
-
|
| 297 |
-
**Status.** Open against the foundations repo. Filed for tracking via
|
| 298 |
-
this MR; no action in `simready-explorer` is needed beyond surfacing
|
| 299 |
-
the issue here. Fix 1 above is **verified to work locally** (one-line
|
| 300 |
-
edit to `base.usda`) — upstream PR against `NVIDIA/simready-foundation`
|
| 301 |
-
is the right channel to land it. Fixes 2–5 still open. Re-validate
|
| 302 |
-
after the foundation team updates the sample assets, the rule code
|
| 303 |
-
(notably `RC.003 RobotNaming`), or relaxes the unmatched-feature
|
| 304 |
-
behavior.
|
| 305 |
-
|
| 306 |
-
---
|
| 307 |
-
|
| 308 |
-
## How to file a new entry here
|
| 309 |
-
|
| 310 |
-
Use this skeleton:
|
| 311 |
-
|
| 312 |
-
```
|
| 313 |
-
## P<N> — <one-line title>
|
| 314 |
-
|
| 315 |
-
**Symptom.** What the user/reader sees that's wrong.
|
| 316 |
-
|
| 317 |
-
**Root cause.** Where the bug actually lives (which repo / file / step).
|
| 318 |
-
|
| 319 |
-
**How to address.** Concrete fix path(s), upstream or local.
|
| 320 |
-
|
| 321 |
-
**In-repo workaround.** What this repo does today to mitigate, if anything.
|
| 322 |
-
|
| 323 |
-
**Status.** Open / mitigated / fixed (link to commit).
|
| 324 |
-
```
|
| 325 |
-
|
| 326 |
-
Bump the number; don't reuse retired ones — the git history is the
|
| 327 |
-
canonical record once an entry is resolved and removed.
|
|
|
|
| 1 |
+
# Known problems
|
| 2 |
+
|
| 3 |
+
Issues encountered with the validation pipeline, the foundation specs, or the
|
| 4 |
+
SimReady SDK that aren't bugs in this repo's code but affect the report
|
| 5 |
+
output. Each entry: **symptom → root cause → workaround / how to address →
|
| 6 |
+
status**.
|
| 7 |
+
|
| 8 |
+
When you hit a new issue, add it here. Keep the list short — fold resolved
|
| 9 |
+
items into git history once they're fixed.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## P1 — Profiles silently drop features that aren't registered
|
| 14 |
+
|
| 15 |
+
**Symptom.** A profile loads with far fewer features than its
|
| 16 |
+
`profiles.toml` declares. Concretely on the current foundations checkout:
|
| 17 |
+
|
| 18 |
+
| Profile | Declared in `profiles.toml` | Loaded by validator | Dropped feature IDs |
|
| 19 |
+
|---|---|---|---|
|
| 20 |
+
| `Robot-Body-Runnable` v1.0.0 | 5 | **1** | `FET004_ROBOT_PHYSX` v0.2.0, `FET021_ROBOT_CORE_RUNNABLE` v0.2.0, `FET022_DRIVEN_JOINTS_PHYSX` v0.1.0, `FET024_BASE_ARTICULATION_PHYSX` v0.1.0 |
|
| 21 |
+
| `Robot-Body-Isaac` v1.0.0 | 6 | 2 | `FET004_ROBOT_PHYSX` v0.2.0, `FET021_ROBOT_CORE_ISAAC` v0.2.0, `FET022_DRIVEN_JOINTS_ISAAC` v0.1.0, `FET024_BASE_ARTICULATION_PHYSX` v0.1.0 |
|
| 22 |
+
| `Robot-Body-Neutral` v1.0.0 | 5 | 3 | `FET022_DRIVEN_JOINTS_NEUTRAL` v0.1.0, `FET024_BASE_ARTICULATION_NEUTRAL` v0.1.0 |
|
| 23 |
+
| `Prop-Robotics-Neutral` v2.0.0 | 6 | 5 | `FET006_BASE_MDL` v0.1.0 |
|
| 24 |
+
|
| 25 |
+
What's actually registered for the FET02x family (after `repo
|
| 26 |
+
usd_profiles_codegen`):
|
| 27 |
+
|
| 28 |
+
- `fet_021_robot_core` v0.2.0 (no variant suffix)
|
| 29 |
+
- `fet_022_driven_joints` v0.1.0
|
| 30 |
+
- `fet_023_robot_materials` v0.1.0
|
| 31 |
+
- `fet_024_base_articulation` v0.1.0
|
| 32 |
+
|
| 33 |
+
And for FET004/FET006 the only `_BASE_NEUTRAL` / `_BASE_USDPREVIEW`
|
| 34 |
+
forms are registered, not the `_ROBOT_PHYSX` / `_BASE_MDL` variants the
|
| 35 |
+
profiles ask for.
|
| 36 |
+
|
| 37 |
+
**Root cause.** Profile entries in
|
| 38 |
+
`<foundations>/nv_core/sr_specs/docs/profiles/profiles.toml` reference
|
| 39 |
+
variant feature IDs that the local foundations registry doesn't ship — e.g.
|
| 40 |
+
profiles ask for `FET021_ROBOT_CORE_RUNNABLE`, `FET022_DRIVEN_JOINTS_PHYSX`,
|
| 41 |
+
`FET004_ROBOT_PHYSX`, `FET024_BASE_ARTICULATION_NEUTRAL`. What is actually
|
| 42 |
+
registered (after `repo usd_profiles_codegen` runs against our checkout) is
|
| 43 |
+
only the unsuffixed `_BASE_NEUTRAL` / `_BASE_USDPREVIEW` / `_CORE` /
|
| 44 |
+
`_BASE_ISAACSIM` variants. The variant features the profiles ask for are
|
| 45 |
+
silently absent.
|
| 46 |
+
|
| 47 |
+
`omni.asset_validator.ProfileRegistry` resolves features by ID + version and
|
| 48 |
+
silently drops any it can't find. There's no warning emitted at profile
|
| 49 |
+
load time.
|
| 50 |
+
|
| 51 |
+
**Verified the gap is local, not universal.** Sample reports from another
|
| 52 |
+
SimReady environment (Fanuc content, sr9iar.usd.usd) show all five
|
| 53 |
+
declared `Robot-Body-Runnable` features actually loaded —
|
| 54 |
+
`FET001_BASE_NEUTRAL` (failed), plus `FET004_ROBOT_PHYSX`,
|
| 55 |
+
`FET021_ROBOT_CORE_RUNNABLE`, `FET022_DRIVEN_JOINTS_PHYSX`,
|
| 56 |
+
`FET024_BASE_ARTICULATION_PHYSX`, `FET024_BASE_ARTICULATION_NEUTRAL`
|
| 57 |
+
(passed). So the variants *are* registrable; our local codegen output
|
| 58 |
+
just doesn't include them.
|
| 59 |
+
|
| 60 |
+
**Where the variants live.** The foundation source ships JSON variant
|
| 61 |
+
definitions next to each feature's markdown. For FET004:
|
| 62 |
+
|
| 63 |
+
```
|
| 64 |
+
docs/features/FET_004-simulate_multi_body_physics.md (md, _BASE_NEUTRAL only)
|
| 65 |
+
docs/features/FET_004_base_neutral-0.1.0-...json
|
| 66 |
+
docs/features/FET_004_base_physx-0.1.0-...json
|
| 67 |
+
docs/features/FET_004_base_physx-0.2.0-...json
|
| 68 |
+
docs/features/FET_004_robot_physx-0.1.0-...json <- this is the one profiles ask for
|
| 69 |
+
docs/features/FET_004_robot_physx-0.2.0-...json
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Each JSON declares an `id` (e.g. `FET004_ROBOT_PHYSX`), `version`, `path`,
|
| 73 |
+
and a `requirements` list. **Our `repo usd_profiles_codegen` run only
|
| 74 |
+
parses the `.md` files** — the generated
|
| 75 |
+
`_build/python/omni/capabilities/_features.py` contains zero
|
| 76 |
+
`_ROBOT_PHYSX` / `_BASE_PHYSX` / `_RUNNABLE` entries even though the JSON
|
| 77 |
+
sources are right there.
|
| 78 |
+
|
| 79 |
+
**How to address.** Three paths, in priority order:
|
| 80 |
+
|
| 81 |
+
1. **Codegen — pick up the JSON variant files.** The foundations
|
| 82 |
+
`repo usd_profiles_codegen` tool needs to also enumerate
|
| 83 |
+
`FET_*_*-*-...json` files in the features directory and emit Feature
|
| 84 |
+
entries for each, not just the `Internal ID` declared in the `.md`.
|
| 85 |
+
This closes the gap without changing source data — the variants
|
| 86 |
+
already exist.
|
| 87 |
+
2. **Validator — emit a warning when a profile entry doesn't resolve.**
|
| 88 |
+
`omni.asset_validator.ProfileRegistry` silently drops missing
|
| 89 |
+
features. A `WARNING:` log at profile-load time would have made this
|
| 90 |
+
bug visible from day one.
|
| 91 |
+
3. **Documentation** — once #1 lands, `repo.toml`'s codegen description
|
| 92 |
+
should call out that JSON variants are part of the source of truth so
|
| 93 |
+
downstream tooling doesn't repeat the mistake.
|
| 94 |
+
|
| 95 |
+
Open this with the SimReady foundations team
|
| 96 |
+
(`#omni-simready` / `#simready-next-support`); cite the discrepancy
|
| 97 |
+
between our local codegen output and the working Fanuc environment
|
| 98 |
+
sample.
|
| 99 |
+
|
| 100 |
+
**In-repo workaround.** The dashboard now surfaces the coverage gap
|
| 101 |
+
explicitly: when a profile declares more features than the validator
|
| 102 |
+
loaded, a banner at the top lists the missing IDs and tells the user which
|
| 103 |
+
ones got silently dropped. The validation result the report shows is
|
| 104 |
+
genuinely against the *loaded* feature set — running with
|
| 105 |
+
`Robot-Body-Runnable` does *not* check the four declared-but-missing
|
| 106 |
+
features.
|
| 107 |
+
|
| 108 |
+
**Status.** Local patch applied — see "Local patch applied" below.
|
| 109 |
+
Upstream codegen fix still needed. Banner workaround still in place
|
| 110 |
+
for environments without the patch.
|
| 111 |
+
|
| 112 |
+
### Local patch applied
|
| 113 |
+
|
| 114 |
+
A patch lives in
|
| 115 |
+
`plugins/simready-report/skills/simready-report/validate.py` —
|
| 116 |
+
function `_patch_register_json_variant_features()`. It runs at module
|
| 117 |
+
load (so worker processes pick it up too) right after
|
| 118 |
+
`import omni.asset_validator as oav`. It is loud about itself: every
|
| 119 |
+
run prints a `[PATCH P1] FeatureRegistry: +N JSON-variant feature(s)
|
| 120 |
+
... ProfileRegistry: rebuilt feature lists for M profile(s) ...`
|
| 121 |
+
line so it's never invisible.
|
| 122 |
+
|
| 123 |
+
**Concretely on the Yaskawa-local checkout** the patch lifts:
|
| 124 |
+
- FeatureRegistry: 11 features → 30 features (+19 from JSON variants).
|
| 125 |
+
- Robot-Body-Runnable v1.0.0: 1 feature / 8 requirements → **5 / 41**.
|
| 126 |
+
- 11 profiles got their `.features` lists rebuilt; +24 feature
|
| 127 |
+
references appended in total.
|
| 128 |
+
|
| 129 |
+
**Exactly what the patch does**, step by step:
|
| 130 |
+
|
| 131 |
+
1. **Discover variant JSON files.** Globs
|
| 132 |
+
`<foundations>/nv_core/sr_specs/docs/features/FET_*.json`. The
|
| 133 |
+
filename styles in this repo are inconsistent
|
| 134 |
+
(`FET_021-robot_core_isaac-0.1.0.json`,
|
| 135 |
+
`FET_021_robot_core_runnable_0.2.0.json`,
|
| 136 |
+
`FET_004_base_physx-0.2.0-simulate_multi_body_phyics.json`), so we
|
| 137 |
+
ignore the filename and read the `id` + `version` fields from the
|
| 138 |
+
JSON content.
|
| 139 |
+
|
| 140 |
+
2. **Build a Requirement code lookup.** `r.code -> r` for every
|
| 141 |
+
`Requirement` in `omni.asset_validator.RequirementsRegistry()`.
|
| 142 |
+
The JSON variants list dotted code strings like `"RB.COL.003"`,
|
| 143 |
+
`"DJ.001"`, `"BA.002"` — those are looked up here.
|
| 144 |
+
|
| 145 |
+
3. **Construct a Feature-protocol object per JSON variant.**
|
| 146 |
+
`omni.asset_validator._features.Feature` is a `typing.Protocol`
|
| 147 |
+
declaring `id: str, version: str, path: str, requirements: list`.
|
| 148 |
+
We use a frozen dataclass that satisfies the protocol structurally:
|
| 149 |
+
|
| 150 |
+
```python
|
| 151 |
+
@dataclass(frozen=True)
|
| 152 |
+
class _PatchedFeature:
|
| 153 |
+
id: str
|
| 154 |
+
version: str
|
| 155 |
+
path: str
|
| 156 |
+
requirements: list
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
4. **Skip duplicates and unresolvable variants.** If
|
| 160 |
+
`FeatureRegistry.find(fid, fver)` already returns something, skip
|
| 161 |
+
(don't double-register). If any of the JSON's requirement code
|
| 162 |
+
strings doesn't resolve in the code lookup, skip the whole feature
|
| 163 |
+
and record it in `unresolved_codes`. (Currently zero unresolved on
|
| 164 |
+
our checkout — every JSON variant's required codes exist.)
|
| 165 |
+
|
| 166 |
+
5. **Register via `FeatureRegistry().add(feat)`.** The internal
|
| 167 |
+
`create_key` uses `.id` + `.version` so our dataclass satisfies it.
|
| 168 |
+
|
| 169 |
+
6. **Re-resolve every profile's feature list — the critical step.**
|
| 170 |
+
`Profile` is a frozen dataclass, so we can't reassign
|
| 171 |
+
`profile.features`. But the field is a *mutable list*, and the
|
| 172 |
+
freeze only prevents field reassignment, not mutation of held
|
| 173 |
+
collections. So for every entry in `profiles.toml`'s declared
|
| 174 |
+
features that isn't already in the live profile, we look it up in
|
| 175 |
+
the now-fuller `FeatureRegistry` and `profile.features.append(...)`
|
| 176 |
+
in place. **Without this step the patch is invisible** — adding to
|
| 177 |
+
`FeatureRegistry` after profiles are already built doesn't
|
| 178 |
+
retroactively join existing Profile objects.
|
| 179 |
+
|
| 180 |
+
**What the patch does NOT touch**: foundation source files, the
|
| 181 |
+
codegen tool, generated `_features.py`, profile definitions in
|
| 182 |
+
`profiles.toml`, or any rule code. The local patch is purely in our
|
| 183 |
+
validation runner.
|
| 184 |
+
|
| 185 |
+
**Remove the patch when**: the foundation `repo usd_profiles_codegen`
|
| 186 |
+
tool is updated to enumerate JSON variant files alongside the
|
| 187 |
+
markdown. Once `_features.py` carries every variant on its own,
|
| 188 |
+
delete `_patch_register_json_variant_features()` and its call site.
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## P2 — Public ur10 sample asset fails current foundation specs
|
| 193 |
+
|
| 194 |
+
**Symptom.** Running `/simready-report` against the public ur10 sample
|
| 195 |
+
in
|
| 196 |
+
`simready_foundations/sample_content/common_assets/robots_general/ur10/`
|
| 197 |
+
(release `2026.04.0`, commit `805d2c5`) reports failures on every
|
| 198 |
+
variant when validated against its declared profile (or against
|
| 199 |
+
Robot-Body-Runnable cross-profile). Concretely, against each interface
|
| 200 |
+
USD's declared profile, with `--use-kit` so PhysX rules actually run:
|
| 201 |
+
|
| 202 |
+
| Variant interface | Declared profile | Result | Failing requirements |
|
| 203 |
+
|---|---|---|---|
|
| 204 |
+
| `simready_usd/ur10.usda` | Robot-Body-Neutral v1.0.0 | **PASS** | — |
|
| 205 |
+
| `simready_physx_usd/ur10.usda` | Robot-Body-Physx v1.0.0 | **FAIL** | (profile evaluates 0 features against the asset; `features_summary` empty in JSON output) |
|
| 206 |
+
| `simready_isaac_usd/ur10.usda` | Robot-Body-Isaac v1.0.0 | **FAIL** | `RC.005` (`VerifyRobotPhysicsAttributesSourceLayer`), `RC.008` (`robot-type`), `RC.009` (`root-joint-pinned`), `DJ.010`, `ISA.001` |
|
| 207 |
+
|
| 208 |
+
Under the cross-profile `Robot-Body-Runnable` run, Neutral and PhysX
|
| 209 |
+
variants additionally light up with `RC.007` (`robot-schema` — no
|
| 210 |
+
`IsaacRobotAPI` on the default prim) because neither of those variants
|
| 211 |
+
applies `IsaacRobotAPI`, only the Isaac variant does.
|
| 212 |
+
|
| 213 |
+
**Root cause.** Asset-side metadata gaps and layer-organization issues
|
| 214 |
+
in the shipped public sample, *not* validator or skill code. The asset
|
| 215 |
+
file is tracked by Git LFS at the same OID as the release (`b30f9c1b…`
|
| 216 |
+
for `simready_isaac_usd/payloads/base.usda`), so no in-tree edit
|
| 217 |
+
caused this. Each asset's `customLayerData.SimReady_Metadata.validation`
|
| 218 |
+
records `validated_features` against a snapshot dated **2026-01-09**;
|
| 219 |
+
between then and the current `805d2c5` foundation specs the rules in
|
| 220 |
+
question were tightened (or added), so the asset that passed in
|
| 221 |
+
January no longer passes today. Specifically:
|
| 222 |
+
|
| 223 |
+
- **`RC.008` (`robot-type`)** — `simready_isaac_usd/payloads/base.usda`
|
| 224 |
+
lines 81-83 declare `token isaac:robotType` with **no value**. The
|
| 225 |
+
current rule requires a valid schema-defined token (e.g.
|
| 226 |
+
`"Manipulator"`, `"End Effector"`); the placeholder `"Default"` and
|
| 227 |
+
the absent-value case both fail.
|
| 228 |
+
- **`RC.009` (`root-joint-pinned`)** — depends on `isaac:robotType`
|
| 229 |
+
to decide whether the root joint must be pinned. The asset's
|
| 230 |
+
`root_joint` *is* a `PhysicsFixedJoint` with empty `physics:body0`
|
| 231 |
+
(pinned to world, correct for a Manipulator), but without a valid
|
| 232 |
+
`robotType` value the rule can't evaluate and fails.
|
| 233 |
+
- **`RC.005` (`VerifyRobotPhysicsAttributesSourceLayer`)** — physics
|
| 234 |
+
attributes (`physics:axis`, `physics:mass`, `physics:centerOfMass`,
|
| 235 |
+
`physics:diagonalInertia`, `physics:principalAxes`) on each robot
|
| 236 |
+
link are authored in *both* `payloads/base.usda` and
|
| 237 |
+
`payloads/Physics/physics.usda`. The current rule wants them in the
|
| 238 |
+
physics payload only.
|
| 239 |
+
- **PhysX variant empty `features_summary`** — Robot-Body-Physx v1.0.0
|
| 240 |
+
evaluates zero features against the asset, suggesting the profile's
|
| 241 |
+
declared feature set no longer matches what the asset has stamped
|
| 242 |
+
(related to P1, but for a different feature family).
|
| 243 |
+
|
| 244 |
+
**How to address.** Asset fixes belong in
|
| 245 |
+
`github.com/NVIDIA/simready-foundation`, not here:
|
| 246 |
+
|
| 247 |
+
1. **`RC.008`/`RC.009`** — *verified one-liner*. In
|
| 248 |
+
`simready_isaac_usd/payloads/base.usda` line 81, change
|
| 249 |
+
`token isaac:robotType (…)` to
|
| 250 |
+
`token isaac:robotType = "Manipulator" (…)`. Verified locally with
|
| 251 |
+
`--use-kit`:
|
| 252 |
+
- Against `Robot-Body-Isaac`, `FET021_ROBOT_CORE_ISAAC` failing
|
| 253 |
+
codes went from `[RC.005, RC.008, RC.009]` →
|
| 254 |
+
**`[RC.003, RC.005]`** (RC.008 and RC.009 cleared; RC.003 was
|
| 255 |
+
previously cascade-suppressed and now surfaces).
|
| 256 |
+
- Against `Robot-Body-Runnable` cross-profile, the **Isaac variant
|
| 257 |
+
now PASSES** (`FET021_ROBOT_CORE_RUNNABLE` only requires
|
| 258 |
+
RC.007/008/009 — all three OK once `isaac:robotType` is set,
|
| 259 |
+
because IsaacRobotAPI is already applied with the correct
|
| 260 |
+
joints/links relationships and the root joint is pinned via
|
| 261 |
+
empty `physics:body0`). Cross-profile summary moved from
|
| 262 |
+
`0 passed / 3 failed` → `1 passed / 2 failed` (Isaac passes,
|
| 263 |
+
Neutral and PhysX still fail RC.007/008/009 because they don't
|
| 264 |
+
apply IsaacRobotAPI at all).
|
| 265 |
+
2. **`RC.005`**: move every `physics:*` attribute currently authored
|
| 266 |
+
on links in `payloads/base.usda` into `payloads/Physics/physics.usda`
|
| 267 |
+
only (or refactor so `base.usda` carries no physics attributes at
|
| 268 |
+
all). This is a meaningful layer-organization change, not a
|
| 269 |
+
one-liner. Still failing after fix 1 (88 RC.005 issues remain).
|
| 270 |
+
3. **`RC.003` (`RobotNaming`)** — surfaced *only after* fix 1. The
|
| 271 |
+
rule compares the **immediate folder name** (`simready_isaac_usd`)
|
| 272 |
+
to the **interface USD stem** (`ur10`). Under the standard SimReady
|
| 273 |
+
packaging convention these never match — every Isaac variant of
|
| 274 |
+
every SimReady asset fails RC.003. Likely a rule bug (should
|
| 275 |
+
probably look at the *bundle* parent, e.g. `ur10/`, not the
|
| 276 |
+
variant folder) or a misalignment between rule and packaging spec.
|
| 277 |
+
Worth raising upstream.
|
| 278 |
+
4. **`RC.007` (cross-profile only)**: if the intent is that the
|
| 279 |
+
Neutral and PhysX variants should also satisfy Robot-Body-Runnable,
|
| 280 |
+
apply `IsaacRobotAPI` to their default prims with the same
|
| 281 |
+
`isaac:physics:robotJoints`/`robotLinks` relationships used in the
|
| 282 |
+
Isaac variant. If the intent is that each variant only ever
|
| 283 |
+
validates against its own declared profile, this is not needed.
|
| 284 |
+
5. **PhysX empty features**: align the variant USDs and Robot-Body-Physx
|
| 285 |
+
profile's expected feature set — likely needs the assets to carry
|
| 286 |
+
the variant feature IDs that the profile declares (intersect with
|
| 287 |
+
P1's missing-feature list once that's resolved). Also: `validate.py`
|
| 288 |
+
currently emits an empty `.reports/<…>/` dir when zero features
|
| 289 |
+
match (no `index.html` / `results.json` written). Tracked
|
| 290 |
+
separately as a `validate.py` robustness issue.
|
| 291 |
+
|
| 292 |
+
**In-repo workaround.** None — this is downstream of the validator. The
|
| 293 |
+
`simready-report` skill faithfully reports what the rules say. The
|
| 294 |
+
dashboard's "Failing requirements" panel surfaces the offending codes
|
| 295 |
+
so users can map each failure back to its requirement doc.
|
| 296 |
+
|
| 297 |
+
**Status.** Open against the foundations repo. Filed for tracking via
|
| 298 |
+
this MR; no action in `simready-explorer` is needed beyond surfacing
|
| 299 |
+
the issue here. Fix 1 above is **verified to work locally** (one-line
|
| 300 |
+
edit to `base.usda`) — upstream PR against `NVIDIA/simready-foundation`
|
| 301 |
+
is the right channel to land it. Fixes 2–5 still open. Re-validate
|
| 302 |
+
after the foundation team updates the sample assets, the rule code
|
| 303 |
+
(notably `RC.003 RobotNaming`), or relaxes the unmatched-feature
|
| 304 |
+
behavior.
|
| 305 |
+
|
| 306 |
+
---
|
| 307 |
+
|
| 308 |
+
## How to file a new entry here
|
| 309 |
+
|
| 310 |
+
Use this skeleton:
|
| 311 |
+
|
| 312 |
+
```
|
| 313 |
+
## P<N> — <one-line title>
|
| 314 |
+
|
| 315 |
+
**Symptom.** What the user/reader sees that's wrong.
|
| 316 |
+
|
| 317 |
+
**Root cause.** Where the bug actually lives (which repo / file / step).
|
| 318 |
+
|
| 319 |
+
**How to address.** Concrete fix path(s), upstream or local.
|
| 320 |
+
|
| 321 |
+
**In-repo workaround.** What this repo does today to mitigate, if anything.
|
| 322 |
+
|
| 323 |
+
**Status.** Open / mitigated / fixed (link to commit).
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
Bump the number; don't reuse retired ones — the git history is the
|
| 327 |
+
canonical record once an entry is resolved and removed.
|
tools/validation/README.md
CHANGED
|
@@ -1,139 +1,139 @@
|
|
| 1 |
-
# Validation reporting — staging branch
|
| 2 |
-
|
| 3 |
-
## Install the Claude Code plugin
|
| 4 |
-
|
| 5 |
-
The `simready-report` plugin ships as a Claude Code marketplace under
|
| 6 |
-
`validation/.claude-plugin/`. From a checkout of this repo, run inside Claude Code:
|
| 7 |
-
|
| 8 |
-
```
|
| 9 |
-
/plugin marketplace add <path-to-repo>/validation
|
| 10 |
-
/plugin install simready-report@simready-playbook
|
| 11 |
-
```
|
| 12 |
-
|
| 13 |
-
That registers two slash commands: `/simready-report` and `/simready-package`.
|
| 14 |
-
You still need a Python with the SimReady runtime deps and the foundations + sdk
|
| 15 |
-
checkouts on disk — see [To run the demo as-is](#to-run-the-demo-as-is) below.
|
| 16 |
-
|
| 17 |
-
> [!IMPORTANT]
|
| 18 |
-
> ## Read [`PROBLEMS.md`](PROBLEMS.md) before trusting any report
|
| 19 |
-
>
|
| 20 |
-
> Reports produced by this prototype lean on a **local patch** for
|
| 21 |
-
> [PROBLEMS.md P1](PROBLEMS.md): without it, every Robot-Body profile
|
| 22 |
-
> silently drops 4 of its 5 declared features at load time, because
|
| 23 |
-
> the foundation `repo usd_profiles_codegen` step doesn't read the
|
| 24 |
-
> JSON variant definitions next to each feature's markdown. A
|
| 25 |
-
> "passing" report against `Robot-Body-Runnable` would only have
|
| 26 |
-
> checked 1 feature out of 5.
|
| 27 |
-
>
|
| 28 |
-
> The patch lives in
|
| 29 |
-
> `plugins/simready-report/skills/simready-report/validate.py` and is
|
| 30 |
-
> loud about itself — every run prints a `[PATCH P1]` line. With the
|
| 31 |
-
> patch active, `Robot-Body-Runnable` resolves to **5/5 features, 41
|
| 32 |
-
> requirements** instead of 1/8.
|
| 33 |
-
>
|
| 34 |
-
> The dashboard's **Caveats** panel still surfaces the gap if anyone
|
| 35 |
-
> runs without the patch.
|
| 36 |
-
>
|
| 37 |
-
> [`PROBLEMS.md`](PROBLEMS.md) **P1** has the full root cause, the
|
| 38 |
-
> patch walkthrough, and the upstream fix path. The patch is meant to
|
| 39 |
-
> be temporary — once foundations codegen is fixed, delete
|
| 40 |
-
> `_patch_register_json_variant_features()` and its call site.
|
| 41 |
-
|
| 42 |
-
---
|
| 43 |
-
|
| 44 |
-
> **Status: demo / review staging.** Not part of `simready-explorer` proper yet.
|
| 45 |
-
>
|
| 46 |
-
> This directory is a verbatim copy of the prototype work currently living at
|
| 47 |
-
> [`loginowskid/simready-playbook`](https://github.com/loginowskid/simready-playbook),
|
| 48 |
-
> dropped here on the `dev/dloginowski/validation-reporting` branch so the
|
| 49 |
-
> SimReady team can review the artifact before we refactor it into the repo's
|
| 50 |
-
> existing library + skills patterns.
|
| 51 |
-
|
| 52 |
-
## What this is
|
| 53 |
-
|
| 54 |
-
A Claude-Code-driven validation reporting pipeline for SimReady customer assets.
|
| 55 |
-
On top of the existing `omni.asset_validator` engine and SimReady profile registry,
|
| 56 |
-
it adds:
|
| 57 |
-
|
| 58 |
-
- **HTML dashboard** with summary cards, per-asset feature pass/fail, an
|
| 59 |
-
asset filter, and a "Other failed requirements" panel for issues outside the
|
| 60 |
-
loaded profile.
|
| 61 |
-
- **External-dependencies provenance report** — walks USD composition arcs per
|
| 62 |
-
asset, classifies each layer as internal/external relative to the target,
|
| 63 |
-
records actions taken to obtain each.
|
| 64 |
-
- **Foundation doc rendering** — renders the markdown source for every
|
| 65 |
-
feature/requirement code referenced by the report into HTML inside the output.
|
| 66 |
-
- **Auto-packaging convenience** — for a raw asset bundle under
|
| 67 |
-
`assets_to_validate/<name>/`, runs `simready ingest usd` per interface USD
|
| 68 |
-
before validating against the packaged tree.
|
| 69 |
-
- **Spec selector** — `--list-profiles` enumerates registered profiles so the
|
| 70 |
-
user can pick a SimReady profile interactively.
|
| 71 |
-
- **Parallel asset validation** — `ProcessPoolExecutor` workers, default auto.
|
| 72 |
-
|
| 73 |
-
Two slash commands ship together as a single Claude Code plugin:
|
| 74 |
-
|
| 75 |
-
- `/simready-report` — validate + dashboard + provenance + docs.
|
| 76 |
-
- `/simready-package` — standalone packaging (no validation).
|
| 77 |
-
|
| 78 |
-
## Layout (mirrors the source repo)
|
| 79 |
-
|
| 80 |
-
```
|
| 81 |
-
validation/
|
| 82 |
-
|
| 83 |
-
├── marketplace.json # Claude Code marketplace manifest
|
| 84 |
-
├── plugins/simready-report/
|
| 85 |
-
│ ├── plugin.json
|
| 86 |
-
│ └── skills/
|
| 87 |
-
│ ├── simready-report/ # HTML dashboard + validation
|
| 88 |
-
│ └── simready-package/ # standalone packaging
|
| 89 |
-
└── playbooks/
|
| 90 |
-
└── foundations-deviations.md # observed gaps between specs and asset_validator
|
| 91 |
-
```
|
| 92 |
-
|
| 93 |
-
## Why it doesn't fit the repo's existing patterns yet
|
| 94 |
-
|
| 95 |
-
`simready-explorer` already ships `source/libraries/validate/` with a clean
|
| 96 |
-
public API (`validate_asset`, `validate_asset_list`, etc.) and three skills in
|
| 97 |
-
the `agentskills.io/specification` format (`validate-asset`, `validate-batch`,
|
| 98 |
-
`validate-metadata`). The work in this `validation/` directory:
|
| 99 |
-
|
| 100 |
-
- Reinvents a thinner version of `simready.validate.validate_asset_list` —
|
| 101 |
-
this is duplication that should be removed during integration.
|
| 102 |
-
- Uses a different skill format (Claude Code marketplace plugin) — should be
|
| 103 |
-
rewritten as `agentskills.io` skills that call into the library.
|
| 104 |
-
- Is a standalone-script architecture (`python validate.py …`) — should be
|
| 105 |
-
exposed via the existing `repo.bat` / library entry points.
|
| 106 |
-
|
| 107 |
-
## Proposed integration after review
|
| 108 |
-
|
| 109 |
-
1. New skill `validate-report` under
|
| 110 |
-
`source/libraries/validate/src/validate/skills/validate-report/` —
|
| 111 |
-
instructional doc that drives a new `simready.validate.report` module.
|
| 112 |
-
2. New module `source/libraries/validate/src/validate/report/` containing the
|
| 113 |
-
dashboard renderer, doc renderer, external-deps tracker, thumbnail mirrorer
|
| 114 |
-
— extracted from `report.py` + `external_deps.py` and adapted to consume
|
| 115 |
-
`AssetValidationResult` instead of the raw `omni.asset_validator` issue
|
| 116 |
-
stream.
|
| 117 |
-
3. Optional new skill `validate-package` — thin wrapper around
|
| 118 |
-
`simready ingest usd` (independent of validation).
|
| 119 |
-
4. Drop `bootstrap.ps1` / `marketplace.json` / Claude-Code-plugin layout — not
|
| 120 |
-
relevant here; the team uses `repo.bat` + `_build/` flow.
|
| 121 |
-
|
| 122 |
-
## To run the demo as-is
|
| 123 |
-
|
| 124 |
-
The scripts in this directory are self-contained and need a Python with the
|
| 125 |
-
SimReady runtime deps installed plus the foundations + sdk checkouts on disk.
|
| 126 |
-
Easiest path on Windows: `& bootstrap.ps1` (creates a venv at
|
| 127 |
-
`%LOCALAPPDATA%\simready\` and persists `SIMREADY_PYTHON` /
|
| 128 |
-
`SIMREADY_FOUNDATIONS_PATH` / `SIMREADY_SDK_PATH` env vars). On other systems
|
| 129 |
-
or custom layouts, install the deps into your Python and set those env vars
|
| 130 |
-
manually.
|
| 131 |
-
|
| 132 |
-
After that:
|
| 133 |
-
|
| 134 |
-
```
|
| 135 |
-
python plugins/simready-report/skills/simready-report/validate.py \
|
| 136 |
-
<path-to-asset-bundle> --profile Robot-Body-Isaac --version 1.0.0
|
| 137 |
-
```
|
| 138 |
-
|
| 139 |
-
Output lands at `<bundle>/.reports/<bundle-name>.<profile>/index.html`.
|
|
|
|
| 1 |
+
# Validation reporting — staging branch
|
| 2 |
+
|
| 3 |
+
## Install the Claude Code plugin
|
| 4 |
+
|
| 5 |
+
The `simready-report` plugin ships as a Claude Code marketplace under
|
| 6 |
+
`validation/.claude-plugin/`. From a checkout of this repo, run inside Claude Code:
|
| 7 |
+
|
| 8 |
+
```
|
| 9 |
+
/plugin marketplace add <path-to-repo>/validation
|
| 10 |
+
/plugin install simready-report@simready-playbook
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
That registers two slash commands: `/simready-report` and `/simready-package`.
|
| 14 |
+
You still need a Python with the SimReady runtime deps and the foundations + sdk
|
| 15 |
+
checkouts on disk — see [To run the demo as-is](#to-run-the-demo-as-is) below.
|
| 16 |
+
|
| 17 |
+
> [!IMPORTANT]
|
| 18 |
+
> ## Read [`PROBLEMS.md`](PROBLEMS.md) before trusting any report
|
| 19 |
+
>
|
| 20 |
+
> Reports produced by this prototype lean on a **local patch** for
|
| 21 |
+
> [PROBLEMS.md P1](PROBLEMS.md): without it, every Robot-Body profile
|
| 22 |
+
> silently drops 4 of its 5 declared features at load time, because
|
| 23 |
+
> the foundation `repo usd_profiles_codegen` step doesn't read the
|
| 24 |
+
> JSON variant definitions next to each feature's markdown. A
|
| 25 |
+
> "passing" report against `Robot-Body-Runnable` would only have
|
| 26 |
+
> checked 1 feature out of 5.
|
| 27 |
+
>
|
| 28 |
+
> The patch lives in
|
| 29 |
+
> `plugins/simready-report/skills/simready-report/validate.py` and is
|
| 30 |
+
> loud about itself — every run prints a `[PATCH P1]` line. With the
|
| 31 |
+
> patch active, `Robot-Body-Runnable` resolves to **5/5 features, 41
|
| 32 |
+
> requirements** instead of 1/8.
|
| 33 |
+
>
|
| 34 |
+
> The dashboard's **Caveats** panel still surfaces the gap if anyone
|
| 35 |
+
> runs without the patch.
|
| 36 |
+
>
|
| 37 |
+
> [`PROBLEMS.md`](PROBLEMS.md) **P1** has the full root cause, the
|
| 38 |
+
> patch walkthrough, and the upstream fix path. The patch is meant to
|
| 39 |
+
> be temporary — once foundations codegen is fixed, delete
|
| 40 |
+
> `_patch_register_json_variant_features()` and its call site.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
> **Status: demo / review staging.** Not part of `simready-explorer` proper yet.
|
| 45 |
+
>
|
| 46 |
+
> This directory is a verbatim copy of the prototype work currently living at
|
| 47 |
+
> [`loginowskid/simready-playbook`](https://github.com/loginowskid/simready-playbook),
|
| 48 |
+
> dropped here on the `dev/dloginowski/validation-reporting` branch so the
|
| 49 |
+
> SimReady team can review the artifact before we refactor it into the repo's
|
| 50 |
+
> existing library + skills patterns.
|
| 51 |
+
|
| 52 |
+
## What this is
|
| 53 |
+
|
| 54 |
+
A Claude-Code-driven validation reporting pipeline for SimReady customer assets.
|
| 55 |
+
On top of the existing `omni.asset_validator` engine and SimReady profile registry,
|
| 56 |
+
it adds:
|
| 57 |
+
|
| 58 |
+
- **HTML dashboard** with summary cards, per-asset feature pass/fail, an
|
| 59 |
+
asset filter, and a "Other failed requirements" panel for issues outside the
|
| 60 |
+
loaded profile.
|
| 61 |
+
- **External-dependencies provenance report** — walks USD composition arcs per
|
| 62 |
+
asset, classifies each layer as internal/external relative to the target,
|
| 63 |
+
records actions taken to obtain each.
|
| 64 |
+
- **Foundation doc rendering** — renders the markdown source for every
|
| 65 |
+
feature/requirement code referenced by the report into HTML inside the output.
|
| 66 |
+
- **Auto-packaging convenience** — for a raw asset bundle under
|
| 67 |
+
`assets_to_validate/<name>/`, runs `simready ingest usd` per interface USD
|
| 68 |
+
before validating against the packaged tree.
|
| 69 |
+
- **Spec selector** — `--list-profiles` enumerates registered profiles so the
|
| 70 |
+
user can pick a SimReady profile interactively.
|
| 71 |
+
- **Parallel asset validation** — `ProcessPoolExecutor` workers, default auto.
|
| 72 |
+
|
| 73 |
+
Two slash commands ship together as a single Claude Code plugin:
|
| 74 |
+
|
| 75 |
+
- `/simready-report` — validate + dashboard + provenance + docs.
|
| 76 |
+
- `/simready-package` — standalone packaging (no validation).
|
| 77 |
+
|
| 78 |
+
## Layout (mirrors the source repo)
|
| 79 |
+
|
| 80 |
+
```
|
| 81 |
+
validation/
|
| 82 |
+
��── bootstrap.ps1 # Windows convenience installer
|
| 83 |
+
├── marketplace.json # Claude Code marketplace manifest
|
| 84 |
+
├── plugins/simready-report/
|
| 85 |
+
│ ├── plugin.json
|
| 86 |
+
│ └── skills/
|
| 87 |
+
│ ├── simready-report/ # HTML dashboard + validation
|
| 88 |
+
│ └── simready-package/ # standalone packaging
|
| 89 |
+
└── playbooks/
|
| 90 |
+
└── foundations-deviations.md # observed gaps between specs and asset_validator
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
## Why it doesn't fit the repo's existing patterns yet
|
| 94 |
+
|
| 95 |
+
`simready-explorer` already ships `source/libraries/validate/` with a clean
|
| 96 |
+
public API (`validate_asset`, `validate_asset_list`, etc.) and three skills in
|
| 97 |
+
the `agentskills.io/specification` format (`validate-asset`, `validate-batch`,
|
| 98 |
+
`validate-metadata`). The work in this `validation/` directory:
|
| 99 |
+
|
| 100 |
+
- Reinvents a thinner version of `simready.validate.validate_asset_list` —
|
| 101 |
+
this is duplication that should be removed during integration.
|
| 102 |
+
- Uses a different skill format (Claude Code marketplace plugin) — should be
|
| 103 |
+
rewritten as `agentskills.io` skills that call into the library.
|
| 104 |
+
- Is a standalone-script architecture (`python validate.py …`) — should be
|
| 105 |
+
exposed via the existing `repo.bat` / library entry points.
|
| 106 |
+
|
| 107 |
+
## Proposed integration after review
|
| 108 |
+
|
| 109 |
+
1. New skill `validate-report` under
|
| 110 |
+
`source/libraries/validate/src/validate/skills/validate-report/` —
|
| 111 |
+
instructional doc that drives a new `simready.validate.report` module.
|
| 112 |
+
2. New module `source/libraries/validate/src/validate/report/` containing the
|
| 113 |
+
dashboard renderer, doc renderer, external-deps tracker, thumbnail mirrorer
|
| 114 |
+
— extracted from `report.py` + `external_deps.py` and adapted to consume
|
| 115 |
+
`AssetValidationResult` instead of the raw `omni.asset_validator` issue
|
| 116 |
+
stream.
|
| 117 |
+
3. Optional new skill `validate-package` — thin wrapper around
|
| 118 |
+
`simready ingest usd` (independent of validation).
|
| 119 |
+
4. Drop `bootstrap.ps1` / `marketplace.json` / Claude-Code-plugin layout — not
|
| 120 |
+
relevant here; the team uses `repo.bat` + `_build/` flow.
|
| 121 |
+
|
| 122 |
+
## To run the demo as-is
|
| 123 |
+
|
| 124 |
+
The scripts in this directory are self-contained and need a Python with the
|
| 125 |
+
SimReady runtime deps installed plus the foundations + sdk checkouts on disk.
|
| 126 |
+
Easiest path on Windows: `& bootstrap.ps1` (creates a venv at
|
| 127 |
+
`%LOCALAPPDATA%\simready\` and persists `SIMREADY_PYTHON` /
|
| 128 |
+
`SIMREADY_FOUNDATIONS_PATH` / `SIMREADY_SDK_PATH` env vars). On other systems
|
| 129 |
+
or custom layouts, install the deps into your Python and set those env vars
|
| 130 |
+
manually.
|
| 131 |
+
|
| 132 |
+
After that:
|
| 133 |
+
|
| 134 |
+
```
|
| 135 |
+
python plugins/simready-report/skills/simready-report/validate.py \
|
| 136 |
+
<path-to-asset-bundle> --profile Robot-Body-Isaac --version 1.0.0
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
Output lands at `<bundle>/.reports/<bundle-name>.<profile>/index.html`.
|
tools/validation/UPSTREAM.md
CHANGED
|
@@ -1,66 +1,66 @@
|
|
| 1 |
-
# Upstream sync
|
| 2 |
-
|
| 3 |
-
`tools/validation/` is a **verbatim fork** of the `validation/` tree from:
|
| 4 |
-
|
| 5 |
-
- **Repo:** `omniverse/kit-extensions/simready-explorer`
|
| 6 |
-
- **Host:** `gitlab-master.nvidia.com`
|
| 7 |
-
- **Branch:** `dev/dloginowski/validation-reporting`
|
| 8 |
-
- **URL:** https://gitlab-master.nvidia.com/omniverse/kit-extensions/simready-explorer/-/tree/dev/dloginowski/validation-reporting
|
| 9 |
-
|
| 10 |
-
It is **not** a git submodule. Upstream changes do not flow here automatically.
|
| 11 |
-
|
| 12 |
-
## Current pin
|
| 13 |
-
|
| 14 |
-
```
|
| 15 |
-
upstream_sha: 4bd42394685156d97f3e3bf371117f53e8dd8ab1
|
| 16 |
-
synced_at: 2026-05-22
|
| 17 |
-
synced_by: dloginowski
|
| 18 |
-
```
|
| 19 |
-
|
| 20 |
-
> Bump these three fields whenever you re-run `_sync.sh`.
|
| 21 |
-
|
| 22 |
-
## Check for drift
|
| 23 |
-
|
| 24 |
-
```bash
|
| 25 |
-
git ls-remote https://gitlab-master.nvidia.com/omniverse/kit-extensions/simready-explorer.git \
|
| 26 |
-
refs/heads/dev/dloginowski/validation-reporting
|
| 27 |
-
```
|
| 28 |
-
|
| 29 |
-
Compare the printed SHA to `upstream_sha` above. If they differ, upstream has
|
| 30 |
-
new commits we don't have.
|
| 31 |
-
|
| 32 |
-
## Pull a new version
|
| 33 |
-
|
| 34 |
-
```bash
|
| 35 |
-
bash tools/validation/_sync.sh
|
| 36 |
-
```
|
| 37 |
-
|
| 38 |
-
The script:
|
| 39 |
-
|
| 40 |
-
1. Shallow-clones the upstream branch to a temp dir.
|
| 41 |
-
2. Reports the new SHA and how many commits ahead of `upstream_sha` it is.
|
| 42 |
-
3. Rsyncs upstream `validation/*` over `tools/validation/*`, preserving
|
| 43 |
-
`UPSTREAM.md` and `_sync.sh` themselves.
|
| 44 |
-
4. Prints a diff stat so you can see what changed.
|
| 45 |
-
|
| 46 |
-
After running, review the diff, **manually update the pin block above**, then
|
| 47 |
-
commit:
|
| 48 |
-
|
| 49 |
-
```bash
|
| 50 |
-
git add tools/validation/
|
| 51 |
-
git commit -m "Sync tools/validation/ from upstream <short-sha>"
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
## When upstream lands in `simready-explorer` proper
|
| 55 |
-
|
| 56 |
-
The upstream README (`tools/validation/README.md`) calls this directory
|
| 57 |
-
"demo / review staging" and outlines the intended integration into
|
| 58 |
-
`simready-explorer`'s existing library + skills patterns. Once that lands:
|
| 59 |
-
|
| 60 |
-
1. Delete `tools/validation/`.
|
| 61 |
-
2. Replace the runner-side driver `tools/hf_watch/validate.py` with calls
|
| 62 |
-
into `simready-explorer`'s public CLI (likely `repo.bat validate` or
|
| 63 |
-
the `simready.validate` library entry points).
|
| 64 |
-
3. Drop this file.
|
| 65 |
-
|
| 66 |
-
Until then, treat this fork as a snapshot.
|
|
|
|
| 1 |
+
# Upstream sync
|
| 2 |
+
|
| 3 |
+
`tools/validation/` is a **verbatim fork** of the `validation/` tree from:
|
| 4 |
+
|
| 5 |
+
- **Repo:** `omniverse/kit-extensions/simready-explorer`
|
| 6 |
+
- **Host:** `gitlab-master.nvidia.com`
|
| 7 |
+
- **Branch:** `dev/dloginowski/validation-reporting`
|
| 8 |
+
- **URL:** https://gitlab-master.nvidia.com/omniverse/kit-extensions/simready-explorer/-/tree/dev/dloginowski/validation-reporting
|
| 9 |
+
|
| 10 |
+
It is **not** a git submodule. Upstream changes do not flow here automatically.
|
| 11 |
+
|
| 12 |
+
## Current pin
|
| 13 |
+
|
| 14 |
+
```
|
| 15 |
+
upstream_sha: 4bd42394685156d97f3e3bf371117f53e8dd8ab1
|
| 16 |
+
synced_at: 2026-05-22
|
| 17 |
+
synced_by: dloginowski
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
> Bump these three fields whenever you re-run `_sync.sh`.
|
| 21 |
+
|
| 22 |
+
## Check for drift
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
git ls-remote https://gitlab-master.nvidia.com/omniverse/kit-extensions/simready-explorer.git \
|
| 26 |
+
refs/heads/dev/dloginowski/validation-reporting
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Compare the printed SHA to `upstream_sha` above. If they differ, upstream has
|
| 30 |
+
new commits we don't have.
|
| 31 |
+
|
| 32 |
+
## Pull a new version
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
bash tools/validation/_sync.sh
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
The script:
|
| 39 |
+
|
| 40 |
+
1. Shallow-clones the upstream branch to a temp dir.
|
| 41 |
+
2. Reports the new SHA and how many commits ahead of `upstream_sha` it is.
|
| 42 |
+
3. Rsyncs upstream `validation/*` over `tools/validation/*`, preserving
|
| 43 |
+
`UPSTREAM.md` and `_sync.sh` themselves.
|
| 44 |
+
4. Prints a diff stat so you can see what changed.
|
| 45 |
+
|
| 46 |
+
After running, review the diff, **manually update the pin block above**, then
|
| 47 |
+
commit:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
git add tools/validation/
|
| 51 |
+
git commit -m "Sync tools/validation/ from upstream <short-sha>"
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## When upstream lands in `simready-explorer` proper
|
| 55 |
+
|
| 56 |
+
The upstream README (`tools/validation/README.md`) calls this directory
|
| 57 |
+
"demo / review staging" and outlines the intended integration into
|
| 58 |
+
`simready-explorer`'s existing library + skills patterns. Once that lands:
|
| 59 |
+
|
| 60 |
+
1. Delete `tools/validation/`.
|
| 61 |
+
2. Replace the runner-side driver `tools/hf_watch/validate.py` with calls
|
| 62 |
+
into `simready-explorer`'s public CLI (likely `repo.bat validate` or
|
| 63 |
+
the `simready.validate` library entry points).
|
| 64 |
+
3. Drop this file.
|
| 65 |
+
|
| 66 |
+
Until then, treat this fork as a snapshot.
|
tools/validation/_sync.sh
CHANGED
|
@@ -1,98 +1,98 @@
|
|
| 1 |
-
#!/usr/bin/env bash
|
| 2 |
-
# Sync tools/validation/ from the upstream GitLab branch.
|
| 3 |
-
#
|
| 4 |
-
# Run from the repo root:
|
| 5 |
-
# bash tools/validation/_sync.sh
|
| 6 |
-
#
|
| 7 |
-
# After running, review the diff, bump the pin block in
|
| 8 |
-
# tools/validation/UPSTREAM.md, and commit.
|
| 9 |
-
|
| 10 |
-
set -euo pipefail
|
| 11 |
-
|
| 12 |
-
UPSTREAM_URL="https://gitlab-master.nvidia.com/omniverse/kit-extensions/simready-explorer.git"
|
| 13 |
-
UPSTREAM_BRANCH="dev/dloginowski/validation-reporting"
|
| 14 |
-
UPSTREAM_PATH="validation"
|
| 15 |
-
LOCAL_PATH="tools/validation"
|
| 16 |
-
KEEP=(UPSTREAM.md _sync.sh)
|
| 17 |
-
|
| 18 |
-
if [[ ! -d "${LOCAL_PATH}" ]]; then
|
| 19 |
-
echo "expected ${LOCAL_PATH} to exist; are you in the repo root?" >&2
|
| 20 |
-
exit 1
|
| 21 |
-
fi
|
| 22 |
-
|
| 23 |
-
# Compare current pin to upstream HEAD.
|
| 24 |
-
current_pin="$(grep -oE 'upstream_sha:\s*[0-9a-f]+' "${LOCAL_PATH}/UPSTREAM.md" \
|
| 25 |
-
| awk '{print $2}' | head -n1 || true)"
|
| 26 |
-
upstream_sha="$(git ls-remote "${UPSTREAM_URL}" "refs/heads/${UPSTREAM_BRANCH}" \
|
| 27 |
-
| awk '{print $1}')"
|
| 28 |
-
|
| 29 |
-
if [[ -z "${upstream_sha}" ]]; then
|
| 30 |
-
echo "could not resolve upstream branch SHA — check network / auth" >&2
|
| 31 |
-
exit 2
|
| 32 |
-
fi
|
| 33 |
-
|
| 34 |
-
echo "current pin: ${current_pin:-(unset)}"
|
| 35 |
-
echo "upstream HEAD: ${upstream_sha}"
|
| 36 |
-
|
| 37 |
-
if [[ "${current_pin}" == "${upstream_sha}" ]]; then
|
| 38 |
-
echo "already in sync — nothing to do"
|
| 39 |
-
exit 0
|
| 40 |
-
fi
|
| 41 |
-
|
| 42 |
-
tmpdir="$(mktemp -d)"
|
| 43 |
-
trap 'rm -rf "${tmpdir}"' EXIT
|
| 44 |
-
|
| 45 |
-
echo ">> shallow-clone upstream"
|
| 46 |
-
git clone --depth 50 --branch "${UPSTREAM_BRANCH}" --single-branch \
|
| 47 |
-
"${UPSTREAM_URL}" "${tmpdir}/upstream" >/dev/null
|
| 48 |
-
|
| 49 |
-
# How many commits ahead of the current pin (best-effort; falls back to "?")
|
| 50 |
-
ahead="?"
|
| 51 |
-
if [[ -n "${current_pin}" ]]; then
|
| 52 |
-
if git -C "${tmpdir}/upstream" cat-file -e "${current_pin}" 2>/dev/null; then
|
| 53 |
-
ahead="$(git -C "${tmpdir}/upstream" rev-list --count "${current_pin}..HEAD")"
|
| 54 |
-
fi
|
| 55 |
-
fi
|
| 56 |
-
echo "commits ahead of pin: ${ahead}"
|
| 57 |
-
|
| 58 |
-
upstream_dir="${tmpdir}/upstream/${UPSTREAM_PATH}"
|
| 59 |
-
if [[ ! -d "${upstream_dir}" ]]; then
|
| 60 |
-
echo "upstream ${UPSTREAM_PATH}/ missing in branch ${UPSTREAM_BRANCH}" >&2
|
| 61 |
-
exit 3
|
| 62 |
-
fi
|
| 63 |
-
|
| 64 |
-
echo ">> rsync upstream → ${LOCAL_PATH}"
|
| 65 |
-
# Stash KEEP files so rsync --delete doesn't remove them.
|
| 66 |
-
keep_stash="$(mktemp -d)"
|
| 67 |
-
for f in "${KEEP[@]}"; do
|
| 68 |
-
if [[ -f "${LOCAL_PATH}/${f}" ]]; then
|
| 69 |
-
cp -p "${LOCAL_PATH}/${f}" "${keep_stash}/${f}"
|
| 70 |
-
fi
|
| 71 |
-
done
|
| 72 |
-
|
| 73 |
-
rsync -a --delete "${upstream_dir}/" "${LOCAL_PATH}/"
|
| 74 |
-
|
| 75 |
-
for f in "${KEEP[@]}"; do
|
| 76 |
-
if [[ -f "${keep_stash}/${f}" ]]; then
|
| 77 |
-
cp -p "${keep_stash}/${f}" "${LOCAL_PATH}/${f}"
|
| 78 |
-
fi
|
| 79 |
-
done
|
| 80 |
-
rm -rf "${keep_stash}"
|
| 81 |
-
|
| 82 |
-
echo
|
| 83 |
-
echo ">> changes:"
|
| 84 |
-
git -c color.ui=always diff --stat -- "${LOCAL_PATH}" | tail -20 || true
|
| 85 |
-
|
| 86 |
-
cat <<EOF
|
| 87 |
-
|
| 88 |
-
Sync staged. Next steps:
|
| 89 |
-
|
| 90 |
-
1. Bump the pin block in ${LOCAL_PATH}/UPSTREAM.md:
|
| 91 |
-
|
| 92 |
-
upstream_sha: ${upstream_sha}
|
| 93 |
-
synced_at: $(date -u +%Y-%m-%d)
|
| 94 |
-
synced_by: \$(git config user.name)
|
| 95 |
-
|
| 96 |
-
2. Review the diff: git diff -- ${LOCAL_PATH}
|
| 97 |
-
3. Commit: git add ${LOCAL_PATH} && git commit -m "Sync tools/validation/ from upstream ${upstream_sha:0:12}"
|
| 98 |
-
EOF
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Sync tools/validation/ from the upstream GitLab branch.
|
| 3 |
+
#
|
| 4 |
+
# Run from the repo root:
|
| 5 |
+
# bash tools/validation/_sync.sh
|
| 6 |
+
#
|
| 7 |
+
# After running, review the diff, bump the pin block in
|
| 8 |
+
# tools/validation/UPSTREAM.md, and commit.
|
| 9 |
+
|
| 10 |
+
set -euo pipefail
|
| 11 |
+
|
| 12 |
+
UPSTREAM_URL="https://gitlab-master.nvidia.com/omniverse/kit-extensions/simready-explorer.git"
|
| 13 |
+
UPSTREAM_BRANCH="dev/dloginowski/validation-reporting"
|
| 14 |
+
UPSTREAM_PATH="validation"
|
| 15 |
+
LOCAL_PATH="tools/validation"
|
| 16 |
+
KEEP=(UPSTREAM.md _sync.sh)
|
| 17 |
+
|
| 18 |
+
if [[ ! -d "${LOCAL_PATH}" ]]; then
|
| 19 |
+
echo "expected ${LOCAL_PATH} to exist; are you in the repo root?" >&2
|
| 20 |
+
exit 1
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# Compare current pin to upstream HEAD.
|
| 24 |
+
current_pin="$(grep -oE 'upstream_sha:\s*[0-9a-f]+' "${LOCAL_PATH}/UPSTREAM.md" \
|
| 25 |
+
| awk '{print $2}' | head -n1 || true)"
|
| 26 |
+
upstream_sha="$(git ls-remote "${UPSTREAM_URL}" "refs/heads/${UPSTREAM_BRANCH}" \
|
| 27 |
+
| awk '{print $1}')"
|
| 28 |
+
|
| 29 |
+
if [[ -z "${upstream_sha}" ]]; then
|
| 30 |
+
echo "could not resolve upstream branch SHA — check network / auth" >&2
|
| 31 |
+
exit 2
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
echo "current pin: ${current_pin:-(unset)}"
|
| 35 |
+
echo "upstream HEAD: ${upstream_sha}"
|
| 36 |
+
|
| 37 |
+
if [[ "${current_pin}" == "${upstream_sha}" ]]; then
|
| 38 |
+
echo "already in sync — nothing to do"
|
| 39 |
+
exit 0
|
| 40 |
+
fi
|
| 41 |
+
|
| 42 |
+
tmpdir="$(mktemp -d)"
|
| 43 |
+
trap 'rm -rf "${tmpdir}"' EXIT
|
| 44 |
+
|
| 45 |
+
echo ">> shallow-clone upstream"
|
| 46 |
+
git clone --depth 50 --branch "${UPSTREAM_BRANCH}" --single-branch \
|
| 47 |
+
"${UPSTREAM_URL}" "${tmpdir}/upstream" >/dev/null
|
| 48 |
+
|
| 49 |
+
# How many commits ahead of the current pin (best-effort; falls back to "?")
|
| 50 |
+
ahead="?"
|
| 51 |
+
if [[ -n "${current_pin}" ]]; then
|
| 52 |
+
if git -C "${tmpdir}/upstream" cat-file -e "${current_pin}" 2>/dev/null; then
|
| 53 |
+
ahead="$(git -C "${tmpdir}/upstream" rev-list --count "${current_pin}..HEAD")"
|
| 54 |
+
fi
|
| 55 |
+
fi
|
| 56 |
+
echo "commits ahead of pin: ${ahead}"
|
| 57 |
+
|
| 58 |
+
upstream_dir="${tmpdir}/upstream/${UPSTREAM_PATH}"
|
| 59 |
+
if [[ ! -d "${upstream_dir}" ]]; then
|
| 60 |
+
echo "upstream ${UPSTREAM_PATH}/ missing in branch ${UPSTREAM_BRANCH}" >&2
|
| 61 |
+
exit 3
|
| 62 |
+
fi
|
| 63 |
+
|
| 64 |
+
echo ">> rsync upstream → ${LOCAL_PATH}"
|
| 65 |
+
# Stash KEEP files so rsync --delete doesn't remove them.
|
| 66 |
+
keep_stash="$(mktemp -d)"
|
| 67 |
+
for f in "${KEEP[@]}"; do
|
| 68 |
+
if [[ -f "${LOCAL_PATH}/${f}" ]]; then
|
| 69 |
+
cp -p "${LOCAL_PATH}/${f}" "${keep_stash}/${f}"
|
| 70 |
+
fi
|
| 71 |
+
done
|
| 72 |
+
|
| 73 |
+
rsync -a --delete "${upstream_dir}/" "${LOCAL_PATH}/"
|
| 74 |
+
|
| 75 |
+
for f in "${KEEP[@]}"; do
|
| 76 |
+
if [[ -f "${keep_stash}/${f}" ]]; then
|
| 77 |
+
cp -p "${keep_stash}/${f}" "${LOCAL_PATH}/${f}"
|
| 78 |
+
fi
|
| 79 |
+
done
|
| 80 |
+
rm -rf "${keep_stash}"
|
| 81 |
+
|
| 82 |
+
echo
|
| 83 |
+
echo ">> changes:"
|
| 84 |
+
git -c color.ui=always diff --stat -- "${LOCAL_PATH}" | tail -20 || true
|
| 85 |
+
|
| 86 |
+
cat <<EOF
|
| 87 |
+
|
| 88 |
+
Sync staged. Next steps:
|
| 89 |
+
|
| 90 |
+
1. Bump the pin block in ${LOCAL_PATH}/UPSTREAM.md:
|
| 91 |
+
|
| 92 |
+
upstream_sha: ${upstream_sha}
|
| 93 |
+
synced_at: $(date -u +%Y-%m-%d)
|
| 94 |
+
synced_by: \$(git config user.name)
|
| 95 |
+
|
| 96 |
+
2. Review the diff: git diff -- ${LOCAL_PATH}
|
| 97 |
+
3. Commit: git add ${LOCAL_PATH} && git commit -m "Sync tools/validation/ from upstream ${upstream_sha:0:12}"
|
| 98 |
+
EOF
|
tools/validation/bootstrap.ps1
CHANGED
|
@@ -1,207 +1,207 @@
|
|
| 1 |
-
<#
|
| 2 |
-
.SYNOPSIS
|
| 3 |
-
Bootstrap the SimReady Pipeline Bot runtime dependencies.
|
| 4 |
-
|
| 5 |
-
.DESCRIPTION
|
| 6 |
-
Idempotent. Run once on a fresh machine, or re-run any time to fix a
|
| 7 |
-
missing dependency. Creates a Python venv, clones the foundation + SDK
|
| 8 |
-
repos, and pip-installs everything the /simready-report skill needs.
|
| 9 |
-
|
| 10 |
-
The foundation specs are *not* editable-installed any more. validate.py
|
| 11 |
-
now defaults to populating the OAV registries via the simready-validate
|
| 12 |
-
CLI loader against on-disk paths under the foundations checkout.
|
| 13 |
-
Avoids the `repo usd_profiles_codegen` codegen step (which depends on
|
| 14 |
-
internal packman tooling not shipped in the public GitHub repo) and the
|
| 15 |
-
Forced-include trap in nv_core/sr_specs/pyproject.toml.
|
| 16 |
-
|
| 17 |
-
Layout produced (all under -DepsRoot, default $env:LOCALAPPDATA\simready):
|
| 18 |
-
|
| 19 |
-
<DepsRoot>\
|
| 20 |
-
├── venv\ # Python venv (created if missing)
|
| 21 |
-
├── simready_foundations\ # cloned from NVIDIA GitHub
|
| 22 |
-
└── simready-oem-sdk-poc\ # cloned from NVIDIA-dev GitHub
|
| 23 |
-
|
| 24 |
-
After a successful install the script writes three User-level environment
|
| 25 |
-
variables so the /simready-report skill can locate everything from any shell:
|
| 26 |
-
|
| 27 |
-
SIMREADY_PYTHON = <DepsRoot>\venv\Scripts\python.exe
|
| 28 |
-
SIMREADY_FOUNDATIONS_PATH = <DepsRoot>\simready_foundations
|
| 29 |
-
SIMREADY_SDK_PATH = <DepsRoot>\simready-oem-sdk-poc
|
| 30 |
-
|
| 31 |
-
Override the location with -DepsRoot, e.g.:
|
| 32 |
-
.\bootstrap.ps1 -DepsRoot D:\simready-deps
|
| 33 |
-
|
| 34 |
-
Already have these repos checked out elsewhere? Skip this script and
|
| 35 |
-
set the two env vars manually.
|
| 36 |
-
|
| 37 |
-
Optional (not bootstrapped): a `usd_validation_dashboard_final\` clone —
|
| 38 |
-
used only to copy Sphinx _static\ assets into rendered doc pages. If
|
| 39 |
-
absent, the dashboard's doc pages fall back to minimal inline styling.
|
| 40 |
-
|
| 41 |
-
.NOTES
|
| 42 |
-
- Reversible: the dirs created here are safe to delete; rerun this
|
| 43 |
-
script to re-create them. The two env vars can be cleared with
|
| 44 |
-
`[Environment]::SetEnvironmentVariable("SIMREADY_FOUNDATIONS_PATH", $null, "User")`.
|
| 45 |
-
- Requires: git, python (3.11+ recommended), and access to
|
| 46 |
-
github.com/NVIDIA + github.com/NVIDIA-dev (creds via the user's
|
| 47 |
-
existing git/gh auth).
|
| 48 |
-
- Never elevates / never modifies system PATH.
|
| 49 |
-
#>
|
| 50 |
-
[CmdletBinding()]
|
| 51 |
-
param(
|
| 52 |
-
[switch]$Force, # re-clone even if dirs exist
|
| 53 |
-
[string]$Python = "python", # python interpreter to seed the venv
|
| 54 |
-
[string]$DepsRoot = (Join-Path $env:LOCALAPPDATA "simready"),
|
| 55 |
-
[string]$FoundationsRepo = "https://github.com/NVIDIA/simready-foundation.git",
|
| 56 |
-
[string]$SdkRepo = "https://github.com/NVIDIA-dev/simready-oem-sdk-poc.git"
|
| 57 |
-
)
|
| 58 |
-
|
| 59 |
-
$ErrorActionPreference = "Stop"
|
| 60 |
-
|
| 61 |
-
if (-not (Test-Path $DepsRoot)) {
|
| 62 |
-
New-Item -ItemType Directory -Path $DepsRoot -Force | Out-Null
|
| 63 |
-
}
|
| 64 |
-
$DepsRoot = (Resolve-Path $DepsRoot).Path
|
| 65 |
-
|
| 66 |
-
$VenvPath = Join-Path $DepsRoot "venv"
|
| 67 |
-
$FoundationsPath = Join-Path $DepsRoot "simready_foundations"
|
| 68 |
-
$SdkPath = Join-Path $DepsRoot "simready-oem-sdk-poc"
|
| 69 |
-
|
| 70 |
-
function Step($msg) { Write-Host ""; Write-Host "==> $msg" -ForegroundColor Cyan }
|
| 71 |
-
function Note($msg) { Write-Host " $msg" -ForegroundColor DarkGray }
|
| 72 |
-
function Done($msg) { Write-Host " OK: $msg" -ForegroundColor Green }
|
| 73 |
-
function Fail($msg) { Write-Host " FAIL: $msg" -ForegroundColor Red; exit 1 }
|
| 74 |
-
|
| 75 |
-
Write-Host "DepsRoot: $DepsRoot" -ForegroundColor Yellow
|
| 76 |
-
|
| 77 |
-
# ---- Pre-flight ----------------------------------------------------------
|
| 78 |
-
|
| 79 |
-
Step "Pre-flight"
|
| 80 |
-
foreach ($cmd in @("git", $Python)) {
|
| 81 |
-
$found = Get-Command $cmd -ErrorAction SilentlyContinue
|
| 82 |
-
if (-not $found) { Fail "$cmd not on PATH" }
|
| 83 |
-
Note "$cmd -> $($found.Source)"
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
# ---- Clone the repos -----------------------------------------------------
|
| 87 |
-
|
| 88 |
-
function Ensure-Repo($name, $url, $path) {
|
| 89 |
-
if ((Test-Path $path) -and -not $Force) {
|
| 90 |
-
if (Test-Path (Join-Path $path ".git")) {
|
| 91 |
-
$existingUrl = $null
|
| 92 |
-
Push-Location $path
|
| 93 |
-
try { $existingUrl = (git remote get-url origin 2>$null) } finally { Pop-Location }
|
| 94 |
-
if ($existingUrl -and ($existingUrl.Trim() -ne $url.Trim())) {
|
| 95 |
-
Fail "$name at $path has origin '$($existingUrl.Trim())' but expected '$url'. Re-run with -Force to re-clone, or fix manually: git -C `"$path`" remote set-url origin `"$url`""
|
| 96 |
-
}
|
| 97 |
-
Note "$name already cloned at $path (use -Force to re-clone)"
|
| 98 |
-
return
|
| 99 |
-
} else {
|
| 100 |
-
Fail "$path exists but is not a git checkout. Move or delete it first."
|
| 101 |
-
}
|
| 102 |
-
}
|
| 103 |
-
if ((Test-Path $path) -and $Force) {
|
| 104 |
-
Note "Removing $path (-Force)"
|
| 105 |
-
Remove-Item -Recurse -Force $path
|
| 106 |
-
}
|
| 107 |
-
Note "git clone $url $path"
|
| 108 |
-
git clone $url $path
|
| 109 |
-
if ($LASTEXITCODE -ne 0) { Fail "git clone $name failed" }
|
| 110 |
-
Done "$name cloned"
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
Step "simready_foundations"
|
| 114 |
-
Ensure-Repo "simready_foundations" $FoundationsRepo $FoundationsPath
|
| 115 |
-
|
| 116 |
-
Step "simready-oem-sdk-poc"
|
| 117 |
-
Ensure-Repo "simready-oem-sdk-poc" $SdkRepo $SdkPath
|
| 118 |
-
|
| 119 |
-
# ---- Python venv ---------------------------------------------------------
|
| 120 |
-
|
| 121 |
-
Step "Python venv at $VenvPath"
|
| 122 |
-
if (-not (Test-Path (Join-Path $VenvPath "Scripts\python.exe"))) {
|
| 123 |
-
& $Python -m venv $VenvPath
|
| 124 |
-
if ($LASTEXITCODE -ne 0) { Fail "venv creation failed" }
|
| 125 |
-
Done "venv created"
|
| 126 |
-
} else {
|
| 127 |
-
Note "venv already present"
|
| 128 |
-
}
|
| 129 |
-
$VenvPython = Join-Path $VenvPath "Scripts\python.exe"
|
| 130 |
-
$VenvPip = Join-Path $VenvPath "Scripts\pip.exe"
|
| 131 |
-
|
| 132 |
-
# ---- Pip installs --------------------------------------------------------
|
| 133 |
-
|
| 134 |
-
Step "Upgrade pip / install runtime deps"
|
| 135 |
-
& $VenvPython -m pip install --upgrade pip wheel setuptools | Out-Null
|
| 136 |
-
if ($LASTEXITCODE -ne 0) { Fail "pip self-upgrade failed" }
|
| 137 |
-
|
| 138 |
-
$Pkgs = @(
|
| 139 |
-
"usd-core==26.5",
|
| 140 |
-
"omniverse-asset-validator==1.15.1",
|
| 141 |
-
"omniverse-usd-profiles==1.11.0",
|
| 142 |
-
"markdown-it-py>=3.0",
|
| 143 |
-
"click>=8.0",
|
| 144 |
-
"simready-validate>=2026.4.8"
|
| 145 |
-
)
|
| 146 |
-
& $VenvPip install @Pkgs
|
| 147 |
-
if ($LASTEXITCODE -ne 0) { Fail "pip install (runtime deps) failed" }
|
| 148 |
-
Done "runtime deps installed (incl. simready-validate from PyPI — provides the CLI loader)"
|
| 149 |
-
|
| 150 |
-
# Editable install of nv_core/sr_specs is no longer required. validate.py
|
| 151 |
-
# now defaults to populating the OAV registries via
|
| 152 |
-
# simready.validate.impl.loader.load_validation_implementation against
|
| 153 |
-
# on-disk paths under the foundations checkout. The legacy editable
|
| 154 |
-
# install + repo usd_profiles_codegen flow is reachable via the
|
| 155 |
-
# --use-plugin opt-in flag if needed.
|
| 156 |
-
|
| 157 |
-
Step "Editable install of simready-oem-sdk-poc (for auto-package step)"
|
| 158 |
-
& $VenvPip install -e $SdkPath
|
| 159 |
-
if ($LASTEXITCODE -ne 0) { Fail "pip install -e simready-oem-sdk-poc failed" }
|
| 160 |
-
Done "SDK editable install complete"
|
| 161 |
-
|
| 162 |
-
# ---- Smoke check ---------------------------------------------------------
|
| 163 |
-
|
| 164 |
-
Step "Smoke check"
|
| 165 |
-
& $VenvPython -c @"
|
| 166 |
-
import omni.asset_validator as oav
|
| 167 |
-
from pxr import Usd
|
| 168 |
-
from markdown_it import MarkdownIt
|
| 169 |
-
import simready_sdk
|
| 170 |
-
from simready.validate.impl.loader import load_validation_implementation
|
| 171 |
-
from pathlib import Path
|
| 172 |
-
import os
|
| 173 |
-
foundations = Path(os.environ.get('SIMREADY_FOUNDATIONS_PATH') or r'$FoundationsPath')
|
| 174 |
-
load_validation_implementation(
|
| 175 |
-
rules_and_requirements_paths=[foundations / 'nv_core/sr_specs/docs/capabilities'],
|
| 176 |
-
features_paths=[foundations / 'nv_core/sr_specs/docs/features'],
|
| 177 |
-
profiles_paths=[foundations / 'nv_core/sr_specs/docs/profiles/profiles.toml'],
|
| 178 |
-
)
|
| 179 |
-
pr = oav.ProfileRegistry()
|
| 180 |
-
profiles = list(pr.values())
|
| 181 |
-
print(f' asset-validator: {oav.__version__ if hasattr(oav,"__version__") else "(no version attr)"}')
|
| 182 |
-
print(f' profiles loaded: {len(profiles)}')
|
| 183 |
-
print(f' simready_sdk: {simready_sdk.__file__}')
|
| 184 |
-
"@
|
| 185 |
-
if ($LASTEXITCODE -ne 0) { Fail "smoke check failed" }
|
| 186 |
-
Done "imports + CLI-loader populated ProfileRegistry working"
|
| 187 |
-
|
| 188 |
-
# ---- Persist env vars so validate.py finds the deps without args -------
|
| 189 |
-
|
| 190 |
-
Step "Persist locator env vars (User scope)"
|
| 191 |
-
[Environment]::SetEnvironmentVariable("SIMREADY_PYTHON", $VenvPython, "User")
|
| 192 |
-
[Environment]::SetEnvironmentVariable("SIMREADY_FOUNDATIONS_PATH", $FoundationsPath, "User")
|
| 193 |
-
[Environment]::SetEnvironmentVariable("SIMREADY_SDK_PATH", $SdkPath, "User")
|
| 194 |
-
$env:SIMREADY_PYTHON = $VenvPython
|
| 195 |
-
$env:SIMREADY_FOUNDATIONS_PATH = $FoundationsPath
|
| 196 |
-
$env:SIMREADY_SDK_PATH = $SdkPath
|
| 197 |
-
Note "SIMREADY_PYTHON = $VenvPython"
|
| 198 |
-
Note "SIMREADY_FOUNDATIONS_PATH = $FoundationsPath"
|
| 199 |
-
Note "SIMREADY_SDK_PATH = $SdkPath"
|
| 200 |
-
Done "env vars set for current user"
|
| 201 |
-
|
| 202 |
-
# ---- Done ---------------------------------------------------------------
|
| 203 |
-
|
| 204 |
-
Write-Host ""
|
| 205 |
-
Write-Host "Bootstrap complete." -ForegroundColor Green
|
| 206 |
-
Write-Host "DepsRoot: $DepsRoot" -ForegroundColor Green
|
| 207 |
-
Write-Host "Next: invoke /simready-report against any asset directory." -ForegroundColor Green
|
|
|
|
| 1 |
+
<#
|
| 2 |
+
.SYNOPSIS
|
| 3 |
+
Bootstrap the SimReady Pipeline Bot runtime dependencies.
|
| 4 |
+
|
| 5 |
+
.DESCRIPTION
|
| 6 |
+
Idempotent. Run once on a fresh machine, or re-run any time to fix a
|
| 7 |
+
missing dependency. Creates a Python venv, clones the foundation + SDK
|
| 8 |
+
repos, and pip-installs everything the /simready-report skill needs.
|
| 9 |
+
|
| 10 |
+
The foundation specs are *not* editable-installed any more. validate.py
|
| 11 |
+
now defaults to populating the OAV registries via the simready-validate
|
| 12 |
+
CLI loader against on-disk paths under the foundations checkout.
|
| 13 |
+
Avoids the `repo usd_profiles_codegen` codegen step (which depends on
|
| 14 |
+
internal packman tooling not shipped in the public GitHub repo) and the
|
| 15 |
+
Forced-include trap in nv_core/sr_specs/pyproject.toml.
|
| 16 |
+
|
| 17 |
+
Layout produced (all under -DepsRoot, default $env:LOCALAPPDATA\simready):
|
| 18 |
+
|
| 19 |
+
<DepsRoot>\
|
| 20 |
+
├── venv\ # Python venv (created if missing)
|
| 21 |
+
├── simready_foundations\ # cloned from NVIDIA GitHub
|
| 22 |
+
└── simready-oem-sdk-poc\ # cloned from NVIDIA-dev GitHub
|
| 23 |
+
|
| 24 |
+
After a successful install the script writes three User-level environment
|
| 25 |
+
variables so the /simready-report skill can locate everything from any shell:
|
| 26 |
+
|
| 27 |
+
SIMREADY_PYTHON = <DepsRoot>\venv\Scripts\python.exe
|
| 28 |
+
SIMREADY_FOUNDATIONS_PATH = <DepsRoot>\simready_foundations
|
| 29 |
+
SIMREADY_SDK_PATH = <DepsRoot>\simready-oem-sdk-poc
|
| 30 |
+
|
| 31 |
+
Override the location with -DepsRoot, e.g.:
|
| 32 |
+
.\bootstrap.ps1 -DepsRoot D:\simready-deps
|
| 33 |
+
|
| 34 |
+
Already have these repos checked out elsewhere? Skip this script and
|
| 35 |
+
set the two env vars manually.
|
| 36 |
+
|
| 37 |
+
Optional (not bootstrapped): a `usd_validation_dashboard_final\` clone —
|
| 38 |
+
used only to copy Sphinx _static\ assets into rendered doc pages. If
|
| 39 |
+
absent, the dashboard's doc pages fall back to minimal inline styling.
|
| 40 |
+
|
| 41 |
+
.NOTES
|
| 42 |
+
- Reversible: the dirs created here are safe to delete; rerun this
|
| 43 |
+
script to re-create them. The two env vars can be cleared with
|
| 44 |
+
`[Environment]::SetEnvironmentVariable("SIMREADY_FOUNDATIONS_PATH", $null, "User")`.
|
| 45 |
+
- Requires: git, python (3.11+ recommended), and access to
|
| 46 |
+
github.com/NVIDIA + github.com/NVIDIA-dev (creds via the user's
|
| 47 |
+
existing git/gh auth).
|
| 48 |
+
- Never elevates / never modifies system PATH.
|
| 49 |
+
#>
|
| 50 |
+
[CmdletBinding()]
|
| 51 |
+
param(
|
| 52 |
+
[switch]$Force, # re-clone even if dirs exist
|
| 53 |
+
[string]$Python = "python", # python interpreter to seed the venv
|
| 54 |
+
[string]$DepsRoot = (Join-Path $env:LOCALAPPDATA "simready"),
|
| 55 |
+
[string]$FoundationsRepo = "https://github.com/NVIDIA/simready-foundation.git",
|
| 56 |
+
[string]$SdkRepo = "https://github.com/NVIDIA-dev/simready-oem-sdk-poc.git"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
$ErrorActionPreference = "Stop"
|
| 60 |
+
|
| 61 |
+
if (-not (Test-Path $DepsRoot)) {
|
| 62 |
+
New-Item -ItemType Directory -Path $DepsRoot -Force | Out-Null
|
| 63 |
+
}
|
| 64 |
+
$DepsRoot = (Resolve-Path $DepsRoot).Path
|
| 65 |
+
|
| 66 |
+
$VenvPath = Join-Path $DepsRoot "venv"
|
| 67 |
+
$FoundationsPath = Join-Path $DepsRoot "simready_foundations"
|
| 68 |
+
$SdkPath = Join-Path $DepsRoot "simready-oem-sdk-poc"
|
| 69 |
+
|
| 70 |
+
function Step($msg) { Write-Host ""; Write-Host "==> $msg" -ForegroundColor Cyan }
|
| 71 |
+
function Note($msg) { Write-Host " $msg" -ForegroundColor DarkGray }
|
| 72 |
+
function Done($msg) { Write-Host " OK: $msg" -ForegroundColor Green }
|
| 73 |
+
function Fail($msg) { Write-Host " FAIL: $msg" -ForegroundColor Red; exit 1 }
|
| 74 |
+
|
| 75 |
+
Write-Host "DepsRoot: $DepsRoot" -ForegroundColor Yellow
|
| 76 |
+
|
| 77 |
+
# ---- Pre-flight ----------------------------------------------------------
|
| 78 |
+
|
| 79 |
+
Step "Pre-flight"
|
| 80 |
+
foreach ($cmd in @("git", $Python)) {
|
| 81 |
+
$found = Get-Command $cmd -ErrorAction SilentlyContinue
|
| 82 |
+
if (-not $found) { Fail "$cmd not on PATH" }
|
| 83 |
+
Note "$cmd -> $($found.Source)"
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# ---- Clone the repos -----------------------------------------------------
|
| 87 |
+
|
| 88 |
+
function Ensure-Repo($name, $url, $path) {
|
| 89 |
+
if ((Test-Path $path) -and -not $Force) {
|
| 90 |
+
if (Test-Path (Join-Path $path ".git")) {
|
| 91 |
+
$existingUrl = $null
|
| 92 |
+
Push-Location $path
|
| 93 |
+
try { $existingUrl = (git remote get-url origin 2>$null) } finally { Pop-Location }
|
| 94 |
+
if ($existingUrl -and ($existingUrl.Trim() -ne $url.Trim())) {
|
| 95 |
+
Fail "$name at $path has origin '$($existingUrl.Trim())' but expected '$url'. Re-run with -Force to re-clone, or fix manually: git -C `"$path`" remote set-url origin `"$url`""
|
| 96 |
+
}
|
| 97 |
+
Note "$name already cloned at $path (use -Force to re-clone)"
|
| 98 |
+
return
|
| 99 |
+
} else {
|
| 100 |
+
Fail "$path exists but is not a git checkout. Move or delete it first."
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
if ((Test-Path $path) -and $Force) {
|
| 104 |
+
Note "Removing $path (-Force)"
|
| 105 |
+
Remove-Item -Recurse -Force $path
|
| 106 |
+
}
|
| 107 |
+
Note "git clone $url $path"
|
| 108 |
+
git clone $url $path
|
| 109 |
+
if ($LASTEXITCODE -ne 0) { Fail "git clone $name failed" }
|
| 110 |
+
Done "$name cloned"
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
Step "simready_foundations"
|
| 114 |
+
Ensure-Repo "simready_foundations" $FoundationsRepo $FoundationsPath
|
| 115 |
+
|
| 116 |
+
Step "simready-oem-sdk-poc"
|
| 117 |
+
Ensure-Repo "simready-oem-sdk-poc" $SdkRepo $SdkPath
|
| 118 |
+
|
| 119 |
+
# ---- Python venv ---------------------------------------------------------
|
| 120 |
+
|
| 121 |
+
Step "Python venv at $VenvPath"
|
| 122 |
+
if (-not (Test-Path (Join-Path $VenvPath "Scripts\python.exe"))) {
|
| 123 |
+
& $Python -m venv $VenvPath
|
| 124 |
+
if ($LASTEXITCODE -ne 0) { Fail "venv creation failed" }
|
| 125 |
+
Done "venv created"
|
| 126 |
+
} else {
|
| 127 |
+
Note "venv already present"
|
| 128 |
+
}
|
| 129 |
+
$VenvPython = Join-Path $VenvPath "Scripts\python.exe"
|
| 130 |
+
$VenvPip = Join-Path $VenvPath "Scripts\pip.exe"
|
| 131 |
+
|
| 132 |
+
# ---- Pip installs --------------------------------------------------------
|
| 133 |
+
|
| 134 |
+
Step "Upgrade pip / install runtime deps"
|
| 135 |
+
& $VenvPython -m pip install --upgrade pip wheel setuptools | Out-Null
|
| 136 |
+
if ($LASTEXITCODE -ne 0) { Fail "pip self-upgrade failed" }
|
| 137 |
+
|
| 138 |
+
$Pkgs = @(
|
| 139 |
+
"usd-core==26.5",
|
| 140 |
+
"omniverse-asset-validator==1.15.1",
|
| 141 |
+
"omniverse-usd-profiles==1.11.0",
|
| 142 |
+
"markdown-it-py>=3.0",
|
| 143 |
+
"click>=8.0",
|
| 144 |
+
"simready-validate>=2026.4.8"
|
| 145 |
+
)
|
| 146 |
+
& $VenvPip install @Pkgs
|
| 147 |
+
if ($LASTEXITCODE -ne 0) { Fail "pip install (runtime deps) failed" }
|
| 148 |
+
Done "runtime deps installed (incl. simready-validate from PyPI — provides the CLI loader)"
|
| 149 |
+
|
| 150 |
+
# Editable install of nv_core/sr_specs is no longer required. validate.py
|
| 151 |
+
# now defaults to populating the OAV registries via
|
| 152 |
+
# simready.validate.impl.loader.load_validation_implementation against
|
| 153 |
+
# on-disk paths under the foundations checkout. The legacy editable
|
| 154 |
+
# install + repo usd_profiles_codegen flow is reachable via the
|
| 155 |
+
# --use-plugin opt-in flag if needed.
|
| 156 |
+
|
| 157 |
+
Step "Editable install of simready-oem-sdk-poc (for auto-package step)"
|
| 158 |
+
& $VenvPip install -e $SdkPath
|
| 159 |
+
if ($LASTEXITCODE -ne 0) { Fail "pip install -e simready-oem-sdk-poc failed" }
|
| 160 |
+
Done "SDK editable install complete"
|
| 161 |
+
|
| 162 |
+
# ---- Smoke check ---------------------------------------------------------
|
| 163 |
+
|
| 164 |
+
Step "Smoke check"
|
| 165 |
+
& $VenvPython -c @"
|
| 166 |
+
import omni.asset_validator as oav
|
| 167 |
+
from pxr import Usd
|
| 168 |
+
from markdown_it import MarkdownIt
|
| 169 |
+
import simready_sdk
|
| 170 |
+
from simready.validate.impl.loader import load_validation_implementation
|
| 171 |
+
from pathlib import Path
|
| 172 |
+
import os
|
| 173 |
+
foundations = Path(os.environ.get('SIMREADY_FOUNDATIONS_PATH') or r'$FoundationsPath')
|
| 174 |
+
load_validation_implementation(
|
| 175 |
+
rules_and_requirements_paths=[foundations / 'nv_core/sr_specs/docs/capabilities'],
|
| 176 |
+
features_paths=[foundations / 'nv_core/sr_specs/docs/features'],
|
| 177 |
+
profiles_paths=[foundations / 'nv_core/sr_specs/docs/profiles/profiles.toml'],
|
| 178 |
+
)
|
| 179 |
+
pr = oav.ProfileRegistry()
|
| 180 |
+
profiles = list(pr.values())
|
| 181 |
+
print(f' asset-validator: {oav.__version__ if hasattr(oav,"__version__") else "(no version attr)"}')
|
| 182 |
+
print(f' profiles loaded: {len(profiles)}')
|
| 183 |
+
print(f' simready_sdk: {simready_sdk.__file__}')
|
| 184 |
+
"@
|
| 185 |
+
if ($LASTEXITCODE -ne 0) { Fail "smoke check failed" }
|
| 186 |
+
Done "imports + CLI-loader populated ProfileRegistry working"
|
| 187 |
+
|
| 188 |
+
# ---- Persist env vars so validate.py finds the deps without args -------
|
| 189 |
+
|
| 190 |
+
Step "Persist locator env vars (User scope)"
|
| 191 |
+
[Environment]::SetEnvironmentVariable("SIMREADY_PYTHON", $VenvPython, "User")
|
| 192 |
+
[Environment]::SetEnvironmentVariable("SIMREADY_FOUNDATIONS_PATH", $FoundationsPath, "User")
|
| 193 |
+
[Environment]::SetEnvironmentVariable("SIMREADY_SDK_PATH", $SdkPath, "User")
|
| 194 |
+
$env:SIMREADY_PYTHON = $VenvPython
|
| 195 |
+
$env:SIMREADY_FOUNDATIONS_PATH = $FoundationsPath
|
| 196 |
+
$env:SIMREADY_SDK_PATH = $SdkPath
|
| 197 |
+
Note "SIMREADY_PYTHON = $VenvPython"
|
| 198 |
+
Note "SIMREADY_FOUNDATIONS_PATH = $FoundationsPath"
|
| 199 |
+
Note "SIMREADY_SDK_PATH = $SdkPath"
|
| 200 |
+
Done "env vars set for current user"
|
| 201 |
+
|
| 202 |
+
# ---- Done ---------------------------------------------------------------
|
| 203 |
+
|
| 204 |
+
Write-Host ""
|
| 205 |
+
Write-Host "Bootstrap complete." -ForegroundColor Green
|
| 206 |
+
Write-Host "DepsRoot: $DepsRoot" -ForegroundColor Green
|
| 207 |
+
Write-Host "Next: invoke /simready-report against any asset directory." -ForegroundColor Green
|
tools/validation/playbooks/foundations-deviations.md
CHANGED
|
@@ -1,591 +1,591 @@
|
|
| 1 |
-
# SimReady Foundations — process deviations
|
| 2 |
-
|
| 3 |
-
Issues encountered while running the `/simready-report` skill against `yaskawa_local`
|
| 4 |
-
that require changes in **`simready_foundations`** (or a clearly documented
|
| 5 |
-
client-side workaround). These are not playbook bugs — the playbook is working
|
| 6 |
-
around upstream gaps.
|
| 7 |
-
|
| 8 |
-
Source data for each item: paths/codes observed during the validation run on
|
| 9 |
-
`packages/yaskawa_local/` against `Robot-Body-Isaac` v1.0.0.
|
| 10 |
-
|
| 11 |
-
---
|
| 12 |
-
|
| 13 |
-
## 1. `Profile.capabilities` returns an empty list for every profile
|
| 14 |
-
|
| 15 |
-
**Where:** `omni.asset_validator` profile schema (Python registry).
|
| 16 |
-
|
| 17 |
-
**Observed:**
|
| 18 |
-
```python
|
| 19 |
-
profile = oav.ProfileRegistry().find('Robot-Body-Isaac', '1.0.0')
|
| 20 |
-
profile.capabilities # → []
|
| 21 |
-
profile.features # → 2 features, 8 requirements
|
| 22 |
-
```
|
| 23 |
-
Every profile in the registry returns `[]` for `.capabilities`. Profiles now
|
| 24 |
-
declare scope through `.features` — each feature owns its requirements
|
| 25 |
-
directly. The `capabilities` attribute is a leftover from an older schema.
|
| 26 |
-
|
| 27 |
-
**Impact:** `validate_yaskawa.py` and earlier versions of this playbook's
|
| 28 |
-
`validate.py` filtered the engine's enabled capabilities against
|
| 29 |
-
`profile.capabilities`. Because the latter is always empty, the filter
|
| 30 |
-
disabled **every** capability and reported `enabled capabilities: 0` while
|
| 31 |
-
producing 6,551 issues from rules that ignored the disable calls (see #2).
|
| 32 |
-
|
| 33 |
-
**Required:** remove `Profile.capabilities`, or populate it from the union of
|
| 34 |
-
the features' capabilities, and update all reference scripts/docs that still
|
| 35 |
-
use it.
|
| 36 |
-
|
| 37 |
-
---
|
| 38 |
-
|
| 39 |
-
## 2. `engine.enable_*` / `disable_*` do not constrain rule execution
|
| 40 |
-
|
| 41 |
-
**Where:** `omni.asset_validator.ValidationEngine`.
|
| 42 |
-
|
| 43 |
-
**Observed:** `engine.enable_feature(f)` and `engine.enable_requirement(r)`
|
| 44 |
-
update `engine.enabled_features` / `engine.enabled_requirements` counters,
|
| 45 |
-
but `engine.validate(stage)` still runs every rule registered via
|
| 46 |
-
`@register_rule`. `engine.enabled_rules` stays at 0 even after enabling
|
| 47 |
-
features and requirements; rules execute regardless.
|
| 48 |
-
|
| 49 |
-
**Impact:** profile-scoping has to happen post-hoc, by filtering issues
|
| 50 |
-
against the profile's requirement codes. The current playbook does this in
|
| 51 |
-
`validate_one()` (filters failures by `code in profile_codes`).
|
| 52 |
-
|
| 53 |
-
**Required:** either
|
| 54 |
-
1. make `enable_*` / `disable_*` actually scope rule execution, or
|
| 55 |
-
2. document explicitly that they are display-only and that callers must
|
| 56 |
-
filter results themselves. Today the API name strongly implies (1).
|
| 57 |
-
|
| 58 |
-
---
|
| 59 |
-
|
| 60 |
-
## 3. `Robot-Body-Isaac` v1.0.0 silently drops 4 of 6 declared features
|
| 61 |
-
(TOML ↔ Python registry mismatch)
|
| 62 |
-
|
| 63 |
-
**Where:** `simready_foundations/_build/target-deps/pip_prebundle/simready/foundation/core/profiles/profiles.toml`
|
| 64 |
-
vs the `Features` enum loaded from
|
| 65 |
-
`simready.foundation.core.features`.
|
| 66 |
-
|
| 67 |
-
**Observed:** `profiles.toml` declares Robot-Body-Isaac v1.0.0 with six
|
| 68 |
-
features:
|
| 69 |
-
|
| 70 |
-
| TOML entry | Loads? | Notes |
|
| 71 |
-
|---|---|---|
|
| 72 |
-
| `FET001_BASE_NEUTRAL` v0.1.0 | ✓ | 8 requirements |
|
| 73 |
-
| `FET004_ROBOT_PHYSX` v0.2.0 | ✗ | No such name in `Features` enum (only `FET004_BASE_NEUTRAL`) |
|
| 74 |
-
| `FET021_ROBOT_CORE_ISAAC` v0.2.0 | ✗ | Closest is `FET_021_ROBOT_CORE` v0.2.0 (note underscore + missing `_ISAAC`) |
|
| 75 |
-
| `FET022_DRIVEN_JOINTS_ISAAC` v0.1.0 | ✗ | Closest is `FET_022_DRIVEN_JOINTS` v0.1.0 |
|
| 76 |
-
| `FET024_BASE_ARTICULATION_PHYSX` v0.1.0 | ✗ | Closest is `FET_024_BASE_ARTICULATION` v0.1.0 |
|
| 77 |
-
| `FET100_BASE_ISAACSIM` v0.1.0 | ✓ | 0 requirements |
|
| 78 |
-
|
| 79 |
-
`pr.find('Robot-Body-Isaac', '1.0.0').features` therefore returns only the
|
| 80 |
-
two features that successfully resolve. Four features (Robot Core, Driven
|
| 81 |
-
Joints, Base Articulation, Robot Physx) are silently dropped — no warning,
|
| 82 |
-
no error.
|
| 83 |
-
|
| 84 |
-
The rules associated with the dropped features (RC.*, JT.*, RB.*, AT.*) still
|
| 85 |
-
execute (per #2 the engine ignores feature gating), so they appear in the
|
| 86 |
-
raw issue list, but they cannot be attributed to a profile feature for
|
| 87 |
-
roll-up reporting.
|
| 88 |
-
|
| 89 |
-
**Required:** reconcile the names. Either rename the TOML entries to match
|
| 90 |
-
the `Features` enum (`FET_021_ROBOT_CORE` etc.), or add the `_ISAAC` /
|
| 91 |
-
`_PHYSX` variants to the foundation as real feature implementations. Also
|
| 92 |
-
make profile-feature lookup *fail loudly* when a declared feature can't be
|
| 93 |
-
resolved — silent drop is the worst outcome.
|
| 94 |
-
|
| 95 |
-
Validation also surfaced real failures with codes from features that
|
| 96 |
-
**aren't even declared in the TOML profile**:
|
| 97 |
-
|
| 98 |
-
| Code(s) | Source | Notes |
|
| 99 |
-
|---|---|---|
|
| 100 |
-
| `RC.002`, `RC.004` (`thumbnail-exist`), `RC.008`, `RC.009` | `capabilities/isaac_sim/robot_core` | Robot-core rules registered & raising issues, but no profile feature includes them |
|
| 101 |
-
| `NP.001`, `NP.005`, `NP.006`, `NP.008` | `capabilities/visualization/nonvisual_materials` | |
|
| 102 |
-
| `HI.001`, `HI.002` | `capabilities/hierarchy` | (`HI.004` is in profile, others aren't) |
|
| 103 |
-
| `VG.007`, `VG.009`, `VG.016`, `VG.019`, `VG.MESH.001` | `capabilities/visualization/geometry` | (`VG.001` is in profile, the rest aren't) |
|
| 104 |
-
| `EX.01`, `EX.03`, `GSP.001`, `ISA.001`, `VM.MDL.001` | various | |
|
| 105 |
-
|
| 106 |
-
A "Robot-Body-Isaac" profile that does not require the asset to have a
|
| 107 |
-
thumbnail, robot type, or pinned root joint, but the foundations repo ships
|
| 108 |
-
those as registered rules, is a process gap.
|
| 109 |
-
|
| 110 |
-
**Required:** extend the profile's features to include the robot-core,
|
| 111 |
-
nonvisual-materials, hierarchy-completeness, and geometry-completeness
|
| 112 |
-
features that are already authored in the foundation; align the JSON capability
|
| 113 |
-
configs (`config/robot-core.json` etc.) with the Python `Profiles` enum.
|
| 114 |
-
|
| 115 |
-
---
|
| 116 |
-
|
| 117 |
-
## 4. JSON capability config and Python profile registry are not linked
|
| 118 |
-
|
| 119 |
-
**Where:** `simready_foundations/nv_core/sr_specs/config/robot-core.json` vs
|
| 120 |
-
`simready.foundation.core` Python plugin.
|
| 121 |
-
|
| 122 |
-
**Observed:** `robot-core.json` declares `RobotCore` capability v1.0.0 with
|
| 123 |
-
the `thumbnail-exist` requirement (and others). The Python registry exposes
|
| 124 |
-
that requirement to the rule machinery (issues with `RC.004` are emitted),
|
| 125 |
-
but no `Profile` exposes `RobotCore` as a feature, so client code that
|
| 126 |
-
introspects `profile.features` to compute pass/fail per feature can't see it.
|
| 127 |
-
|
| 128 |
-
**Impact:** validation reports list `RC.*` codes in raw issues but cannot map
|
| 129 |
-
them to a feature, so they appear as "uncategorized" failures.
|
| 130 |
-
|
| 131 |
-
**Required:** wire the JSON capability declarations into the `Features` /
|
| 132 |
-
`Profiles` enums (or into a new `profile.capabilities` accessor that returns
|
| 133 |
-
real entries — see #1).
|
| 134 |
-
|
| 135 |
-
---
|
| 136 |
-
|
| 137 |
-
## 5. ~87% of issues report `code: UNKNOWN`
|
| 138 |
-
|
| 139 |
-
**Where:** rules registered in `omni.asset_validator` and SimReady plugins.
|
| 140 |
-
|
| 141 |
-
**Observed:** of 6,551 issues across 7 robots, **5,695** have `iss.requirement
|
| 142 |
-
is None`, so the issue cannot be attributed to any requirement code:
|
| 143 |
-
```
|
| 144 |
-
5695 UNKNOWN
|
| 145 |
-
129 NP.001
|
| 146 |
-
116 HI.002
|
| 147 |
-
116 VG.009
|
| 148 |
-
96 VG.007
|
| 149 |
-
...
|
| 150 |
-
```
|
| 151 |
-
These come from rules that don't decorate with `@register_requirements`, so
|
| 152 |
-
they run but have no requirement linkage.
|
| 153 |
-
|
| 154 |
-
**Impact:** profile-scoped filtering (#2's workaround) drops 87% of the signal;
|
| 155 |
-
those failures appear in the raw issue list but never roll up to a feature
|
| 156 |
-
pass/fail.
|
| 157 |
-
|
| 158 |
-
**Required:** every rule registered in foundations should declare the
|
| 159 |
-
requirement(s) it enforces via `@register_requirements`, so issues are
|
| 160 |
-
attributable.
|
| 161 |
-
|
| 162 |
-
---
|
| 163 |
-
|
| 164 |
-
## 6. `init_rules` defaults are inconsistent across consumers
|
| 165 |
-
|
| 166 |
-
**Where:** `omni.asset_validator.ValidationEngine` constructor and the
|
| 167 |
-
SimReady SDK's engine wrapper.
|
| 168 |
-
|
| 169 |
-
**Observed:** `validate_yaskawa.py` (top-level reference) explicitly passes
|
| 170 |
-
`init_rules=True` and notes that `simready_sdk.core.engine` passes
|
| 171 |
-
`init_rules=False`, which leaves the engine empty:
|
| 172 |
-
```
|
| 173 |
-
The SDK passes init_rules=False which leaves the engine with no checks,
|
| 174 |
-
producing 'No rules or requirements have been enabled.' for every asset.
|
| 175 |
-
```
|
| 176 |
-
With `init_rules=True` the engine has 124 rules / 118 requirements. With
|
| 177 |
-
`False` it has 0 / 0.
|
| 178 |
-
|
| 179 |
-
**Required:** make `init_rules=True` the default in the engine, or remove the
|
| 180 |
-
parameter; update `simready_sdk.core.engine` to call it correctly. Today
|
| 181 |
-
choosing the wrong path silently produces a "passes everything" report.
|
| 182 |
-
|
| 183 |
-
---
|
| 184 |
-
|
| 185 |
-
## 7. Foundation has the wrapper but ships no runnable thumbnail generator
|
| 186 |
-
|
| 187 |
-
**Where:** `simready_foundations/nv_core/testing_tools/testing_framework/source/job_runner/core/engines/usdrecord/`
|
| 188 |
-
plus lighting rigs at `simready_foundations/nv_core/common_tools/thumbnails/auto_thumbnail_indoor_lighting_rig_{small,medium,large}.usd`.
|
| 189 |
-
|
| 190 |
-
**Observed:** the foundation defines `UsdRecordCommandBuilder` which builds
|
| 191 |
-
the command `<usdrecord_exe> <asset.usd> <output.png>` and references three
|
| 192 |
-
preset lighting USDs intended to be composed with the asset before rendering.
|
| 193 |
-
But:
|
| 194 |
-
- The `<usdrecord_exe>` is taken from `job.runner_config.executable`. The
|
| 195 |
-
foundation does not bundle the binary, the venv does not have it, and
|
| 196 |
-
`pxr.UsdAppUtils` (the Python library backing `usdrecord`) is absent.
|
| 197 |
-
- The `auto_thumbnail_indoor_lighting_rig_*.usd` rigs have no documented
|
| 198 |
-
composition recipe — there is no published "open the rig, payload the
|
| 199 |
-
asset, render to 256×256" wrapper script.
|
| 200 |
-
|
| 201 |
-
`usdrecord` is part of OpenUSD; on this machine it must be installed
|
| 202 |
-
externally. Three working install paths (commands documented for the record;
|
| 203 |
-
none are run automatically by the validator):
|
| 204 |
-
|
| 205 |
-
1. **Kit Kernel from NGC** (the user already has `ngc` on PATH):
|
| 206 |
-
```powershell
|
| 207 |
-
$env:KIT_VERSION = "106.5.0"
|
| 208 |
-
ngc registry resource download-version "nvidia/omniverse/kit:$env:KIT_VERSION" `
|
| 209 |
-
--dest "$env:USERPROFILE\.kit"
|
| 210 |
-
# `usdrecord.bat` lives at <kit_root>\dev\tools\packman\repo\bld\target-deps\usd\release\bin\usdrecord.bat
|
| 211 |
-
$env:USDRECORD_BIN = "$env:USERPROFILE\.kit\kit-$env:KIT_VERSION\...\usdrecord.bat"
|
| 212 |
-
```
|
| 213 |
-
2. **OpenUSD prebuilt** (NVIDIA developer release):
|
| 214 |
-
```powershell
|
| 215 |
-
# Download from https://developer.nvidia.com/usd (signed-in NGC link),
|
| 216 |
-
# extract, and point to the binary:
|
| 217 |
-
$env:USDRECORD_BIN = "C:\openusd\bin\usdrecord.cmd"
|
| 218 |
-
```
|
| 219 |
-
3. **Build OpenUSD from source** (heaviest, only if the prebuilt isn't
|
| 220 |
-
acceptable):
|
| 221 |
-
```powershell
|
| 222 |
-
git clone https://github.com/PixarAnimationStudios/OpenUSD.git C:\src\OpenUSD
|
| 223 |
-
python C:\src\OpenUSD\build_scripts\build_usd.py --tools C:\openusd
|
| 224 |
-
$env:USDRECORD_BIN = "C:\openusd\bin\usdrecord.cmd"
|
| 225 |
-
```
|
| 226 |
-
|
| 227 |
-
Once `USDRECORD_BIN` is set (or the binary is on PATH), `validate.py`'s
|
| 228 |
-
`_find_usdrecord()` will pick it up and the validator will invoke it for
|
| 229 |
-
each asset missing a thumbnail at the spec path. Generated PNGs go to
|
| 230 |
-
`report/<set>/_generated_thumbs/<filename>.png` — never into the
|
| 231 |
-
packaged asset tree. Every command run is logged in `results.json` under
|
| 232 |
-
`thumbnail_generation.attempts` and surfaced in the dashboard's "External
|
| 233 |
-
tooling used" panel.
|
| 234 |
-
|
| 235 |
-
For `yaskawa_local`, six of seven thumbnails were instead pulled from
|
| 236 |
-
`Assets/Isaac/6.0/Isaac/Robots/Yaskawa/Motoman Next/.../.thumbs/256x256/`
|
| 237 |
-
on the Omniverse content S3 bucket (free; no install required). One robot
|
| 238 |
-
(`NHC10DE-A00`) has no S3 thumbnail; without `usdrecord` installed, its tile
|
| 239 |
-
renders the inline SVG "no thumbnail" placeholder.
|
| 240 |
-
|
| 241 |
-
**Required:** either ship `usdrecord` in the foundation's bundled toolchain,
|
| 242 |
-
or distribute a foundation-side wrapper (e.g. a Kit ext that does
|
| 243 |
-
`UsdAppUtils.framerec.FrameRecorder` programmatically) so `simready ingest
|
| 244 |
-
usd` can produce the spec thumbnail without external installs.
|
| 245 |
-
|
| 246 |
-
---
|
| 247 |
-
|
| 248 |
-
## 8. Spec docs disagree on the thumbnail filename
|
| 249 |
-
|
| 250 |
-
**Where:**
|
| 251 |
-
- Spec doc: `nv_core/sr_specs/docs/capabilities/isaac_sim/robot_core/requirements/thumbnail-exist.md`
|
| 252 |
-
- Validation rule: `nv_core/sr_specs/docs/capabilities/isaac_sim/robot_core/validation.py:213`
|
| 253 |
-
- OEM publishing guide: `simready-oem-sdk-poc/docs/oem-publishing-guide-hf.md`
|
| 254 |
-
|
| 255 |
-
**Observed:**
|
| 256 |
-
- The spec doc's example uses `…/Robot_Name/.thumbs/256x256/Robot.png`
|
| 257 |
-
(filename = USD stem only, no `.usd` suffix).
|
| 258 |
-
- The rule code computes `…/<filename>.png` where `<filename>` is the full
|
| 259 |
-
USD filename **including** the `.usd` extension, e.g. `nex4_c00.usd.png`.
|
| 260 |
-
- The OEM publishing guide also uses `robot-a.usd.png` (with `.usd.`).
|
| 261 |
-
- The Isaac S3 bucket uses `NEX4_C00.usd.png` (with `.usd.`).
|
| 262 |
-
|
| 263 |
-
The rule and most published assets agree (`<filename>.png` with `.usd`); the
|
| 264 |
-
spec doc's example is wrong.
|
| 265 |
-
|
| 266 |
-
**Required:** fix the spec example in `thumbnail-exist.md` to match the rule.
|
| 267 |
-
|
| 268 |
-
---
|
| 269 |
-
|
| 270 |
-
## 9. `simready ingest usd` does not lift companion thumbnails
|
| 271 |
-
|
| 272 |
-
**Where:** `simready-oem-sdk-poc/src/simready_sdk/packager/organizer.py`.
|
| 273 |
-
|
| 274 |
-
**Observed:** `simready ingest usd ./robot/robot.usd` produces the spec
|
| 275 |
-
layout (`<name>/<name>.usd`, `configuration/`, `materials/`,
|
| 276 |
-
`.thumbs/256x256/` folder) but **does not** copy/rename a thumbnail from any
|
| 277 |
-
companion location into `.thumbs/256x256/<file>.usd.png`. The packager docs
|
| 278 |
-
(`SKILL.md`) call it "thumbnail placeholder" — the directory is created
|
| 279 |
-
empty.
|
| 280 |
-
|
| 281 |
-
For `yaskawa_local/images/<stem>.usd.usd.png`, no automated path exists for
|
| 282 |
-
the packager to know that file should land at the spec's thumbnail path.
|
| 283 |
-
|
| 284 |
-
**Required:** the packager should accept a companion-images directory hint
|
| 285 |
-
(e.g. `--thumbs-from <dir>` or a sibling `images/` convention) and lift
|
| 286 |
-
matching files into `.thumbs/256x256/<file>.usd.png` during organize.
|
| 287 |
-
|
| 288 |
-
---
|
| 289 |
-
|
| 290 |
-
## 10. A foundation rule crashes with `'NoneType' object has no attribute 'JointStateAPI'`
|
| 291 |
-
|
| 292 |
-
**Where:** somewhere in the `simready.foundation.core` plugin's joint-state
|
| 293 |
-
checker (rule not yet pinned down — issue is reported as `code: UNKNOWN`,
|
| 294 |
-
`rule: None`).
|
| 295 |
-
|
| 296 |
-
**Observed:** the rule fires on every prim it visits and produces an
|
| 297 |
-
*Uncaught error* issue, accounting for between **566 and 698 issues per
|
| 298 |
-
asset** in our run — i.e., most of what makes the report look noisy:
|
| 299 |
-
```
|
| 300 |
-
UNKNOWN ×668 Uncaught error: 'NoneType' object has no attribute 'JointStateAPI'
|
| 301 |
-
UNKNOWN ×566 Uncaught error: 'NoneType' object has no attribute 'JointStateAPI'
|
| 302 |
-
UNKNOWN ×698 Uncaught error: 'NoneType' object has no attribute 'JointStateAPI'
|
| 303 |
-
```
|
| 304 |
-
The remaining ~150 issues per asset (HI.*, NP.*, RC.*, etc.) are the *real*
|
| 305 |
-
findings.
|
| 306 |
-
|
| 307 |
-
The crash strongly suggests the rule reads
|
| 308 |
-
`stage.GetPrimAtPath(...).GetAPISchemas()`-style and assumes the result is
|
| 309 |
-
non-`None`. A defensive `if foo is None: continue` would silence the noise;
|
| 310 |
-
the underlying joint-state coverage is presumably what the rule was meant to
|
| 311 |
-
be checking, but currently it produces nothing useful.
|
| 312 |
-
|
| 313 |
-
**Required:** pin down the rule (grep `JointStateAPI` across
|
| 314 |
-
`simready.foundation.core`), add the missing None-guard, and either register
|
| 315 |
-
the rule's requirement properly or remove it from the engine until it works.
|
| 316 |
-
|
| 317 |
-
---
|
| 318 |
-
|
| 319 |
-
## 11. `NP.005` — layout mismatch between the rule's spec and `simready ingest usd`, plus a path-printing bug
|
| 320 |
-
|
| 321 |
-
**Where:**
|
| 322 |
-
- Rule code: `simready_foundations/nv_core/sr_specs/docs/capabilities/core/naming_paths/validation.py:271-320` (`AssetFolderStructureChecker`).
|
| 323 |
-
- Rule spec doc: `simready_foundations/nv_core/sr_specs/docs/capabilities/core/naming_paths/requirements/asset-folder-structure.md`.
|
| 324 |
-
- Packager output: `simready-oem-sdk-poc/src/simready_sdk/packager/organizer.py` (invoked via `simready ingest usd`).
|
| 325 |
-
|
| 326 |
-
### 11a. Layout mismatch
|
| 327 |
-
|
| 328 |
-
The NP.005 spec doc requires this structure:
|
| 329 |
-
|
| 330 |
-
```
|
| 331 |
-
<asset>/ ← asset folder
|
| 332 |
-
├── <intermediate>/ ← exactly ONE intermediate folder (any name)
|
| 333 |
-
│ └── <main>.usd ← main USD; NO other .usd files here or below
|
| 334 |
-
└── materials/ ← supporting folders are SIBLINGS of <intermediate>/
|
| 335 |
-
```
|
| 336 |
-
|
| 337 |
-
The reference dashboard's FANUC content follows this exactly:
|
| 338 |
-
|
| 339 |
-
```
|
| 340 |
-
robots/CR/cr_50f_16b/isaac_ready_usd/cr_50f_16b.usd.usd
|
| 341 |
-
robots/CR/cr_50f_16b/configuration/...
|
| 342 |
-
```
|
| 343 |
-
|
| 344 |
-
But `simready ingest usd` produces a *flat* layout — main USD at the asset
|
| 345 |
-
root, sublayers nested inside the same dir:
|
| 346 |
-
|
| 347 |
-
```
|
| 348 |
-
<asset>/
|
| 349 |
-
├── <asset>.usd
|
| 350 |
-
└── configuration/
|
| 351 |
-
├── <asset>_base.usd
|
| 352 |
-
├── <asset>_physics.usd
|
| 353 |
-
└── <asset>_sensor.usd
|
| 354 |
-
```
|
| 355 |
-
|
| 356 |
-
The rule walks `os.path.dirname(stage_path)` (which is `<asset>/` in the
|
| 357 |
-
flat layout) and flags every sublayer as "stray". The sublayers aren't
|
| 358 |
-
stray — they're the spec-required configuration files — but the rule's
|
| 359 |
-
walk-set includes them because the packager produced an
|
| 360 |
-
NP.005-non-compliant layout.
|
| 361 |
-
|
| 362 |
-
So: **two foundation artifacts disagree on the canonical asset layout.**
|
| 363 |
-
The packaging spec (`simready-packaging`'s SKILL.md) and `simready ingest
|
| 364 |
-
usd` produce one layout; NP.005's spec doc and the FANUC reference content
|
| 365 |
-
use a different layout.
|
| 366 |
-
|
| 367 |
-
### 11b. Path-printing bug (independent)
|
| 368 |
-
|
| 369 |
-
```python
|
| 370 |
-
# capabilities/core/naming_paths/validation.py:308-313
|
| 371 |
-
for root, dirs, files in os.walk(current_dir):
|
| 372 |
-
for file in files:
|
| 373 |
-
found_file = os.path.join(root, file).replace("\\", "/")
|
| 374 |
-
if file.endswith((".usd", ".usda")) and found_file != stage_path:
|
| 375 |
-
relative_path = os.path.relpath(file, current_dir) # ← bug
|
| 376 |
-
other_usd_files.append(relative_path)
|
| 377 |
-
```
|
| 378 |
-
|
| 379 |
-
`file` here is just the basename returned by `os.walk` (e.g.
|
| 380 |
-
`nex10_c00_base.usd`). `os.path.relpath` resolves it against
|
| 381 |
-
`os.getcwd()`, not against `root`. With CWD =
|
| 382 |
-
`<workspace>/.claude/skills/simready-report/`, the rule prints:
|
| 383 |
-
|
| 384 |
-
```
|
| 385 |
-
..\..\..\.claude\skills\simready-report\nex10_c00_base.usd
|
| 386 |
-
```
|
| 387 |
-
|
| 388 |
-
— a path back to the validator's CWD, where the file emphatically does not
|
| 389 |
-
exist. Should be `os.path.relpath(found_file, current_dir)` (`found_file`
|
| 390 |
-
is the correct joined path, already computed on the line above and
|
| 391 |
-
otherwise unused). With that one-character fix the cited path becomes
|
| 392 |
-
`configuration/nex10_c00_base.usd` — pointing at the real file.
|
| 393 |
-
|
| 394 |
-
### Required
|
| 395 |
-
|
| 396 |
-
**Pick one canonical layout and reconcile both sides.** Either:
|
| 397 |
-
|
| 398 |
-
- **Update the packager** (`simready ingest usd`) to emit the
|
| 399 |
-
NP.005-compliant `<asset>/<intermediate>/<main>.usd` structure with
|
| 400 |
-
`configuration/` and `materials/` as siblings of `<intermediate>/`. This
|
| 401 |
-
is what the FANUC reference content already uses. The packaging skill
|
| 402 |
-
doc and any consumers (manifest tooling, dashboard, etc.) need updating
|
| 403 |
-
in lockstep.
|
| 404 |
-
|
| 405 |
-
- **Or update NP.005's spec doc and rule** to bless the flat layout that
|
| 406 |
-
`simready ingest usd` actually produces — i.e., explicitly whitelist the
|
| 407 |
-
spec-mandated subdirectories (`configuration/`, `materials/`,
|
| 408 |
-
`.thumbs/`, `.simready/`) when scanning for stray USDs.
|
| 409 |
-
|
| 410 |
-
Whichever is chosen, fix the path-printing bug at the same time
|
| 411 |
-
(`relpath(file, …)` → `relpath(found_file, …)`) so the message names a
|
| 412 |
-
real on-disk path.
|
| 413 |
-
|
| 414 |
-
---
|
| 415 |
-
|
| 416 |
-
## 12. The "example" capability is active in production validation
|
| 417 |
-
|
| 418 |
-
**Where:**
|
| 419 |
-
`simready_foundations/nv_core/sr_specs/docs/capabilities/example/example.py`,
|
| 420 |
-
codes `EX.01`, `EX.02`, `EX.03`.
|
| 421 |
-
|
| 422 |
-
**Observed:** every asset fails:
|
| 423 |
-
- `EX.01` — *"Stage has no mesh prims."*
|
| 424 |
-
- `EX.03` — *"Test prim 'TestBlob' not found under default prim."*
|
| 425 |
-
|
| 426 |
-
The source comment on this capability is literally `"example"` and the
|
| 427 |
-
rule docs (`example.md`) describe it as a tutorial — `EX.03` requires a
|
| 428 |
-
prim named `TestBlob` to exist as a child of the default prim. That's
|
| 429 |
-
clearly not an asset requirement; it's pedagogical scaffolding to teach
|
| 430 |
-
plugin authors how to register a rule.
|
| 431 |
-
|
| 432 |
-
**Required:** unregister the `Example` capability from production rule
|
| 433 |
-
loading (move it to test fixtures, gate it behind an env var, or delete it
|
| 434 |
-
from the shipped plugin). Today every real asset takes 2–3 spurious EX
|
| 435 |
-
failures.
|
| 436 |
-
|
| 437 |
-
---
|
| 438 |
-
|
| 439 |
-
## 13. `EX.01` and `VG.MESH.001` are duplicate checks under different codes
|
| 440 |
-
|
| 441 |
-
**Where:**
|
| 442 |
-
- `capabilities/example/example.py:52` — `EX.01`: *"Stage has no mesh prims."*
|
| 443 |
-
- `capabilities/visualization/geometry/validation.py:836` — `VG.MESH.001`:
|
| 444 |
-
*"Stage does not contain any meshes."*
|
| 445 |
-
|
| 446 |
-
**Observed:** both fire on every asset, with effectively identical
|
| 447 |
-
semantics. `EX.01` is the example/tutorial version; `VG.MESH.001` is the
|
| 448 |
-
real version. Reporting both is noise.
|
| 449 |
-
|
| 450 |
-
**Required:** when #12 is fixed, this disappears for free. Otherwise,
|
| 451 |
-
`EX.01` should be retired and any consumer that still references it should
|
| 452 |
-
be redirected to `VG.MESH.001`.
|
| 453 |
-
|
| 454 |
-
---
|
| 455 |
-
|
| 456 |
-
## 14. `HI.002` rejects valid USD xform-op decompositions
|
| 457 |
-
|
| 458 |
-
**Where:** `capabilities/hierarchy/validation.py:62-104`
|
| 459 |
-
(`ExclusiveXFormParentChecker`).
|
| 460 |
-
|
| 461 |
-
**Observed:** ~14 issues per asset, message *"Prim Parent has no
|
| 462 |
-
xformOp:rotate."* The rule requires every Gprim parent to author both
|
| 463 |
-
`xformOp:translate` and (a substring-matching) `xformOp:rotate*`:
|
| 464 |
-
|
| 465 |
-
```python
|
| 466 |
-
if not any("xformOp:rotate" in op.GetAttr().GetName() for op in xform_ops):
|
| 467 |
-
self._AddFailedCheck("Prim Parent has no xformOp:rotate.", …)
|
| 468 |
-
```
|
| 469 |
-
|
| 470 |
-
That's an over-strict reading of USD. Valid alternatives the rule rejects:
|
| 471 |
-
|
| 472 |
-
- `xformOp:transform` (single 4×4 matrix — encodes translate + rotate +
|
| 473 |
-
scale together).
|
| 474 |
-
- No xform ops at all (identity transform — also a valid stateless prim).
|
| 475 |
-
- Any combination authored on an inherited parent and overridden by the
|
| 476 |
-
Gprim's own ops.
|
| 477 |
-
|
| 478 |
-
Yaskawa's robots author matrix transforms in many places, which is what
|
| 479 |
-
trips this. The check should accept *any* xformable spec that resolves to a
|
| 480 |
-
defined transform — likely via `Xformable.GetLocalTransformation()`
|
| 481 |
-
returning a non-error result — rather than pattern-matching specific op
|
| 482 |
-
names.
|
| 483 |
-
|
| 484 |
-
**Required:** rewrite the check to validate *that the prim resolves to a
|
| 485 |
-
defined transform*, not that it authored a particular op vocabulary.
|
| 486 |
-
|
| 487 |
-
---
|
| 488 |
-
|
| 489 |
-
## 15. `NP.001` flags the conventional `/Looks` scope as a naming violation
|
| 490 |
-
|
| 491 |
-
**Where:** `capabilities/core/naming_paths/validation.py:125-160`
|
| 492 |
-
(`PrimNamingConventionChecker`).
|
| 493 |
-
|
| 494 |
-
**Observed:** every asset fails NP.001 ×8 with *"Prim 'Looks' does not
|
| 495 |
-
follow consistent naming convention (camelCase or snake_case)."*
|
| 496 |
-
|
| 497 |
-
`Looks` is the long-established USD convention for the materials scope
|
| 498 |
-
under an asset's default prim — it's used by Pixar's published USD
|
| 499 |
-
samples, NVIDIA's Isaac/Omniverse content, and most authoring tools
|
| 500 |
-
(Houdini, Maya USD, Kit). The rule's `CAMEL_CASE_PATTERN` and
|
| 501 |
-
`SNAKE_CASE_PATTERN` regexes don't accept the `Looks` PascalCase
|
| 502 |
-
single-word, so it reports a violation on a name the broader USD ecosystem
|
| 503 |
-
treats as canonical.
|
| 504 |
-
|
| 505 |
-
**Required:** either whitelist the well-known USD scope names (`Looks`,
|
| 506 |
-
`Geometry`, `Cameras`, `Lights`, `Animation`, `Render`, etc.) or accept
|
| 507 |
-
PascalCase as a valid convention alongside camelCase / snake_case.
|
| 508 |
-
|
| 509 |
-
---
|
| 510 |
-
|
| 511 |
-
## 16. `VM.MDL.001` and `NP.008` double-report the same MDL resolution failure
|
| 512 |
-
|
| 513 |
-
**Where:**
|
| 514 |
-
- `capabilities/visualization/materials/...` (VM.MDL.001 — *"The path
|
| 515 |
-
OmniPBR.mdl does not exist."*)
|
| 516 |
-
- `capabilities/core/naming_paths/...` (NP.008 — *"Asset path 'OmniPBR.mdl'
|
| 517 |
-
on attribute 'info:mdl:sourceAsset' at prim '/.../Looks/.../...' does not
|
| 518 |
-
resolve to an existing file."*)
|
| 519 |
-
|
| 520 |
-
**Observed:** every asset emits the same number of VM.MDL.001 and NP.008
|
| 521 |
-
issues (9 each on `nex10_c00`, 6 each on `nex20_c00`, etc.) — both rules
|
| 522 |
-
fire on the same `info:mdl:sourceAsset = @OmniPBR.mdl@` attribute. NP.008
|
| 523 |
-
gives a useful prim path; VM.MDL.001 just says "doesn't exist".
|
| 524 |
-
|
| 525 |
-
Separately: `OmniPBR.mdl` is a **standard NVIDIA MDL** that ships with Kit
|
| 526 |
-
and Isaac. The validator's asset resolver doesn't know where to find it
|
| 527 |
-
(likely missing an MDL search path in the SimReady plugin's resolver
|
| 528 |
-
config), so a perfectly portable, NVIDIA-blessed material reference is
|
| 529 |
-
flagged as broken on every asset.
|
| 530 |
-
|
| 531 |
-
**Required:**
|
| 532 |
-
1. Configure the SimReady validator plugin to register Kit's standard MDL
|
| 533 |
-
search paths (`mdl/`, `omni/mdl/`, etc.) so `OmniPBR.mdl` and other
|
| 534 |
-
shipped MDLs resolve without the customer having to vendor them.
|
| 535 |
-
2. Pick one of VM.MDL.001 / NP.008 as the canonical "MDL doesn't resolve"
|
| 536 |
-
code. Have the other defer when the first has fired against the same
|
| 537 |
-
attribute.
|
| 538 |
-
|
| 539 |
-
---
|
| 540 |
-
|
| 541 |
-
## 17. `RC.002` flags spec-mandated robot metadata as "overrides"
|
| 542 |
-
|
| 543 |
-
**Where:** `capabilities/isaac_sim/robot_core/...`, `RC.002`.
|
| 544 |
-
|
| 545 |
-
**Observed:** every asset fails RC.002 with messages like:
|
| 546 |
-
|
| 547 |
-
```
|
| 548 |
-
Prim is overridden: /nex10_c00, ['isaac:description', 'isaac:license',
|
| 549 |
-
'isaac:namespace', 'isaac:robotType']
|
| 550 |
-
```
|
| 551 |
-
|
| 552 |
-
`isaac:description`, `isaac:license`, `isaac:namespace`,
|
| 553 |
-
`isaac:robotType` look like exactly the kind of metadata the SimReady
|
| 554 |
-
robot spec wants you to author on the robot's default prim — but RC.002
|
| 555 |
-
is treating any authored override of the default prim's properties as a
|
| 556 |
-
violation.
|
| 557 |
-
|
| 558 |
-
If those four `isaac:*` attributes are spec-mandated, the rule should
|
| 559 |
-
*require* them, not flag their presence. If they're optional metadata, the
|
| 560 |
-
rule should ignore them — overriding metadata fields on the default prim
|
| 561 |
-
is a normal authoring pattern, not a layout error.
|
| 562 |
-
|
| 563 |
-
**Required:** clarify the spec around the `isaac:*` namespace on the
|
| 564 |
-
robot default prim and update RC.002 to either require those fields, or
|
| 565 |
-
exclude them from the "this prim shouldn't be overridden" check, or split
|
| 566 |
-
into two rules (one for "structural" overrides, one for the spec
|
| 567 |
-
metadata).
|
| 568 |
-
|
| 569 |
-
---
|
| 570 |
-
|
| 571 |
-
## Summary
|
| 572 |
-
|
| 573 |
-
| # | Severity | Area | One-line |
|
| 574 |
-
|---|---|---|---|
|
| 575 |
-
| 1 | high | profile schema | `Profile.capabilities` is dead; should be removed or populated |
|
| 576 |
-
| 2 | high | engine API | `enable_*`/`disable_*` don't actually scope rule execution |
|
| 577 |
-
| 3 | high | profile content | Robot-Body-Isaac v1.0.0 silently drops 4 of 6 declared features |
|
| 578 |
-
| 4 | high | profile schema | JSON capability configs aren't linked to the Python profile registry |
|
| 579 |
-
| 5 | high | rule registration | Most rules don't declare `@register_requirements` → 87% UNKNOWN |
|
| 580 |
-
| 6 | medium | engine API | `init_rules` defaults inconsistent; SDK silently turns off all checks |
|
| 581 |
-
| 7 | medium | tooling | No headless thumbnail generator ships with foundations |
|
| 582 |
-
| 8 | low | spec docs | `thumbnail-exist.md` example contradicts the actual rule |
|
| 583 |
-
| 9 | medium | packaging | `simready ingest usd` doesn't lift companion thumbnails into spec layout |
|
| 584 |
-
| 10 | high | rule reliability | Rule crashes on `JointStateAPI` access → ~600 UNKNOWN issues per asset |
|
| 585 |
-
| 11 | high | spec/packager mismatch | NP.005 expects `<asset>/<intermediate>/<main>.usd`; packager produces flat layout. Plus `relpath(file,…)` should be `relpath(found_file,…)` |
|
| 586 |
-
| 12 | high | rule scoping | "Example" tutorial capability (`EX.*`) is active in production validation |
|
| 587 |
-
| 13 | medium | rule duplication | `EX.01` and `VG.MESH.001` are the same check |
|
| 588 |
-
| 14 | high | rule correctness | `HI.002` rejects matrix transforms (`xformOp:transform`) and identity transforms |
|
| 589 |
-
| 15 | medium | rule correctness | `NP.001` rejects the conventional `/Looks` scope name |
|
| 590 |
-
| 16 | high | rule correctness | `OmniPBR.mdl` (standard NVIDIA MDL) doesn't resolve; both VM.MDL.001 and NP.008 fire on the same attribute |
|
| 591 |
-
| 17 | medium | spec ambiguity | `RC.002` flags spec-looking `isaac:*` metadata as illegal "overrides" |
|
|
|
|
| 1 |
+
# SimReady Foundations — process deviations
|
| 2 |
+
|
| 3 |
+
Issues encountered while running the `/simready-report` skill against `yaskawa_local`
|
| 4 |
+
that require changes in **`simready_foundations`** (or a clearly documented
|
| 5 |
+
client-side workaround). These are not playbook bugs — the playbook is working
|
| 6 |
+
around upstream gaps.
|
| 7 |
+
|
| 8 |
+
Source data for each item: paths/codes observed during the validation run on
|
| 9 |
+
`packages/yaskawa_local/` against `Robot-Body-Isaac` v1.0.0.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 1. `Profile.capabilities` returns an empty list for every profile
|
| 14 |
+
|
| 15 |
+
**Where:** `omni.asset_validator` profile schema (Python registry).
|
| 16 |
+
|
| 17 |
+
**Observed:**
|
| 18 |
+
```python
|
| 19 |
+
profile = oav.ProfileRegistry().find('Robot-Body-Isaac', '1.0.0')
|
| 20 |
+
profile.capabilities # → []
|
| 21 |
+
profile.features # → 2 features, 8 requirements
|
| 22 |
+
```
|
| 23 |
+
Every profile in the registry returns `[]` for `.capabilities`. Profiles now
|
| 24 |
+
declare scope through `.features` — each feature owns its requirements
|
| 25 |
+
directly. The `capabilities` attribute is a leftover from an older schema.
|
| 26 |
+
|
| 27 |
+
**Impact:** `validate_yaskawa.py` and earlier versions of this playbook's
|
| 28 |
+
`validate.py` filtered the engine's enabled capabilities against
|
| 29 |
+
`profile.capabilities`. Because the latter is always empty, the filter
|
| 30 |
+
disabled **every** capability and reported `enabled capabilities: 0` while
|
| 31 |
+
producing 6,551 issues from rules that ignored the disable calls (see #2).
|
| 32 |
+
|
| 33 |
+
**Required:** remove `Profile.capabilities`, or populate it from the union of
|
| 34 |
+
the features' capabilities, and update all reference scripts/docs that still
|
| 35 |
+
use it.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## 2. `engine.enable_*` / `disable_*` do not constrain rule execution
|
| 40 |
+
|
| 41 |
+
**Where:** `omni.asset_validator.ValidationEngine`.
|
| 42 |
+
|
| 43 |
+
**Observed:** `engine.enable_feature(f)` and `engine.enable_requirement(r)`
|
| 44 |
+
update `engine.enabled_features` / `engine.enabled_requirements` counters,
|
| 45 |
+
but `engine.validate(stage)` still runs every rule registered via
|
| 46 |
+
`@register_rule`. `engine.enabled_rules` stays at 0 even after enabling
|
| 47 |
+
features and requirements; rules execute regardless.
|
| 48 |
+
|
| 49 |
+
**Impact:** profile-scoping has to happen post-hoc, by filtering issues
|
| 50 |
+
against the profile's requirement codes. The current playbook does this in
|
| 51 |
+
`validate_one()` (filters failures by `code in profile_codes`).
|
| 52 |
+
|
| 53 |
+
**Required:** either
|
| 54 |
+
1. make `enable_*` / `disable_*` actually scope rule execution, or
|
| 55 |
+
2. document explicitly that they are display-only and that callers must
|
| 56 |
+
filter results themselves. Today the API name strongly implies (1).
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## 3. `Robot-Body-Isaac` v1.0.0 silently drops 4 of 6 declared features
|
| 61 |
+
(TOML ↔ Python registry mismatch)
|
| 62 |
+
|
| 63 |
+
**Where:** `simready_foundations/_build/target-deps/pip_prebundle/simready/foundation/core/profiles/profiles.toml`
|
| 64 |
+
vs the `Features` enum loaded from
|
| 65 |
+
`simready.foundation.core.features`.
|
| 66 |
+
|
| 67 |
+
**Observed:** `profiles.toml` declares Robot-Body-Isaac v1.0.0 with six
|
| 68 |
+
features:
|
| 69 |
+
|
| 70 |
+
| TOML entry | Loads? | Notes |
|
| 71 |
+
|---|---|---|
|
| 72 |
+
| `FET001_BASE_NEUTRAL` v0.1.0 | ✓ | 8 requirements |
|
| 73 |
+
| `FET004_ROBOT_PHYSX` v0.2.0 | ✗ | No such name in `Features` enum (only `FET004_BASE_NEUTRAL`) |
|
| 74 |
+
| `FET021_ROBOT_CORE_ISAAC` v0.2.0 | ✗ | Closest is `FET_021_ROBOT_CORE` v0.2.0 (note underscore + missing `_ISAAC`) |
|
| 75 |
+
| `FET022_DRIVEN_JOINTS_ISAAC` v0.1.0 | ✗ | Closest is `FET_022_DRIVEN_JOINTS` v0.1.0 |
|
| 76 |
+
| `FET024_BASE_ARTICULATION_PHYSX` v0.1.0 | ✗ | Closest is `FET_024_BASE_ARTICULATION` v0.1.0 |
|
| 77 |
+
| `FET100_BASE_ISAACSIM` v0.1.0 | ✓ | 0 requirements |
|
| 78 |
+
|
| 79 |
+
`pr.find('Robot-Body-Isaac', '1.0.0').features` therefore returns only the
|
| 80 |
+
two features that successfully resolve. Four features (Robot Core, Driven
|
| 81 |
+
Joints, Base Articulation, Robot Physx) are silently dropped — no warning,
|
| 82 |
+
no error.
|
| 83 |
+
|
| 84 |
+
The rules associated with the dropped features (RC.*, JT.*, RB.*, AT.*) still
|
| 85 |
+
execute (per #2 the engine ignores feature gating), so they appear in the
|
| 86 |
+
raw issue list, but they cannot be attributed to a profile feature for
|
| 87 |
+
roll-up reporting.
|
| 88 |
+
|
| 89 |
+
**Required:** reconcile the names. Either rename the TOML entries to match
|
| 90 |
+
the `Features` enum (`FET_021_ROBOT_CORE` etc.), or add the `_ISAAC` /
|
| 91 |
+
`_PHYSX` variants to the foundation as real feature implementations. Also
|
| 92 |
+
make profile-feature lookup *fail loudly* when a declared feature can't be
|
| 93 |
+
resolved — silent drop is the worst outcome.
|
| 94 |
+
|
| 95 |
+
Validation also surfaced real failures with codes from features that
|
| 96 |
+
**aren't even declared in the TOML profile**:
|
| 97 |
+
|
| 98 |
+
| Code(s) | Source | Notes |
|
| 99 |
+
|---|---|---|
|
| 100 |
+
| `RC.002`, `RC.004` (`thumbnail-exist`), `RC.008`, `RC.009` | `capabilities/isaac_sim/robot_core` | Robot-core rules registered & raising issues, but no profile feature includes them |
|
| 101 |
+
| `NP.001`, `NP.005`, `NP.006`, `NP.008` | `capabilities/visualization/nonvisual_materials` | |
|
| 102 |
+
| `HI.001`, `HI.002` | `capabilities/hierarchy` | (`HI.004` is in profile, others aren't) |
|
| 103 |
+
| `VG.007`, `VG.009`, `VG.016`, `VG.019`, `VG.MESH.001` | `capabilities/visualization/geometry` | (`VG.001` is in profile, the rest aren't) |
|
| 104 |
+
| `EX.01`, `EX.03`, `GSP.001`, `ISA.001`, `VM.MDL.001` | various | |
|
| 105 |
+
|
| 106 |
+
A "Robot-Body-Isaac" profile that does not require the asset to have a
|
| 107 |
+
thumbnail, robot type, or pinned root joint, but the foundations repo ships
|
| 108 |
+
those as registered rules, is a process gap.
|
| 109 |
+
|
| 110 |
+
**Required:** extend the profile's features to include the robot-core,
|
| 111 |
+
nonvisual-materials, hierarchy-completeness, and geometry-completeness
|
| 112 |
+
features that are already authored in the foundation; align the JSON capability
|
| 113 |
+
configs (`config/robot-core.json` etc.) with the Python `Profiles` enum.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## 4. JSON capability config and Python profile registry are not linked
|
| 118 |
+
|
| 119 |
+
**Where:** `simready_foundations/nv_core/sr_specs/config/robot-core.json` vs
|
| 120 |
+
`simready.foundation.core` Python plugin.
|
| 121 |
+
|
| 122 |
+
**Observed:** `robot-core.json` declares `RobotCore` capability v1.0.0 with
|
| 123 |
+
the `thumbnail-exist` requirement (and others). The Python registry exposes
|
| 124 |
+
that requirement to the rule machinery (issues with `RC.004` are emitted),
|
| 125 |
+
but no `Profile` exposes `RobotCore` as a feature, so client code that
|
| 126 |
+
introspects `profile.features` to compute pass/fail per feature can't see it.
|
| 127 |
+
|
| 128 |
+
**Impact:** validation reports list `RC.*` codes in raw issues but cannot map
|
| 129 |
+
them to a feature, so they appear as "uncategorized" failures.
|
| 130 |
+
|
| 131 |
+
**Required:** wire the JSON capability declarations into the `Features` /
|
| 132 |
+
`Profiles` enums (or into a new `profile.capabilities` accessor that returns
|
| 133 |
+
real entries — see #1).
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## 5. ~87% of issues report `code: UNKNOWN`
|
| 138 |
+
|
| 139 |
+
**Where:** rules registered in `omni.asset_validator` and SimReady plugins.
|
| 140 |
+
|
| 141 |
+
**Observed:** of 6,551 issues across 7 robots, **5,695** have `iss.requirement
|
| 142 |
+
is None`, so the issue cannot be attributed to any requirement code:
|
| 143 |
+
```
|
| 144 |
+
5695 UNKNOWN
|
| 145 |
+
129 NP.001
|
| 146 |
+
116 HI.002
|
| 147 |
+
116 VG.009
|
| 148 |
+
96 VG.007
|
| 149 |
+
...
|
| 150 |
+
```
|
| 151 |
+
These come from rules that don't decorate with `@register_requirements`, so
|
| 152 |
+
they run but have no requirement linkage.
|
| 153 |
+
|
| 154 |
+
**Impact:** profile-scoped filtering (#2's workaround) drops 87% of the signal;
|
| 155 |
+
those failures appear in the raw issue list but never roll up to a feature
|
| 156 |
+
pass/fail.
|
| 157 |
+
|
| 158 |
+
**Required:** every rule registered in foundations should declare the
|
| 159 |
+
requirement(s) it enforces via `@register_requirements`, so issues are
|
| 160 |
+
attributable.
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
## 6. `init_rules` defaults are inconsistent across consumers
|
| 165 |
+
|
| 166 |
+
**Where:** `omni.asset_validator.ValidationEngine` constructor and the
|
| 167 |
+
SimReady SDK's engine wrapper.
|
| 168 |
+
|
| 169 |
+
**Observed:** `validate_yaskawa.py` (top-level reference) explicitly passes
|
| 170 |
+
`init_rules=True` and notes that `simready_sdk.core.engine` passes
|
| 171 |
+
`init_rules=False`, which leaves the engine empty:
|
| 172 |
+
```
|
| 173 |
+
The SDK passes init_rules=False which leaves the engine with no checks,
|
| 174 |
+
producing 'No rules or requirements have been enabled.' for every asset.
|
| 175 |
+
```
|
| 176 |
+
With `init_rules=True` the engine has 124 rules / 118 requirements. With
|
| 177 |
+
`False` it has 0 / 0.
|
| 178 |
+
|
| 179 |
+
**Required:** make `init_rules=True` the default in the engine, or remove the
|
| 180 |
+
parameter; update `simready_sdk.core.engine` to call it correctly. Today
|
| 181 |
+
choosing the wrong path silently produces a "passes everything" report.
|
| 182 |
+
|
| 183 |
+
---
|
| 184 |
+
|
| 185 |
+
## 7. Foundation has the wrapper but ships no runnable thumbnail generator
|
| 186 |
+
|
| 187 |
+
**Where:** `simready_foundations/nv_core/testing_tools/testing_framework/source/job_runner/core/engines/usdrecord/`
|
| 188 |
+
plus lighting rigs at `simready_foundations/nv_core/common_tools/thumbnails/auto_thumbnail_indoor_lighting_rig_{small,medium,large}.usd`.
|
| 189 |
+
|
| 190 |
+
**Observed:** the foundation defines `UsdRecordCommandBuilder` which builds
|
| 191 |
+
the command `<usdrecord_exe> <asset.usd> <output.png>` and references three
|
| 192 |
+
preset lighting USDs intended to be composed with the asset before rendering.
|
| 193 |
+
But:
|
| 194 |
+
- The `<usdrecord_exe>` is taken from `job.runner_config.executable`. The
|
| 195 |
+
foundation does not bundle the binary, the venv does not have it, and
|
| 196 |
+
`pxr.UsdAppUtils` (the Python library backing `usdrecord`) is absent.
|
| 197 |
+
- The `auto_thumbnail_indoor_lighting_rig_*.usd` rigs have no documented
|
| 198 |
+
composition recipe — there is no published "open the rig, payload the
|
| 199 |
+
asset, render to 256×256" wrapper script.
|
| 200 |
+
|
| 201 |
+
`usdrecord` is part of OpenUSD; on this machine it must be installed
|
| 202 |
+
externally. Three working install paths (commands documented for the record;
|
| 203 |
+
none are run automatically by the validator):
|
| 204 |
+
|
| 205 |
+
1. **Kit Kernel from NGC** (the user already has `ngc` on PATH):
|
| 206 |
+
```powershell
|
| 207 |
+
$env:KIT_VERSION = "106.5.0"
|
| 208 |
+
ngc registry resource download-version "nvidia/omniverse/kit:$env:KIT_VERSION" `
|
| 209 |
+
--dest "$env:USERPROFILE\.kit"
|
| 210 |
+
# `usdrecord.bat` lives at <kit_root>\dev\tools\packman\repo\bld\target-deps\usd\release\bin\usdrecord.bat
|
| 211 |
+
$env:USDRECORD_BIN = "$env:USERPROFILE\.kit\kit-$env:KIT_VERSION\...\usdrecord.bat"
|
| 212 |
+
```
|
| 213 |
+
2. **OpenUSD prebuilt** (NVIDIA developer release):
|
| 214 |
+
```powershell
|
| 215 |
+
# Download from https://developer.nvidia.com/usd (signed-in NGC link),
|
| 216 |
+
# extract, and point to the binary:
|
| 217 |
+
$env:USDRECORD_BIN = "C:\openusd\bin\usdrecord.cmd"
|
| 218 |
+
```
|
| 219 |
+
3. **Build OpenUSD from source** (heaviest, only if the prebuilt isn't
|
| 220 |
+
acceptable):
|
| 221 |
+
```powershell
|
| 222 |
+
git clone https://github.com/PixarAnimationStudios/OpenUSD.git C:\src\OpenUSD
|
| 223 |
+
python C:\src\OpenUSD\build_scripts\build_usd.py --tools C:\openusd
|
| 224 |
+
$env:USDRECORD_BIN = "C:\openusd\bin\usdrecord.cmd"
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
Once `USDRECORD_BIN` is set (or the binary is on PATH), `validate.py`'s
|
| 228 |
+
`_find_usdrecord()` will pick it up and the validator will invoke it for
|
| 229 |
+
each asset missing a thumbnail at the spec path. Generated PNGs go to
|
| 230 |
+
`report/<set>/_generated_thumbs/<filename>.png` — never into the
|
| 231 |
+
packaged asset tree. Every command run is logged in `results.json` under
|
| 232 |
+
`thumbnail_generation.attempts` and surfaced in the dashboard's "External
|
| 233 |
+
tooling used" panel.
|
| 234 |
+
|
| 235 |
+
For `yaskawa_local`, six of seven thumbnails were instead pulled from
|
| 236 |
+
`Assets/Isaac/6.0/Isaac/Robots/Yaskawa/Motoman Next/.../.thumbs/256x256/`
|
| 237 |
+
on the Omniverse content S3 bucket (free; no install required). One robot
|
| 238 |
+
(`NHC10DE-A00`) has no S3 thumbnail; without `usdrecord` installed, its tile
|
| 239 |
+
renders the inline SVG "no thumbnail" placeholder.
|
| 240 |
+
|
| 241 |
+
**Required:** either ship `usdrecord` in the foundation's bundled toolchain,
|
| 242 |
+
or distribute a foundation-side wrapper (e.g. a Kit ext that does
|
| 243 |
+
`UsdAppUtils.framerec.FrameRecorder` programmatically) so `simready ingest
|
| 244 |
+
usd` can produce the spec thumbnail without external installs.
|
| 245 |
+
|
| 246 |
+
---
|
| 247 |
+
|
| 248 |
+
## 8. Spec docs disagree on the thumbnail filename
|
| 249 |
+
|
| 250 |
+
**Where:**
|
| 251 |
+
- Spec doc: `nv_core/sr_specs/docs/capabilities/isaac_sim/robot_core/requirements/thumbnail-exist.md`
|
| 252 |
+
- Validation rule: `nv_core/sr_specs/docs/capabilities/isaac_sim/robot_core/validation.py:213`
|
| 253 |
+
- OEM publishing guide: `simready-oem-sdk-poc/docs/oem-publishing-guide-hf.md`
|
| 254 |
+
|
| 255 |
+
**Observed:**
|
| 256 |
+
- The spec doc's example uses `…/Robot_Name/.thumbs/256x256/Robot.png`
|
| 257 |
+
(filename = USD stem only, no `.usd` suffix).
|
| 258 |
+
- The rule code computes `…/<filename>.png` where `<filename>` is the full
|
| 259 |
+
USD filename **including** the `.usd` extension, e.g. `nex4_c00.usd.png`.
|
| 260 |
+
- The OEM publishing guide also uses `robot-a.usd.png` (with `.usd.`).
|
| 261 |
+
- The Isaac S3 bucket uses `NEX4_C00.usd.png` (with `.usd.`).
|
| 262 |
+
|
| 263 |
+
The rule and most published assets agree (`<filename>.png` with `.usd`); the
|
| 264 |
+
spec doc's example is wrong.
|
| 265 |
+
|
| 266 |
+
**Required:** fix the spec example in `thumbnail-exist.md` to match the rule.
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
## 9. `simready ingest usd` does not lift companion thumbnails
|
| 271 |
+
|
| 272 |
+
**Where:** `simready-oem-sdk-poc/src/simready_sdk/packager/organizer.py`.
|
| 273 |
+
|
| 274 |
+
**Observed:** `simready ingest usd ./robot/robot.usd` produces the spec
|
| 275 |
+
layout (`<name>/<name>.usd`, `configuration/`, `materials/`,
|
| 276 |
+
`.thumbs/256x256/` folder) but **does not** copy/rename a thumbnail from any
|
| 277 |
+
companion location into `.thumbs/256x256/<file>.usd.png`. The packager docs
|
| 278 |
+
(`SKILL.md`) call it "thumbnail placeholder" — the directory is created
|
| 279 |
+
empty.
|
| 280 |
+
|
| 281 |
+
For `yaskawa_local/images/<stem>.usd.usd.png`, no automated path exists for
|
| 282 |
+
the packager to know that file should land at the spec's thumbnail path.
|
| 283 |
+
|
| 284 |
+
**Required:** the packager should accept a companion-images directory hint
|
| 285 |
+
(e.g. `--thumbs-from <dir>` or a sibling `images/` convention) and lift
|
| 286 |
+
matching files into `.thumbs/256x256/<file>.usd.png` during organize.
|
| 287 |
+
|
| 288 |
+
---
|
| 289 |
+
|
| 290 |
+
## 10. A foundation rule crashes with `'NoneType' object has no attribute 'JointStateAPI'`
|
| 291 |
+
|
| 292 |
+
**Where:** somewhere in the `simready.foundation.core` plugin's joint-state
|
| 293 |
+
checker (rule not yet pinned down — issue is reported as `code: UNKNOWN`,
|
| 294 |
+
`rule: None`).
|
| 295 |
+
|
| 296 |
+
**Observed:** the rule fires on every prim it visits and produces an
|
| 297 |
+
*Uncaught error* issue, accounting for between **566 and 698 issues per
|
| 298 |
+
asset** in our run — i.e., most of what makes the report look noisy:
|
| 299 |
+
```
|
| 300 |
+
UNKNOWN ×668 Uncaught error: 'NoneType' object has no attribute 'JointStateAPI'
|
| 301 |
+
UNKNOWN ×566 Uncaught error: 'NoneType' object has no attribute 'JointStateAPI'
|
| 302 |
+
UNKNOWN ×698 Uncaught error: 'NoneType' object has no attribute 'JointStateAPI'
|
| 303 |
+
```
|
| 304 |
+
The remaining ~150 issues per asset (HI.*, NP.*, RC.*, etc.) are the *real*
|
| 305 |
+
findings.
|
| 306 |
+
|
| 307 |
+
The crash strongly suggests the rule reads
|
| 308 |
+
`stage.GetPrimAtPath(...).GetAPISchemas()`-style and assumes the result is
|
| 309 |
+
non-`None`. A defensive `if foo is None: continue` would silence the noise;
|
| 310 |
+
the underlying joint-state coverage is presumably what the rule was meant to
|
| 311 |
+
be checking, but currently it produces nothing useful.
|
| 312 |
+
|
| 313 |
+
**Required:** pin down the rule (grep `JointStateAPI` across
|
| 314 |
+
`simready.foundation.core`), add the missing None-guard, and either register
|
| 315 |
+
the rule's requirement properly or remove it from the engine until it works.
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
## 11. `NP.005` — layout mismatch between the rule's spec and `simready ingest usd`, plus a path-printing bug
|
| 320 |
+
|
| 321 |
+
**Where:**
|
| 322 |
+
- Rule code: `simready_foundations/nv_core/sr_specs/docs/capabilities/core/naming_paths/validation.py:271-320` (`AssetFolderStructureChecker`).
|
| 323 |
+
- Rule spec doc: `simready_foundations/nv_core/sr_specs/docs/capabilities/core/naming_paths/requirements/asset-folder-structure.md`.
|
| 324 |
+
- Packager output: `simready-oem-sdk-poc/src/simready_sdk/packager/organizer.py` (invoked via `simready ingest usd`).
|
| 325 |
+
|
| 326 |
+
### 11a. Layout mismatch
|
| 327 |
+
|
| 328 |
+
The NP.005 spec doc requires this structure:
|
| 329 |
+
|
| 330 |
+
```
|
| 331 |
+
<asset>/ ← asset folder
|
| 332 |
+
├── <intermediate>/ ← exactly ONE intermediate folder (any name)
|
| 333 |
+
│ └── <main>.usd ← main USD; NO other .usd files here or below
|
| 334 |
+
└── materials/ ← supporting folders are SIBLINGS of <intermediate>/
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
The reference dashboard's FANUC content follows this exactly:
|
| 338 |
+
|
| 339 |
+
```
|
| 340 |
+
robots/CR/cr_50f_16b/isaac_ready_usd/cr_50f_16b.usd.usd
|
| 341 |
+
robots/CR/cr_50f_16b/configuration/...
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
But `simready ingest usd` produces a *flat* layout — main USD at the asset
|
| 345 |
+
root, sublayers nested inside the same dir:
|
| 346 |
+
|
| 347 |
+
```
|
| 348 |
+
<asset>/
|
| 349 |
+
├── <asset>.usd
|
| 350 |
+
└── configuration/
|
| 351 |
+
├── <asset>_base.usd
|
| 352 |
+
├── <asset>_physics.usd
|
| 353 |
+
└── <asset>_sensor.usd
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
The rule walks `os.path.dirname(stage_path)` (which is `<asset>/` in the
|
| 357 |
+
flat layout) and flags every sublayer as "stray". The sublayers aren't
|
| 358 |
+
stray — they're the spec-required configuration files — but the rule's
|
| 359 |
+
walk-set includes them because the packager produced an
|
| 360 |
+
NP.005-non-compliant layout.
|
| 361 |
+
|
| 362 |
+
So: **two foundation artifacts disagree on the canonical asset layout.**
|
| 363 |
+
The packaging spec (`simready-packaging`'s SKILL.md) and `simready ingest
|
| 364 |
+
usd` produce one layout; NP.005's spec doc and the FANUC reference content
|
| 365 |
+
use a different layout.
|
| 366 |
+
|
| 367 |
+
### 11b. Path-printing bug (independent)
|
| 368 |
+
|
| 369 |
+
```python
|
| 370 |
+
# capabilities/core/naming_paths/validation.py:308-313
|
| 371 |
+
for root, dirs, files in os.walk(current_dir):
|
| 372 |
+
for file in files:
|
| 373 |
+
found_file = os.path.join(root, file).replace("\\", "/")
|
| 374 |
+
if file.endswith((".usd", ".usda")) and found_file != stage_path:
|
| 375 |
+
relative_path = os.path.relpath(file, current_dir) # ← bug
|
| 376 |
+
other_usd_files.append(relative_path)
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
`file` here is just the basename returned by `os.walk` (e.g.
|
| 380 |
+
`nex10_c00_base.usd`). `os.path.relpath` resolves it against
|
| 381 |
+
`os.getcwd()`, not against `root`. With CWD =
|
| 382 |
+
`<workspace>/.claude/skills/simready-report/`, the rule prints:
|
| 383 |
+
|
| 384 |
+
```
|
| 385 |
+
..\..\..\.claude\skills\simready-report\nex10_c00_base.usd
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
— a path back to the validator's CWD, where the file emphatically does not
|
| 389 |
+
exist. Should be `os.path.relpath(found_file, current_dir)` (`found_file`
|
| 390 |
+
is the correct joined path, already computed on the line above and
|
| 391 |
+
otherwise unused). With that one-character fix the cited path becomes
|
| 392 |
+
`configuration/nex10_c00_base.usd` — pointing at the real file.
|
| 393 |
+
|
| 394 |
+
### Required
|
| 395 |
+
|
| 396 |
+
**Pick one canonical layout and reconcile both sides.** Either:
|
| 397 |
+
|
| 398 |
+
- **Update the packager** (`simready ingest usd`) to emit the
|
| 399 |
+
NP.005-compliant `<asset>/<intermediate>/<main>.usd` structure with
|
| 400 |
+
`configuration/` and `materials/` as siblings of `<intermediate>/`. This
|
| 401 |
+
is what the FANUC reference content already uses. The packaging skill
|
| 402 |
+
doc and any consumers (manifest tooling, dashboard, etc.) need updating
|
| 403 |
+
in lockstep.
|
| 404 |
+
|
| 405 |
+
- **Or update NP.005's spec doc and rule** to bless the flat layout that
|
| 406 |
+
`simready ingest usd` actually produces — i.e., explicitly whitelist the
|
| 407 |
+
spec-mandated subdirectories (`configuration/`, `materials/`,
|
| 408 |
+
`.thumbs/`, `.simready/`) when scanning for stray USDs.
|
| 409 |
+
|
| 410 |
+
Whichever is chosen, fix the path-printing bug at the same time
|
| 411 |
+
(`relpath(file, …)` → `relpath(found_file, …)`) so the message names a
|
| 412 |
+
real on-disk path.
|
| 413 |
+
|
| 414 |
+
---
|
| 415 |
+
|
| 416 |
+
## 12. The "example" capability is active in production validation
|
| 417 |
+
|
| 418 |
+
**Where:**
|
| 419 |
+
`simready_foundations/nv_core/sr_specs/docs/capabilities/example/example.py`,
|
| 420 |
+
codes `EX.01`, `EX.02`, `EX.03`.
|
| 421 |
+
|
| 422 |
+
**Observed:** every asset fails:
|
| 423 |
+
- `EX.01` — *"Stage has no mesh prims."*
|
| 424 |
+
- `EX.03` — *"Test prim 'TestBlob' not found under default prim."*
|
| 425 |
+
|
| 426 |
+
The source comment on this capability is literally `"example"` and the
|
| 427 |
+
rule docs (`example.md`) describe it as a tutorial — `EX.03` requires a
|
| 428 |
+
prim named `TestBlob` to exist as a child of the default prim. That's
|
| 429 |
+
clearly not an asset requirement; it's pedagogical scaffolding to teach
|
| 430 |
+
plugin authors how to register a rule.
|
| 431 |
+
|
| 432 |
+
**Required:** unregister the `Example` capability from production rule
|
| 433 |
+
loading (move it to test fixtures, gate it behind an env var, or delete it
|
| 434 |
+
from the shipped plugin). Today every real asset takes 2–3 spurious EX
|
| 435 |
+
failures.
|
| 436 |
+
|
| 437 |
+
---
|
| 438 |
+
|
| 439 |
+
## 13. `EX.01` and `VG.MESH.001` are duplicate checks under different codes
|
| 440 |
+
|
| 441 |
+
**Where:**
|
| 442 |
+
- `capabilities/example/example.py:52` — `EX.01`: *"Stage has no mesh prims."*
|
| 443 |
+
- `capabilities/visualization/geometry/validation.py:836` — `VG.MESH.001`:
|
| 444 |
+
*"Stage does not contain any meshes."*
|
| 445 |
+
|
| 446 |
+
**Observed:** both fire on every asset, with effectively identical
|
| 447 |
+
semantics. `EX.01` is the example/tutorial version; `VG.MESH.001` is the
|
| 448 |
+
real version. Reporting both is noise.
|
| 449 |
+
|
| 450 |
+
**Required:** when #12 is fixed, this disappears for free. Otherwise,
|
| 451 |
+
`EX.01` should be retired and any consumer that still references it should
|
| 452 |
+
be redirected to `VG.MESH.001`.
|
| 453 |
+
|
| 454 |
+
---
|
| 455 |
+
|
| 456 |
+
## 14. `HI.002` rejects valid USD xform-op decompositions
|
| 457 |
+
|
| 458 |
+
**Where:** `capabilities/hierarchy/validation.py:62-104`
|
| 459 |
+
(`ExclusiveXFormParentChecker`).
|
| 460 |
+
|
| 461 |
+
**Observed:** ~14 issues per asset, message *"Prim Parent has no
|
| 462 |
+
xformOp:rotate."* The rule requires every Gprim parent to author both
|
| 463 |
+
`xformOp:translate` and (a substring-matching) `xformOp:rotate*`:
|
| 464 |
+
|
| 465 |
+
```python
|
| 466 |
+
if not any("xformOp:rotate" in op.GetAttr().GetName() for op in xform_ops):
|
| 467 |
+
self._AddFailedCheck("Prim Parent has no xformOp:rotate.", …)
|
| 468 |
+
```
|
| 469 |
+
|
| 470 |
+
That's an over-strict reading of USD. Valid alternatives the rule rejects:
|
| 471 |
+
|
| 472 |
+
- `xformOp:transform` (single 4×4 matrix — encodes translate + rotate +
|
| 473 |
+
scale together).
|
| 474 |
+
- No xform ops at all (identity transform — also a valid stateless prim).
|
| 475 |
+
- Any combination authored on an inherited parent and overridden by the
|
| 476 |
+
Gprim's own ops.
|
| 477 |
+
|
| 478 |
+
Yaskawa's robots author matrix transforms in many places, which is what
|
| 479 |
+
trips this. The check should accept *any* xformable spec that resolves to a
|
| 480 |
+
defined transform — likely via `Xformable.GetLocalTransformation()`
|
| 481 |
+
returning a non-error result — rather than pattern-matching specific op
|
| 482 |
+
names.
|
| 483 |
+
|
| 484 |
+
**Required:** rewrite the check to validate *that the prim resolves to a
|
| 485 |
+
defined transform*, not that it authored a particular op vocabulary.
|
| 486 |
+
|
| 487 |
+
---
|
| 488 |
+
|
| 489 |
+
## 15. `NP.001` flags the conventional `/Looks` scope as a naming violation
|
| 490 |
+
|
| 491 |
+
**Where:** `capabilities/core/naming_paths/validation.py:125-160`
|
| 492 |
+
(`PrimNamingConventionChecker`).
|
| 493 |
+
|
| 494 |
+
**Observed:** every asset fails NP.001 ×8 with *"Prim 'Looks' does not
|
| 495 |
+
follow consistent naming convention (camelCase or snake_case)."*
|
| 496 |
+
|
| 497 |
+
`Looks` is the long-established USD convention for the materials scope
|
| 498 |
+
under an asset's default prim — it's used by Pixar's published USD
|
| 499 |
+
samples, NVIDIA's Isaac/Omniverse content, and most authoring tools
|
| 500 |
+
(Houdini, Maya USD, Kit). The rule's `CAMEL_CASE_PATTERN` and
|
| 501 |
+
`SNAKE_CASE_PATTERN` regexes don't accept the `Looks` PascalCase
|
| 502 |
+
single-word, so it reports a violation on a name the broader USD ecosystem
|
| 503 |
+
treats as canonical.
|
| 504 |
+
|
| 505 |
+
**Required:** either whitelist the well-known USD scope names (`Looks`,
|
| 506 |
+
`Geometry`, `Cameras`, `Lights`, `Animation`, `Render`, etc.) or accept
|
| 507 |
+
PascalCase as a valid convention alongside camelCase / snake_case.
|
| 508 |
+
|
| 509 |
+
---
|
| 510 |
+
|
| 511 |
+
## 16. `VM.MDL.001` and `NP.008` double-report the same MDL resolution failure
|
| 512 |
+
|
| 513 |
+
**Where:**
|
| 514 |
+
- `capabilities/visualization/materials/...` (VM.MDL.001 — *"The path
|
| 515 |
+
OmniPBR.mdl does not exist."*)
|
| 516 |
+
- `capabilities/core/naming_paths/...` (NP.008 — *"Asset path 'OmniPBR.mdl'
|
| 517 |
+
on attribute 'info:mdl:sourceAsset' at prim '/.../Looks/.../...' does not
|
| 518 |
+
resolve to an existing file."*)
|
| 519 |
+
|
| 520 |
+
**Observed:** every asset emits the same number of VM.MDL.001 and NP.008
|
| 521 |
+
issues (9 each on `nex10_c00`, 6 each on `nex20_c00`, etc.) — both rules
|
| 522 |
+
fire on the same `info:mdl:sourceAsset = @OmniPBR.mdl@` attribute. NP.008
|
| 523 |
+
gives a useful prim path; VM.MDL.001 just says "doesn't exist".
|
| 524 |
+
|
| 525 |
+
Separately: `OmniPBR.mdl` is a **standard NVIDIA MDL** that ships with Kit
|
| 526 |
+
and Isaac. The validator's asset resolver doesn't know where to find it
|
| 527 |
+
(likely missing an MDL search path in the SimReady plugin's resolver
|
| 528 |
+
config), so a perfectly portable, NVIDIA-blessed material reference is
|
| 529 |
+
flagged as broken on every asset.
|
| 530 |
+
|
| 531 |
+
**Required:**
|
| 532 |
+
1. Configure the SimReady validator plugin to register Kit's standard MDL
|
| 533 |
+
search paths (`mdl/`, `omni/mdl/`, etc.) so `OmniPBR.mdl` and other
|
| 534 |
+
shipped MDLs resolve without the customer having to vendor them.
|
| 535 |
+
2. Pick one of VM.MDL.001 / NP.008 as the canonical "MDL doesn't resolve"
|
| 536 |
+
code. Have the other defer when the first has fired against the same
|
| 537 |
+
attribute.
|
| 538 |
+
|
| 539 |
+
---
|
| 540 |
+
|
| 541 |
+
## 17. `RC.002` flags spec-mandated robot metadata as "overrides"
|
| 542 |
+
|
| 543 |
+
**Where:** `capabilities/isaac_sim/robot_core/...`, `RC.002`.
|
| 544 |
+
|
| 545 |
+
**Observed:** every asset fails RC.002 with messages like:
|
| 546 |
+
|
| 547 |
+
```
|
| 548 |
+
Prim is overridden: /nex10_c00, ['isaac:description', 'isaac:license',
|
| 549 |
+
'isaac:namespace', 'isaac:robotType']
|
| 550 |
+
```
|
| 551 |
+
|
| 552 |
+
`isaac:description`, `isaac:license`, `isaac:namespace`,
|
| 553 |
+
`isaac:robotType` look like exactly the kind of metadata the SimReady
|
| 554 |
+
robot spec wants you to author on the robot's default prim — but RC.002
|
| 555 |
+
is treating any authored override of the default prim's properties as a
|
| 556 |
+
violation.
|
| 557 |
+
|
| 558 |
+
If those four `isaac:*` attributes are spec-mandated, the rule should
|
| 559 |
+
*require* them, not flag their presence. If they're optional metadata, the
|
| 560 |
+
rule should ignore them — overriding metadata fields on the default prim
|
| 561 |
+
is a normal authoring pattern, not a layout error.
|
| 562 |
+
|
| 563 |
+
**Required:** clarify the spec around the `isaac:*` namespace on the
|
| 564 |
+
robot default prim and update RC.002 to either require those fields, or
|
| 565 |
+
exclude them from the "this prim shouldn't be overridden" check, or split
|
| 566 |
+
into two rules (one for "structural" overrides, one for the spec
|
| 567 |
+
metadata).
|
| 568 |
+
|
| 569 |
+
---
|
| 570 |
+
|
| 571 |
+
## Summary
|
| 572 |
+
|
| 573 |
+
| # | Severity | Area | One-line |
|
| 574 |
+
|---|---|---|---|
|
| 575 |
+
| 1 | high | profile schema | `Profile.capabilities` is dead; should be removed or populated |
|
| 576 |
+
| 2 | high | engine API | `enable_*`/`disable_*` don't actually scope rule execution |
|
| 577 |
+
| 3 | high | profile content | Robot-Body-Isaac v1.0.0 silently drops 4 of 6 declared features |
|
| 578 |
+
| 4 | high | profile schema | JSON capability configs aren't linked to the Python profile registry |
|
| 579 |
+
| 5 | high | rule registration | Most rules don't declare `@register_requirements` → 87% UNKNOWN |
|
| 580 |
+
| 6 | medium | engine API | `init_rules` defaults inconsistent; SDK silently turns off all checks |
|
| 581 |
+
| 7 | medium | tooling | No headless thumbnail generator ships with foundations |
|
| 582 |
+
| 8 | low | spec docs | `thumbnail-exist.md` example contradicts the actual rule |
|
| 583 |
+
| 9 | medium | packaging | `simready ingest usd` doesn't lift companion thumbnails into spec layout |
|
| 584 |
+
| 10 | high | rule reliability | Rule crashes on `JointStateAPI` access → ~600 UNKNOWN issues per asset |
|
| 585 |
+
| 11 | high | spec/packager mismatch | NP.005 expects `<asset>/<intermediate>/<main>.usd`; packager produces flat layout. Plus `relpath(file,…)` should be `relpath(found_file,…)` |
|
| 586 |
+
| 12 | high | rule scoping | "Example" tutorial capability (`EX.*`) is active in production validation |
|
| 587 |
+
| 13 | medium | rule duplication | `EX.01` and `VG.MESH.001` are the same check |
|
| 588 |
+
| 14 | high | rule correctness | `HI.002` rejects matrix transforms (`xformOp:transform`) and identity transforms |
|
| 589 |
+
| 15 | medium | rule correctness | `NP.001` rejects the conventional `/Looks` scope name |
|
| 590 |
+
| 16 | high | rule correctness | `OmniPBR.mdl` (standard NVIDIA MDL) doesn't resolve; both VM.MDL.001 and NP.008 fire on the same attribute |
|
| 591 |
+
| 17 | medium | spec ambiguity | `RC.002` flags spec-looking `isaac:*` metadata as illegal "overrides" |
|
tools/validation/plugins/simready-report/plugin.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
{
|
| 2 |
-
"$schema": "https://json.schemastore.org/claude-code-plugin",
|
| 3 |
-
"name": "simready-report",
|
| 4 |
-
"version": "0.2.0",
|
| 5 |
-
"description": "SimReady asset pipeline tooling. Ships two slash commands: /simready-report (validate customer USD assets and emit an HTML dashboard + external-deps provenance report) and /simready-package (run the simready ingest packaging step alone, no validation).",
|
| 6 |
-
"author": {
|
| 7 |
-
"name": "loginowskid",
|
| 8 |
-
"email": "dloginowski@nvidia.com"
|
| 9 |
-
},
|
| 10 |
-
"homepage": "https://github.com/loginowskid/simready-playbook"
|
| 11 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json.schemastore.org/claude-code-plugin",
|
| 3 |
+
"name": "simready-report",
|
| 4 |
+
"version": "0.2.0",
|
| 5 |
+
"description": "SimReady asset pipeline tooling. Ships two slash commands: /simready-report (validate customer USD assets and emit an HTML dashboard + external-deps provenance report) and /simready-package (run the simready ingest packaging step alone, no validation).",
|
| 6 |
+
"author": {
|
| 7 |
+
"name": "loginowskid",
|
| 8 |
+
"email": "dloginowski@nvidia.com"
|
| 9 |
+
},
|
| 10 |
+
"homepage": "https://github.com/loginowskid/simready-playbook"
|
| 11 |
+
}
|
tools/validation/plugins/simready-report/skills/simready-package/SKILL.md
CHANGED
|
@@ -1,62 +1,62 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: simready-package
|
| 3 |
-
description: Package SimReady customer assets by running `simready ingest usd` per interface USD. Standalone alternative to /simready-report's auto-package step. Invoke with /simready-package <name-or-path>.
|
| 4 |
-
---
|
| 5 |
-
|
| 6 |
-
# /simready-package
|
| 7 |
-
|
| 8 |
-
Run the SimReady packaging step on a directory of customer asset subfolders, without running validation or generating a dashboard.
|
| 9 |
-
|
| 10 |
-
## Usage
|
| 11 |
-
|
| 12 |
-
- `/simready-package` (no arg) — package the **current working directory**.
|
| 13 |
-
- `/simready-package <path>` — package the given directory by absolute or relative path.
|
| 14 |
-
|
| 15 |
-
## When to use
|
| 16 |
-
|
| 17 |
-
- You have a directory like `assets_to_validate/<name>/` with one subfolder per asset (each containing an interface USD), and want to produce a packaged tree without running validation.
|
| 18 |
-
- You want to inspect the packaged output before invoking `/simready-report`.
|
| 19 |
-
- You're iterating on packaging configuration and don't want the validation overhead each time.
|
| 20 |
-
|
| 21 |
-
`/simready-report` still auto-packages when its target sits under an `assets_to_validate/` dir, so you don't *need* `/simready-package` to use the report. They're independent.
|
| 22 |
-
|
| 23 |
-
## Steps for Claude when `/simready-package` is invoked
|
| 24 |
-
|
| 25 |
-
1. **Resolve the target.** No arg → CWD. With an arg, pass it through to the script.
|
| 26 |
-
2. **Run the packager** from this skill directory using whatever Python the user's environment provides — prefer `$env:SIMREADY_PYTHON` (set by `bootstrap.ps1`) over `python` on PATH:
|
| 27 |
-
|
| 28 |
-
PowerShell:
|
| 29 |
-
```
|
| 30 |
-
$py = if ($env:SIMREADY_PYTHON) { $env:SIMREADY_PYTHON } else { "python" }
|
| 31 |
-
& $py package.py "<arg-or-empty>"
|
| 32 |
-
```
|
| 33 |
-
Bash/zsh:
|
| 34 |
-
```
|
| 35 |
-
"${SIMREADY_PYTHON:-python}" package.py "<arg-or-empty>"
|
| 36 |
-
```
|
| 37 |
-
Add `--profile NAME` only if the user explicitly asked for a non-default profile. Add `--output DIR` only if the user explicitly asked for a non-default output.
|
| 38 |
-
3. **Surface results.** Echo the script's `SUMMARY:` line and the absolute `OUTPUT:` path.
|
| 39 |
-
4. **If the script exits with "simready CLI not found"**, relay verbatim. The user needs `simready-oem-sdk-poc` installed into the active Python, or to run `bootstrap.ps1`. Don't run the bootstrap on the user's behalf without explicit confirmation.
|
| 40 |
-
|
| 41 |
-
## Default output location
|
| 42 |
-
|
| 43 |
-
- When the target sits under an `assets_to_validate/<name>/` directory, output goes to the sibling `packages/<name>/`.
|
| 44 |
-
- Otherwise, output goes to `<target>.parent/packages/<target.name>/`.
|
| 45 |
-
- Override with `--output <path>`.
|
| 46 |
-
|
| 47 |
-
## What the packager does
|
| 48 |
-
|
| 49 |
-
- **Interface discovery.** For each immediate subfolder of the target, picks one interface USD per asset:
|
| 50 |
-
- exactly one USD at root → that one
|
| 51 |
-
- otherwise → the USD whose stem matches the folder name (case- and hyphen/underscore-insensitive)
|
| 52 |
-
- otherwise → all USDs (rare)
|
| 53 |
-
- subfolders starting with `.` or `_` are ignored
|
| 54 |
-
- **Idempotent.** Skips assets already packaged (existing `<output>/<stem>/<filename>.usd`).
|
| 55 |
-
- **Per-asset invocation.** Calls `simready ingest usd <interface> -o <output> -p <profile> --no-validate` for each interface, with a 120s timeout.
|
| 56 |
-
- **Summary.** Prints `SUMMARY: packaged=N skipped=N failed=N` and the resolved `OUTPUT:` path. Exits non-zero if any asset failed to ingest (the rest still get packaged).
|
| 57 |
-
|
| 58 |
-
## Configuration
|
| 59 |
-
|
| 60 |
-
- `SIMREADY_PYTHON` (recommended): full path to the Python interpreter that has `simready-oem-sdk-poc` installed. Set automatically by `bootstrap.ps1`.
|
| 61 |
-
- Default profile: `Robot-Body-Runnable`. Override with `--profile NAME`.
|
| 62 |
-
- Default output: see above. Override with `--output DIR`.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: simready-package
|
| 3 |
+
description: Package SimReady customer assets by running `simready ingest usd` per interface USD. Standalone alternative to /simready-report's auto-package step. Invoke with /simready-package <name-or-path>.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# /simready-package
|
| 7 |
+
|
| 8 |
+
Run the SimReady packaging step on a directory of customer asset subfolders, without running validation or generating a dashboard.
|
| 9 |
+
|
| 10 |
+
## Usage
|
| 11 |
+
|
| 12 |
+
- `/simready-package` (no arg) — package the **current working directory**.
|
| 13 |
+
- `/simready-package <path>` — package the given directory by absolute or relative path.
|
| 14 |
+
|
| 15 |
+
## When to use
|
| 16 |
+
|
| 17 |
+
- You have a directory like `assets_to_validate/<name>/` with one subfolder per asset (each containing an interface USD), and want to produce a packaged tree without running validation.
|
| 18 |
+
- You want to inspect the packaged output before invoking `/simready-report`.
|
| 19 |
+
- You're iterating on packaging configuration and don't want the validation overhead each time.
|
| 20 |
+
|
| 21 |
+
`/simready-report` still auto-packages when its target sits under an `assets_to_validate/` dir, so you don't *need* `/simready-package` to use the report. They're independent.
|
| 22 |
+
|
| 23 |
+
## Steps for Claude when `/simready-package` is invoked
|
| 24 |
+
|
| 25 |
+
1. **Resolve the target.** No arg → CWD. With an arg, pass it through to the script.
|
| 26 |
+
2. **Run the packager** from this skill directory using whatever Python the user's environment provides — prefer `$env:SIMREADY_PYTHON` (set by `bootstrap.ps1`) over `python` on PATH:
|
| 27 |
+
|
| 28 |
+
PowerShell:
|
| 29 |
+
```
|
| 30 |
+
$py = if ($env:SIMREADY_PYTHON) { $env:SIMREADY_PYTHON } else { "python" }
|
| 31 |
+
& $py package.py "<arg-or-empty>"
|
| 32 |
+
```
|
| 33 |
+
Bash/zsh:
|
| 34 |
+
```
|
| 35 |
+
"${SIMREADY_PYTHON:-python}" package.py "<arg-or-empty>"
|
| 36 |
+
```
|
| 37 |
+
Add `--profile NAME` only if the user explicitly asked for a non-default profile. Add `--output DIR` only if the user explicitly asked for a non-default output.
|
| 38 |
+
3. **Surface results.** Echo the script's `SUMMARY:` line and the absolute `OUTPUT:` path.
|
| 39 |
+
4. **If the script exits with "simready CLI not found"**, relay verbatim. The user needs `simready-oem-sdk-poc` installed into the active Python, or to run `bootstrap.ps1`. Don't run the bootstrap on the user's behalf without explicit confirmation.
|
| 40 |
+
|
| 41 |
+
## Default output location
|
| 42 |
+
|
| 43 |
+
- When the target sits under an `assets_to_validate/<name>/` directory, output goes to the sibling `packages/<name>/`.
|
| 44 |
+
- Otherwise, output goes to `<target>.parent/packages/<target.name>/`.
|
| 45 |
+
- Override with `--output <path>`.
|
| 46 |
+
|
| 47 |
+
## What the packager does
|
| 48 |
+
|
| 49 |
+
- **Interface discovery.** For each immediate subfolder of the target, picks one interface USD per asset:
|
| 50 |
+
- exactly one USD at root → that one
|
| 51 |
+
- otherwise → the USD whose stem matches the folder name (case- and hyphen/underscore-insensitive)
|
| 52 |
+
- otherwise → all USDs (rare)
|
| 53 |
+
- subfolders starting with `.` or `_` are ignored
|
| 54 |
+
- **Idempotent.** Skips assets already packaged (existing `<output>/<stem>/<filename>.usd`).
|
| 55 |
+
- **Per-asset invocation.** Calls `simready ingest usd <interface> -o <output> -p <profile> --no-validate` for each interface, with a 120s timeout.
|
| 56 |
+
- **Summary.** Prints `SUMMARY: packaged=N skipped=N failed=N` and the resolved `OUTPUT:` path. Exits non-zero if any asset failed to ingest (the rest still get packaged).
|
| 57 |
+
|
| 58 |
+
## Configuration
|
| 59 |
+
|
| 60 |
+
- `SIMREADY_PYTHON` (recommended): full path to the Python interpreter that has `simready-oem-sdk-poc` installed. Set automatically by `bootstrap.ps1`.
|
| 61 |
+
- Default profile: `Robot-Body-Runnable`. Override with `--profile NAME`.
|
| 62 |
+
- Default output: see above. Override with `--output DIR`.
|
tools/validation/plugins/simready-report/skills/simready-package/package.py
CHANGED
|
@@ -1,143 +1,143 @@
|
|
| 1 |
-
"""SimReady asset packaging — entry point for the /simready-package skill.
|
| 2 |
-
|
| 3 |
-
Usage:
|
| 4 |
-
python package.py <target_dir> [--profile NAME] [--output DIR]
|
| 5 |
-
|
| 6 |
-
For each immediate subfolder of <target_dir> containing a USD file, picks
|
| 7 |
-
the interface USD and runs `simready ingest usd` on it, producing a
|
| 8 |
-
packaged tree under <output_dir>. Independent of /simready-report — runs
|
| 9 |
-
the packaging step alone, without validation or dashboard generation.
|
| 10 |
-
"""
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import argparse
|
| 14 |
-
import os
|
| 15 |
-
import shutil
|
| 16 |
-
import subprocess
|
| 17 |
-
import sys
|
| 18 |
-
from pathlib import Path
|
| 19 |
-
|
| 20 |
-
USD_EXTS = (".usd", ".usda", ".usdc", ".usdz")
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def _find_simready_cli() -> str | None:
|
| 24 |
-
"""Locate the `simready` CLI in a layout-agnostic way.
|
| 25 |
-
|
| 26 |
-
Order: (1) on PATH, (2) alongside `sys.executable` (covers any active
|
| 27 |
-
venv/conda regardless of OS).
|
| 28 |
-
"""
|
| 29 |
-
on_path = shutil.which("simready")
|
| 30 |
-
if on_path:
|
| 31 |
-
return on_path
|
| 32 |
-
py_bin = Path(sys.executable).parent
|
| 33 |
-
for candidate in (py_bin / "simready.exe", py_bin / "simready"):
|
| 34 |
-
if candidate.is_file():
|
| 35 |
-
return str(candidate)
|
| 36 |
-
return None
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def _discover_interface_usds(target: Path) -> list[Path]:
|
| 40 |
-
"""Pick one interface USD per immediate subfolder of `target`.
|
| 41 |
-
|
| 42 |
-
Heuristic per subfolder:
|
| 43 |
-
- If exactly one USD at root, that's the interface.
|
| 44 |
-
- Else prefer the USD whose stem matches the folder name (case- and
|
| 45 |
-
hyphen/underscore-insensitive).
|
| 46 |
-
- Else fall back to packaging all of them.
|
| 47 |
-
Subfolders starting with "." or "_" are ignored.
|
| 48 |
-
"""
|
| 49 |
-
interface_files: list[Path] = []
|
| 50 |
-
for sub in sorted(target.iterdir()):
|
| 51 |
-
if not sub.is_dir() or sub.name.startswith((".", "_")):
|
| 52 |
-
continue
|
| 53 |
-
usds = [p for ext in USD_EXTS for p in sub.glob(f"*{ext}")]
|
| 54 |
-
if not usds:
|
| 55 |
-
continue
|
| 56 |
-
if len(usds) == 1:
|
| 57 |
-
interface_files.append(usds[0])
|
| 58 |
-
continue
|
| 59 |
-
named = [p for p in usds if p.stem.lower() == sub.name.lower().replace("-", "_")]
|
| 60 |
-
if named:
|
| 61 |
-
interface_files.append(named[0])
|
| 62 |
-
else:
|
| 63 |
-
interface_files.extend(usds)
|
| 64 |
-
return interface_files
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def _default_output(target: Path) -> Path:
|
| 68 |
-
"""Pick a default output dir based on target location."""
|
| 69 |
-
target_str = str(target).replace("\\", "/")
|
| 70 |
-
if "/assets_to_validate/" in target_str:
|
| 71 |
-
# Workspace-style layout: <X>/assets_to_validate/<name>
|
| 72 |
-
# → output at sibling packages dir: <X>/packages/<name>
|
| 73 |
-
return target.parent.parent / "packages" / target.name
|
| 74 |
-
return target.parent / "packages" / target.name
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def main() -> int:
|
| 78 |
-
ap = argparse.ArgumentParser(description="Package SimReady assets via `simready ingest usd`.")
|
| 79 |
-
ap.add_argument("target", nargs="?", default=None,
|
| 80 |
-
help="Directory containing one or more asset subfolders (default: cwd)")
|
| 81 |
-
ap.add_argument("--profile", default="Robot-Body-Runnable",
|
| 82 |
-
help="Profile passed to `simready ingest usd -p` (default: Robot-Body-Runnable)")
|
| 83 |
-
ap.add_argument("--output", default=None,
|
| 84 |
-
help="Output dir (default: <target>.parent/packages/<target.name>, "
|
| 85 |
-
"or sibling-of-assets_to_validate/packages/<name> when applicable)")
|
| 86 |
-
args = ap.parse_args()
|
| 87 |
-
|
| 88 |
-
target = Path(args.target).resolve() if args.target else Path.cwd().resolve()
|
| 89 |
-
if not target.is_dir():
|
| 90 |
-
print(f"ERROR: target is not a directory: {target}", flush=True)
|
| 91 |
-
return 2
|
| 92 |
-
|
| 93 |
-
simready_exe = _find_simready_cli()
|
| 94 |
-
if simready_exe is None:
|
| 95 |
-
print("ERROR: `simready` CLI not found on PATH or alongside the active Python.", flush=True)
|
| 96 |
-
print(" Install simready-oem-sdk-poc into the active Python, or run bootstrap.ps1.", flush=True)
|
| 97 |
-
return 2
|
| 98 |
-
|
| 99 |
-
interface_files = _discover_interface_usds(target)
|
| 100 |
-
if not interface_files:
|
| 101 |
-
print(f"ERROR: no USD files found in immediate subfolders of {target}.", flush=True)
|
| 102 |
-
print(" /simready-package expects each asset to live in its own subfolder "
|
| 103 |
-
"with an interface USD.", flush=True)
|
| 104 |
-
return 2
|
| 105 |
-
|
| 106 |
-
output = Path(args.output).resolve() if args.output else _default_output(target)
|
| 107 |
-
output.mkdir(parents=True, exist_ok=True)
|
| 108 |
-
|
| 109 |
-
print(f"Target: {target}", flush=True)
|
| 110 |
-
print(f"Output: {output}", flush=True)
|
| 111 |
-
print(f"Profile: {args.profile}", flush=True)
|
| 112 |
-
print(f"CLI: {simready_exe}", flush=True)
|
| 113 |
-
print(f"Discovered {len(interface_files)} interface USD(s).", flush=True)
|
| 114 |
-
|
| 115 |
-
env = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"}
|
| 116 |
-
packaged = 0
|
| 117 |
-
skipped = 0
|
| 118 |
-
failures: list[tuple[str, str]] = []
|
| 119 |
-
|
| 120 |
-
for usd in interface_files:
|
| 121 |
-
already = output / usd.stem / usd.name
|
| 122 |
-
if already.is_file():
|
| 123 |
-
print(f" skip (already packaged): {usd.name}", flush=True)
|
| 124 |
-
skipped += 1
|
| 125 |
-
continue
|
| 126 |
-
print(f" package: {usd.name}", flush=True)
|
| 127 |
-
cmd = [simready_exe, "ingest", "usd", str(usd),
|
| 128 |
-
"-o", str(output), "-p", args.profile, "--no-validate"]
|
| 129 |
-
try:
|
| 130 |
-
subprocess.run(cmd, env=env, check=True, capture_output=True, text=True, timeout=120)
|
| 131 |
-
packaged += 1
|
| 132 |
-
except subprocess.CalledProcessError as e:
|
| 133 |
-
err = (e.stderr or "")[-300:] or str(e)
|
| 134 |
-
failures.append((usd.name, err))
|
| 135 |
-
print(f" FAILED: {usd.name} - {err}", flush=True)
|
| 136 |
-
|
| 137 |
-
print(f"\nSUMMARY: packaged={packaged} skipped={skipped} failed={len(failures)}", flush=True)
|
| 138 |
-
print(f"OUTPUT: {output}", flush=True)
|
| 139 |
-
return 0 if not failures else 1
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
if __name__ == "__main__":
|
| 143 |
-
sys.exit(main())
|
|
|
|
| 1 |
+
"""SimReady asset packaging — entry point for the /simready-package skill.
|
| 2 |
+
|
| 3 |
+
Usage:
|
| 4 |
+
python package.py <target_dir> [--profile NAME] [--output DIR]
|
| 5 |
+
|
| 6 |
+
For each immediate subfolder of <target_dir> containing a USD file, picks
|
| 7 |
+
the interface USD and runs `simready ingest usd` on it, producing a
|
| 8 |
+
packaged tree under <output_dir>. Independent of /simready-report — runs
|
| 9 |
+
the packaging step alone, without validation or dashboard generation.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import os
|
| 15 |
+
import shutil
|
| 16 |
+
import subprocess
|
| 17 |
+
import sys
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
USD_EXTS = (".usd", ".usda", ".usdc", ".usdz")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _find_simready_cli() -> str | None:
|
| 24 |
+
"""Locate the `simready` CLI in a layout-agnostic way.
|
| 25 |
+
|
| 26 |
+
Order: (1) on PATH, (2) alongside `sys.executable` (covers any active
|
| 27 |
+
venv/conda regardless of OS).
|
| 28 |
+
"""
|
| 29 |
+
on_path = shutil.which("simready")
|
| 30 |
+
if on_path:
|
| 31 |
+
return on_path
|
| 32 |
+
py_bin = Path(sys.executable).parent
|
| 33 |
+
for candidate in (py_bin / "simready.exe", py_bin / "simready"):
|
| 34 |
+
if candidate.is_file():
|
| 35 |
+
return str(candidate)
|
| 36 |
+
return None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _discover_interface_usds(target: Path) -> list[Path]:
|
| 40 |
+
"""Pick one interface USD per immediate subfolder of `target`.
|
| 41 |
+
|
| 42 |
+
Heuristic per subfolder:
|
| 43 |
+
- If exactly one USD at root, that's the interface.
|
| 44 |
+
- Else prefer the USD whose stem matches the folder name (case- and
|
| 45 |
+
hyphen/underscore-insensitive).
|
| 46 |
+
- Else fall back to packaging all of them.
|
| 47 |
+
Subfolders starting with "." or "_" are ignored.
|
| 48 |
+
"""
|
| 49 |
+
interface_files: list[Path] = []
|
| 50 |
+
for sub in sorted(target.iterdir()):
|
| 51 |
+
if not sub.is_dir() or sub.name.startswith((".", "_")):
|
| 52 |
+
continue
|
| 53 |
+
usds = [p for ext in USD_EXTS for p in sub.glob(f"*{ext}")]
|
| 54 |
+
if not usds:
|
| 55 |
+
continue
|
| 56 |
+
if len(usds) == 1:
|
| 57 |
+
interface_files.append(usds[0])
|
| 58 |
+
continue
|
| 59 |
+
named = [p for p in usds if p.stem.lower() == sub.name.lower().replace("-", "_")]
|
| 60 |
+
if named:
|
| 61 |
+
interface_files.append(named[0])
|
| 62 |
+
else:
|
| 63 |
+
interface_files.extend(usds)
|
| 64 |
+
return interface_files
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _default_output(target: Path) -> Path:
|
| 68 |
+
"""Pick a default output dir based on target location."""
|
| 69 |
+
target_str = str(target).replace("\\", "/")
|
| 70 |
+
if "/assets_to_validate/" in target_str:
|
| 71 |
+
# Workspace-style layout: <X>/assets_to_validate/<name>
|
| 72 |
+
# → output at sibling packages dir: <X>/packages/<name>
|
| 73 |
+
return target.parent.parent / "packages" / target.name
|
| 74 |
+
return target.parent / "packages" / target.name
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def main() -> int:
|
| 78 |
+
ap = argparse.ArgumentParser(description="Package SimReady assets via `simready ingest usd`.")
|
| 79 |
+
ap.add_argument("target", nargs="?", default=None,
|
| 80 |
+
help="Directory containing one or more asset subfolders (default: cwd)")
|
| 81 |
+
ap.add_argument("--profile", default="Robot-Body-Runnable",
|
| 82 |
+
help="Profile passed to `simready ingest usd -p` (default: Robot-Body-Runnable)")
|
| 83 |
+
ap.add_argument("--output", default=None,
|
| 84 |
+
help="Output dir (default: <target>.parent/packages/<target.name>, "
|
| 85 |
+
"or sibling-of-assets_to_validate/packages/<name> when applicable)")
|
| 86 |
+
args = ap.parse_args()
|
| 87 |
+
|
| 88 |
+
target = Path(args.target).resolve() if args.target else Path.cwd().resolve()
|
| 89 |
+
if not target.is_dir():
|
| 90 |
+
print(f"ERROR: target is not a directory: {target}", flush=True)
|
| 91 |
+
return 2
|
| 92 |
+
|
| 93 |
+
simready_exe = _find_simready_cli()
|
| 94 |
+
if simready_exe is None:
|
| 95 |
+
print("ERROR: `simready` CLI not found on PATH or alongside the active Python.", flush=True)
|
| 96 |
+
print(" Install simready-oem-sdk-poc into the active Python, or run bootstrap.ps1.", flush=True)
|
| 97 |
+
return 2
|
| 98 |
+
|
| 99 |
+
interface_files = _discover_interface_usds(target)
|
| 100 |
+
if not interface_files:
|
| 101 |
+
print(f"ERROR: no USD files found in immediate subfolders of {target}.", flush=True)
|
| 102 |
+
print(" /simready-package expects each asset to live in its own subfolder "
|
| 103 |
+
"with an interface USD.", flush=True)
|
| 104 |
+
return 2
|
| 105 |
+
|
| 106 |
+
output = Path(args.output).resolve() if args.output else _default_output(target)
|
| 107 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 108 |
+
|
| 109 |
+
print(f"Target: {target}", flush=True)
|
| 110 |
+
print(f"Output: {output}", flush=True)
|
| 111 |
+
print(f"Profile: {args.profile}", flush=True)
|
| 112 |
+
print(f"CLI: {simready_exe}", flush=True)
|
| 113 |
+
print(f"Discovered {len(interface_files)} interface USD(s).", flush=True)
|
| 114 |
+
|
| 115 |
+
env = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"}
|
| 116 |
+
packaged = 0
|
| 117 |
+
skipped = 0
|
| 118 |
+
failures: list[tuple[str, str]] = []
|
| 119 |
+
|
| 120 |
+
for usd in interface_files:
|
| 121 |
+
already = output / usd.stem / usd.name
|
| 122 |
+
if already.is_file():
|
| 123 |
+
print(f" skip (already packaged): {usd.name}", flush=True)
|
| 124 |
+
skipped += 1
|
| 125 |
+
continue
|
| 126 |
+
print(f" package: {usd.name}", flush=True)
|
| 127 |
+
cmd = [simready_exe, "ingest", "usd", str(usd),
|
| 128 |
+
"-o", str(output), "-p", args.profile, "--no-validate"]
|
| 129 |
+
try:
|
| 130 |
+
subprocess.run(cmd, env=env, check=True, capture_output=True, text=True, timeout=120)
|
| 131 |
+
packaged += 1
|
| 132 |
+
except subprocess.CalledProcessError as e:
|
| 133 |
+
err = (e.stderr or "")[-300:] or str(e)
|
| 134 |
+
failures.append((usd.name, err))
|
| 135 |
+
print(f" FAILED: {usd.name} - {err}", flush=True)
|
| 136 |
+
|
| 137 |
+
print(f"\nSUMMARY: packaged={packaged} skipped={skipped} failed={len(failures)}", flush=True)
|
| 138 |
+
print(f"OUTPUT: {output}", flush=True)
|
| 139 |
+
return 0 if not failures else 1
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
sys.exit(main())
|
tools/validation/plugins/simready-report/skills/simready-report/SKILL.md
CHANGED
|
@@ -1,137 +1,137 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: simready-report
|
| 3 |
-
description: Validate SimReady customer assets against the foundation specs. Produces a dashboard HTML report and an external-dependencies provenance report. Invoke with /simready-report <name-or-path>.
|
| 4 |
-
---
|
| 5 |
-
|
| 6 |
-
# /simready-report
|
| 7 |
-
|
| 8 |
-
Validate a SimReady customer asset set.
|
| 9 |
-
|
| 10 |
-
## Usage
|
| 11 |
-
|
| 12 |
-
- `/simready-report` (no arg) — validate the **current working directory**. This is the normal way to run the skill: `cd` into the directory holding the customer asset(s) and invoke.
|
| 13 |
-
- `/simready-report <path>` — validate any directory by absolute or relative path. Override for the no-arg default.
|
| 14 |
-
- `/simready-report <name>` — bare name (no path separators). Tries `<cwd>/<name>` first; if running inside a playbook clone, also tries `<workspace>/assets_to_validate/<name>` for back-compat.
|
| 15 |
-
|
| 16 |
-
## Install paths
|
| 17 |
-
|
| 18 |
-
The skill is layout-agnostic — it auto-detects whether it's running inside a playbook clone (`BOT.md` in an ancestor directory) and adjusts defaults accordingly.
|
| 19 |
-
|
| 20 |
-
**Plugin marketplace install (no clone needed):**
|
| 21 |
-
```
|
| 22 |
-
/plugin marketplace add loginowskid/simready-playbook
|
| 23 |
-
/plugin install simready-report
|
| 24 |
-
```
|
| 25 |
-
Then `/simready-report` works from any directory. Reports default to `<cwd>/report/<target-name>/`. Foundation specs and SDK locations come from env vars (see Configuration).
|
| 26 |
-
|
| 27 |
-
**Cloned playbook (project skill or local marketplace):**
|
| 28 |
-
If `simready-playbook` is cloned, the skill walks up from its own location to find `BOT.md` and uses workspace-relative defaults: reports under `<workspace>/report/`, optional auto-package into `<workspace>/packages/`, default foundations/sdk lookup at `<workspace>/../simready_foundations` and `<workspace>/../simready-oem-sdk-poc`.
|
| 29 |
-
|
| 30 |
-
## Runtime dependencies
|
| 31 |
-
|
| 32 |
-
Wherever the skill is installed, it needs:
|
| 33 |
-
|
| 34 |
-
- A Python with `usd-core`, `omniverse-asset-validator`, `markdown-it-py`, and `simready-validate>=2026.4.8` importable. Any Python works (system, venv, conda). The default validation path populates the OAV registries via `simready.validate.impl.loader.load_validation_implementation`, matching the official `simready-validate` CLI workflow — no editable install of the foundation specs is required.
|
| 35 |
-
- A `simready_foundations` checkout cloned from `github.com/NVIDIA/simready-foundation` (or the `simready-foundation-staging` mirror). Override default lookup with `SIMREADY_FOUNDATIONS_PATH=<path>`.
|
| 36 |
-
- The `simready-oem-sdk-poc` checkout (only if the user wants the auto-package step for raw asset trees). Override with `SIMREADY_SDK_PATH=<path>`. The `simready` CLI itself is found via `PATH` first, then alongside the active Python.
|
| 37 |
-
|
| 38 |
-
Legacy fallback: pass `--use-plugin` to load via the `simready.foundation.core:SimReadyPlugin` setuptools entry-point instead (requires the editable install of `nv_core/sr_specs` + the internal `repo usd_profiles_codegen` step). Kept for the GitLab-era workflow; not needed for GitHub-based runs.
|
| 39 |
-
|
| 40 |
-
For a Windows convenience setup that creates a sibling venv + clones, run `bootstrap.ps1` from a cloned playbook. **Don't run it without confirming with the user first** — they may already have these dependencies elsewhere, or be on a non-Windows system.
|
| 41 |
-
|
| 42 |
-
Optional: a `usd_validation_dashboard_final/` clone next to the playbook supplies a Sphinx `_static/` theme for rendered doc pages. Without it, doc pages use minimal inline styling — the dashboard still works.
|
| 43 |
-
|
| 44 |
-
## Steps for Claude when `/simready-report` is invoked
|
| 45 |
-
|
| 46 |
-
The Python invocation pattern (used in steps 2 and 3 below) — prefer `$env:SIMREADY_PYTHON` (set by `bootstrap.ps1`), fall back to `python` on PATH:
|
| 47 |
-
|
| 48 |
-
PowerShell: `$py = if ($env:SIMREADY_PYTHON) { $env:SIMREADY_PYTHON } else { "python" }; & $py …`
|
| 49 |
-
Bash/zsh: `"${SIMREADY_PYTHON:-python}" …`
|
| 50 |
-
|
| 51 |
-
1. **Resolve the target.**
|
| 52 |
-
- No arg → CWD.
|
| 53 |
-
- With an arg: pass it through. The script tries it as a path, then `<cwd>/<name>`, then `<workspace>/assets_to_validate/<name>` (clone only) for back-compat.
|
| 54 |
-
2. **Pick the profile (spec selector).** Unless the user already named a specific profile in their request, list the registered profiles and ask them to pick before validating:
|
| 55 |
-
```
|
| 56 |
-
<py> validate.py --list-profiles
|
| 57 |
-
```
|
| 58 |
-
This emits one `PROFILE: <id> v<version>` line per registered profile (typically ~11 across the Robot-Body / Prop-Robotics / Package families). Present them grouped by family with `Robot-Body-Runnable v1.0.0` marked as the default; let the user accept the default or pick another. Pass the chosen `<id>` and `<version>` through as `--profile` and `--version` in step 3.
|
| 59 |
-
3. **Run the validator** from the skill directory:
|
| 60 |
-
```
|
| 61 |
-
<py> validate.py "<arg-or-empty>" --profile <id> --version <version>
|
| 62 |
-
```
|
| 63 |
-
If you skipped the selector because the user named a profile up front, use that. If the user said something like "just use the default", pass `Robot-Body-Runnable` v1.0.0 without prompting.
|
| 64 |
-
4. **Surface results.** Echo the script's `SUMMARY:` line and the absolute path to the report's `index.html`.
|
| 65 |
-
5. **If `validate.py` exits with "runtime prerequisites missing"**, relay the message verbatim. If running inside a clone on Windows, suggest `bootstrap.ps1` (after confirming with the user). Otherwise direct the user to install the deps into their Python and/or set `SIMREADY_FOUNDATIONS_PATH` / `SIMREADY_SDK_PATH` to existing checkouts. Do not run the bootstrap on the user's behalf without explicit confirmation.
|
| 66 |
-
6. **If `validate.py` exits with "target … failed sanity check"**, relay the listed problems verbatim. Don't retry without an explicit user instruction — the check is there to prevent accidental whole-drive scans, output-dir self-validation, and empty-dir runs.
|
| 67 |
-
|
| 68 |
-
## What `validate.py` does on its own
|
| 69 |
-
|
| 70 |
-
- **Workspace detection.** Walks up from the skill's directory and treats the first ancestor containing `BOT.md` as the workspace. None of these defaults matter when overridden by env vars / `--output`.
|
| 71 |
-
- **Prerequisite check.** Verifies `omni.asset_validator` / `pxr` / `markdown_it` import in the active Python and that the foundations checkout is findable. On miss: prints actionable error and exits 2.
|
| 72 |
-
- **Sanity check on the resolved target.** Refuses (exit 2) if any of:
|
| 73 |
-
- Inside a clone: target is the workspace root, or under `<workspace>/.claude/`, `playbooks/`, `report/`, `packages/`, or `plugins/`.
|
| 74 |
-
- Target is too high in the filesystem (≤2 path components — e.g. `C:\` or `C:\Users\`).
|
| 75 |
-
- Target has no USD files at root **and** none in any of its immediate subfolders.
|
| 76 |
-
- **Auto-package.** When the resolved target lives under an `assets_to_validate/<name>/` directory, calls `simready ingest usd` per interface USD into a sibling `packages/<name>/` (workspace-anchored when in a clone, else sibling-of-`assets_to_validate/`). Skipped per asset if already packaged. Skipped with a warning if the `simready` CLI isn't on PATH.
|
| 77 |
-
- **Asset thumbnails.** For each validated asset, copies `<asset_dir>/.thumbs/256x256/<filename>.png` (if present at the spec path) into `<out_dir>/images/<filename>.png`. Missing thumbnails render the inline placeholder.
|
| 78 |
-
- **Doc rendering.** For every feature/requirement code referenced by the report, renders the foundation's source markdown to HTML inside `<out_dir>/docs/<rel_path>`. If `usd_validation_dashboard_final/_static/` exists, it's copied once so pages match the reference Sphinx theme; otherwise pages use minimal inline CSS.
|
| 79 |
-
|
| 80 |
-
## Outputs
|
| 81 |
-
|
| 82 |
-
Written to the resolved output directory. **Default**: `<target>/.reports/<target-name>.<profile>/` — i.e. dropped inside the asset dir the user pointed at, in a `.reports/` subdir. Examples:
|
| 83 |
-
|
| 84 |
-
- `/simready-report yaskawa_local` (default profile) → `yaskawa_local/.reports/yaskawa_local.Robot-Body-Runnable/`
|
| 85 |
-
- `/simready-report yaskawa_local` (with `--profile Prop-Robotics-Neutral --version 2.0.0`) → `yaskawa_local/.reports/yaskawa_local.Prop-Robotics-Neutral/`
|
| 86 |
-
|
| 87 |
-
The `.reports/` prefix starts with `.`, so `discover_assets` automatically skips it on subsequent runs — re-running `/simready-report` from inside the target is safe. Multiple profiles against the same target produce sibling subfolders under `.reports/`, never overwriting each other. Override the whole path with `--output`.
|
| 88 |
-
|
| 89 |
-
- `index.html` — dashboard: summary cards, asset filter, per-asset failed/passed features, "Other failed requirements" panel, compliance footer.
|
| 90 |
-
- `results.json` — raw issues per asset, plus thumbnail provenance.
|
| 91 |
-
- `images/<filename>.png` — per-asset thumbnails (mirrored from the spec path inside the packaged tree).
|
| 92 |
-
- `docs/` — foundation feature/requirement docs rendered to HTML; `_static/` carries the Sphinx theme assets so pages match the reference dashboard.
|
| 93 |
-
- `external_dependencies_report.html` + `.json` — **only emitted when there's something to report** (one or more externally-resolved layers, or one or more scan-time issues). Anonymous in-memory layers are not counted as external. Three sections when present:
|
| 94 |
-
1. External assets needed (anything resolved outside the target dir).
|
| 95 |
-
2. What was done to obtain each (provenance log per layer).
|
| 96 |
-
3. Issues encountered while building the report.
|
| 97 |
-
|
| 98 |
-
## Kit-rooted mode (`--use-kit`)
|
| 99 |
-
|
| 100 |
-
**Auto-default:** `validate.py` auto-enables `--use-kit` whenever the chosen profile is in the PhysX-bearing set (`Robot-Body-Physx`, `Robot-Body-Isaac`, `Robot-Body-Runnable`, `Prop-Robotics-Physx`, `Prop-Robotics-Isaac`). The pip-only path's P2 filter silently drops `physxschema_unavailable` / `omnipbr_unresolved` findings, which makes a "passed" result misleading on those profiles — the checks that matter never actually ran. Pass `--no-use-kit` to opt out (e.g. when you knowingly want the fast filtered run); pass `--use-kit` explicitly to force it on a Neutral-family profile.
|
| 101 |
-
|
| 102 |
-
Under the hood: when `--use-kit` is on, the script re-execs itself through `_kit_wrapper.py` using a Kit-rooted Python that boots `isaacsim.SimulationApp({"headless": True})` before importing `pxr`, so `PhysxSchema`, the PhysX joint rules, and Kit's stock MDL resolution all work for real. When off (Neutral-family profiles by default, or after `--no-use-kit`), the pip-only venv runs and the P2 filter trims the resulting noise (~900 issues dropped on a typical robot asset) so reports stay readable.
|
| 103 |
-
|
| 104 |
-
- Resolution order for the Kit Python: `--kit-python <path>` → `$SIMREADY_KIT_PYTHON` → `C:\isaacsim6\_build\windows-x86_64\release\python.bat` (Isaac Sim 6 source build on this team's machines).
|
| 105 |
-
- Cost: ~10–30 s of SimulationApp boot on top of normal validation time. The script automatically forces `--workers 1` in Kit mode so we don't pay the boot again per worker.
|
| 106 |
-
- One-time setup: the Kit-rooted Python needs the same runtime deps the pip venv has. From within the Kit Python:
|
| 107 |
-
```
|
| 108 |
-
<kit-python.bat> -m pip install omniverse-asset-validator markdown-it-py
|
| 109 |
-
<kit-python.bat> -m pip install -e <foundations>/nv_core/sr_specs
|
| 110 |
-
<kit-python.bat> -m pip install -e <simready-oem-sdk-poc> # only if auto-packaging is needed
|
| 111 |
-
```
|
| 112 |
-
If `validate.py` aborts with "runtime prerequisites missing" inside Kit mode, the error includes a copy-pasteable hint pointing at the active Kit Python.
|
| 113 |
-
|
| 114 |
-
See `PROBLEMS.md` P2 for the architectural context behind the env-blocked-issue filter and the Kit-vs-pip split.
|
| 115 |
-
|
| 116 |
-
## Loader paths
|
| 117 |
-
|
| 118 |
-
The plugin can populate the OAV registries two ways. The default matches the GitHub-canonical workflow.
|
| 119 |
-
|
| 120 |
-
**Default — CLI loader (no flag).** `validate.py` calls `simready.validate.impl.loader.load_validation_implementation` against on-disk paths under `$SIMREADY_FOUNDATIONS_PATH` — the same loader the official `simready-validate` CLI uses. Requires only `simready-validate>=2026.4.8` (PyPI) and a foundations checkout from `github.com/NVIDIA/simready-foundation*`. No editable install of `nv_core/sr_specs`, no `repo usd_profiles_codegen` step. The loader generates `omni.capabilities` at runtime via `omni.usd_profiles.codegen`.
|
| 121 |
-
|
| 122 |
-
**Legacy — `--use-plugin`.** Loads via the `simready.foundation.core:SimReadyPlugin` setuptools entry-point. Requires the editable install of `nv_core/sr_specs` after running `repo usd_profiles_codegen`. Useful only for the GitLab-era workflow on machines that still have that editable install in place; not a recommended path for new setups. When this mode is active, the P1 JSON-variant patch in `validate.py` is also applied (it's redundant under the default loader).
|
| 123 |
-
|
| 124 |
-
Compose with `--use-kit`: e.g. `python validate.py … --use-kit` runs PhysX checks inside Kit using the default loader path.
|
| 125 |
-
|
| 126 |
-
See `PROBLEMS.md` P2 for the env-blocked-issue filter that runs alongside both paths.
|
| 127 |
-
|
| 128 |
-
## Configuration
|
| 129 |
-
|
| 130 |
-
- `SIMREADY_PYTHON` (recommended): full path to the Python interpreter that has the runtime deps installed. Set automatically by `bootstrap.ps1`. If unset, the skill falls back to `python` on PATH.
|
| 131 |
-
- `SIMREADY_KIT_PYTHON` (optional, used only with `--use-kit`): full path to a Kit-rooted `python.bat`. Default: `C:\isaacsim6\_build\windows-x86_64\release\python.bat`.
|
| 132 |
-
- Default profile: `Robot-Body-Runnable` v1.0.0
|
| 133 |
-
- `SIMREADY_FOUNDATIONS_PATH` (optional): override the foundations checkout location. Default in a clone: `<workspace>/../simready_foundations`. Required when running outside a clone (set automatically by `bootstrap.ps1`).
|
| 134 |
-
- `SIMREADY_SDK_PATH` (optional): override the SDK checkout location. Default in a clone: `<workspace>/../simready-oem-sdk-poc`. Only relevant for auto-packaging (set automatically by `bootstrap.ps1`).
|
| 135 |
-
- `SIMREADY_SPECS_PATH` is set automatically from the resolved foundations path so the validator's spec loader picks it up.
|
| 136 |
-
- `SIMREADY_INSIDE_KIT` is set automatically by `_kit_wrapper.py` to `1` once SimulationApp is booted; `validate.py` uses it to skip the pip-only assumptions and force sequential workers.
|
| 137 |
-
- `OMNI_ASSET_VALIDATOR_ISOLATE_ENTRYPOINTS` is set automatically to `omni.asset_validator:DefaultPlugin,simready.foundation.core:SimReadyPlugin`
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: simready-report
|
| 3 |
+
description: Validate SimReady customer assets against the foundation specs. Produces a dashboard HTML report and an external-dependencies provenance report. Invoke with /simready-report <name-or-path>.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# /simready-report
|
| 7 |
+
|
| 8 |
+
Validate a SimReady customer asset set.
|
| 9 |
+
|
| 10 |
+
## Usage
|
| 11 |
+
|
| 12 |
+
- `/simready-report` (no arg) — validate the **current working directory**. This is the normal way to run the skill: `cd` into the directory holding the customer asset(s) and invoke.
|
| 13 |
+
- `/simready-report <path>` — validate any directory by absolute or relative path. Override for the no-arg default.
|
| 14 |
+
- `/simready-report <name>` — bare name (no path separators). Tries `<cwd>/<name>` first; if running inside a playbook clone, also tries `<workspace>/assets_to_validate/<name>` for back-compat.
|
| 15 |
+
|
| 16 |
+
## Install paths
|
| 17 |
+
|
| 18 |
+
The skill is layout-agnostic — it auto-detects whether it's running inside a playbook clone (`BOT.md` in an ancestor directory) and adjusts defaults accordingly.
|
| 19 |
+
|
| 20 |
+
**Plugin marketplace install (no clone needed):**
|
| 21 |
+
```
|
| 22 |
+
/plugin marketplace add loginowskid/simready-playbook
|
| 23 |
+
/plugin install simready-report
|
| 24 |
+
```
|
| 25 |
+
Then `/simready-report` works from any directory. Reports default to `<cwd>/report/<target-name>/`. Foundation specs and SDK locations come from env vars (see Configuration).
|
| 26 |
+
|
| 27 |
+
**Cloned playbook (project skill or local marketplace):**
|
| 28 |
+
If `simready-playbook` is cloned, the skill walks up from its own location to find `BOT.md` and uses workspace-relative defaults: reports under `<workspace>/report/`, optional auto-package into `<workspace>/packages/`, default foundations/sdk lookup at `<workspace>/../simready_foundations` and `<workspace>/../simready-oem-sdk-poc`.
|
| 29 |
+
|
| 30 |
+
## Runtime dependencies
|
| 31 |
+
|
| 32 |
+
Wherever the skill is installed, it needs:
|
| 33 |
+
|
| 34 |
+
- A Python with `usd-core`, `omniverse-asset-validator`, `markdown-it-py`, and `simready-validate>=2026.4.8` importable. Any Python works (system, venv, conda). The default validation path populates the OAV registries via `simready.validate.impl.loader.load_validation_implementation`, matching the official `simready-validate` CLI workflow — no editable install of the foundation specs is required.
|
| 35 |
+
- A `simready_foundations` checkout cloned from `github.com/NVIDIA/simready-foundation` (or the `simready-foundation-staging` mirror). Override default lookup with `SIMREADY_FOUNDATIONS_PATH=<path>`.
|
| 36 |
+
- The `simready-oem-sdk-poc` checkout (only if the user wants the auto-package step for raw asset trees). Override with `SIMREADY_SDK_PATH=<path>`. The `simready` CLI itself is found via `PATH` first, then alongside the active Python.
|
| 37 |
+
|
| 38 |
+
Legacy fallback: pass `--use-plugin` to load via the `simready.foundation.core:SimReadyPlugin` setuptools entry-point instead (requires the editable install of `nv_core/sr_specs` + the internal `repo usd_profiles_codegen` step). Kept for the GitLab-era workflow; not needed for GitHub-based runs.
|
| 39 |
+
|
| 40 |
+
For a Windows convenience setup that creates a sibling venv + clones, run `bootstrap.ps1` from a cloned playbook. **Don't run it without confirming with the user first** — they may already have these dependencies elsewhere, or be on a non-Windows system.
|
| 41 |
+
|
| 42 |
+
Optional: a `usd_validation_dashboard_final/` clone next to the playbook supplies a Sphinx `_static/` theme for rendered doc pages. Without it, doc pages use minimal inline styling — the dashboard still works.
|
| 43 |
+
|
| 44 |
+
## Steps for Claude when `/simready-report` is invoked
|
| 45 |
+
|
| 46 |
+
The Python invocation pattern (used in steps 2 and 3 below) — prefer `$env:SIMREADY_PYTHON` (set by `bootstrap.ps1`), fall back to `python` on PATH:
|
| 47 |
+
|
| 48 |
+
PowerShell: `$py = if ($env:SIMREADY_PYTHON) { $env:SIMREADY_PYTHON } else { "python" }; & $py …`
|
| 49 |
+
Bash/zsh: `"${SIMREADY_PYTHON:-python}" …`
|
| 50 |
+
|
| 51 |
+
1. **Resolve the target.**
|
| 52 |
+
- No arg → CWD.
|
| 53 |
+
- With an arg: pass it through. The script tries it as a path, then `<cwd>/<name>`, then `<workspace>/assets_to_validate/<name>` (clone only) for back-compat.
|
| 54 |
+
2. **Pick the profile (spec selector).** Unless the user already named a specific profile in their request, list the registered profiles and ask them to pick before validating:
|
| 55 |
+
```
|
| 56 |
+
<py> validate.py --list-profiles
|
| 57 |
+
```
|
| 58 |
+
This emits one `PROFILE: <id> v<version>` line per registered profile (typically ~11 across the Robot-Body / Prop-Robotics / Package families). Present them grouped by family with `Robot-Body-Runnable v1.0.0` marked as the default; let the user accept the default or pick another. Pass the chosen `<id>` and `<version>` through as `--profile` and `--version` in step 3.
|
| 59 |
+
3. **Run the validator** from the skill directory:
|
| 60 |
+
```
|
| 61 |
+
<py> validate.py "<arg-or-empty>" --profile <id> --version <version>
|
| 62 |
+
```
|
| 63 |
+
If you skipped the selector because the user named a profile up front, use that. If the user said something like "just use the default", pass `Robot-Body-Runnable` v1.0.0 without prompting.
|
| 64 |
+
4. **Surface results.** Echo the script's `SUMMARY:` line and the absolute path to the report's `index.html`.
|
| 65 |
+
5. **If `validate.py` exits with "runtime prerequisites missing"**, relay the message verbatim. If running inside a clone on Windows, suggest `bootstrap.ps1` (after confirming with the user). Otherwise direct the user to install the deps into their Python and/or set `SIMREADY_FOUNDATIONS_PATH` / `SIMREADY_SDK_PATH` to existing checkouts. Do not run the bootstrap on the user's behalf without explicit confirmation.
|
| 66 |
+
6. **If `validate.py` exits with "target … failed sanity check"**, relay the listed problems verbatim. Don't retry without an explicit user instruction — the check is there to prevent accidental whole-drive scans, output-dir self-validation, and empty-dir runs.
|
| 67 |
+
|
| 68 |
+
## What `validate.py` does on its own
|
| 69 |
+
|
| 70 |
+
- **Workspace detection.** Walks up from the skill's directory and treats the first ancestor containing `BOT.md` as the workspace. None of these defaults matter when overridden by env vars / `--output`.
|
| 71 |
+
- **Prerequisite check.** Verifies `omni.asset_validator` / `pxr` / `markdown_it` import in the active Python and that the foundations checkout is findable. On miss: prints actionable error and exits 2.
|
| 72 |
+
- **Sanity check on the resolved target.** Refuses (exit 2) if any of:
|
| 73 |
+
- Inside a clone: target is the workspace root, or under `<workspace>/.claude/`, `playbooks/`, `report/`, `packages/`, or `plugins/`.
|
| 74 |
+
- Target is too high in the filesystem (≤2 path components — e.g. `C:\` or `C:\Users\`).
|
| 75 |
+
- Target has no USD files at root **and** none in any of its immediate subfolders.
|
| 76 |
+
- **Auto-package.** When the resolved target lives under an `assets_to_validate/<name>/` directory, calls `simready ingest usd` per interface USD into a sibling `packages/<name>/` (workspace-anchored when in a clone, else sibling-of-`assets_to_validate/`). Skipped per asset if already packaged. Skipped with a warning if the `simready` CLI isn't on PATH.
|
| 77 |
+
- **Asset thumbnails.** For each validated asset, copies `<asset_dir>/.thumbs/256x256/<filename>.png` (if present at the spec path) into `<out_dir>/images/<filename>.png`. Missing thumbnails render the inline placeholder.
|
| 78 |
+
- **Doc rendering.** For every feature/requirement code referenced by the report, renders the foundation's source markdown to HTML inside `<out_dir>/docs/<rel_path>`. If `usd_validation_dashboard_final/_static/` exists, it's copied once so pages match the reference Sphinx theme; otherwise pages use minimal inline CSS.
|
| 79 |
+
|
| 80 |
+
## Outputs
|
| 81 |
+
|
| 82 |
+
Written to the resolved output directory. **Default**: `<target>/.reports/<target-name>.<profile>/` — i.e. dropped inside the asset dir the user pointed at, in a `.reports/` subdir. Examples:
|
| 83 |
+
|
| 84 |
+
- `/simready-report yaskawa_local` (default profile) → `yaskawa_local/.reports/yaskawa_local.Robot-Body-Runnable/`
|
| 85 |
+
- `/simready-report yaskawa_local` (with `--profile Prop-Robotics-Neutral --version 2.0.0`) → `yaskawa_local/.reports/yaskawa_local.Prop-Robotics-Neutral/`
|
| 86 |
+
|
| 87 |
+
The `.reports/` prefix starts with `.`, so `discover_assets` automatically skips it on subsequent runs — re-running `/simready-report` from inside the target is safe. Multiple profiles against the same target produce sibling subfolders under `.reports/`, never overwriting each other. Override the whole path with `--output`.
|
| 88 |
+
|
| 89 |
+
- `index.html` — dashboard: summary cards, asset filter, per-asset failed/passed features, "Other failed requirements" panel, compliance footer.
|
| 90 |
+
- `results.json` — raw issues per asset, plus thumbnail provenance.
|
| 91 |
+
- `images/<filename>.png` — per-asset thumbnails (mirrored from the spec path inside the packaged tree).
|
| 92 |
+
- `docs/` — foundation feature/requirement docs rendered to HTML; `_static/` carries the Sphinx theme assets so pages match the reference dashboard.
|
| 93 |
+
- `external_dependencies_report.html` + `.json` — **only emitted when there's something to report** (one or more externally-resolved layers, or one or more scan-time issues). Anonymous in-memory layers are not counted as external. Three sections when present:
|
| 94 |
+
1. External assets needed (anything resolved outside the target dir).
|
| 95 |
+
2. What was done to obtain each (provenance log per layer).
|
| 96 |
+
3. Issues encountered while building the report.
|
| 97 |
+
|
| 98 |
+
## Kit-rooted mode (`--use-kit`)
|
| 99 |
+
|
| 100 |
+
**Auto-default:** `validate.py` auto-enables `--use-kit` whenever the chosen profile is in the PhysX-bearing set (`Robot-Body-Physx`, `Robot-Body-Isaac`, `Robot-Body-Runnable`, `Prop-Robotics-Physx`, `Prop-Robotics-Isaac`). The pip-only path's P2 filter silently drops `physxschema_unavailable` / `omnipbr_unresolved` findings, which makes a "passed" result misleading on those profiles — the checks that matter never actually ran. Pass `--no-use-kit` to opt out (e.g. when you knowingly want the fast filtered run); pass `--use-kit` explicitly to force it on a Neutral-family profile.
|
| 101 |
+
|
| 102 |
+
Under the hood: when `--use-kit` is on, the script re-execs itself through `_kit_wrapper.py` using a Kit-rooted Python that boots `isaacsim.SimulationApp({"headless": True})` before importing `pxr`, so `PhysxSchema`, the PhysX joint rules, and Kit's stock MDL resolution all work for real. When off (Neutral-family profiles by default, or after `--no-use-kit`), the pip-only venv runs and the P2 filter trims the resulting noise (~900 issues dropped on a typical robot asset) so reports stay readable.
|
| 103 |
+
|
| 104 |
+
- Resolution order for the Kit Python: `--kit-python <path>` → `$SIMREADY_KIT_PYTHON` → `C:\isaacsim6\_build\windows-x86_64\release\python.bat` (Isaac Sim 6 source build on this team's machines).
|
| 105 |
+
- Cost: ~10–30 s of SimulationApp boot on top of normal validation time. The script automatically forces `--workers 1` in Kit mode so we don't pay the boot again per worker.
|
| 106 |
+
- One-time setup: the Kit-rooted Python needs the same runtime deps the pip venv has. From within the Kit Python:
|
| 107 |
+
```
|
| 108 |
+
<kit-python.bat> -m pip install omniverse-asset-validator markdown-it-py
|
| 109 |
+
<kit-python.bat> -m pip install -e <foundations>/nv_core/sr_specs
|
| 110 |
+
<kit-python.bat> -m pip install -e <simready-oem-sdk-poc> # only if auto-packaging is needed
|
| 111 |
+
```
|
| 112 |
+
If `validate.py` aborts with "runtime prerequisites missing" inside Kit mode, the error includes a copy-pasteable hint pointing at the active Kit Python.
|
| 113 |
+
|
| 114 |
+
See `PROBLEMS.md` P2 for the architectural context behind the env-blocked-issue filter and the Kit-vs-pip split.
|
| 115 |
+
|
| 116 |
+
## Loader paths
|
| 117 |
+
|
| 118 |
+
The plugin can populate the OAV registries two ways. The default matches the GitHub-canonical workflow.
|
| 119 |
+
|
| 120 |
+
**Default — CLI loader (no flag).** `validate.py` calls `simready.validate.impl.loader.load_validation_implementation` against on-disk paths under `$SIMREADY_FOUNDATIONS_PATH` — the same loader the official `simready-validate` CLI uses. Requires only `simready-validate>=2026.4.8` (PyPI) and a foundations checkout from `github.com/NVIDIA/simready-foundation*`. No editable install of `nv_core/sr_specs`, no `repo usd_profiles_codegen` step. The loader generates `omni.capabilities` at runtime via `omni.usd_profiles.codegen`.
|
| 121 |
+
|
| 122 |
+
**Legacy — `--use-plugin`.** Loads via the `simready.foundation.core:SimReadyPlugin` setuptools entry-point. Requires the editable install of `nv_core/sr_specs` after running `repo usd_profiles_codegen`. Useful only for the GitLab-era workflow on machines that still have that editable install in place; not a recommended path for new setups. When this mode is active, the P1 JSON-variant patch in `validate.py` is also applied (it's redundant under the default loader).
|
| 123 |
+
|
| 124 |
+
Compose with `--use-kit`: e.g. `python validate.py … --use-kit` runs PhysX checks inside Kit using the default loader path.
|
| 125 |
+
|
| 126 |
+
See `PROBLEMS.md` P2 for the env-blocked-issue filter that runs alongside both paths.
|
| 127 |
+
|
| 128 |
+
## Configuration
|
| 129 |
+
|
| 130 |
+
- `SIMREADY_PYTHON` (recommended): full path to the Python interpreter that has the runtime deps installed. Set automatically by `bootstrap.ps1`. If unset, the skill falls back to `python` on PATH.
|
| 131 |
+
- `SIMREADY_KIT_PYTHON` (optional, used only with `--use-kit`): full path to a Kit-rooted `python.bat`. Default: `C:\isaacsim6\_build\windows-x86_64\release\python.bat`.
|
| 132 |
+
- Default profile: `Robot-Body-Runnable` v1.0.0
|
| 133 |
+
- `SIMREADY_FOUNDATIONS_PATH` (optional): override the foundations checkout location. Default in a clone: `<workspace>/../simready_foundations`. Required when running outside a clone (set automatically by `bootstrap.ps1`).
|
| 134 |
+
- `SIMREADY_SDK_PATH` (optional): override the SDK checkout location. Default in a clone: `<workspace>/../simready-oem-sdk-poc`. Only relevant for auto-packaging (set automatically by `bootstrap.ps1`).
|
| 135 |
+
- `SIMREADY_SPECS_PATH` is set automatically from the resolved foundations path so the validator's spec loader picks it up.
|
| 136 |
+
- `SIMREADY_INSIDE_KIT` is set automatically by `_kit_wrapper.py` to `1` once SimulationApp is booted; `validate.py` uses it to skip the pip-only assumptions and force sequential workers.
|
| 137 |
+
- `OMNI_ASSET_VALIDATOR_ISOLATE_ENTRYPOINTS` is set automatically to `omni.asset_validator:DefaultPlugin,simready.foundation.core:SimReadyPlugin`
|
tools/validation/plugins/simready-report/skills/simready-report/_kit_wrapper.py
CHANGED
|
@@ -1,33 +1,33 @@
|
|
| 1 |
-
"""Kit-rooted entry point for validate.py.
|
| 2 |
-
|
| 3 |
-
Boots `isaacsim.SimulationApp({"headless": True})` once in the calling
|
| 4 |
-
process so `pxr.PhysxSchema` and Kit's MDL search path are registered,
|
| 5 |
-
then hands control to `validate.py` via `runpy`. Used by `--use-kit`
|
| 6 |
-
in validate.py — see PROBLEMS.md P2 for why this exists.
|
| 7 |
-
|
| 8 |
-
This file is intentionally tiny so SimulationApp's boot happens before
|
| 9 |
-
*any* pxr / omni.asset_validator import. Don't add other imports above
|
| 10 |
-
the SimulationApp() call.
|
| 11 |
-
"""
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import os
|
| 15 |
-
import runpy
|
| 16 |
-
import sys
|
| 17 |
-
from pathlib import Path
|
| 18 |
-
|
| 19 |
-
from isaacsim import SimulationApp
|
| 20 |
-
|
| 21 |
-
_sim = SimulationApp({"headless": True})
|
| 22 |
-
os.environ["SIMREADY_INSIDE_KIT"] = "1"
|
| 23 |
-
|
| 24 |
-
_this_dir = Path(__file__).resolve().parent
|
| 25 |
-
_target = _this_dir / "validate.py"
|
| 26 |
-
sys.argv = [str(_target)] + sys.argv[1:]
|
| 27 |
-
try:
|
| 28 |
-
runpy.run_path(str(_target), run_name="__main__")
|
| 29 |
-
finally:
|
| 30 |
-
try:
|
| 31 |
-
_sim.close()
|
| 32 |
-
except Exception:
|
| 33 |
-
pass
|
|
|
|
| 1 |
+
"""Kit-rooted entry point for validate.py.
|
| 2 |
+
|
| 3 |
+
Boots `isaacsim.SimulationApp({"headless": True})` once in the calling
|
| 4 |
+
process so `pxr.PhysxSchema` and Kit's MDL search path are registered,
|
| 5 |
+
then hands control to `validate.py` via `runpy`. Used by `--use-kit`
|
| 6 |
+
in validate.py — see PROBLEMS.md P2 for why this exists.
|
| 7 |
+
|
| 8 |
+
This file is intentionally tiny so SimulationApp's boot happens before
|
| 9 |
+
*any* pxr / omni.asset_validator import. Don't add other imports above
|
| 10 |
+
the SimulationApp() call.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import runpy
|
| 16 |
+
import sys
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
from isaacsim import SimulationApp
|
| 20 |
+
|
| 21 |
+
_sim = SimulationApp({"headless": True})
|
| 22 |
+
os.environ["SIMREADY_INSIDE_KIT"] = "1"
|
| 23 |
+
|
| 24 |
+
_this_dir = Path(__file__).resolve().parent
|
| 25 |
+
_target = _this_dir / "validate.py"
|
| 26 |
+
sys.argv = [str(_target)] + sys.argv[1:]
|
| 27 |
+
try:
|
| 28 |
+
runpy.run_path(str(_target), run_name="__main__")
|
| 29 |
+
finally:
|
| 30 |
+
try:
|
| 31 |
+
_sim.close()
|
| 32 |
+
except Exception:
|
| 33 |
+
pass
|
tools/validation/plugins/simready-report/skills/simready-report/external_deps.py
CHANGED
|
@@ -1,259 +1,259 @@
|
|
| 1 |
-
"""External-dependency tracker for the /simready-report skill.
|
| 2 |
-
|
| 3 |
-
For each USD stage, walks all used layers (sublayers, references, payloads,
|
| 4 |
-
clips) and classifies each as internal (under target_root) or external.
|
| 5 |
-
Tracks per-layer provenance ("what was done to obtain it") and any issues
|
| 6 |
-
encountered while scanning.
|
| 7 |
-
|
| 8 |
-
Outputs:
|
| 9 |
-
external_dependencies_report.json
|
| 10 |
-
external_dependencies_report.html
|
| 11 |
-
"""
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import html
|
| 15 |
-
import json
|
| 16 |
-
from dataclasses import dataclass, field, asdict
|
| 17 |
-
from datetime import datetime, timezone
|
| 18 |
-
from pathlib import Path
|
| 19 |
-
|
| 20 |
-
from pxr import Usd
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
@dataclass
|
| 24 |
-
class ExternalLayerRecord:
|
| 25 |
-
layer_identifier: str
|
| 26 |
-
resolved_path: str
|
| 27 |
-
referenced_by: list[str] = field(default_factory=list)
|
| 28 |
-
actions: list[str] = field(default_factory=list)
|
| 29 |
-
status: str = "resolved" # resolved | missing | error
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
@dataclass
|
| 33 |
-
class IssueRecord:
|
| 34 |
-
asset: str
|
| 35 |
-
message: str
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
class ExternalDepsTracker:
|
| 39 |
-
"""Collects external-layer references across all validated stages."""
|
| 40 |
-
|
| 41 |
-
def __init__(self, target_root: Path):
|
| 42 |
-
self.target_root = target_root.resolve()
|
| 43 |
-
self.external: dict[str, ExternalLayerRecord] = {}
|
| 44 |
-
self.issues: list[IssueRecord] = []
|
| 45 |
-
|
| 46 |
-
# ---- public API ----
|
| 47 |
-
|
| 48 |
-
def scan_stage(self, stage: Usd.Stage, asset_path: Path, target_root: Path) -> None:
|
| 49 |
-
try:
|
| 50 |
-
used = stage.GetUsedLayers(includeClipLayers=True)
|
| 51 |
-
except Exception as e:
|
| 52 |
-
self.record_issue(str(asset_path), f"GetUsedLayers failed: {type(e).__name__}: {e}")
|
| 53 |
-
return
|
| 54 |
-
|
| 55 |
-
root_layer = stage.GetRootLayer()
|
| 56 |
-
for layer in used:
|
| 57 |
-
try:
|
| 58 |
-
if layer is None or layer == root_layer:
|
| 59 |
-
continue
|
| 60 |
-
identifier = layer.identifier
|
| 61 |
-
# Skip anonymous (in-memory) layers — `anon:0x...` identifiers
|
| 62 |
-
# are USD-internal handles, not external dependencies.
|
| 63 |
-
if identifier.startswith("anon:") or (hasattr(layer, "anonymous") and layer.anonymous):
|
| 64 |
-
continue
|
| 65 |
-
resolved = layer.realPath or (layer.resolvedPath.GetPathString() if hasattr(layer, "resolvedPath") else "")
|
| 66 |
-
|
| 67 |
-
if self._is_internal(resolved):
|
| 68 |
-
continue
|
| 69 |
-
|
| 70 |
-
rec = self.external.get(identifier)
|
| 71 |
-
if rec is None:
|
| 72 |
-
rec = ExternalLayerRecord(
|
| 73 |
-
layer_identifier=identifier,
|
| 74 |
-
resolved_path=resolved or "",
|
| 75 |
-
)
|
| 76 |
-
if resolved and Path(resolved).exists():
|
| 77 |
-
rec.actions.append(f"Resolved by USD asset resolver to: {resolved}")
|
| 78 |
-
rec.status = "resolved"
|
| 79 |
-
elif not resolved:
|
| 80 |
-
rec.actions.append(
|
| 81 |
-
f"Could not resolve layer identifier '{identifier}' to a filesystem path."
|
| 82 |
-
)
|
| 83 |
-
rec.status = "missing"
|
| 84 |
-
else:
|
| 85 |
-
rec.actions.append(f"Resolved to '{resolved}' but path does not exist on disk.")
|
| 86 |
-
rec.status = "missing"
|
| 87 |
-
self.external[identifier] = rec
|
| 88 |
-
|
| 89 |
-
ref = str(asset_path.relative_to(self.target_root)) if asset_path.is_relative_to(self.target_root) else str(asset_path)
|
| 90 |
-
if ref not in rec.referenced_by:
|
| 91 |
-
rec.referenced_by.append(ref)
|
| 92 |
-
except Exception as e:
|
| 93 |
-
self.record_issue(str(asset_path), f"Error scanning layer {layer}: {type(e).__name__}: {e}")
|
| 94 |
-
|
| 95 |
-
def record_issue(self, asset: str, message: str) -> None:
|
| 96 |
-
self.issues.append(IssueRecord(asset=asset, message=message))
|
| 97 |
-
|
| 98 |
-
def external_count(self) -> int:
|
| 99 |
-
return len(self.external)
|
| 100 |
-
|
| 101 |
-
def write_reports(self, out_dir: Path) -> None:
|
| 102 |
-
# Skip emitting the report entirely when there's nothing to say —
|
| 103 |
-
# no external assets resolved AND no issues encountered.
|
| 104 |
-
if not self.external and not self.issues:
|
| 105 |
-
for stale in ("external_dependencies_report.json", "external_dependencies_report.html"):
|
| 106 |
-
p = out_dir / stale
|
| 107 |
-
if p.is_file():
|
| 108 |
-
p.unlink()
|
| 109 |
-
return
|
| 110 |
-
payload = {
|
| 111 |
-
"generated": datetime.now(timezone.utc).isoformat(),
|
| 112 |
-
"target_root": str(self.target_root),
|
| 113 |
-
"external_assets": [asdict(r) for r in self.external.values()],
|
| 114 |
-
"issues": [asdict(i) for i in self.issues],
|
| 115 |
-
"summary": {
|
| 116 |
-
"external_count": len(self.external),
|
| 117 |
-
"missing_count": sum(1 for r in self.external.values() if r.status == "missing"),
|
| 118 |
-
"issue_count": len(self.issues),
|
| 119 |
-
},
|
| 120 |
-
}
|
| 121 |
-
(out_dir / "external_dependencies_report.json").write_text(
|
| 122 |
-
json.dumps(payload, indent=2), encoding="utf-8"
|
| 123 |
-
)
|
| 124 |
-
(out_dir / "external_dependencies_report.html").write_text(
|
| 125 |
-
self._render_html(payload), encoding="utf-8"
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
# ---- internal ----
|
| 129 |
-
|
| 130 |
-
def _is_internal(self, resolved_path: str) -> bool:
|
| 131 |
-
if not resolved_path:
|
| 132 |
-
return False
|
| 133 |
-
try:
|
| 134 |
-
p = Path(resolved_path).resolve()
|
| 135 |
-
except Exception:
|
| 136 |
-
return False
|
| 137 |
-
try:
|
| 138 |
-
return p.is_relative_to(self.target_root)
|
| 139 |
-
except AttributeError:
|
| 140 |
-
try:
|
| 141 |
-
p.relative_to(self.target_root)
|
| 142 |
-
return True
|
| 143 |
-
except ValueError:
|
| 144 |
-
return False
|
| 145 |
-
|
| 146 |
-
def _render_html(self, payload: dict) -> str:
|
| 147 |
-
s = payload["summary"]
|
| 148 |
-
rows = []
|
| 149 |
-
for rec in payload["external_assets"]:
|
| 150 |
-
actions_html = "".join(f"<li>{html.escape(a)}</li>" for a in rec["actions"]) or "<li>(none)</li>"
|
| 151 |
-
refs_html = "".join(f"<li><code>{html.escape(r)}</code></li>" for r in rec["referenced_by"]) or "<li>(unknown)</li>"
|
| 152 |
-
status_class = "status-" + rec["status"]
|
| 153 |
-
rows.append(f"""
|
| 154 |
-
<tr>
|
| 155 |
-
<td><code>{html.escape(rec['layer_identifier'])}</code></td>
|
| 156 |
-
<td><code>{html.escape(rec['resolved_path'] or '')}</code></td>
|
| 157 |
-
<td><span class="badge {status_class}">{html.escape(rec['status'])}</span></td>
|
| 158 |
-
<td><ul>{refs_html}</ul></td>
|
| 159 |
-
<td><ul>{actions_html}</ul></td>
|
| 160 |
-
</tr>""")
|
| 161 |
-
|
| 162 |
-
issues_html = "".join(
|
| 163 |
-
f"<tr><td><code>{html.escape(i['asset'])}</code></td><td>{html.escape(i['message'])}</td></tr>"
|
| 164 |
-
for i in payload["issues"]
|
| 165 |
-
)
|
| 166 |
-
if not issues_html:
|
| 167 |
-
issues_html = '<tr><td colspan="2" class="muted">No issues recorded.</td></tr>'
|
| 168 |
-
|
| 169 |
-
return f"""<!doctype html>
|
| 170 |
-
<html lang="en"><head><meta charset="utf-8" />
|
| 171 |
-
<title>External Dependencies Report</title>
|
| 172 |
-
<script>(function(){{var t=null;try{{t=localStorage.getItem('vr-theme');}}catch(e){{}}if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t);}})();</script>
|
| 173 |
-
<style>
|
| 174 |
-
:root {{ --bg:#f4f6fb; --panel:#fff; --text:#1f2a44; --muted:#7280a7; --border:#e6ebf5; --resolved:#0a0; --missing:#c00; --error:#b35900; }}
|
| 175 |
-
@media (prefers-color-scheme: dark) {{
|
| 176 |
-
:root:not([data-theme="light"]) {{ --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --resolved:#4ade80; --missing:#ff6b6b; --error:#fbbf24; }}
|
| 177 |
-
}}
|
| 178 |
-
:root[data-theme="dark"] {{ --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --resolved:#4ade80; --missing:#ff6b6b; --error:#fbbf24; }}
|
| 179 |
-
* {{ box-sizing: border-box; }}
|
| 180 |
-
body {{ font-family: "Segoe UI", Arial, sans-serif; margin:0; padding:16px; background:var(--bg); color:var(--text); }}
|
| 181 |
-
header {{ background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:12px 16px; margin-bottom:16px; display:flex; align-items:center; gap:12px; }}
|
| 182 |
-
header .header-text {{ flex:1; }}
|
| 183 |
-
header h1 {{ margin:0; font-size:18px; }}
|
| 184 |
-
header .meta {{ color:var(--muted); font-size:12px; margin-top:4px; }}
|
| 185 |
-
.theme-toggle {{ padding:6px 12px; background:transparent; border:1px solid var(--border); border-radius:8px; color:var(--text); cursor:pointer; font-size:13px; }}
|
| 186 |
-
.theme-toggle:hover {{ background:var(--bg); }}
|
| 187 |
-
.summary {{ display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:20px; }}
|
| 188 |
-
.summary .card {{ background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; }}
|
| 189 |
-
.summary .label {{ color:var(--muted); font-size:12px; }}
|
| 190 |
-
.summary .value {{ font-size:20px; font-weight:700; }}
|
| 191 |
-
section {{ background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; margin-bottom:14px; }}
|
| 192 |
-
section h2 {{ margin:0 0 10px 0; font-size:15px; }}
|
| 193 |
-
table {{ width:100%; border-collapse:collapse; font-size:13px; }}
|
| 194 |
-
th, td {{ text-align:left; vertical-align:top; padding:8px 10px; border-bottom:1px solid var(--border); }}
|
| 195 |
-
th {{ color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:0.04em; }}
|
| 196 |
-
ul {{ margin:0; padding-left:18px; }}
|
| 197 |
-
code {{ font-family: ui-monospace, monospace; font-size:12px; word-break:break-all; }}
|
| 198 |
-
.badge {{ display:inline-block; padding:2px 8px; border-radius:999px; font-size:11px; font-weight:700; color:#fff; }}
|
| 199 |
-
.status-resolved {{ background:var(--resolved); }}
|
| 200 |
-
.status-missing {{ background:var(--missing); }}
|
| 201 |
-
.status-error {{ background:var(--error); }}
|
| 202 |
-
.muted {{ color:var(--muted); }}
|
| 203 |
-
</style></head>
|
| 204 |
-
<body>
|
| 205 |
-
<header>
|
| 206 |
-
<div class="header-text">
|
| 207 |
-
<h1>External Dependencies Report</h1>
|
| 208 |
-
<div class="meta">Target: <code>{html.escape(payload['target_root'])}</code> · Generated: {html.escape(payload['generated'])}</div>
|
| 209 |
-
</div>
|
| 210 |
-
<button type="button" id="themeToggle" class="theme-toggle" aria-label="Toggle theme">Dark mode</button>
|
| 211 |
-
</header>
|
| 212 |
-
<div class="summary">
|
| 213 |
-
<div class="card"><div class="label">External assets</div><div class="value">{s['external_count']}</div></div>
|
| 214 |
-
<div class="card"><div class="label">Missing</div><div class="value">{s['missing_count']}</div></div>
|
| 215 |
-
<div class="card"><div class="label">Issues</div><div class="value">{s['issue_count']}</div></div>
|
| 216 |
-
</div>
|
| 217 |
-
|
| 218 |
-
<section>
|
| 219 |
-
<h2>External assets — what was needed and how it was obtained</h2>
|
| 220 |
-
<table>
|
| 221 |
-
<thead><tr><th>Layer identifier</th><th>Resolved path</th><th>Status</th><th>Referenced by</th><th>Actions taken</th></tr></thead>
|
| 222 |
-
<tbody>{''.join(rows) or '<tr><td colspan="5" class="muted">No external assets referenced.</td></tr>'}</tbody>
|
| 223 |
-
</table>
|
| 224 |
-
</section>
|
| 225 |
-
|
| 226 |
-
<section>
|
| 227 |
-
<h2>Issues encountered while building this report</h2>
|
| 228 |
-
<table>
|
| 229 |
-
<thead><tr><th>Asset</th><th>Message</th></tr></thead>
|
| 230 |
-
<tbody>{issues_html}</tbody>
|
| 231 |
-
</table>
|
| 232 |
-
</section>
|
| 233 |
-
<script>
|
| 234 |
-
(function() {{
|
| 235 |
-
var btn = document.getElementById("themeToggle");
|
| 236 |
-
var mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
| 237 |
-
function isDark() {{
|
| 238 |
-
var attr = document.documentElement.getAttribute("data-theme");
|
| 239 |
-
if (attr === "dark") return true;
|
| 240 |
-
if (attr === "light") return false;
|
| 241 |
-
return mq ? mq.matches : false;
|
| 242 |
-
}}
|
| 243 |
-
function syncLabel() {{
|
| 244 |
-
if (btn) btn.textContent = isDark() ? "Light mode" : "Dark mode";
|
| 245 |
-
}}
|
| 246 |
-
syncLabel();
|
| 247 |
-
if (mq && mq.addEventListener) mq.addEventListener("change", syncLabel);
|
| 248 |
-
if (btn) {{
|
| 249 |
-
btn.addEventListener("click", function() {{
|
| 250 |
-
var next = isDark() ? "light" : "dark";
|
| 251 |
-
document.documentElement.setAttribute("data-theme", next);
|
| 252 |
-
try {{ localStorage.setItem("vr-theme", next); }} catch(e) {{}}
|
| 253 |
-
syncLabel();
|
| 254 |
-
}});
|
| 255 |
-
}}
|
| 256 |
-
}})();
|
| 257 |
-
</script>
|
| 258 |
-
</body></html>
|
| 259 |
-
"""
|
|
|
|
| 1 |
+
"""External-dependency tracker for the /simready-report skill.
|
| 2 |
+
|
| 3 |
+
For each USD stage, walks all used layers (sublayers, references, payloads,
|
| 4 |
+
clips) and classifies each as internal (under target_root) or external.
|
| 5 |
+
Tracks per-layer provenance ("what was done to obtain it") and any issues
|
| 6 |
+
encountered while scanning.
|
| 7 |
+
|
| 8 |
+
Outputs:
|
| 9 |
+
external_dependencies_report.json
|
| 10 |
+
external_dependencies_report.html
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import html
|
| 15 |
+
import json
|
| 16 |
+
from dataclasses import dataclass, field, asdict
|
| 17 |
+
from datetime import datetime, timezone
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
from pxr import Usd
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class ExternalLayerRecord:
|
| 25 |
+
layer_identifier: str
|
| 26 |
+
resolved_path: str
|
| 27 |
+
referenced_by: list[str] = field(default_factory=list)
|
| 28 |
+
actions: list[str] = field(default_factory=list)
|
| 29 |
+
status: str = "resolved" # resolved | missing | error
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class IssueRecord:
|
| 34 |
+
asset: str
|
| 35 |
+
message: str
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class ExternalDepsTracker:
|
| 39 |
+
"""Collects external-layer references across all validated stages."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, target_root: Path):
|
| 42 |
+
self.target_root = target_root.resolve()
|
| 43 |
+
self.external: dict[str, ExternalLayerRecord] = {}
|
| 44 |
+
self.issues: list[IssueRecord] = []
|
| 45 |
+
|
| 46 |
+
# ---- public API ----
|
| 47 |
+
|
| 48 |
+
def scan_stage(self, stage: Usd.Stage, asset_path: Path, target_root: Path) -> None:
|
| 49 |
+
try:
|
| 50 |
+
used = stage.GetUsedLayers(includeClipLayers=True)
|
| 51 |
+
except Exception as e:
|
| 52 |
+
self.record_issue(str(asset_path), f"GetUsedLayers failed: {type(e).__name__}: {e}")
|
| 53 |
+
return
|
| 54 |
+
|
| 55 |
+
root_layer = stage.GetRootLayer()
|
| 56 |
+
for layer in used:
|
| 57 |
+
try:
|
| 58 |
+
if layer is None or layer == root_layer:
|
| 59 |
+
continue
|
| 60 |
+
identifier = layer.identifier
|
| 61 |
+
# Skip anonymous (in-memory) layers — `anon:0x...` identifiers
|
| 62 |
+
# are USD-internal handles, not external dependencies.
|
| 63 |
+
if identifier.startswith("anon:") or (hasattr(layer, "anonymous") and layer.anonymous):
|
| 64 |
+
continue
|
| 65 |
+
resolved = layer.realPath or (layer.resolvedPath.GetPathString() if hasattr(layer, "resolvedPath") else "")
|
| 66 |
+
|
| 67 |
+
if self._is_internal(resolved):
|
| 68 |
+
continue
|
| 69 |
+
|
| 70 |
+
rec = self.external.get(identifier)
|
| 71 |
+
if rec is None:
|
| 72 |
+
rec = ExternalLayerRecord(
|
| 73 |
+
layer_identifier=identifier,
|
| 74 |
+
resolved_path=resolved or "",
|
| 75 |
+
)
|
| 76 |
+
if resolved and Path(resolved).exists():
|
| 77 |
+
rec.actions.append(f"Resolved by USD asset resolver to: {resolved}")
|
| 78 |
+
rec.status = "resolved"
|
| 79 |
+
elif not resolved:
|
| 80 |
+
rec.actions.append(
|
| 81 |
+
f"Could not resolve layer identifier '{identifier}' to a filesystem path."
|
| 82 |
+
)
|
| 83 |
+
rec.status = "missing"
|
| 84 |
+
else:
|
| 85 |
+
rec.actions.append(f"Resolved to '{resolved}' but path does not exist on disk.")
|
| 86 |
+
rec.status = "missing"
|
| 87 |
+
self.external[identifier] = rec
|
| 88 |
+
|
| 89 |
+
ref = str(asset_path.relative_to(self.target_root)) if asset_path.is_relative_to(self.target_root) else str(asset_path)
|
| 90 |
+
if ref not in rec.referenced_by:
|
| 91 |
+
rec.referenced_by.append(ref)
|
| 92 |
+
except Exception as e:
|
| 93 |
+
self.record_issue(str(asset_path), f"Error scanning layer {layer}: {type(e).__name__}: {e}")
|
| 94 |
+
|
| 95 |
+
def record_issue(self, asset: str, message: str) -> None:
|
| 96 |
+
self.issues.append(IssueRecord(asset=asset, message=message))
|
| 97 |
+
|
| 98 |
+
def external_count(self) -> int:
|
| 99 |
+
return len(self.external)
|
| 100 |
+
|
| 101 |
+
def write_reports(self, out_dir: Path) -> None:
|
| 102 |
+
# Skip emitting the report entirely when there's nothing to say —
|
| 103 |
+
# no external assets resolved AND no issues encountered.
|
| 104 |
+
if not self.external and not self.issues:
|
| 105 |
+
for stale in ("external_dependencies_report.json", "external_dependencies_report.html"):
|
| 106 |
+
p = out_dir / stale
|
| 107 |
+
if p.is_file():
|
| 108 |
+
p.unlink()
|
| 109 |
+
return
|
| 110 |
+
payload = {
|
| 111 |
+
"generated": datetime.now(timezone.utc).isoformat(),
|
| 112 |
+
"target_root": str(self.target_root),
|
| 113 |
+
"external_assets": [asdict(r) for r in self.external.values()],
|
| 114 |
+
"issues": [asdict(i) for i in self.issues],
|
| 115 |
+
"summary": {
|
| 116 |
+
"external_count": len(self.external),
|
| 117 |
+
"missing_count": sum(1 for r in self.external.values() if r.status == "missing"),
|
| 118 |
+
"issue_count": len(self.issues),
|
| 119 |
+
},
|
| 120 |
+
}
|
| 121 |
+
(out_dir / "external_dependencies_report.json").write_text(
|
| 122 |
+
json.dumps(payload, indent=2), encoding="utf-8"
|
| 123 |
+
)
|
| 124 |
+
(out_dir / "external_dependencies_report.html").write_text(
|
| 125 |
+
self._render_html(payload), encoding="utf-8"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# ---- internal ----
|
| 129 |
+
|
| 130 |
+
def _is_internal(self, resolved_path: str) -> bool:
|
| 131 |
+
if not resolved_path:
|
| 132 |
+
return False
|
| 133 |
+
try:
|
| 134 |
+
p = Path(resolved_path).resolve()
|
| 135 |
+
except Exception:
|
| 136 |
+
return False
|
| 137 |
+
try:
|
| 138 |
+
return p.is_relative_to(self.target_root)
|
| 139 |
+
except AttributeError:
|
| 140 |
+
try:
|
| 141 |
+
p.relative_to(self.target_root)
|
| 142 |
+
return True
|
| 143 |
+
except ValueError:
|
| 144 |
+
return False
|
| 145 |
+
|
| 146 |
+
def _render_html(self, payload: dict) -> str:
|
| 147 |
+
s = payload["summary"]
|
| 148 |
+
rows = []
|
| 149 |
+
for rec in payload["external_assets"]:
|
| 150 |
+
actions_html = "".join(f"<li>{html.escape(a)}</li>" for a in rec["actions"]) or "<li>(none)</li>"
|
| 151 |
+
refs_html = "".join(f"<li><code>{html.escape(r)}</code></li>" for r in rec["referenced_by"]) or "<li>(unknown)</li>"
|
| 152 |
+
status_class = "status-" + rec["status"]
|
| 153 |
+
rows.append(f"""
|
| 154 |
+
<tr>
|
| 155 |
+
<td><code>{html.escape(rec['layer_identifier'])}</code></td>
|
| 156 |
+
<td><code>{html.escape(rec['resolved_path'] or '')}</code></td>
|
| 157 |
+
<td><span class="badge {status_class}">{html.escape(rec['status'])}</span></td>
|
| 158 |
+
<td><ul>{refs_html}</ul></td>
|
| 159 |
+
<td><ul>{actions_html}</ul></td>
|
| 160 |
+
</tr>""")
|
| 161 |
+
|
| 162 |
+
issues_html = "".join(
|
| 163 |
+
f"<tr><td><code>{html.escape(i['asset'])}</code></td><td>{html.escape(i['message'])}</td></tr>"
|
| 164 |
+
for i in payload["issues"]
|
| 165 |
+
)
|
| 166 |
+
if not issues_html:
|
| 167 |
+
issues_html = '<tr><td colspan="2" class="muted">No issues recorded.</td></tr>'
|
| 168 |
+
|
| 169 |
+
return f"""<!doctype html>
|
| 170 |
+
<html lang="en"><head><meta charset="utf-8" />
|
| 171 |
+
<title>External Dependencies Report</title>
|
| 172 |
+
<script>(function(){{var t=null;try{{t=localStorage.getItem('vr-theme');}}catch(e){{}}if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t);}})();</script>
|
| 173 |
+
<style>
|
| 174 |
+
:root {{ --bg:#f4f6fb; --panel:#fff; --text:#1f2a44; --muted:#7280a7; --border:#e6ebf5; --resolved:#0a0; --missing:#c00; --error:#b35900; }}
|
| 175 |
+
@media (prefers-color-scheme: dark) {{
|
| 176 |
+
:root:not([data-theme="light"]) {{ --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --resolved:#4ade80; --missing:#ff6b6b; --error:#fbbf24; }}
|
| 177 |
+
}}
|
| 178 |
+
:root[data-theme="dark"] {{ --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --resolved:#4ade80; --missing:#ff6b6b; --error:#fbbf24; }}
|
| 179 |
+
* {{ box-sizing: border-box; }}
|
| 180 |
+
body {{ font-family: "Segoe UI", Arial, sans-serif; margin:0; padding:16px; background:var(--bg); color:var(--text); }}
|
| 181 |
+
header {{ background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:12px 16px; margin-bottom:16px; display:flex; align-items:center; gap:12px; }}
|
| 182 |
+
header .header-text {{ flex:1; }}
|
| 183 |
+
header h1 {{ margin:0; font-size:18px; }}
|
| 184 |
+
header .meta {{ color:var(--muted); font-size:12px; margin-top:4px; }}
|
| 185 |
+
.theme-toggle {{ padding:6px 12px; background:transparent; border:1px solid var(--border); border-radius:8px; color:var(--text); cursor:pointer; font-size:13px; }}
|
| 186 |
+
.theme-toggle:hover {{ background:var(--bg); }}
|
| 187 |
+
.summary {{ display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:20px; }}
|
| 188 |
+
.summary .card {{ background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; }}
|
| 189 |
+
.summary .label {{ color:var(--muted); font-size:12px; }}
|
| 190 |
+
.summary .value {{ font-size:20px; font-weight:700; }}
|
| 191 |
+
section {{ background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; margin-bottom:14px; }}
|
| 192 |
+
section h2 {{ margin:0 0 10px 0; font-size:15px; }}
|
| 193 |
+
table {{ width:100%; border-collapse:collapse; font-size:13px; }}
|
| 194 |
+
th, td {{ text-align:left; vertical-align:top; padding:8px 10px; border-bottom:1px solid var(--border); }}
|
| 195 |
+
th {{ color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:0.04em; }}
|
| 196 |
+
ul {{ margin:0; padding-left:18px; }}
|
| 197 |
+
code {{ font-family: ui-monospace, monospace; font-size:12px; word-break:break-all; }}
|
| 198 |
+
.badge {{ display:inline-block; padding:2px 8px; border-radius:999px; font-size:11px; font-weight:700; color:#fff; }}
|
| 199 |
+
.status-resolved {{ background:var(--resolved); }}
|
| 200 |
+
.status-missing {{ background:var(--missing); }}
|
| 201 |
+
.status-error {{ background:var(--error); }}
|
| 202 |
+
.muted {{ color:var(--muted); }}
|
| 203 |
+
</style></head>
|
| 204 |
+
<body>
|
| 205 |
+
<header>
|
| 206 |
+
<div class="header-text">
|
| 207 |
+
<h1>External Dependencies Report</h1>
|
| 208 |
+
<div class="meta">Target: <code>{html.escape(payload['target_root'])}</code> · Generated: {html.escape(payload['generated'])}</div>
|
| 209 |
+
</div>
|
| 210 |
+
<button type="button" id="themeToggle" class="theme-toggle" aria-label="Toggle theme">Dark mode</button>
|
| 211 |
+
</header>
|
| 212 |
+
<div class="summary">
|
| 213 |
+
<div class="card"><div class="label">External assets</div><div class="value">{s['external_count']}</div></div>
|
| 214 |
+
<div class="card"><div class="label">Missing</div><div class="value">{s['missing_count']}</div></div>
|
| 215 |
+
<div class="card"><div class="label">Issues</div><div class="value">{s['issue_count']}</div></div>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<section>
|
| 219 |
+
<h2>External assets — what was needed and how it was obtained</h2>
|
| 220 |
+
<table>
|
| 221 |
+
<thead><tr><th>Layer identifier</th><th>Resolved path</th><th>Status</th><th>Referenced by</th><th>Actions taken</th></tr></thead>
|
| 222 |
+
<tbody>{''.join(rows) or '<tr><td colspan="5" class="muted">No external assets referenced.</td></tr>'}</tbody>
|
| 223 |
+
</table>
|
| 224 |
+
</section>
|
| 225 |
+
|
| 226 |
+
<section>
|
| 227 |
+
<h2>Issues encountered while building this report</h2>
|
| 228 |
+
<table>
|
| 229 |
+
<thead><tr><th>Asset</th><th>Message</th></tr></thead>
|
| 230 |
+
<tbody>{issues_html}</tbody>
|
| 231 |
+
</table>
|
| 232 |
+
</section>
|
| 233 |
+
<script>
|
| 234 |
+
(function() {{
|
| 235 |
+
var btn = document.getElementById("themeToggle");
|
| 236 |
+
var mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
| 237 |
+
function isDark() {{
|
| 238 |
+
var attr = document.documentElement.getAttribute("data-theme");
|
| 239 |
+
if (attr === "dark") return true;
|
| 240 |
+
if (attr === "light") return false;
|
| 241 |
+
return mq ? mq.matches : false;
|
| 242 |
+
}}
|
| 243 |
+
function syncLabel() {{
|
| 244 |
+
if (btn) btn.textContent = isDark() ? "Light mode" : "Dark mode";
|
| 245 |
+
}}
|
| 246 |
+
syncLabel();
|
| 247 |
+
if (mq && mq.addEventListener) mq.addEventListener("change", syncLabel);
|
| 248 |
+
if (btn) {{
|
| 249 |
+
btn.addEventListener("click", function() {{
|
| 250 |
+
var next = isDark() ? "light" : "dark";
|
| 251 |
+
document.documentElement.setAttribute("data-theme", next);
|
| 252 |
+
try {{ localStorage.setItem("vr-theme", next); }} catch(e) {{}}
|
| 253 |
+
syncLabel();
|
| 254 |
+
}});
|
| 255 |
+
}}
|
| 256 |
+
}})();
|
| 257 |
+
</script>
|
| 258 |
+
</body></html>
|
| 259 |
+
"""
|
tools/validation/plugins/simready-report/skills/simready-report/report.py
CHANGED
|
@@ -1,860 +1,860 @@
|
|
| 1 |
-
"""Dashboard HTML generator for the /simready-report skill.
|
| 2 |
-
|
| 3 |
-
Given the per-asset results produced by validate.py, emits index.html in the
|
| 4 |
-
style of usd_validation_dashboard_final/: summary cards, asset filter,
|
| 5 |
-
per-asset blocks (failed/passed features, affected prims), compliance footer.
|
| 6 |
-
|
| 7 |
-
Thumbnails are embedded inline (base64 data URI) from the spec path
|
| 8 |
-
<asset_dir>/.thumbs/256x256/<filename>.png — matching the ThumbnailExists rule.
|
| 9 |
-
"""
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import html
|
| 13 |
-
from datetime import datetime, timezone
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def _ensure_static_copied(out_docs_dir: Path, dashboard_docs_dir: Path | None) -> bool:
|
| 18 |
-
"""Mirror the reference dashboard's `_static/` into `<out_docs_dir>/_static/` once.
|
| 19 |
-
|
| 20 |
-
Returns True if the static tree is available (so pages can link to it),
|
| 21 |
-
False if the dashboard isn't present or the copy failed.
|
| 22 |
-
"""
|
| 23 |
-
if dashboard_docs_dir is None:
|
| 24 |
-
return False
|
| 25 |
-
src = dashboard_docs_dir / "_static"
|
| 26 |
-
if not src.is_dir():
|
| 27 |
-
return False
|
| 28 |
-
dest = out_docs_dir / "_static"
|
| 29 |
-
if dest.exists():
|
| 30 |
-
return True
|
| 31 |
-
import shutil
|
| 32 |
-
try:
|
| 33 |
-
shutil.copytree(src, dest)
|
| 34 |
-
return True
|
| 35 |
-
except Exception:
|
| 36 |
-
return False
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def _render_md_page(title: str, rel_path: str, source_path: Path, body_html: str,
|
| 40 |
-
has_static: bool) -> str:
|
| 41 |
-
"""Render a markdown page using the reference dashboard's Sphinx assets.
|
| 42 |
-
|
| 43 |
-
`rel_path` is the doc's path relative to `out_docs_dir` (e.g.
|
| 44 |
-
`features/FET_001-minimal.html`); we compute how many `../` are needed to
|
| 45 |
-
reach `_static/` based on its depth.
|
| 46 |
-
"""
|
| 47 |
-
depth = rel_path.replace("\\", "/").count("/")
|
| 48 |
-
static_prefix = "../" * depth + "_static/"
|
| 49 |
-
if has_static:
|
| 50 |
-
head_links = f"""
|
| 51 |
-
<link rel="stylesheet" href="{static_prefix}styles/theme.css" />
|
| 52 |
-
<link rel="stylesheet" href="{static_prefix}styles/bootstrap.css" />
|
| 53 |
-
<link rel="stylesheet" href="{static_prefix}styles/pydata-sphinx-theme.css" />
|
| 54 |
-
<link rel="stylesheet" href="{static_prefix}styles/nvidia-sphinx-theme.css" />
|
| 55 |
-
<link rel="stylesheet" href="{static_prefix}pygments.css" />
|
| 56 |
-
<link rel="stylesheet" href="{static_prefix}_style.css" />
|
| 57 |
-
<link rel="stylesheet" href="{static_prefix}custom.css" />
|
| 58 |
-
<link rel="stylesheet" href="{static_prefix}sphinx-design.min.css" />
|
| 59 |
-
<link rel="stylesheet" href="{static_prefix}vendor/fontawesome/6.5.2/css/all.min.css" />
|
| 60 |
-
"""
|
| 61 |
-
body_open = '<main class="bd-main"><div class="bd-content"><div class="bd-article-container"><article class="bd-article">'
|
| 62 |
-
body_close = '</article></div></div></main>'
|
| 63 |
-
else:
|
| 64 |
-
head_links = ""
|
| 65 |
-
body_open = '<article style="max-width:880px;margin:24px auto;padding:24px 28px;font-family:Segoe UI,Arial,sans-serif;line-height:1.55;">'
|
| 66 |
-
body_close = '</article>'
|
| 67 |
-
|
| 68 |
-
# `source_path` is an absolute machine-specific path inside the foundations
|
| 69 |
-
# checkout — don't render it. Show the source's path relative to the
|
| 70 |
-
# foundation docs root (i.e. `rel_path` minus the .html for the .md form),
|
| 71 |
-
# which is portable across machines that share the same foundations layout.
|
| 72 |
-
source_md_rel = (rel_path[:-5] + ".md") if rel_path.endswith(".html") else rel_path
|
| 73 |
-
return f"""<!DOCTYPE html>
|
| 74 |
-
<html lang="en" data-content_root="{'../' * depth}">
|
| 75 |
-
<head>
|
| 76 |
-
<meta charset="utf-8" />
|
| 77 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 78 |
-
<title>{html.escape(title)}</title>
|
| 79 |
-
<script>document.documentElement.dataset.mode = localStorage.getItem("mode") || "";document.documentElement.dataset.theme = localStorage.getItem("theme") || "";</script>{head_links}
|
| 80 |
-
</head>
|
| 81 |
-
<body data-bs-spy="scroll">
|
| 82 |
-
{body_open}
|
| 83 |
-
<p class="text-muted" style="font-size:12px;margin-bottom:14px;">Source: <code>nv_core/sr_specs/docs/{html.escape(source_md_rel)}</code></p>
|
| 84 |
-
{body_html}
|
| 85 |
-
{body_close}
|
| 86 |
-
</body>
|
| 87 |
-
</html>
|
| 88 |
-
"""
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
_NO_SPEC_TAG = ' <span class="no-spec">(no spec source)</span>'
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def _relativize_msg(msg: str, target: Path | None) -> str:
|
| 95 |
-
"""Strip machine-specific absolute path prefixes from validator messages.
|
| 96 |
-
|
| 97 |
-
`omni.asset_validator` rule messages embed the absolute path of each USD
|
| 98 |
-
being inspected. We replace the target's path prefix (and the path up to
|
| 99 |
-
`packages/` / `assets_to_validate/`) with relative tokens so reports
|
| 100 |
-
don't leak the report-author's user dir.
|
| 101 |
-
"""
|
| 102 |
-
if not msg or target is None:
|
| 103 |
-
return msg
|
| 104 |
-
out = msg
|
| 105 |
-
target_fwd = str(target).replace("\\", "/")
|
| 106 |
-
target_bwd = str(target).replace("/", "\\")
|
| 107 |
-
for prefix in (target_fwd, target_bwd):
|
| 108 |
-
if prefix and prefix in out:
|
| 109 |
-
out = out.replace(prefix, "<target>")
|
| 110 |
-
# Catch absolute paths that point at the workspace's packages/ or
|
| 111 |
-
# assets_to_validate/ — same machine-specificity, different anchor.
|
| 112 |
-
for marker in ("/packages/", "\\packages\\", "/assets_to_validate/", "\\assets_to_validate\\"):
|
| 113 |
-
idx = out.find(marker)
|
| 114 |
-
while idx != -1:
|
| 115 |
-
# Find the start of the absolute path containing this marker
|
| 116 |
-
# (a drive letter "X:\" or POSIX "/" boundary going left).
|
| 117 |
-
start = idx
|
| 118 |
-
while start > 0 and out[start - 1] not in (" ", "<", "(", "[", '"', "'", "\n", "\t"):
|
| 119 |
-
start -= 1
|
| 120 |
-
# Replace from `start` through the marker with the marker's tail.
|
| 121 |
-
anchor = marker.strip("/\\").replace("\\", "/") # "packages" / "assets_to_validate"
|
| 122 |
-
tail_pos = idx + len(marker)
|
| 123 |
-
out = out[:start] + f"<{anchor}>/" + out[tail_pos:]
|
| 124 |
-
idx = out.find(marker, start + len(anchor) + 3)
|
| 125 |
-
return out
|
| 126 |
-
|
| 127 |
-
_md_renderer = None
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def _md_to_html(md_text: str) -> str:
|
| 131 |
-
global _md_renderer
|
| 132 |
-
if _md_renderer is None:
|
| 133 |
-
from markdown_it import MarkdownIt
|
| 134 |
-
# html:True passes raw HTML through unescaped — safe only because the
|
| 135 |
-
# markdown comes from the trusted foundations repo, not user input.
|
| 136 |
-
_md_renderer = MarkdownIt("commonmark", {"html": True, "linkify": True, "typographer": True}).enable("table")
|
| 137 |
-
return _md_renderer.render(md_text)
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
_IMG_PLACEHOLDER_SVG = (
|
| 141 |
-
"data:image/svg+xml;utf8,"
|
| 142 |
-
"<svg xmlns='http://www.w3.org/2000/svg' width='320' height='180' viewBox='0 0 320 180'>"
|
| 143 |
-
"<rect width='320' height='180' fill='%23f0f2f8' stroke='%23c7cee0' stroke-dasharray='6 4' stroke-width='2'/>"
|
| 144 |
-
"<text x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' "
|
| 145 |
-
"font-family='Segoe UI,Arial,sans-serif' font-size='14' fill='%237280a7'>image missing</text>"
|
| 146 |
-
"</svg>"
|
| 147 |
-
)
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def _localize_images(body_html: str, md_source: Path, out_html_path: Path) -> str:
|
| 151 |
-
"""Walk every `<img src="...">` in `body_html`. If src is a relative path,
|
| 152 |
-
copy the source file (relative to `md_source.parent`) into the equivalent
|
| 153 |
-
location next to `out_html_path`. When the source file is missing, replace
|
| 154 |
-
the src with an inline SVG placeholder so the page never 404s.
|
| 155 |
-
"""
|
| 156 |
-
import re
|
| 157 |
-
import shutil
|
| 158 |
-
|
| 159 |
-
md_dir = md_source.parent
|
| 160 |
-
out_dir = out_html_path.parent
|
| 161 |
-
|
| 162 |
-
def repl(match):
|
| 163 |
-
full_tag = match.group(0)
|
| 164 |
-
src_match = re.search(r'src="([^"]+)"', full_tag)
|
| 165 |
-
if not src_match:
|
| 166 |
-
return full_tag
|
| 167 |
-
src = src_match.group(1)
|
| 168 |
-
if src.startswith(("http://", "https://", "data:", "//", "/")):
|
| 169 |
-
return full_tag # absolute / data URI / protocol-relative — leave alone
|
| 170 |
-
rel = src.lstrip("./").replace("\\", "/")
|
| 171 |
-
source_file = (md_dir / src).resolve()
|
| 172 |
-
# Containment guard on the SOURCE: a crafted ../ src must not read
|
| 173 |
-
# files outside the markdown tree. Escape -> skip to placeholder.
|
| 174 |
-
try:
|
| 175 |
-
source_file.relative_to(md_dir.resolve())
|
| 176 |
-
except ValueError:
|
| 177 |
-
import sys
|
| 178 |
-
print(f"[report] skipping out-of-tree image src: {src!r}", file=sys.stderr)
|
| 179 |
-
return re.sub(r'src="[^"]+"', f'src="{_IMG_PLACEHOLDER_SVG}"', full_tag)
|
| 180 |
-
if source_file.is_file():
|
| 181 |
-
dest_file = (out_dir / rel).resolve()
|
| 182 |
-
try:
|
| 183 |
-
dest_file.relative_to(out_dir.resolve()) # guard against ../ escape
|
| 184 |
-
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
| 185 |
-
if not dest_file.exists() or dest_file.stat().st_size != source_file.stat().st_size:
|
| 186 |
-
shutil.copy2(source_file, dest_file)
|
| 187 |
-
return full_tag
|
| 188 |
-
except (ValueError, OSError):
|
| 189 |
-
pass
|
| 190 |
-
# Missing source — swap to placeholder
|
| 191 |
-
return re.sub(r'src="[^"]+"', f'src="{_IMG_PLACEHOLDER_SVG}"', full_tag)
|
| 192 |
-
|
| 193 |
-
return re.sub(r"<img\b[^>]*>", repl, body_html)
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
_A_TAG_RE = None
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
def _localize_links(body_html: str, md_source: Path, out_html_path: Path,
|
| 200 |
-
specs_docs_dir: Path, out_docs_root: Path,
|
| 201 |
-
resolver) -> str:
|
| 202 |
-
"""Rewrite relative <a href="..."> targets so the docs tree is self-contained.
|
| 203 |
-
|
| 204 |
-
- .md targets that resolve to a file inside `specs_docs_dir`: rewrite href
|
| 205 |
-
to .html and recursively render via `resolver(<rel>.html)`.
|
| 206 |
-
- non-.md targets inside `specs_docs_dir`: copy verbatim into the mirrored
|
| 207 |
-
location under `out_docs_root` and rewrite href to that copy.
|
| 208 |
-
- root-relative ("/...") hrefs treated as relative to `specs_docs_dir`;
|
| 209 |
-
bare paths without an extension are tried as `.md`.
|
| 210 |
-
- links pointing outside `specs_docs_dir` or at non-existent targets are
|
| 211 |
-
replaced with <span class="docref-disabled" title="...">TEXT</span> so
|
| 212 |
-
the visible text remains but no href can 404.
|
| 213 |
-
"""
|
| 214 |
-
import os
|
| 215 |
-
import re
|
| 216 |
-
import shutil
|
| 217 |
-
|
| 218 |
-
global _A_TAG_RE
|
| 219 |
-
if _A_TAG_RE is None:
|
| 220 |
-
_A_TAG_RE = re.compile(r'<a\s+([^>]*?)>(.*?)</a>', re.DOTALL | re.IGNORECASE)
|
| 221 |
-
|
| 222 |
-
md_dir = md_source.parent
|
| 223 |
-
out_dir = out_html_path.parent
|
| 224 |
-
specs_root = specs_docs_dir.resolve()
|
| 225 |
-
out_docs_resolved = out_docs_root.resolve()
|
| 226 |
-
|
| 227 |
-
def _disable(inner: str, why: str = "link target not available in report") -> str:
|
| 228 |
-
return f'<span class="docref-disabled" title="{html.escape(why)}">{inner}</span>'
|
| 229 |
-
|
| 230 |
-
def _resolve_target(href_path: str) -> Path | None:
|
| 231 |
-
if href_path.startswith("/"):
|
| 232 |
-
base = specs_root / href_path.lstrip("/")
|
| 233 |
-
if not base.suffix:
|
| 234 |
-
base = base.with_suffix(".md")
|
| 235 |
-
return base
|
| 236 |
-
return (md_dir / href_path)
|
| 237 |
-
|
| 238 |
-
def repl(m: "re.Match[str]") -> str:
|
| 239 |
-
attrs = m.group(1)
|
| 240 |
-
inner = m.group(2)
|
| 241 |
-
href_m = re.search(r'href\s*=\s*"([^"]*)"', attrs)
|
| 242 |
-
if not href_m:
|
| 243 |
-
return m.group(0)
|
| 244 |
-
href = href_m.group(1).strip()
|
| 245 |
-
if not href:
|
| 246 |
-
return m.group(0)
|
| 247 |
-
if href.startswith(("http://", "https://", "data:", "mailto:", "//", "#", "javascript:")):
|
| 248 |
-
return m.group(0)
|
| 249 |
-
|
| 250 |
-
if "#" in href:
|
| 251 |
-
path_part, frag = href.split("#", 1)
|
| 252 |
-
frag = "#" + frag
|
| 253 |
-
else:
|
| 254 |
-
path_part, frag = href, ""
|
| 255 |
-
if not path_part:
|
| 256 |
-
return m.group(0)
|
| 257 |
-
|
| 258 |
-
target = _resolve_target(path_part)
|
| 259 |
-
if target is None:
|
| 260 |
-
return _disable(inner)
|
| 261 |
-
try:
|
| 262 |
-
target = target.resolve()
|
| 263 |
-
except OSError:
|
| 264 |
-
return _disable(inner)
|
| 265 |
-
try:
|
| 266 |
-
rel_to_specs = target.relative_to(specs_root)
|
| 267 |
-
except ValueError:
|
| 268 |
-
return _disable(inner, "link target outside foundation docs")
|
| 269 |
-
if not target.exists():
|
| 270 |
-
return _disable(inner, "link target missing in foundation docs")
|
| 271 |
-
|
| 272 |
-
rel_str = str(rel_to_specs).replace("\\", "/")
|
| 273 |
-
|
| 274 |
-
if target.suffix.lower() == ".md":
|
| 275 |
-
html_rel = rel_str[:-3] + ".html"
|
| 276 |
-
url = resolver(html_rel)
|
| 277 |
-
if url is None:
|
| 278 |
-
return _disable(inner)
|
| 279 |
-
target_out = out_docs_resolved / html_rel
|
| 280 |
-
else:
|
| 281 |
-
target_out = out_docs_resolved / rel_str
|
| 282 |
-
try:
|
| 283 |
-
target_out.parent.mkdir(parents=True, exist_ok=True)
|
| 284 |
-
if not target_out.exists() or target_out.stat().st_size != target.stat().st_size:
|
| 285 |
-
shutil.copy2(target, target_out)
|
| 286 |
-
except OSError:
|
| 287 |
-
return _disable(inner, "could not copy link target into report")
|
| 288 |
-
|
| 289 |
-
try:
|
| 290 |
-
new_href = os.path.relpath(target_out, out_dir).replace("\\", "/") + frag
|
| 291 |
-
except ValueError:
|
| 292 |
-
return _disable(inner)
|
| 293 |
-
new_attrs = re.sub(r'href\s*=\s*"[^"]*"', f'href="{new_href}"', attrs, count=1)
|
| 294 |
-
return f'<a {new_attrs}>{inner}</a>'
|
| 295 |
-
|
| 296 |
-
return _A_TAG_RE.sub(repl, body_html)
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
def _make_doc_url_resolver(out_dir: Path, dashboard_docs_dir: Path | None,
|
| 300 |
-
specs_docs_dir: Path | None):
|
| 301 |
-
"""Build a `_doc_url(rel_path)` callable.
|
| 302 |
-
|
| 303 |
-
Every resolvable doc is rendered from the foundation markdown source into
|
| 304 |
-
`<out_dir>/docs/<rel_path>` so the dashboard has a single, self-contained
|
| 305 |
-
docs tree. The reference-dashboard pre-built HTML is no longer used as a
|
| 306 |
-
link target — it would split the source of truth.
|
| 307 |
-
|
| 308 |
-
`dashboard_docs_dir` is kept on the signature for API stability but not
|
| 309 |
-
consulted here.
|
| 310 |
-
"""
|
| 311 |
-
rendered_cache: dict[str, str] = {}
|
| 312 |
-
out_docs_dir = out_dir / "docs"
|
| 313 |
-
out_docs_dir.mkdir(parents=True, exist_ok=True)
|
| 314 |
-
has_static = _ensure_static_copied(out_docs_dir, dashboard_docs_dir)
|
| 315 |
-
|
| 316 |
-
def resolve(rel_path: str | None) -> str | None:
|
| 317 |
-
if not rel_path or specs_docs_dir is None:
|
| 318 |
-
return None
|
| 319 |
-
if rel_path in rendered_cache:
|
| 320 |
-
return rendered_cache[rel_path]
|
| 321 |
-
md_rel = rel_path[:-5] + ".md" if rel_path.endswith(".html") else rel_path
|
| 322 |
-
md_source = specs_docs_dir / md_rel
|
| 323 |
-
if not md_source.is_file():
|
| 324 |
-
return None
|
| 325 |
-
# Cache the URL before rendering body so cycles in the foundation's
|
| 326 |
-
# cross-doc links (e.g. profile -> feature -> profile) terminate.
|
| 327 |
-
url = "docs/" + rel_path.replace("\\", "/")
|
| 328 |
-
rendered_cache[rel_path] = url
|
| 329 |
-
out_path = out_docs_dir / rel_path
|
| 330 |
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 331 |
-
body = _md_to_html(md_source.read_text(encoding="utf-8"))
|
| 332 |
-
body = _localize_images(body, md_source, out_path)
|
| 333 |
-
body = _localize_links(body, md_source, out_path, specs_docs_dir, out_docs_dir, resolve)
|
| 334 |
-
out_path.write_text(
|
| 335 |
-
_render_md_page(rel_path, rel_path, md_source, body, has_static),
|
| 336 |
-
encoding="utf-8",
|
| 337 |
-
)
|
| 338 |
-
return url
|
| 339 |
-
|
| 340 |
-
return resolve
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
CSS = """
|
| 344 |
-
:root { --bg:#f4f6fb; --panel:#fff; --text:#1f2a44; --muted:#7280a7; --border:#e6ebf5; --fail:#c00; --pass:#0a0; --link:#2b6de2; --note-bg:#fff8e6; --note-border:#f1d38a; --note-text:#6b4f00; --note-code:#fff3cc; --thumb-bg:#f4f6fb; }
|
| 345 |
-
@media (prefers-color-scheme: dark) {
|
| 346 |
-
:root:not([data-theme="light"]) { --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --fail:#ff6b6b; --pass:#4ade80; --link:#7ba9ff; --note-bg:#2a2410; --note-border:#5a4a1f; --note-text:#f1d38a; --note-code:#3a3018; --thumb-bg:#0f1320; }
|
| 347 |
-
}
|
| 348 |
-
:root[data-theme="dark"] { --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --fail:#ff6b6b; --pass:#4ade80; --link:#7ba9ff; --note-bg:#2a2410; --note-border:#5a4a1f; --note-text:#f1d38a; --note-code:#3a3018; --thumb-bg:#0f1320; }
|
| 349 |
-
* { box-sizing: border-box; }
|
| 350 |
-
body { font-family: "Segoe UI", Arial, sans-serif; margin:0; background:var(--bg); color:var(--text); padding:16px; }
|
| 351 |
-
header { display:flex; align-items:center; gap:10px; margin-bottom:16px; padding:12px 16px; background:var(--panel); border-radius:12px; border:1px solid var(--border); }
|
| 352 |
-
header .logo { width:36px; height:36px; border-radius:10px; background:#2b6de2; color:#fff; display:grid; place-items:center; font-weight:700; }
|
| 353 |
-
header h1 { margin:0; font-size:18px; }
|
| 354 |
-
header .meta { color:var(--muted); font-size:12px; }
|
| 355 |
-
.summary { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:20px; }
|
| 356 |
-
.summary .card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; }
|
| 357 |
-
.summary .card .label { color:var(--muted); font-size:12px; }
|
| 358 |
-
.summary .card .value { font-size:20px; font-weight:700; }
|
| 359 |
-
.asset-block { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; margin-bottom:14px; display:flex; gap:14px; align-items:flex-start; }
|
| 360 |
-
.asset-block .asset-thumb-wrap { flex-shrink:0; width:140px; }
|
| 361 |
-
.asset-block .asset-thumb { width:100%; height:auto; aspect-ratio:1; object-fit:cover; border-radius:8px; background:var(--border); display:block; }
|
| 362 |
-
.asset-block .asset-thumb-missing { background:var(--thumb-bg); border:1px dashed var(--border); display:grid; place-items:center; color:var(--muted); font-size:11px; text-align:center; padding:8px; }
|
| 363 |
-
.asset-block .asset-thumb-missing span { line-height:1.3; }
|
| 364 |
-
.code { font-family: ui-monospace, monospace; font-size:12px; padding:1px 6px; border-radius:4px; background:var(--note-code); color:var(--note-text); margin-right:4px; }
|
| 365 |
-
.feat-id { font-family: ui-monospace, monospace; font-size:13px; }
|
| 366 |
-
a.docref { color:var(--link); text-decoration:underline; text-decoration-style:dotted; text-underline-offset:2px; font-size:12px; }
|
| 367 |
-
a.docref:hover { text-decoration-style:solid; }
|
| 368 |
-
a.code { text-decoration:underline; text-decoration-style:dotted; text-underline-offset:2px; }
|
| 369 |
-
a.code:hover { text-decoration-style:solid; }
|
| 370 |
-
.failed-features .feat-id { color:var(--fail); }
|
| 371 |
-
.passed-features .feat-id { color:var(--pass); }
|
| 372 |
-
.asset-block .asset-body { flex:1; min-width:0; }
|
| 373 |
-
.asset-block h2 { margin:0 0 6px 0; font-size:15px; }
|
| 374 |
-
.asset-block .path { font-size:12px; color:var(--muted); margin-bottom:10px; word-break:break-all; }
|
| 375 |
-
.asset-block section { margin-top:10px; }
|
| 376 |
-
.asset-block section h3 { margin:0 0 6px 0; font-size:13px; color:var(--muted); }
|
| 377 |
-
.asset-block ul { margin:0; padding-left:20px; }
|
| 378 |
-
.asset-block li { margin:4px 0; }
|
| 379 |
-
.failed-features { color:var(--fail); }
|
| 380 |
-
.passed-features { color:var(--pass); }
|
| 381 |
-
.filter { margin-bottom:12px; }
|
| 382 |
-
.filter input { padding:8px 12px; width:100%; max-width:320px; border:1px solid var(--border); border-radius:8px; font-size:14px; }
|
| 383 |
-
.compliance { margin-top:28px; background:var(--panel); border-radius:12px; padding:18px; border:1px solid var(--border); }
|
| 384 |
-
.compliance h2 { margin:0 0 12px 0; font-size:16px; }
|
| 385 |
-
.compliance-list { margin:0; padding-left:20px; }
|
| 386 |
-
.compliance-list li { margin:8px 0; }
|
| 387 |
-
.compliance-list .code { font-weight:700; }
|
| 388 |
-
.affected-prims { font-size:12px; color:var(--muted); margin:8px 0; }
|
| 389 |
-
.affected-prims .label { font-weight:600; color:var(--text); }
|
| 390 |
-
.prim-list-inline { list-style:none; padding-left:0; margin:4px 0 0 0; display:flex; flex-wrap:wrap; gap:6px 12px; }
|
| 391 |
-
.prim-list-inline li { font-family: ui-monospace, monospace; font-size:11px; word-break:break-all; }
|
| 392 |
-
.muted { color:var(--muted); }
|
| 393 |
-
.no-spec { color:var(--muted); font-style:italic; font-size:11px; }
|
| 394 |
-
.packaging-note { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 395 |
-
.packaging-note code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:12px; }
|
| 396 |
-
.coverage-banner { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 397 |
-
.coverage-banner code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:12px; }
|
| 398 |
-
.coverage-banner details { margin-top:8px; }
|
| 399 |
-
.coverage-banner summary { cursor:pointer; font-weight:600; }
|
| 400 |
-
.coverage-banner ul { margin:6px 0 0 0; padding-left:20px; }
|
| 401 |
-
.outside-spec-banner { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 402 |
-
.outside-spec-banner code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:12px; }
|
| 403 |
-
.thumbnail-banner { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 404 |
-
.thumbnail-banner code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:11px; word-break:break-all; }
|
| 405 |
-
.thumbnail-banner details { margin-top:8px; }
|
| 406 |
-
.thumbnail-banner summary { cursor:pointer; font-weight:600; }
|
| 407 |
-
.thumbnail-banner ul { margin:6px 0 0 0; padding-left:20px; }
|
| 408 |
-
.header-actions { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
| 409 |
-
.caveat-toggle { padding:6px 12px; background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; color:var(--note-text); cursor:pointer; font-size:13px; font-weight:600; display:inline-flex; align-items:center; gap:6px; animation:caveat-pulse 1.6s ease-in-out infinite; }
|
| 410 |
-
.caveat-toggle:hover { filter:brightness(1.05); }
|
| 411 |
-
.caveat-toggle .caveat-count { background:var(--note-text); color:var(--note-bg); border-radius:999px; padding:1px 8px; font-size:11px; font-weight:700; line-height:1.4; min-width:18px; text-align:center; }
|
| 412 |
-
.caveat-toggle.open { animation:none; box-shadow:none; }
|
| 413 |
-
@keyframes caveat-pulse {
|
| 414 |
-
0%, 100% { box-shadow:0 0 0 0 var(--note-border); }
|
| 415 |
-
50% { box-shadow:0 0 0 6px transparent; }
|
| 416 |
-
}
|
| 417 |
-
#caveats[hidden] { display:none; }
|
| 418 |
-
.theme-toggle { padding:6px 12px; background:transparent; border:1px solid var(--border); border-radius:8px; color:var(--text); cursor:pointer; font-size:13px; }
|
| 419 |
-
.theme-toggle:hover { background:var(--bg); }
|
| 420 |
-
""".strip()
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
def write_dashboard(out_dir: Path, target: Path, profile: str, profile_version: str,
|
| 424 |
-
results: list[dict],
|
| 425 |
-
specs_docs_dir: Path | None = None,
|
| 426 |
-
dashboard_docs_dir: Path | None = None,
|
| 427 |
-
thumbnail_provenance: list[dict] | None = None,
|
| 428 |
-
profile_coverage: dict | None = None) -> Path:
|
| 429 |
-
total = len(results)
|
| 430 |
-
failed = sum(1 for r in results if not r["passed"])
|
| 431 |
-
passed_checks = sum(len(r.get("passed_features", [])) for r in results)
|
| 432 |
-
|
| 433 |
-
doc_url = _make_doc_url_resolver(out_dir, dashboard_docs_dir, specs_docs_dir)
|
| 434 |
-
# Map asset name -> relative path under <out_dir>/images/, only when actually
|
| 435 |
-
# copied. Reference relatively so the dashboard contains no external URLs.
|
| 436 |
-
thumb_rel_by_asset = {
|
| 437 |
-
e["asset"]: e["rel"]
|
| 438 |
-
for e in (thumbnail_provenance or [])
|
| 439 |
-
if e.get("present")
|
| 440 |
-
}
|
| 441 |
-
asset_blocks = "\n".join(_render_asset_block(r, doc_url, thumb_rel_by_asset, target) for r in results)
|
| 442 |
-
thumbnail_banner = _render_thumbnail_banner(thumbnail_provenance or [], target)
|
| 443 |
-
coverage_banner = _render_coverage_banner(profile_coverage)
|
| 444 |
-
outside_spec_banner = _render_outside_spec_banner(results)
|
| 445 |
-
env_blocked_banner = _render_env_blocked_banner(results)
|
| 446 |
-
compliance = _render_compliance(results, doc_url, target)
|
| 447 |
-
generated = datetime.now(timezone.utc).isoformat()
|
| 448 |
-
target_str = str(target).replace("\\", "/")
|
| 449 |
-
# Show only the packages-relative tail, never the absolute path —
|
| 450 |
-
# absolute paths leak machine-specific user dirs into shared reports.
|
| 451 |
-
if "/packages/" in target_str:
|
| 452 |
-
raw_hint = target_str.split("/packages/", 1)[1]
|
| 453 |
-
packaging_note = (
|
| 454 |
-
f'<div class="packaging-note">'
|
| 455 |
-
f'<strong>Packaged tree.</strong> This report validates assets after the '
|
| 456 |
-
f'<code>simready ingest usd</code> step. Raw inputs live at '
|
| 457 |
-
f'<code>assets_to_validate/{html.escape(raw_hint)}</code>; '
|
| 458 |
-
f'packaged output lives at <code>packages/{html.escape(raw_hint)}</code>.'
|
| 459 |
-
f'</div>'
|
| 460 |
-
)
|
| 461 |
-
else:
|
| 462 |
-
packaging_note = ""
|
| 463 |
-
|
| 464 |
-
# Group all the yellow-warning banners under a single `Caveats` toggle so
|
| 465 |
-
# the dashboard stays clean by default and the user opts in to read them.
|
| 466 |
-
caveats_list = [b for b in (env_blocked_banner, coverage_banner, outside_spec_banner, packaging_note, thumbnail_banner) if b]
|
| 467 |
-
caveats_count = len(caveats_list)
|
| 468 |
-
if caveats_count > 0:
|
| 469 |
-
caveats_button = (
|
| 470 |
-
f'<button type="button" id="caveatsToggle" class="caveat-toggle" '
|
| 471 |
-
f'aria-expanded="false" aria-controls="caveats" aria-label="Show caveats">'
|
| 472 |
-
f'<span>Caveats</span>'
|
| 473 |
-
f'<span class="caveat-count">{caveats_count}</span>'
|
| 474 |
-
f'</button>'
|
| 475 |
-
)
|
| 476 |
-
caveats_section = (
|
| 477 |
-
'<div id="caveats" hidden>' + "".join(caveats_list) + '</div>'
|
| 478 |
-
)
|
| 479 |
-
else:
|
| 480 |
-
caveats_button = ""
|
| 481 |
-
caveats_section = ""
|
| 482 |
-
|
| 483 |
-
html_doc = f"""<!doctype html>
|
| 484 |
-
<html lang="en">
|
| 485 |
-
<head>
|
| 486 |
-
<meta charset="utf-8" />
|
| 487 |
-
<title>Validation Report — {html.escape(target.name)}</title>
|
| 488 |
-
<script>(function(){{var t=null;try{{t=localStorage.getItem('vr-theme');}}catch(e){{}}if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t);}})();</script>
|
| 489 |
-
<style>{CSS}</style>
|
| 490 |
-
</head>
|
| 491 |
-
<body>
|
| 492 |
-
<header>
|
| 493 |
-
<div class="logo">V</div>
|
| 494 |
-
<div>
|
| 495 |
-
<h1>Validation Report — {html.escape(target.name)}</h1>
|
| 496 |
-
<div class="meta">Profile: <strong>{html.escape(profile)}</strong> v{html.escape(profile_version)} · Generated: {html.escape(generated)}</div>
|
| 497 |
-
</div>
|
| 498 |
-
<div class="header-actions">
|
| 499 |
-
{caveats_button}
|
| 500 |
-
<button type="button" id="themeToggle" class="theme-toggle" aria-label="Toggle theme">Dark mode</button>
|
| 501 |
-
</div>
|
| 502 |
-
</header>
|
| 503 |
-
{caveats_section}
|
| 504 |
-
<div class="summary">
|
| 505 |
-
<div class="card"><div class="label">Total assets</div><div class="value">{total}</div></div>
|
| 506 |
-
<div class="card"><div class="label">Failed assets</div><div class="value">{failed}</div></div>
|
| 507 |
-
<div class="card"><div class="label">Passed feature checks</div><div class="value">{passed_checks}</div></div>
|
| 508 |
-
</div>
|
| 509 |
-
<div class="filter"><input type="text" id="filterInput" placeholder="Filter by asset name..." /></div>
|
| 510 |
-
{asset_blocks}
|
| 511 |
-
{compliance}
|
| 512 |
-
<script>
|
| 513 |
-
(function() {{
|
| 514 |
-
var input = document.getElementById("filterInput");
|
| 515 |
-
var blocks = document.querySelectorAll(".asset-block");
|
| 516 |
-
if (input) {{
|
| 517 |
-
function filter() {{
|
| 518 |
-
var q = (input.value || "").trim().toLowerCase();
|
| 519 |
-
for (var i = 0; i < blocks.length; i++) {{
|
| 520 |
-
var name = (blocks[i].getAttribute("data-asset-name") || "");
|
| 521 |
-
blocks[i].style.display = (!q || name.indexOf(q) !== -1) ? "" : "none";
|
| 522 |
-
}}
|
| 523 |
-
}}
|
| 524 |
-
input.addEventListener("input", filter);
|
| 525 |
-
input.addEventListener("keyup", filter);
|
| 526 |
-
}}
|
| 527 |
-
var caveatsBtn = document.getElementById("caveatsToggle");
|
| 528 |
-
var caveatsBox = document.getElementById("caveats");
|
| 529 |
-
if (caveatsBtn && caveatsBox) {{
|
| 530 |
-
caveatsBtn.addEventListener("click", function() {{
|
| 531 |
-
var open = caveatsBox.hidden;
|
| 532 |
-
caveatsBox.hidden = !open;
|
| 533 |
-
caveatsBtn.setAttribute("aria-expanded", open ? "true" : "false");
|
| 534 |
-
caveatsBtn.classList.toggle("open", open);
|
| 535 |
-
}});
|
| 536 |
-
}}
|
| 537 |
-
var btn = document.getElementById("themeToggle");
|
| 538 |
-
var mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
| 539 |
-
function isDark() {{
|
| 540 |
-
var attr = document.documentElement.getAttribute("data-theme");
|
| 541 |
-
if (attr === "dark") return true;
|
| 542 |
-
if (attr === "light") return false;
|
| 543 |
-
return mq ? mq.matches : false;
|
| 544 |
-
}}
|
| 545 |
-
function syncLabel() {{
|
| 546 |
-
if (btn) btn.textContent = isDark() ? "Light mode" : "Dark mode";
|
| 547 |
-
}}
|
| 548 |
-
syncLabel();
|
| 549 |
-
if (mq && mq.addEventListener) mq.addEventListener("change", syncLabel);
|
| 550 |
-
if (btn) {{
|
| 551 |
-
btn.addEventListener("click", function() {{
|
| 552 |
-
var next = isDark() ? "light" : "dark";
|
| 553 |
-
document.documentElement.setAttribute("data-theme", next);
|
| 554 |
-
try {{ localStorage.setItem("vr-theme", next); }} catch(e) {{}}
|
| 555 |
-
syncLabel();
|
| 556 |
-
}});
|
| 557 |
-
}}
|
| 558 |
-
}})();
|
| 559 |
-
</script>
|
| 560 |
-
</body>
|
| 561 |
-
</html>
|
| 562 |
-
"""
|
| 563 |
-
out = out_dir / "index.html"
|
| 564 |
-
out.write_text(html_doc, encoding="utf-8")
|
| 565 |
-
return out
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
def _render_outside_spec_banner(results: list[dict]) -> str:
|
| 569 |
-
"""Banner summarizing how many issues the validator reported against
|
| 570 |
-
requirements that aren't part of the loaded profile's feature set.
|
| 571 |
-
These are deliberately suppressed from the per-asset blocks and the
|
| 572 |
-
compliance footer — the report scope is the chosen spec only — so we
|
| 573 |
-
surface the count here for transparency.
|
| 574 |
-
"""
|
| 575 |
-
total_issues = 0
|
| 576 |
-
unique_codes: set[str] = set()
|
| 577 |
-
affected_assets: set[str] = set()
|
| 578 |
-
for r in results:
|
| 579 |
-
if not r.get("extra_failures"):
|
| 580 |
-
continue
|
| 581 |
-
affected_assets.add(r["name"])
|
| 582 |
-
for e in r["extra_failures"]:
|
| 583 |
-
total_issues += int(e.get("count") or 0)
|
| 584 |
-
if e.get("code"):
|
| 585 |
-
unique_codes.add(e["code"])
|
| 586 |
-
if total_issues == 0:
|
| 587 |
-
return ""
|
| 588 |
-
return f'''
|
| 589 |
-
<div class="outside-spec-banner">
|
| 590 |
-
<strong>Out-of-spec issues hidden.</strong> The validator reported
|
| 591 |
-
<strong>{total_issues}</strong> issue(s) against
|
| 592 |
-
<strong>{len(unique_codes)}</strong> requirement code(s) that aren\'t
|
| 593 |
-
part of this profile\'s feature set, across
|
| 594 |
-
<strong>{len(affected_assets)}</strong> asset(s). These are not listed
|
| 595 |
-
in the report — the displayed pass/fail is scoped to the loaded profile
|
| 596 |
-
only. See <code>PROBLEMS.md</code> for why this filter exists.
|
| 597 |
-
</div>
|
| 598 |
-
'''
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
def _render_coverage_banner(coverage: dict | None) -> str:
|
| 602 |
-
"""Banner shown when the loaded profile has fewer features than its
|
| 603 |
-
`profiles.toml` entry declares. The validator silently drops feature
|
| 604 |
-
entries whose ID + version aren't in the registry, which can leave a
|
| 605 |
-
profile reporting against a tiny subset of what it claims to check.
|
| 606 |
-
See PROBLEMS.md P1.
|
| 607 |
-
"""
|
| 608 |
-
if not coverage or coverage.get("declared") is None:
|
| 609 |
-
return ""
|
| 610 |
-
missing = coverage.get("missing") or []
|
| 611 |
-
if not missing:
|
| 612 |
-
return ""
|
| 613 |
-
declared = coverage["declared"]
|
| 614 |
-
loaded = coverage["loaded"]
|
| 615 |
-
missing_items = "".join(
|
| 616 |
-
f'<li><code>{html.escape(m["id"])}</code> '
|
| 617 |
-
f'<span class="muted">v{html.escape(m["version"])}</span></li>'
|
| 618 |
-
for m in missing
|
| 619 |
-
)
|
| 620 |
-
return f'''
|
| 621 |
-
<div class="coverage-banner">
|
| 622 |
-
<strong>Coverage gap.</strong> This profile declares <strong>{declared}</strong> features
|
| 623 |
-
in <code>profiles.toml</code> but the validator loaded only <strong>{loaded}</strong>.
|
| 624 |
-
The remaining {len(missing)} were silently dropped because their ID + version
|
| 625 |
-
isn\'t registered in the foundation specs. The pass/fail numbers below reflect
|
| 626 |
-
only the loaded set — the dropped features were <em>not</em> checked.
|
| 627 |
-
See <code>PROBLEMS.md</code> P1 for root cause and fix path.
|
| 628 |
-
<details><summary>Dropped feature IDs ({len(missing)})</summary>
|
| 629 |
-
<ul>{missing_items}</ul></details>
|
| 630 |
-
</div>
|
| 631 |
-
'''
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
def _render_env_blocked_banner(results: list[dict]) -> str:
|
| 635 |
-
"""Banner shown when validate.py's P2 filter dropped env-blocked issues.
|
| 636 |
-
|
| 637 |
-
Aggregates per-asset env_blocked counts so the user sees both that a
|
| 638 |
-
suppression happened and roughly how much was suppressed. See PROBLEMS.md P2.
|
| 639 |
-
"""
|
| 640 |
-
physx_total = 0
|
| 641 |
-
mdl_total = 0
|
| 642 |
-
for r in results:
|
| 643 |
-
eb = r.get("env_blocked") or {}
|
| 644 |
-
physx_total += int(eb.get("physxschema_unavailable") or 0)
|
| 645 |
-
mdl_total += int(eb.get("omnipbr_unresolved") or 0)
|
| 646 |
-
if not (physx_total or mdl_total):
|
| 647 |
-
return ""
|
| 648 |
-
parts = []
|
| 649 |
-
if physx_total:
|
| 650 |
-
parts.append(
|
| 651 |
-
f'<strong>{physx_total}</strong> PhysX-rule errors '
|
| 652 |
-
f'(<code>PhysxSchema is not available in this environment</code>)'
|
| 653 |
-
)
|
| 654 |
-
if mdl_total:
|
| 655 |
-
parts.append(
|
| 656 |
-
f'<strong>{mdl_total}</strong> stock MDL-reference failures '
|
| 657 |
-
f'(<code>OmniPBR.mdl</code> not resolvable outside Kit)'
|
| 658 |
-
)
|
| 659 |
-
return f'''
|
| 660 |
-
<div class="coverage-banner">
|
| 661 |
-
<strong>Environment-blocked checks suppressed.</strong>
|
| 662 |
-
This run dropped {" and ".join(parts)} because the active Python venv has
|
| 663 |
-
<code>usd-core</code> but not the Kit-bundled <code>PhysxSchema</code> /
|
| 664 |
-
Kit MDL search path. These checks couldn\'t produce useful results here,
|
| 665 |
-
so they would otherwise have inflated the issue count without telling you
|
| 666 |
-
anything about the asset. Pass/fail below reflects only checks that ran
|
| 667 |
-
meaningfully. See <code>PROBLEMS.md</code> P2 for root cause and fix path.
|
| 668 |
-
</div>
|
| 669 |
-
'''
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
def _render_thumbnail_banner(provenance: list[dict], target: Path | None = None) -> str:
|
| 673 |
-
"""Banner explaining where the asset thumbnails came from (since they
|
| 674 |
-
weren't produced by this run's packaging step)."""
|
| 675 |
-
present = [p for p in provenance if p.get("present")]
|
| 676 |
-
if not present:
|
| 677 |
-
return ""
|
| 678 |
-
items = "".join(
|
| 679 |
-
f'<li><code>{html.escape(p["rel"])}</code> — copied from <code>{html.escape(_relativize_msg(p.get("copied_from",""), target))}</code></li>'
|
| 680 |
-
for p in present
|
| 681 |
-
)
|
| 682 |
-
return f'''
|
| 683 |
-
<div class="thumbnail-banner">
|
| 684 |
-
<strong>Asset thumbnails — sourced externally.</strong>
|
| 685 |
-
These tiles were copied into this report\'s <code>images/</code> directory from
|
| 686 |
-
per-asset spec paths (<code><asset_dir>/.thumbs/256x256/<filename>.png</code>) populated
|
| 687 |
-
earlier from NVIDIA\'s Isaac 6.0 content S3 bucket
|
| 688 |
-
(<code>Assets/Isaac/6.0/Isaac/Robots/Yaskawa/Motoman Next/...</code>).
|
| 689 |
-
They are <strong>not</strong> the output of this run\'s packaging step;
|
| 690 |
-
the customer\'s pipeline should generate authoritative thumbnails before the
|
| 691 |
-
validation reports them as spec-compliant.
|
| 692 |
-
<details><summary>Per-tile source ({len(present)} of {len(provenance)})</summary>
|
| 693 |
-
<ul>{items}</ul></details>
|
| 694 |
-
</div>
|
| 695 |
-
'''
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
def _render_asset_block(r: dict, doc_url, thumb_rel_by_asset: dict | None = None,
|
| 699 |
-
target: Path | None = None) -> str:
|
| 700 |
-
name = r["name"]
|
| 701 |
-
rel = r.get("rel_path") or r.get("path", "")
|
| 702 |
-
affected = r.get("affected_prims", [])
|
| 703 |
-
failed = r.get("failed_features", [])
|
| 704 |
-
passed = r.get("passed_features", [])
|
| 705 |
-
|
| 706 |
-
if affected:
|
| 707 |
-
prims_html = (
|
| 708 |
-
'<div class="affected-prims"><span class="label">Affected prims:</span>'
|
| 709 |
-
'<ul class="prim-list-inline">'
|
| 710 |
-
+ "".join(f"<li>{html.escape(p)}</li>" for p in affected)
|
| 711 |
-
+ "</ul></div>"
|
| 712 |
-
)
|
| 713 |
-
else:
|
| 714 |
-
prims_html = ""
|
| 715 |
-
|
| 716 |
-
def _feature_link(rel_path: str | None) -> str:
|
| 717 |
-
"""Inline 'View feature doc.' link — empty string when no doc available."""
|
| 718 |
-
url = doc_url(rel_path)
|
| 719 |
-
if not url:
|
| 720 |
-
return ''
|
| 721 |
-
return f' <a class="docref" href="{html.escape(url)}" target="_blank" rel="noopener">View feature doc.</a>'
|
| 722 |
-
|
| 723 |
-
def _capability_link(rel_path: str | None) -> str:
|
| 724 |
-
"""Inline 'View capability doc' link — empty string when no doc available."""
|
| 725 |
-
url = doc_url(rel_path)
|
| 726 |
-
if not url:
|
| 727 |
-
return ''
|
| 728 |
-
return f' <a class="docref" href="{html.escape(url)}" target="_blank" rel="noopener">View capability doc</a>'
|
| 729 |
-
|
| 730 |
-
if failed:
|
| 731 |
-
items = []
|
| 732 |
-
for f in failed:
|
| 733 |
-
req_paths = f.get("requirement_paths") or {}
|
| 734 |
-
feat_link = _feature_link(f.get("path"))
|
| 735 |
-
codes_parts = []
|
| 736 |
-
for c in f.get("failed_codes", []):
|
| 737 |
-
codes_parts.append(
|
| 738 |
-
f'<span class="code">{html.escape(c)}</span>'
|
| 739 |
-
f'{_capability_link(req_paths.get(c))}'
|
| 740 |
-
)
|
| 741 |
-
items.append(
|
| 742 |
-
f'<li><strong class="feat-id">{html.escape(f["id"])}</strong>'
|
| 743 |
-
f'{feat_link} '
|
| 744 |
-
+ " ".join(codes_parts)
|
| 745 |
-
+ '</li>'
|
| 746 |
-
)
|
| 747 |
-
failed_html = "<ul>" + "".join(items) + "</ul>"
|
| 748 |
-
else:
|
| 749 |
-
failed_html = '<p class="muted" style="margin:0">None</p>'
|
| 750 |
-
|
| 751 |
-
if passed:
|
| 752 |
-
# Passed features render as plain IDs — no doc link, matching the
|
| 753 |
-
# reference dashboard. Users only need quick access to docs when
|
| 754 |
-
# something failed.
|
| 755 |
-
items = [
|
| 756 |
-
f'<li><strong class="feat-id">{html.escape(f["id"])}</strong></li>'
|
| 757 |
-
for f in passed
|
| 758 |
-
]
|
| 759 |
-
passed_html = "<ul>" + "".join(items) + "</ul>"
|
| 760 |
-
else:
|
| 761 |
-
passed_html = '<p class="muted" style="margin:0">None</p>'
|
| 762 |
-
|
| 763 |
-
thumb_rel = (thumb_rel_by_asset or {}).get(name)
|
| 764 |
-
if thumb_rel:
|
| 765 |
-
thumb_html = (
|
| 766 |
-
f'<div class="asset-thumb-wrap">'
|
| 767 |
-
f'<img class="asset-thumb" src="{html.escape(thumb_rel)}" alt="{html.escape(name)} thumbnail" />'
|
| 768 |
-
f'</div>'
|
| 769 |
-
)
|
| 770 |
-
else:
|
| 771 |
-
thumb_html = (
|
| 772 |
-
'<div class="asset-thumb-wrap">'
|
| 773 |
-
'<div class="asset-thumb asset-thumb-missing" aria-label="no thumbnail">'
|
| 774 |
-
'<span>missing image</span>'
|
| 775 |
-
'</div></div>'
|
| 776 |
-
)
|
| 777 |
-
|
| 778 |
-
# `extra_failures` (issues against requirements outside the loaded
|
| 779 |
-
# profile) are intentionally NOT rendered per-asset. They get summarized
|
| 780 |
-
# as a count in the Caveats panel; the report scope is the chosen
|
| 781 |
-
# profile only.
|
| 782 |
-
extras_html = ""
|
| 783 |
-
|
| 784 |
-
return f"""
|
| 785 |
-
<div class="asset-block" data-asset-name="{html.escape(name.lower())}">
|
| 786 |
-
{thumb_html}
|
| 787 |
-
<div class="asset-body">
|
| 788 |
-
<h2>{html.escape(name)}</h2>
|
| 789 |
-
<div class="path">{html.escape(rel)}</div>
|
| 790 |
-
{prims_html}
|
| 791 |
-
<section class="failed-features">
|
| 792 |
-
<h3>Failed features</h3>
|
| 793 |
-
{failed_html}
|
| 794 |
-
</section>
|
| 795 |
-
<section class="passed-features">
|
| 796 |
-
<h3>Passed features</h3>
|
| 797 |
-
{passed_html}
|
| 798 |
-
</section>{extras_html}
|
| 799 |
-
</div>
|
| 800 |
-
</div>"""
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
def _render_compliance(results: list[dict], doc_url=None, target: Path | None = None) -> str:
|
| 804 |
-
"""Footer listing every in-profile requirement code that failed, with its
|
| 805 |
-
short message and (when available) a link to the foundation spec doc
|
| 806 |
-
that defines it. Codes outside the loaded profile are NOT listed here —
|
| 807 |
-
the report scope is the chosen spec only; out-of-spec failure counts
|
| 808 |
-
surface in the Caveats panel instead. Codes without a `path` get a
|
| 809 |
-
"(no spec source)" tag so the accountability gap is visible."""
|
| 810 |
-
# Build the set of in-profile failure codes from `failed_features`.
|
| 811 |
-
# Anything not in this set is an "extra" outside the loaded profile and
|
| 812 |
-
# is intentionally suppressed from the per-asset and footer views.
|
| 813 |
-
profile_failure_codes: set[str] = set()
|
| 814 |
-
for r in results:
|
| 815 |
-
for ff in r.get("failed_features", []):
|
| 816 |
-
profile_failure_codes.update(ff.get("failed_codes", []))
|
| 817 |
-
code_to_entry: dict[str, dict] = {}
|
| 818 |
-
for r in results:
|
| 819 |
-
for issue in r.get("issues", []):
|
| 820 |
-
if issue["severity"] not in ("failure", "error"):
|
| 821 |
-
continue
|
| 822 |
-
code = issue.get("code") or "UNKNOWN"
|
| 823 |
-
if code not in profile_failure_codes:
|
| 824 |
-
continue
|
| 825 |
-
if code not in code_to_entry:
|
| 826 |
-
code_to_entry[code] = {
|
| 827 |
-
"msg": issue.get("msg", ""),
|
| 828 |
-
"path": issue.get("path"),
|
| 829 |
-
}
|
| 830 |
-
if not code_to_entry:
|
| 831 |
-
return ""
|
| 832 |
-
|
| 833 |
-
items_html_parts: list[str] = []
|
| 834 |
-
accounted = 0
|
| 835 |
-
for code, e in sorted(code_to_entry.items()):
|
| 836 |
-
url = doc_url(e["path"]) if doc_url else None
|
| 837 |
-
if url:
|
| 838 |
-
code_html = f'<a class="code" href="{html.escape(url)}" target="_blank" rel="noopener">{html.escape(code)}</a>'
|
| 839 |
-
accounted += 1
|
| 840 |
-
no_spec_tag = ""
|
| 841 |
-
else:
|
| 842 |
-
code_html = f'<span class="code">{html.escape(code)}</span>'
|
| 843 |
-
no_spec_tag = ' <span class="no-spec">(no spec source)</span>'
|
| 844 |
-
msg = _relativize_msg(e["msg"], target)
|
| 845 |
-
items_html_parts.append(
|
| 846 |
-
f'<li>{code_html}: {html.escape(msg) if msg else "No guidance available."}{no_spec_tag}</li>'
|
| 847 |
-
)
|
| 848 |
-
items_html = "".join(items_html_parts)
|
| 849 |
-
coverage = (
|
| 850 |
-
f'<p class="muted">{accounted} of {len(code_to_entry)} requirement '
|
| 851 |
-
f'code(s) link to a foundation spec source. The rest are flagged '
|
| 852 |
-
f'<em>(no spec source)</em>.</p>'
|
| 853 |
-
)
|
| 854 |
-
return f"""
|
| 855 |
-
<section class="compliance">
|
| 856 |
-
<h2>How to comply with failed requirements</h2>
|
| 857 |
-
<p class="muted">Each requirement code below is shown with the first message recorded for it.</p>
|
| 858 |
-
{coverage}
|
| 859 |
-
<ul class="compliance-list">{items_html}</ul>
|
| 860 |
-
</section>"""
|
|
|
|
| 1 |
+
"""Dashboard HTML generator for the /simready-report skill.
|
| 2 |
+
|
| 3 |
+
Given the per-asset results produced by validate.py, emits index.html in the
|
| 4 |
+
style of usd_validation_dashboard_final/: summary cards, asset filter,
|
| 5 |
+
per-asset blocks (failed/passed features, affected prims), compliance footer.
|
| 6 |
+
|
| 7 |
+
Thumbnails are embedded inline (base64 data URI) from the spec path
|
| 8 |
+
<asset_dir>/.thumbs/256x256/<filename>.png — matching the ThumbnailExists rule.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import html
|
| 13 |
+
from datetime import datetime, timezone
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _ensure_static_copied(out_docs_dir: Path, dashboard_docs_dir: Path | None) -> bool:
|
| 18 |
+
"""Mirror the reference dashboard's `_static/` into `<out_docs_dir>/_static/` once.
|
| 19 |
+
|
| 20 |
+
Returns True if the static tree is available (so pages can link to it),
|
| 21 |
+
False if the dashboard isn't present or the copy failed.
|
| 22 |
+
"""
|
| 23 |
+
if dashboard_docs_dir is None:
|
| 24 |
+
return False
|
| 25 |
+
src = dashboard_docs_dir / "_static"
|
| 26 |
+
if not src.is_dir():
|
| 27 |
+
return False
|
| 28 |
+
dest = out_docs_dir / "_static"
|
| 29 |
+
if dest.exists():
|
| 30 |
+
return True
|
| 31 |
+
import shutil
|
| 32 |
+
try:
|
| 33 |
+
shutil.copytree(src, dest)
|
| 34 |
+
return True
|
| 35 |
+
except Exception:
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _render_md_page(title: str, rel_path: str, source_path: Path, body_html: str,
|
| 40 |
+
has_static: bool) -> str:
|
| 41 |
+
"""Render a markdown page using the reference dashboard's Sphinx assets.
|
| 42 |
+
|
| 43 |
+
`rel_path` is the doc's path relative to `out_docs_dir` (e.g.
|
| 44 |
+
`features/FET_001-minimal.html`); we compute how many `../` are needed to
|
| 45 |
+
reach `_static/` based on its depth.
|
| 46 |
+
"""
|
| 47 |
+
depth = rel_path.replace("\\", "/").count("/")
|
| 48 |
+
static_prefix = "../" * depth + "_static/"
|
| 49 |
+
if has_static:
|
| 50 |
+
head_links = f"""
|
| 51 |
+
<link rel="stylesheet" href="{static_prefix}styles/theme.css" />
|
| 52 |
+
<link rel="stylesheet" href="{static_prefix}styles/bootstrap.css" />
|
| 53 |
+
<link rel="stylesheet" href="{static_prefix}styles/pydata-sphinx-theme.css" />
|
| 54 |
+
<link rel="stylesheet" href="{static_prefix}styles/nvidia-sphinx-theme.css" />
|
| 55 |
+
<link rel="stylesheet" href="{static_prefix}pygments.css" />
|
| 56 |
+
<link rel="stylesheet" href="{static_prefix}_style.css" />
|
| 57 |
+
<link rel="stylesheet" href="{static_prefix}custom.css" />
|
| 58 |
+
<link rel="stylesheet" href="{static_prefix}sphinx-design.min.css" />
|
| 59 |
+
<link rel="stylesheet" href="{static_prefix}vendor/fontawesome/6.5.2/css/all.min.css" />
|
| 60 |
+
"""
|
| 61 |
+
body_open = '<main class="bd-main"><div class="bd-content"><div class="bd-article-container"><article class="bd-article">'
|
| 62 |
+
body_close = '</article></div></div></main>'
|
| 63 |
+
else:
|
| 64 |
+
head_links = ""
|
| 65 |
+
body_open = '<article style="max-width:880px;margin:24px auto;padding:24px 28px;font-family:Segoe UI,Arial,sans-serif;line-height:1.55;">'
|
| 66 |
+
body_close = '</article>'
|
| 67 |
+
|
| 68 |
+
# `source_path` is an absolute machine-specific path inside the foundations
|
| 69 |
+
# checkout — don't render it. Show the source's path relative to the
|
| 70 |
+
# foundation docs root (i.e. `rel_path` minus the .html for the .md form),
|
| 71 |
+
# which is portable across machines that share the same foundations layout.
|
| 72 |
+
source_md_rel = (rel_path[:-5] + ".md") if rel_path.endswith(".html") else rel_path
|
| 73 |
+
return f"""<!DOCTYPE html>
|
| 74 |
+
<html lang="en" data-content_root="{'../' * depth}">
|
| 75 |
+
<head>
|
| 76 |
+
<meta charset="utf-8" />
|
| 77 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 78 |
+
<title>{html.escape(title)}</title>
|
| 79 |
+
<script>document.documentElement.dataset.mode = localStorage.getItem("mode") || "";document.documentElement.dataset.theme = localStorage.getItem("theme") || "";</script>{head_links}
|
| 80 |
+
</head>
|
| 81 |
+
<body data-bs-spy="scroll">
|
| 82 |
+
{body_open}
|
| 83 |
+
<p class="text-muted" style="font-size:12px;margin-bottom:14px;">Source: <code>nv_core/sr_specs/docs/{html.escape(source_md_rel)}</code></p>
|
| 84 |
+
{body_html}
|
| 85 |
+
{body_close}
|
| 86 |
+
</body>
|
| 87 |
+
</html>
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
_NO_SPEC_TAG = ' <span class="no-spec">(no spec source)</span>'
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _relativize_msg(msg: str, target: Path | None) -> str:
|
| 95 |
+
"""Strip machine-specific absolute path prefixes from validator messages.
|
| 96 |
+
|
| 97 |
+
`omni.asset_validator` rule messages embed the absolute path of each USD
|
| 98 |
+
being inspected. We replace the target's path prefix (and the path up to
|
| 99 |
+
`packages/` / `assets_to_validate/`) with relative tokens so reports
|
| 100 |
+
don't leak the report-author's user dir.
|
| 101 |
+
"""
|
| 102 |
+
if not msg or target is None:
|
| 103 |
+
return msg
|
| 104 |
+
out = msg
|
| 105 |
+
target_fwd = str(target).replace("\\", "/")
|
| 106 |
+
target_bwd = str(target).replace("/", "\\")
|
| 107 |
+
for prefix in (target_fwd, target_bwd):
|
| 108 |
+
if prefix and prefix in out:
|
| 109 |
+
out = out.replace(prefix, "<target>")
|
| 110 |
+
# Catch absolute paths that point at the workspace's packages/ or
|
| 111 |
+
# assets_to_validate/ — same machine-specificity, different anchor.
|
| 112 |
+
for marker in ("/packages/", "\\packages\\", "/assets_to_validate/", "\\assets_to_validate\\"):
|
| 113 |
+
idx = out.find(marker)
|
| 114 |
+
while idx != -1:
|
| 115 |
+
# Find the start of the absolute path containing this marker
|
| 116 |
+
# (a drive letter "X:\" or POSIX "/" boundary going left).
|
| 117 |
+
start = idx
|
| 118 |
+
while start > 0 and out[start - 1] not in (" ", "<", "(", "[", '"', "'", "\n", "\t"):
|
| 119 |
+
start -= 1
|
| 120 |
+
# Replace from `start` through the marker with the marker's tail.
|
| 121 |
+
anchor = marker.strip("/\\").replace("\\", "/") # "packages" / "assets_to_validate"
|
| 122 |
+
tail_pos = idx + len(marker)
|
| 123 |
+
out = out[:start] + f"<{anchor}>/" + out[tail_pos:]
|
| 124 |
+
idx = out.find(marker, start + len(anchor) + 3)
|
| 125 |
+
return out
|
| 126 |
+
|
| 127 |
+
_md_renderer = None
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _md_to_html(md_text: str) -> str:
|
| 131 |
+
global _md_renderer
|
| 132 |
+
if _md_renderer is None:
|
| 133 |
+
from markdown_it import MarkdownIt
|
| 134 |
+
# html:True passes raw HTML through unescaped — safe only because the
|
| 135 |
+
# markdown comes from the trusted foundations repo, not user input.
|
| 136 |
+
_md_renderer = MarkdownIt("commonmark", {"html": True, "linkify": True, "typographer": True}).enable("table")
|
| 137 |
+
return _md_renderer.render(md_text)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
_IMG_PLACEHOLDER_SVG = (
|
| 141 |
+
"data:image/svg+xml;utf8,"
|
| 142 |
+
"<svg xmlns='http://www.w3.org/2000/svg' width='320' height='180' viewBox='0 0 320 180'>"
|
| 143 |
+
"<rect width='320' height='180' fill='%23f0f2f8' stroke='%23c7cee0' stroke-dasharray='6 4' stroke-width='2'/>"
|
| 144 |
+
"<text x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' "
|
| 145 |
+
"font-family='Segoe UI,Arial,sans-serif' font-size='14' fill='%237280a7'>image missing</text>"
|
| 146 |
+
"</svg>"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def _localize_images(body_html: str, md_source: Path, out_html_path: Path) -> str:
|
| 151 |
+
"""Walk every `<img src="...">` in `body_html`. If src is a relative path,
|
| 152 |
+
copy the source file (relative to `md_source.parent`) into the equivalent
|
| 153 |
+
location next to `out_html_path`. When the source file is missing, replace
|
| 154 |
+
the src with an inline SVG placeholder so the page never 404s.
|
| 155 |
+
"""
|
| 156 |
+
import re
|
| 157 |
+
import shutil
|
| 158 |
+
|
| 159 |
+
md_dir = md_source.parent
|
| 160 |
+
out_dir = out_html_path.parent
|
| 161 |
+
|
| 162 |
+
def repl(match):
|
| 163 |
+
full_tag = match.group(0)
|
| 164 |
+
src_match = re.search(r'src="([^"]+)"', full_tag)
|
| 165 |
+
if not src_match:
|
| 166 |
+
return full_tag
|
| 167 |
+
src = src_match.group(1)
|
| 168 |
+
if src.startswith(("http://", "https://", "data:", "//", "/")):
|
| 169 |
+
return full_tag # absolute / data URI / protocol-relative — leave alone
|
| 170 |
+
rel = src.lstrip("./").replace("\\", "/")
|
| 171 |
+
source_file = (md_dir / src).resolve()
|
| 172 |
+
# Containment guard on the SOURCE: a crafted ../ src must not read
|
| 173 |
+
# files outside the markdown tree. Escape -> skip to placeholder.
|
| 174 |
+
try:
|
| 175 |
+
source_file.relative_to(md_dir.resolve())
|
| 176 |
+
except ValueError:
|
| 177 |
+
import sys
|
| 178 |
+
print(f"[report] skipping out-of-tree image src: {src!r}", file=sys.stderr)
|
| 179 |
+
return re.sub(r'src="[^"]+"', f'src="{_IMG_PLACEHOLDER_SVG}"', full_tag)
|
| 180 |
+
if source_file.is_file():
|
| 181 |
+
dest_file = (out_dir / rel).resolve()
|
| 182 |
+
try:
|
| 183 |
+
dest_file.relative_to(out_dir.resolve()) # guard against ../ escape
|
| 184 |
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
| 185 |
+
if not dest_file.exists() or dest_file.stat().st_size != source_file.stat().st_size:
|
| 186 |
+
shutil.copy2(source_file, dest_file)
|
| 187 |
+
return full_tag
|
| 188 |
+
except (ValueError, OSError):
|
| 189 |
+
pass
|
| 190 |
+
# Missing source — swap to placeholder
|
| 191 |
+
return re.sub(r'src="[^"]+"', f'src="{_IMG_PLACEHOLDER_SVG}"', full_tag)
|
| 192 |
+
|
| 193 |
+
return re.sub(r"<img\b[^>]*>", repl, body_html)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
_A_TAG_RE = None
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _localize_links(body_html: str, md_source: Path, out_html_path: Path,
|
| 200 |
+
specs_docs_dir: Path, out_docs_root: Path,
|
| 201 |
+
resolver) -> str:
|
| 202 |
+
"""Rewrite relative <a href="..."> targets so the docs tree is self-contained.
|
| 203 |
+
|
| 204 |
+
- .md targets that resolve to a file inside `specs_docs_dir`: rewrite href
|
| 205 |
+
to .html and recursively render via `resolver(<rel>.html)`.
|
| 206 |
+
- non-.md targets inside `specs_docs_dir`: copy verbatim into the mirrored
|
| 207 |
+
location under `out_docs_root` and rewrite href to that copy.
|
| 208 |
+
- root-relative ("/...") hrefs treated as relative to `specs_docs_dir`;
|
| 209 |
+
bare paths without an extension are tried as `.md`.
|
| 210 |
+
- links pointing outside `specs_docs_dir` or at non-existent targets are
|
| 211 |
+
replaced with <span class="docref-disabled" title="...">TEXT</span> so
|
| 212 |
+
the visible text remains but no href can 404.
|
| 213 |
+
"""
|
| 214 |
+
import os
|
| 215 |
+
import re
|
| 216 |
+
import shutil
|
| 217 |
+
|
| 218 |
+
global _A_TAG_RE
|
| 219 |
+
if _A_TAG_RE is None:
|
| 220 |
+
_A_TAG_RE = re.compile(r'<a\s+([^>]*?)>(.*?)</a>', re.DOTALL | re.IGNORECASE)
|
| 221 |
+
|
| 222 |
+
md_dir = md_source.parent
|
| 223 |
+
out_dir = out_html_path.parent
|
| 224 |
+
specs_root = specs_docs_dir.resolve()
|
| 225 |
+
out_docs_resolved = out_docs_root.resolve()
|
| 226 |
+
|
| 227 |
+
def _disable(inner: str, why: str = "link target not available in report") -> str:
|
| 228 |
+
return f'<span class="docref-disabled" title="{html.escape(why)}">{inner}</span>'
|
| 229 |
+
|
| 230 |
+
def _resolve_target(href_path: str) -> Path | None:
|
| 231 |
+
if href_path.startswith("/"):
|
| 232 |
+
base = specs_root / href_path.lstrip("/")
|
| 233 |
+
if not base.suffix:
|
| 234 |
+
base = base.with_suffix(".md")
|
| 235 |
+
return base
|
| 236 |
+
return (md_dir / href_path)
|
| 237 |
+
|
| 238 |
+
def repl(m: "re.Match[str]") -> str:
|
| 239 |
+
attrs = m.group(1)
|
| 240 |
+
inner = m.group(2)
|
| 241 |
+
href_m = re.search(r'href\s*=\s*"([^"]*)"', attrs)
|
| 242 |
+
if not href_m:
|
| 243 |
+
return m.group(0)
|
| 244 |
+
href = href_m.group(1).strip()
|
| 245 |
+
if not href:
|
| 246 |
+
return m.group(0)
|
| 247 |
+
if href.startswith(("http://", "https://", "data:", "mailto:", "//", "#", "javascript:")):
|
| 248 |
+
return m.group(0)
|
| 249 |
+
|
| 250 |
+
if "#" in href:
|
| 251 |
+
path_part, frag = href.split("#", 1)
|
| 252 |
+
frag = "#" + frag
|
| 253 |
+
else:
|
| 254 |
+
path_part, frag = href, ""
|
| 255 |
+
if not path_part:
|
| 256 |
+
return m.group(0)
|
| 257 |
+
|
| 258 |
+
target = _resolve_target(path_part)
|
| 259 |
+
if target is None:
|
| 260 |
+
return _disable(inner)
|
| 261 |
+
try:
|
| 262 |
+
target = target.resolve()
|
| 263 |
+
except OSError:
|
| 264 |
+
return _disable(inner)
|
| 265 |
+
try:
|
| 266 |
+
rel_to_specs = target.relative_to(specs_root)
|
| 267 |
+
except ValueError:
|
| 268 |
+
return _disable(inner, "link target outside foundation docs")
|
| 269 |
+
if not target.exists():
|
| 270 |
+
return _disable(inner, "link target missing in foundation docs")
|
| 271 |
+
|
| 272 |
+
rel_str = str(rel_to_specs).replace("\\", "/")
|
| 273 |
+
|
| 274 |
+
if target.suffix.lower() == ".md":
|
| 275 |
+
html_rel = rel_str[:-3] + ".html"
|
| 276 |
+
url = resolver(html_rel)
|
| 277 |
+
if url is None:
|
| 278 |
+
return _disable(inner)
|
| 279 |
+
target_out = out_docs_resolved / html_rel
|
| 280 |
+
else:
|
| 281 |
+
target_out = out_docs_resolved / rel_str
|
| 282 |
+
try:
|
| 283 |
+
target_out.parent.mkdir(parents=True, exist_ok=True)
|
| 284 |
+
if not target_out.exists() or target_out.stat().st_size != target.stat().st_size:
|
| 285 |
+
shutil.copy2(target, target_out)
|
| 286 |
+
except OSError:
|
| 287 |
+
return _disable(inner, "could not copy link target into report")
|
| 288 |
+
|
| 289 |
+
try:
|
| 290 |
+
new_href = os.path.relpath(target_out, out_dir).replace("\\", "/") + frag
|
| 291 |
+
except ValueError:
|
| 292 |
+
return _disable(inner)
|
| 293 |
+
new_attrs = re.sub(r'href\s*=\s*"[^"]*"', f'href="{new_href}"', attrs, count=1)
|
| 294 |
+
return f'<a {new_attrs}>{inner}</a>'
|
| 295 |
+
|
| 296 |
+
return _A_TAG_RE.sub(repl, body_html)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _make_doc_url_resolver(out_dir: Path, dashboard_docs_dir: Path | None,
|
| 300 |
+
specs_docs_dir: Path | None):
|
| 301 |
+
"""Build a `_doc_url(rel_path)` callable.
|
| 302 |
+
|
| 303 |
+
Every resolvable doc is rendered from the foundation markdown source into
|
| 304 |
+
`<out_dir>/docs/<rel_path>` so the dashboard has a single, self-contained
|
| 305 |
+
docs tree. The reference-dashboard pre-built HTML is no longer used as a
|
| 306 |
+
link target — it would split the source of truth.
|
| 307 |
+
|
| 308 |
+
`dashboard_docs_dir` is kept on the signature for API stability but not
|
| 309 |
+
consulted here.
|
| 310 |
+
"""
|
| 311 |
+
rendered_cache: dict[str, str] = {}
|
| 312 |
+
out_docs_dir = out_dir / "docs"
|
| 313 |
+
out_docs_dir.mkdir(parents=True, exist_ok=True)
|
| 314 |
+
has_static = _ensure_static_copied(out_docs_dir, dashboard_docs_dir)
|
| 315 |
+
|
| 316 |
+
def resolve(rel_path: str | None) -> str | None:
|
| 317 |
+
if not rel_path or specs_docs_dir is None:
|
| 318 |
+
return None
|
| 319 |
+
if rel_path in rendered_cache:
|
| 320 |
+
return rendered_cache[rel_path]
|
| 321 |
+
md_rel = rel_path[:-5] + ".md" if rel_path.endswith(".html") else rel_path
|
| 322 |
+
md_source = specs_docs_dir / md_rel
|
| 323 |
+
if not md_source.is_file():
|
| 324 |
+
return None
|
| 325 |
+
# Cache the URL before rendering body so cycles in the foundation's
|
| 326 |
+
# cross-doc links (e.g. profile -> feature -> profile) terminate.
|
| 327 |
+
url = "docs/" + rel_path.replace("\\", "/")
|
| 328 |
+
rendered_cache[rel_path] = url
|
| 329 |
+
out_path = out_docs_dir / rel_path
|
| 330 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 331 |
+
body = _md_to_html(md_source.read_text(encoding="utf-8"))
|
| 332 |
+
body = _localize_images(body, md_source, out_path)
|
| 333 |
+
body = _localize_links(body, md_source, out_path, specs_docs_dir, out_docs_dir, resolve)
|
| 334 |
+
out_path.write_text(
|
| 335 |
+
_render_md_page(rel_path, rel_path, md_source, body, has_static),
|
| 336 |
+
encoding="utf-8",
|
| 337 |
+
)
|
| 338 |
+
return url
|
| 339 |
+
|
| 340 |
+
return resolve
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
CSS = """
|
| 344 |
+
:root { --bg:#f4f6fb; --panel:#fff; --text:#1f2a44; --muted:#7280a7; --border:#e6ebf5; --fail:#c00; --pass:#0a0; --link:#2b6de2; --note-bg:#fff8e6; --note-border:#f1d38a; --note-text:#6b4f00; --note-code:#fff3cc; --thumb-bg:#f4f6fb; }
|
| 345 |
+
@media (prefers-color-scheme: dark) {
|
| 346 |
+
:root:not([data-theme="light"]) { --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --fail:#ff6b6b; --pass:#4ade80; --link:#7ba9ff; --note-bg:#2a2410; --note-border:#5a4a1f; --note-text:#f1d38a; --note-code:#3a3018; --thumb-bg:#0f1320; }
|
| 347 |
+
}
|
| 348 |
+
:root[data-theme="dark"] { --bg:#0f1320; --panel:#1a2030; --text:#e6ebf5; --muted:#8a93b3; --border:#2a3142; --fail:#ff6b6b; --pass:#4ade80; --link:#7ba9ff; --note-bg:#2a2410; --note-border:#5a4a1f; --note-text:#f1d38a; --note-code:#3a3018; --thumb-bg:#0f1320; }
|
| 349 |
+
* { box-sizing: border-box; }
|
| 350 |
+
body { font-family: "Segoe UI", Arial, sans-serif; margin:0; background:var(--bg); color:var(--text); padding:16px; }
|
| 351 |
+
header { display:flex; align-items:center; gap:10px; margin-bottom:16px; padding:12px 16px; background:var(--panel); border-radius:12px; border:1px solid var(--border); }
|
| 352 |
+
header .logo { width:36px; height:36px; border-radius:10px; background:#2b6de2; color:#fff; display:grid; place-items:center; font-weight:700; }
|
| 353 |
+
header h1 { margin:0; font-size:18px; }
|
| 354 |
+
header .meta { color:var(--muted); font-size:12px; }
|
| 355 |
+
.summary { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:20px; }
|
| 356 |
+
.summary .card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; }
|
| 357 |
+
.summary .card .label { color:var(--muted); font-size:12px; }
|
| 358 |
+
.summary .card .value { font-size:20px; font-weight:700; }
|
| 359 |
+
.asset-block { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; margin-bottom:14px; display:flex; gap:14px; align-items:flex-start; }
|
| 360 |
+
.asset-block .asset-thumb-wrap { flex-shrink:0; width:140px; }
|
| 361 |
+
.asset-block .asset-thumb { width:100%; height:auto; aspect-ratio:1; object-fit:cover; border-radius:8px; background:var(--border); display:block; }
|
| 362 |
+
.asset-block .asset-thumb-missing { background:var(--thumb-bg); border:1px dashed var(--border); display:grid; place-items:center; color:var(--muted); font-size:11px; text-align:center; padding:8px; }
|
| 363 |
+
.asset-block .asset-thumb-missing span { line-height:1.3; }
|
| 364 |
+
.code { font-family: ui-monospace, monospace; font-size:12px; padding:1px 6px; border-radius:4px; background:var(--note-code); color:var(--note-text); margin-right:4px; }
|
| 365 |
+
.feat-id { font-family: ui-monospace, monospace; font-size:13px; }
|
| 366 |
+
a.docref { color:var(--link); text-decoration:underline; text-decoration-style:dotted; text-underline-offset:2px; font-size:12px; }
|
| 367 |
+
a.docref:hover { text-decoration-style:solid; }
|
| 368 |
+
a.code { text-decoration:underline; text-decoration-style:dotted; text-underline-offset:2px; }
|
| 369 |
+
a.code:hover { text-decoration-style:solid; }
|
| 370 |
+
.failed-features .feat-id { color:var(--fail); }
|
| 371 |
+
.passed-features .feat-id { color:var(--pass); }
|
| 372 |
+
.asset-block .asset-body { flex:1; min-width:0; }
|
| 373 |
+
.asset-block h2 { margin:0 0 6px 0; font-size:15px; }
|
| 374 |
+
.asset-block .path { font-size:12px; color:var(--muted); margin-bottom:10px; word-break:break-all; }
|
| 375 |
+
.asset-block section { margin-top:10px; }
|
| 376 |
+
.asset-block section h3 { margin:0 0 6px 0; font-size:13px; color:var(--muted); }
|
| 377 |
+
.asset-block ul { margin:0; padding-left:20px; }
|
| 378 |
+
.asset-block li { margin:4px 0; }
|
| 379 |
+
.failed-features { color:var(--fail); }
|
| 380 |
+
.passed-features { color:var(--pass); }
|
| 381 |
+
.filter { margin-bottom:12px; }
|
| 382 |
+
.filter input { padding:8px 12px; width:100%; max-width:320px; border:1px solid var(--border); border-radius:8px; font-size:14px; }
|
| 383 |
+
.compliance { margin-top:28px; background:var(--panel); border-radius:12px; padding:18px; border:1px solid var(--border); }
|
| 384 |
+
.compliance h2 { margin:0 0 12px 0; font-size:16px; }
|
| 385 |
+
.compliance-list { margin:0; padding-left:20px; }
|
| 386 |
+
.compliance-list li { margin:8px 0; }
|
| 387 |
+
.compliance-list .code { font-weight:700; }
|
| 388 |
+
.affected-prims { font-size:12px; color:var(--muted); margin:8px 0; }
|
| 389 |
+
.affected-prims .label { font-weight:600; color:var(--text); }
|
| 390 |
+
.prim-list-inline { list-style:none; padding-left:0; margin:4px 0 0 0; display:flex; flex-wrap:wrap; gap:6px 12px; }
|
| 391 |
+
.prim-list-inline li { font-family: ui-monospace, monospace; font-size:11px; word-break:break-all; }
|
| 392 |
+
.muted { color:var(--muted); }
|
| 393 |
+
.no-spec { color:var(--muted); font-style:italic; font-size:11px; }
|
| 394 |
+
.packaging-note { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 395 |
+
.packaging-note code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:12px; }
|
| 396 |
+
.coverage-banner { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 397 |
+
.coverage-banner code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:12px; }
|
| 398 |
+
.coverage-banner details { margin-top:8px; }
|
| 399 |
+
.coverage-banner summary { cursor:pointer; font-weight:600; }
|
| 400 |
+
.coverage-banner ul { margin:6px 0 0 0; padding-left:20px; }
|
| 401 |
+
.outside-spec-banner { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 402 |
+
.outside-spec-banner code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:12px; }
|
| 403 |
+
.thumbnail-banner { background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; padding:10px 14px; margin:0 0 16px 0; font-size:13px; color:var(--note-text); }
|
| 404 |
+
.thumbnail-banner code { background:var(--note-code); padding:1px 5px; border-radius:4px; font-size:11px; word-break:break-all; }
|
| 405 |
+
.thumbnail-banner details { margin-top:8px; }
|
| 406 |
+
.thumbnail-banner summary { cursor:pointer; font-weight:600; }
|
| 407 |
+
.thumbnail-banner ul { margin:6px 0 0 0; padding-left:20px; }
|
| 408 |
+
.header-actions { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
| 409 |
+
.caveat-toggle { padding:6px 12px; background:var(--note-bg); border:1px solid var(--note-border); border-radius:8px; color:var(--note-text); cursor:pointer; font-size:13px; font-weight:600; display:inline-flex; align-items:center; gap:6px; animation:caveat-pulse 1.6s ease-in-out infinite; }
|
| 410 |
+
.caveat-toggle:hover { filter:brightness(1.05); }
|
| 411 |
+
.caveat-toggle .caveat-count { background:var(--note-text); color:var(--note-bg); border-radius:999px; padding:1px 8px; font-size:11px; font-weight:700; line-height:1.4; min-width:18px; text-align:center; }
|
| 412 |
+
.caveat-toggle.open { animation:none; box-shadow:none; }
|
| 413 |
+
@keyframes caveat-pulse {
|
| 414 |
+
0%, 100% { box-shadow:0 0 0 0 var(--note-border); }
|
| 415 |
+
50% { box-shadow:0 0 0 6px transparent; }
|
| 416 |
+
}
|
| 417 |
+
#caveats[hidden] { display:none; }
|
| 418 |
+
.theme-toggle { padding:6px 12px; background:transparent; border:1px solid var(--border); border-radius:8px; color:var(--text); cursor:pointer; font-size:13px; }
|
| 419 |
+
.theme-toggle:hover { background:var(--bg); }
|
| 420 |
+
""".strip()
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def write_dashboard(out_dir: Path, target: Path, profile: str, profile_version: str,
|
| 424 |
+
results: list[dict],
|
| 425 |
+
specs_docs_dir: Path | None = None,
|
| 426 |
+
dashboard_docs_dir: Path | None = None,
|
| 427 |
+
thumbnail_provenance: list[dict] | None = None,
|
| 428 |
+
profile_coverage: dict | None = None) -> Path:
|
| 429 |
+
total = len(results)
|
| 430 |
+
failed = sum(1 for r in results if not r["passed"])
|
| 431 |
+
passed_checks = sum(len(r.get("passed_features", [])) for r in results)
|
| 432 |
+
|
| 433 |
+
doc_url = _make_doc_url_resolver(out_dir, dashboard_docs_dir, specs_docs_dir)
|
| 434 |
+
# Map asset name -> relative path under <out_dir>/images/, only when actually
|
| 435 |
+
# copied. Reference relatively so the dashboard contains no external URLs.
|
| 436 |
+
thumb_rel_by_asset = {
|
| 437 |
+
e["asset"]: e["rel"]
|
| 438 |
+
for e in (thumbnail_provenance or [])
|
| 439 |
+
if e.get("present")
|
| 440 |
+
}
|
| 441 |
+
asset_blocks = "\n".join(_render_asset_block(r, doc_url, thumb_rel_by_asset, target) for r in results)
|
| 442 |
+
thumbnail_banner = _render_thumbnail_banner(thumbnail_provenance or [], target)
|
| 443 |
+
coverage_banner = _render_coverage_banner(profile_coverage)
|
| 444 |
+
outside_spec_banner = _render_outside_spec_banner(results)
|
| 445 |
+
env_blocked_banner = _render_env_blocked_banner(results)
|
| 446 |
+
compliance = _render_compliance(results, doc_url, target)
|
| 447 |
+
generated = datetime.now(timezone.utc).isoformat()
|
| 448 |
+
target_str = str(target).replace("\\", "/")
|
| 449 |
+
# Show only the packages-relative tail, never the absolute path —
|
| 450 |
+
# absolute paths leak machine-specific user dirs into shared reports.
|
| 451 |
+
if "/packages/" in target_str:
|
| 452 |
+
raw_hint = target_str.split("/packages/", 1)[1]
|
| 453 |
+
packaging_note = (
|
| 454 |
+
f'<div class="packaging-note">'
|
| 455 |
+
f'<strong>Packaged tree.</strong> This report validates assets after the '
|
| 456 |
+
f'<code>simready ingest usd</code> step. Raw inputs live at '
|
| 457 |
+
f'<code>assets_to_validate/{html.escape(raw_hint)}</code>; '
|
| 458 |
+
f'packaged output lives at <code>packages/{html.escape(raw_hint)}</code>.'
|
| 459 |
+
f'</div>'
|
| 460 |
+
)
|
| 461 |
+
else:
|
| 462 |
+
packaging_note = ""
|
| 463 |
+
|
| 464 |
+
# Group all the yellow-warning banners under a single `Caveats` toggle so
|
| 465 |
+
# the dashboard stays clean by default and the user opts in to read them.
|
| 466 |
+
caveats_list = [b for b in (env_blocked_banner, coverage_banner, outside_spec_banner, packaging_note, thumbnail_banner) if b]
|
| 467 |
+
caveats_count = len(caveats_list)
|
| 468 |
+
if caveats_count > 0:
|
| 469 |
+
caveats_button = (
|
| 470 |
+
f'<button type="button" id="caveatsToggle" class="caveat-toggle" '
|
| 471 |
+
f'aria-expanded="false" aria-controls="caveats" aria-label="Show caveats">'
|
| 472 |
+
f'<span>Caveats</span>'
|
| 473 |
+
f'<span class="caveat-count">{caveats_count}</span>'
|
| 474 |
+
f'</button>'
|
| 475 |
+
)
|
| 476 |
+
caveats_section = (
|
| 477 |
+
'<div id="caveats" hidden>' + "".join(caveats_list) + '</div>'
|
| 478 |
+
)
|
| 479 |
+
else:
|
| 480 |
+
caveats_button = ""
|
| 481 |
+
caveats_section = ""
|
| 482 |
+
|
| 483 |
+
html_doc = f"""<!doctype html>
|
| 484 |
+
<html lang="en">
|
| 485 |
+
<head>
|
| 486 |
+
<meta charset="utf-8" />
|
| 487 |
+
<title>Validation Report — {html.escape(target.name)}</title>
|
| 488 |
+
<script>(function(){{var t=null;try{{t=localStorage.getItem('vr-theme');}}catch(e){{}}if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t);}})();</script>
|
| 489 |
+
<style>{CSS}</style>
|
| 490 |
+
</head>
|
| 491 |
+
<body>
|
| 492 |
+
<header>
|
| 493 |
+
<div class="logo">V</div>
|
| 494 |
+
<div>
|
| 495 |
+
<h1>Validation Report — {html.escape(target.name)}</h1>
|
| 496 |
+
<div class="meta">Profile: <strong>{html.escape(profile)}</strong> v{html.escape(profile_version)} · Generated: {html.escape(generated)}</div>
|
| 497 |
+
</div>
|
| 498 |
+
<div class="header-actions">
|
| 499 |
+
{caveats_button}
|
| 500 |
+
<button type="button" id="themeToggle" class="theme-toggle" aria-label="Toggle theme">Dark mode</button>
|
| 501 |
+
</div>
|
| 502 |
+
</header>
|
| 503 |
+
{caveats_section}
|
| 504 |
+
<div class="summary">
|
| 505 |
+
<div class="card"><div class="label">Total assets</div><div class="value">{total}</div></div>
|
| 506 |
+
<div class="card"><div class="label">Failed assets</div><div class="value">{failed}</div></div>
|
| 507 |
+
<div class="card"><div class="label">Passed feature checks</div><div class="value">{passed_checks}</div></div>
|
| 508 |
+
</div>
|
| 509 |
+
<div class="filter"><input type="text" id="filterInput" placeholder="Filter by asset name..." /></div>
|
| 510 |
+
{asset_blocks}
|
| 511 |
+
{compliance}
|
| 512 |
+
<script>
|
| 513 |
+
(function() {{
|
| 514 |
+
var input = document.getElementById("filterInput");
|
| 515 |
+
var blocks = document.querySelectorAll(".asset-block");
|
| 516 |
+
if (input) {{
|
| 517 |
+
function filter() {{
|
| 518 |
+
var q = (input.value || "").trim().toLowerCase();
|
| 519 |
+
for (var i = 0; i < blocks.length; i++) {{
|
| 520 |
+
var name = (blocks[i].getAttribute("data-asset-name") || "");
|
| 521 |
+
blocks[i].style.display = (!q || name.indexOf(q) !== -1) ? "" : "none";
|
| 522 |
+
}}
|
| 523 |
+
}}
|
| 524 |
+
input.addEventListener("input", filter);
|
| 525 |
+
input.addEventListener("keyup", filter);
|
| 526 |
+
}}
|
| 527 |
+
var caveatsBtn = document.getElementById("caveatsToggle");
|
| 528 |
+
var caveatsBox = document.getElementById("caveats");
|
| 529 |
+
if (caveatsBtn && caveatsBox) {{
|
| 530 |
+
caveatsBtn.addEventListener("click", function() {{
|
| 531 |
+
var open = caveatsBox.hidden;
|
| 532 |
+
caveatsBox.hidden = !open;
|
| 533 |
+
caveatsBtn.setAttribute("aria-expanded", open ? "true" : "false");
|
| 534 |
+
caveatsBtn.classList.toggle("open", open);
|
| 535 |
+
}});
|
| 536 |
+
}}
|
| 537 |
+
var btn = document.getElementById("themeToggle");
|
| 538 |
+
var mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
| 539 |
+
function isDark() {{
|
| 540 |
+
var attr = document.documentElement.getAttribute("data-theme");
|
| 541 |
+
if (attr === "dark") return true;
|
| 542 |
+
if (attr === "light") return false;
|
| 543 |
+
return mq ? mq.matches : false;
|
| 544 |
+
}}
|
| 545 |
+
function syncLabel() {{
|
| 546 |
+
if (btn) btn.textContent = isDark() ? "Light mode" : "Dark mode";
|
| 547 |
+
}}
|
| 548 |
+
syncLabel();
|
| 549 |
+
if (mq && mq.addEventListener) mq.addEventListener("change", syncLabel);
|
| 550 |
+
if (btn) {{
|
| 551 |
+
btn.addEventListener("click", function() {{
|
| 552 |
+
var next = isDark() ? "light" : "dark";
|
| 553 |
+
document.documentElement.setAttribute("data-theme", next);
|
| 554 |
+
try {{ localStorage.setItem("vr-theme", next); }} catch(e) {{}}
|
| 555 |
+
syncLabel();
|
| 556 |
+
}});
|
| 557 |
+
}}
|
| 558 |
+
}})();
|
| 559 |
+
</script>
|
| 560 |
+
</body>
|
| 561 |
+
</html>
|
| 562 |
+
"""
|
| 563 |
+
out = out_dir / "index.html"
|
| 564 |
+
out.write_text(html_doc, encoding="utf-8")
|
| 565 |
+
return out
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def _render_outside_spec_banner(results: list[dict]) -> str:
|
| 569 |
+
"""Banner summarizing how many issues the validator reported against
|
| 570 |
+
requirements that aren't part of the loaded profile's feature set.
|
| 571 |
+
These are deliberately suppressed from the per-asset blocks and the
|
| 572 |
+
compliance footer — the report scope is the chosen spec only — so we
|
| 573 |
+
surface the count here for transparency.
|
| 574 |
+
"""
|
| 575 |
+
total_issues = 0
|
| 576 |
+
unique_codes: set[str] = set()
|
| 577 |
+
affected_assets: set[str] = set()
|
| 578 |
+
for r in results:
|
| 579 |
+
if not r.get("extra_failures"):
|
| 580 |
+
continue
|
| 581 |
+
affected_assets.add(r["name"])
|
| 582 |
+
for e in r["extra_failures"]:
|
| 583 |
+
total_issues += int(e.get("count") or 0)
|
| 584 |
+
if e.get("code"):
|
| 585 |
+
unique_codes.add(e["code"])
|
| 586 |
+
if total_issues == 0:
|
| 587 |
+
return ""
|
| 588 |
+
return f'''
|
| 589 |
+
<div class="outside-spec-banner">
|
| 590 |
+
<strong>Out-of-spec issues hidden.</strong> The validator reported
|
| 591 |
+
<strong>{total_issues}</strong> issue(s) against
|
| 592 |
+
<strong>{len(unique_codes)}</strong> requirement code(s) that aren\'t
|
| 593 |
+
part of this profile\'s feature set, across
|
| 594 |
+
<strong>{len(affected_assets)}</strong> asset(s). These are not listed
|
| 595 |
+
in the report — the displayed pass/fail is scoped to the loaded profile
|
| 596 |
+
only. See <code>PROBLEMS.md</code> for why this filter exists.
|
| 597 |
+
</div>
|
| 598 |
+
'''
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
def _render_coverage_banner(coverage: dict | None) -> str:
|
| 602 |
+
"""Banner shown when the loaded profile has fewer features than its
|
| 603 |
+
`profiles.toml` entry declares. The validator silently drops feature
|
| 604 |
+
entries whose ID + version aren't in the registry, which can leave a
|
| 605 |
+
profile reporting against a tiny subset of what it claims to check.
|
| 606 |
+
See PROBLEMS.md P1.
|
| 607 |
+
"""
|
| 608 |
+
if not coverage or coverage.get("declared") is None:
|
| 609 |
+
return ""
|
| 610 |
+
missing = coverage.get("missing") or []
|
| 611 |
+
if not missing:
|
| 612 |
+
return ""
|
| 613 |
+
declared = coverage["declared"]
|
| 614 |
+
loaded = coverage["loaded"]
|
| 615 |
+
missing_items = "".join(
|
| 616 |
+
f'<li><code>{html.escape(m["id"])}</code> '
|
| 617 |
+
f'<span class="muted">v{html.escape(m["version"])}</span></li>'
|
| 618 |
+
for m in missing
|
| 619 |
+
)
|
| 620 |
+
return f'''
|
| 621 |
+
<div class="coverage-banner">
|
| 622 |
+
<strong>Coverage gap.</strong> This profile declares <strong>{declared}</strong> features
|
| 623 |
+
in <code>profiles.toml</code> but the validator loaded only <strong>{loaded}</strong>.
|
| 624 |
+
The remaining {len(missing)} were silently dropped because their ID + version
|
| 625 |
+
isn\'t registered in the foundation specs. The pass/fail numbers below reflect
|
| 626 |
+
only the loaded set — the dropped features were <em>not</em> checked.
|
| 627 |
+
See <code>PROBLEMS.md</code> P1 for root cause and fix path.
|
| 628 |
+
<details><summary>Dropped feature IDs ({len(missing)})</summary>
|
| 629 |
+
<ul>{missing_items}</ul></details>
|
| 630 |
+
</div>
|
| 631 |
+
'''
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
def _render_env_blocked_banner(results: list[dict]) -> str:
|
| 635 |
+
"""Banner shown when validate.py's P2 filter dropped env-blocked issues.
|
| 636 |
+
|
| 637 |
+
Aggregates per-asset env_blocked counts so the user sees both that a
|
| 638 |
+
suppression happened and roughly how much was suppressed. See PROBLEMS.md P2.
|
| 639 |
+
"""
|
| 640 |
+
physx_total = 0
|
| 641 |
+
mdl_total = 0
|
| 642 |
+
for r in results:
|
| 643 |
+
eb = r.get("env_blocked") or {}
|
| 644 |
+
physx_total += int(eb.get("physxschema_unavailable") or 0)
|
| 645 |
+
mdl_total += int(eb.get("omnipbr_unresolved") or 0)
|
| 646 |
+
if not (physx_total or mdl_total):
|
| 647 |
+
return ""
|
| 648 |
+
parts = []
|
| 649 |
+
if physx_total:
|
| 650 |
+
parts.append(
|
| 651 |
+
f'<strong>{physx_total}</strong> PhysX-rule errors '
|
| 652 |
+
f'(<code>PhysxSchema is not available in this environment</code>)'
|
| 653 |
+
)
|
| 654 |
+
if mdl_total:
|
| 655 |
+
parts.append(
|
| 656 |
+
f'<strong>{mdl_total}</strong> stock MDL-reference failures '
|
| 657 |
+
f'(<code>OmniPBR.mdl</code> not resolvable outside Kit)'
|
| 658 |
+
)
|
| 659 |
+
return f'''
|
| 660 |
+
<div class="coverage-banner">
|
| 661 |
+
<strong>Environment-blocked checks suppressed.</strong>
|
| 662 |
+
This run dropped {" and ".join(parts)} because the active Python venv has
|
| 663 |
+
<code>usd-core</code> but not the Kit-bundled <code>PhysxSchema</code> /
|
| 664 |
+
Kit MDL search path. These checks couldn\'t produce useful results here,
|
| 665 |
+
so they would otherwise have inflated the issue count without telling you
|
| 666 |
+
anything about the asset. Pass/fail below reflects only checks that ran
|
| 667 |
+
meaningfully. See <code>PROBLEMS.md</code> P2 for root cause and fix path.
|
| 668 |
+
</div>
|
| 669 |
+
'''
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def _render_thumbnail_banner(provenance: list[dict], target: Path | None = None) -> str:
|
| 673 |
+
"""Banner explaining where the asset thumbnails came from (since they
|
| 674 |
+
weren't produced by this run's packaging step)."""
|
| 675 |
+
present = [p for p in provenance if p.get("present")]
|
| 676 |
+
if not present:
|
| 677 |
+
return ""
|
| 678 |
+
items = "".join(
|
| 679 |
+
f'<li><code>{html.escape(p["rel"])}</code> — copied from <code>{html.escape(_relativize_msg(p.get("copied_from",""), target))}</code></li>'
|
| 680 |
+
for p in present
|
| 681 |
+
)
|
| 682 |
+
return f'''
|
| 683 |
+
<div class="thumbnail-banner">
|
| 684 |
+
<strong>Asset thumbnails — sourced externally.</strong>
|
| 685 |
+
These tiles were copied into this report\'s <code>images/</code> directory from
|
| 686 |
+
per-asset spec paths (<code><asset_dir>/.thumbs/256x256/<filename>.png</code>) populated
|
| 687 |
+
earlier from NVIDIA\'s Isaac 6.0 content S3 bucket
|
| 688 |
+
(<code>Assets/Isaac/6.0/Isaac/Robots/Yaskawa/Motoman Next/...</code>).
|
| 689 |
+
They are <strong>not</strong> the output of this run\'s packaging step;
|
| 690 |
+
the customer\'s pipeline should generate authoritative thumbnails before the
|
| 691 |
+
validation reports them as spec-compliant.
|
| 692 |
+
<details><summary>Per-tile source ({len(present)} of {len(provenance)})</summary>
|
| 693 |
+
<ul>{items}</ul></details>
|
| 694 |
+
</div>
|
| 695 |
+
'''
|
| 696 |
+
|
| 697 |
+
|
| 698 |
+
def _render_asset_block(r: dict, doc_url, thumb_rel_by_asset: dict | None = None,
|
| 699 |
+
target: Path | None = None) -> str:
|
| 700 |
+
name = r["name"]
|
| 701 |
+
rel = r.get("rel_path") or r.get("path", "")
|
| 702 |
+
affected = r.get("affected_prims", [])
|
| 703 |
+
failed = r.get("failed_features", [])
|
| 704 |
+
passed = r.get("passed_features", [])
|
| 705 |
+
|
| 706 |
+
if affected:
|
| 707 |
+
prims_html = (
|
| 708 |
+
'<div class="affected-prims"><span class="label">Affected prims:</span>'
|
| 709 |
+
'<ul class="prim-list-inline">'
|
| 710 |
+
+ "".join(f"<li>{html.escape(p)}</li>" for p in affected)
|
| 711 |
+
+ "</ul></div>"
|
| 712 |
+
)
|
| 713 |
+
else:
|
| 714 |
+
prims_html = ""
|
| 715 |
+
|
| 716 |
+
def _feature_link(rel_path: str | None) -> str:
|
| 717 |
+
"""Inline 'View feature doc.' link — empty string when no doc available."""
|
| 718 |
+
url = doc_url(rel_path)
|
| 719 |
+
if not url:
|
| 720 |
+
return ''
|
| 721 |
+
return f' <a class="docref" href="{html.escape(url)}" target="_blank" rel="noopener">View feature doc.</a>'
|
| 722 |
+
|
| 723 |
+
def _capability_link(rel_path: str | None) -> str:
|
| 724 |
+
"""Inline 'View capability doc' link — empty string when no doc available."""
|
| 725 |
+
url = doc_url(rel_path)
|
| 726 |
+
if not url:
|
| 727 |
+
return ''
|
| 728 |
+
return f' <a class="docref" href="{html.escape(url)}" target="_blank" rel="noopener">View capability doc</a>'
|
| 729 |
+
|
| 730 |
+
if failed:
|
| 731 |
+
items = []
|
| 732 |
+
for f in failed:
|
| 733 |
+
req_paths = f.get("requirement_paths") or {}
|
| 734 |
+
feat_link = _feature_link(f.get("path"))
|
| 735 |
+
codes_parts = []
|
| 736 |
+
for c in f.get("failed_codes", []):
|
| 737 |
+
codes_parts.append(
|
| 738 |
+
f'<span class="code">{html.escape(c)}</span>'
|
| 739 |
+
f'{_capability_link(req_paths.get(c))}'
|
| 740 |
+
)
|
| 741 |
+
items.append(
|
| 742 |
+
f'<li><strong class="feat-id">{html.escape(f["id"])}</strong>'
|
| 743 |
+
f'{feat_link} '
|
| 744 |
+
+ " ".join(codes_parts)
|
| 745 |
+
+ '</li>'
|
| 746 |
+
)
|
| 747 |
+
failed_html = "<ul>" + "".join(items) + "</ul>"
|
| 748 |
+
else:
|
| 749 |
+
failed_html = '<p class="muted" style="margin:0">None</p>'
|
| 750 |
+
|
| 751 |
+
if passed:
|
| 752 |
+
# Passed features render as plain IDs — no doc link, matching the
|
| 753 |
+
# reference dashboard. Users only need quick access to docs when
|
| 754 |
+
# something failed.
|
| 755 |
+
items = [
|
| 756 |
+
f'<li><strong class="feat-id">{html.escape(f["id"])}</strong></li>'
|
| 757 |
+
for f in passed
|
| 758 |
+
]
|
| 759 |
+
passed_html = "<ul>" + "".join(items) + "</ul>"
|
| 760 |
+
else:
|
| 761 |
+
passed_html = '<p class="muted" style="margin:0">None</p>'
|
| 762 |
+
|
| 763 |
+
thumb_rel = (thumb_rel_by_asset or {}).get(name)
|
| 764 |
+
if thumb_rel:
|
| 765 |
+
thumb_html = (
|
| 766 |
+
f'<div class="asset-thumb-wrap">'
|
| 767 |
+
f'<img class="asset-thumb" src="{html.escape(thumb_rel)}" alt="{html.escape(name)} thumbnail" />'
|
| 768 |
+
f'</div>'
|
| 769 |
+
)
|
| 770 |
+
else:
|
| 771 |
+
thumb_html = (
|
| 772 |
+
'<div class="asset-thumb-wrap">'
|
| 773 |
+
'<div class="asset-thumb asset-thumb-missing" aria-label="no thumbnail">'
|
| 774 |
+
'<span>missing image</span>'
|
| 775 |
+
'</div></div>'
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
# `extra_failures` (issues against requirements outside the loaded
|
| 779 |
+
# profile) are intentionally NOT rendered per-asset. They get summarized
|
| 780 |
+
# as a count in the Caveats panel; the report scope is the chosen
|
| 781 |
+
# profile only.
|
| 782 |
+
extras_html = ""
|
| 783 |
+
|
| 784 |
+
return f"""
|
| 785 |
+
<div class="asset-block" data-asset-name="{html.escape(name.lower())}">
|
| 786 |
+
{thumb_html}
|
| 787 |
+
<div class="asset-body">
|
| 788 |
+
<h2>{html.escape(name)}</h2>
|
| 789 |
+
<div class="path">{html.escape(rel)}</div>
|
| 790 |
+
{prims_html}
|
| 791 |
+
<section class="failed-features">
|
| 792 |
+
<h3>Failed features</h3>
|
| 793 |
+
{failed_html}
|
| 794 |
+
</section>
|
| 795 |
+
<section class="passed-features">
|
| 796 |
+
<h3>Passed features</h3>
|
| 797 |
+
{passed_html}
|
| 798 |
+
</section>{extras_html}
|
| 799 |
+
</div>
|
| 800 |
+
</div>"""
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
def _render_compliance(results: list[dict], doc_url=None, target: Path | None = None) -> str:
|
| 804 |
+
"""Footer listing every in-profile requirement code that failed, with its
|
| 805 |
+
short message and (when available) a link to the foundation spec doc
|
| 806 |
+
that defines it. Codes outside the loaded profile are NOT listed here —
|
| 807 |
+
the report scope is the chosen spec only; out-of-spec failure counts
|
| 808 |
+
surface in the Caveats panel instead. Codes without a `path` get a
|
| 809 |
+
"(no spec source)" tag so the accountability gap is visible."""
|
| 810 |
+
# Build the set of in-profile failure codes from `failed_features`.
|
| 811 |
+
# Anything not in this set is an "extra" outside the loaded profile and
|
| 812 |
+
# is intentionally suppressed from the per-asset and footer views.
|
| 813 |
+
profile_failure_codes: set[str] = set()
|
| 814 |
+
for r in results:
|
| 815 |
+
for ff in r.get("failed_features", []):
|
| 816 |
+
profile_failure_codes.update(ff.get("failed_codes", []))
|
| 817 |
+
code_to_entry: dict[str, dict] = {}
|
| 818 |
+
for r in results:
|
| 819 |
+
for issue in r.get("issues", []):
|
| 820 |
+
if issue["severity"] not in ("failure", "error"):
|
| 821 |
+
continue
|
| 822 |
+
code = issue.get("code") or "UNKNOWN"
|
| 823 |
+
if code not in profile_failure_codes:
|
| 824 |
+
continue
|
| 825 |
+
if code not in code_to_entry:
|
| 826 |
+
code_to_entry[code] = {
|
| 827 |
+
"msg": issue.get("msg", ""),
|
| 828 |
+
"path": issue.get("path"),
|
| 829 |
+
}
|
| 830 |
+
if not code_to_entry:
|
| 831 |
+
return ""
|
| 832 |
+
|
| 833 |
+
items_html_parts: list[str] = []
|
| 834 |
+
accounted = 0
|
| 835 |
+
for code, e in sorted(code_to_entry.items()):
|
| 836 |
+
url = doc_url(e["path"]) if doc_url else None
|
| 837 |
+
if url:
|
| 838 |
+
code_html = f'<a class="code" href="{html.escape(url)}" target="_blank" rel="noopener">{html.escape(code)}</a>'
|
| 839 |
+
accounted += 1
|
| 840 |
+
no_spec_tag = ""
|
| 841 |
+
else:
|
| 842 |
+
code_html = f'<span class="code">{html.escape(code)}</span>'
|
| 843 |
+
no_spec_tag = ' <span class="no-spec">(no spec source)</span>'
|
| 844 |
+
msg = _relativize_msg(e["msg"], target)
|
| 845 |
+
items_html_parts.append(
|
| 846 |
+
f'<li>{code_html}: {html.escape(msg) if msg else "No guidance available."}{no_spec_tag}</li>'
|
| 847 |
+
)
|
| 848 |
+
items_html = "".join(items_html_parts)
|
| 849 |
+
coverage = (
|
| 850 |
+
f'<p class="muted">{accounted} of {len(code_to_entry)} requirement '
|
| 851 |
+
f'code(s) link to a foundation spec source. The rest are flagged '
|
| 852 |
+
f'<em>(no spec source)</em>.</p>'
|
| 853 |
+
)
|
| 854 |
+
return f"""
|
| 855 |
+
<section class="compliance">
|
| 856 |
+
<h2>How to comply with failed requirements</h2>
|
| 857 |
+
<p class="muted">Each requirement code below is shown with the first message recorded for it.</p>
|
| 858 |
+
{coverage}
|
| 859 |
+
<ul class="compliance-list">{items_html}</ul>
|
| 860 |
+
</section>"""
|
tools/validation/plugins/simready-report/skills/simready-report/validate.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|