claudecode-omc 5.6.5 β 5.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.local/commands/code-review.md +416 -0
- package/.local/commands/pr.md +250 -0
- package/.local/commands/prp-pr.md +244 -0
- package/.local/guidelines/CLAUDE.md +47 -0
- package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
- package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
- package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
- package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
- package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
- package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
- package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
- package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
- package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
- package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
- package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
- package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
- package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
- package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
- package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
- package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
- package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
- package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
- package/bundled/manifest.json +4 -4
- package/bundled/upstream/anthropic-skills/skills/algorithmic-art/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/brand-guidelines/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/canvas-design/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/SKILL.md +120 -58
- package/bundled/upstream/anthropic-skills/skills/claude-api/curl/examples.md +9 -9
- package/bundled/upstream/anthropic-skills/skills/claude-api/curl/managed-agents.md +336 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/go/managed-agents/README.md +561 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/java/claude-api.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/java/managed-agents/README.md +442 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/php/claude-api.md +10 -10
- package/bundled/upstream/anthropic-skills/skills/claude-api/php/managed-agents/README.md +435 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/README.md +16 -16
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/batches.md +3 -3
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/files-api.md +3 -3
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/streaming.md +7 -7
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/tool-use.md +19 -19
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/managed-agents/README.md +332 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/ruby/claude-api.md +4 -4
- package/bundled/upstream/anthropic-skills/skills/claude-api/ruby/managed-agents/README.md +389 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/agent-design.md +101 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/error-codes.md +11 -4
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/live-sources.md +60 -48
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-api-reference.md +372 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-client-patterns.md +209 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-core.md +220 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-environments.md +211 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-events.md +195 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-memory.md +197 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-multiagent.md +99 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-onboarding.md +114 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-outcomes.md +106 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-overview.md +67 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-tools.md +315 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-webhooks.md +110 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/model-migration.md +779 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/models.md +16 -14
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/prompt-caching.md +45 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/tool-use-concepts.md +28 -6
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/README.md +15 -15
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/batches.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/files-api.md +1 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/streaming.md +5 -5
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/tool-use.md +15 -15
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/managed-agents/README.md +359 -0
- package/bundled/upstream/anthropic-skills/skills/internal-comms/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/mcp-builder/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/skill-creator/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/slack-gif-creator/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/theme-factory/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/web-artifacts-builder/LICENSE.txt +1 -1
- package/bundled/upstream/anthropic-skills/skills/webapp-testing/LICENSE.txt +1 -1
- package/bundled/upstream/ecc/.omc-source/bundle.json +2 -3
- package/bundled/upstream/ecc/.omc-source/manifests/.claude-plugin/marketplace.json +4 -4
- package/bundled/upstream/ecc/agents/a11y-architect.md +10 -2
- package/bundled/upstream/ecc/agents/architect.md +9 -0
- package/bundled/upstream/ecc/agents/build-error-resolver.md +9 -0
- package/bundled/upstream/ecc/agents/chief-of-staff.md +9 -0
- package/bundled/upstream/ecc/agents/code-architect.md +9 -0
- package/bundled/upstream/ecc/agents/code-explorer.md +10 -1
- package/bundled/upstream/ecc/agents/code-reviewer.md +87 -1
- package/bundled/upstream/ecc/agents/code-simplifier.md +9 -0
- package/bundled/upstream/ecc/agents/comment-analyzer.md +10 -1
- package/bundled/upstream/ecc/agents/conversation-analyzer.md +9 -0
- package/bundled/upstream/ecc/agents/cpp-build-resolver.md +9 -0
- package/bundled/upstream/ecc/agents/cpp-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/csharp-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/dart-build-resolver.md +9 -0
- package/bundled/upstream/ecc/agents/database-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/django-build-resolver.md +252 -0
- package/bundled/upstream/ecc/agents/django-reviewer.md +169 -0
- package/bundled/upstream/ecc/agents/doc-updater.md +9 -0
- package/bundled/upstream/ecc/agents/docs-lookup.md +9 -0
- package/bundled/upstream/ecc/agents/e2e-runner.md +9 -0
- package/bundled/upstream/ecc/agents/fastapi-reviewer.md +79 -0
- package/bundled/upstream/ecc/agents/flutter-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/fsharp-reviewer.md +109 -0
- package/bundled/upstream/ecc/agents/gan-evaluator.md +9 -0
- package/bundled/upstream/ecc/agents/gan-generator.md +9 -0
- package/bundled/upstream/ecc/agents/gan-planner.md +9 -0
- package/bundled/upstream/ecc/agents/go-build-resolver.md +9 -0
- package/bundled/upstream/ecc/agents/go-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/harmonyos-app-resolver.md +182 -0
- package/bundled/upstream/ecc/agents/harness-optimizer.md +9 -0
- package/bundled/upstream/ecc/agents/healthcare-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/homelab-architect.md +107 -0
- package/bundled/upstream/ecc/agents/java-build-resolver.md +133 -11
- package/bundled/upstream/ecc/agents/java-reviewer.md +130 -32
- package/bundled/upstream/ecc/agents/kotlin-build-resolver.md +9 -0
- package/bundled/upstream/ecc/agents/kotlin-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/loop-operator.md +9 -0
- package/bundled/upstream/ecc/agents/mle-reviewer.md +162 -0
- package/bundled/upstream/ecc/agents/network-architect.md +106 -0
- package/bundled/upstream/ecc/agents/network-config-reviewer.md +106 -0
- package/bundled/upstream/ecc/agents/network-troubleshooter.md +128 -0
- package/bundled/upstream/ecc/agents/opensource-forker.md +9 -0
- package/bundled/upstream/ecc/agents/opensource-packager.md +9 -0
- package/bundled/upstream/ecc/agents/opensource-sanitizer.md +9 -0
- package/bundled/upstream/ecc/agents/performance-optimizer.md +9 -0
- package/bundled/upstream/ecc/agents/planner.md +9 -0
- package/bundled/upstream/ecc/agents/pr-test-analyzer.md +9 -0
- package/bundled/upstream/ecc/agents/python-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/pytorch-build-resolver.md +12 -3
- package/bundled/upstream/ecc/agents/refactor-cleaner.md +9 -0
- package/bundled/upstream/ecc/agents/rust-build-resolver.md +9 -0
- package/bundled/upstream/ecc/agents/rust-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/security-reviewer.md +9 -0
- package/bundled/upstream/ecc/agents/seo-specialist.md +10 -1
- package/bundled/upstream/ecc/agents/silent-failure-hunter.md +9 -0
- package/bundled/upstream/ecc/agents/swift-build-resolver.md +170 -0
- package/bundled/upstream/ecc/agents/swift-reviewer.md +116 -0
- package/bundled/upstream/ecc/agents/tdd-guide.md +9 -0
- package/bundled/upstream/ecc/agents/type-design-analyzer.md +10 -1
- package/bundled/upstream/ecc/agents/typescript-reviewer.md +9 -0
- package/bundled/upstream/ecc/commands/auto-update.md +28 -0
- package/bundled/upstream/ecc/commands/build-fix.md +4 -0
- package/bundled/upstream/ecc/commands/checkpoint.md +4 -0
- package/bundled/upstream/ecc/commands/code-review.md +3 -3
- package/bundled/upstream/ecc/commands/cost-report.md +107 -0
- package/bundled/upstream/ecc/commands/cpp-build.md +1 -1
- package/bundled/upstream/ecc/commands/cpp-test.md +1 -1
- package/bundled/upstream/ecc/commands/ecc-guide.md +93 -0
- package/bundled/upstream/ecc/commands/fastapi-review.md +39 -0
- package/bundled/upstream/ecc/commands/flutter-build.md +1 -1
- package/bundled/upstream/ecc/commands/flutter-test.md +1 -1
- package/bundled/upstream/ecc/commands/gan-build.md +4 -0
- package/bundled/upstream/ecc/commands/gan-design.md +4 -0
- package/bundled/upstream/ecc/commands/go-build.md +1 -1
- package/bundled/upstream/ecc/commands/go-test.md +1 -1
- package/bundled/upstream/ecc/commands/harness-audit.md +4 -0
- package/bundled/upstream/ecc/commands/jira.md +2 -2
- package/bundled/upstream/ecc/commands/kotlin-build.md +1 -1
- package/bundled/upstream/ecc/commands/kotlin-test.md +1 -1
- package/bundled/upstream/ecc/commands/learn.md +4 -0
- package/bundled/upstream/ecc/commands/loop-start.md +4 -0
- package/bundled/upstream/ecc/commands/loop-status.md +54 -1
- package/bundled/upstream/ecc/commands/model-route.md +4 -0
- package/bundled/upstream/ecc/commands/multi-backend.md +4 -0
- package/bundled/upstream/ecc/commands/multi-execute.md +4 -0
- package/bundled/upstream/ecc/commands/multi-frontend.md +4 -0
- package/bundled/upstream/ecc/commands/multi-plan.md +4 -0
- package/bundled/upstream/ecc/commands/multi-workflow.md +4 -0
- package/bundled/upstream/ecc/commands/plan-prd.md +160 -0
- package/bundled/upstream/ecc/commands/plan.md +96 -13
- package/bundled/upstream/ecc/commands/pm2.md +4 -0
- package/bundled/upstream/ecc/commands/pr.md +184 -0
- package/bundled/upstream/ecc/commands/project-init.md +86 -0
- package/bundled/upstream/ecc/commands/python-review.md +1 -1
- package/bundled/upstream/ecc/commands/quality-gate.md +4 -0
- package/bundled/upstream/ecc/commands/refactor-clean.md +4 -0
- package/bundled/upstream/ecc/commands/rust-build.md +1 -1
- package/bundled/upstream/ecc/commands/rust-test.md +1 -1
- package/bundled/upstream/ecc/commands/security-scan.md +92 -0
- package/bundled/upstream/ecc/commands/sessions.md +6 -6
- package/bundled/upstream/ecc/commands/skill-health.md +3 -3
- package/bundled/upstream/ecc/commands/test-coverage.md +4 -0
- package/bundled/upstream/ecc/commands/update-codemaps.md +4 -0
- package/bundled/upstream/ecc/commands/update-docs.md +4 -0
- package/bundled/upstream/ecc/skills/accessibility/SKILL.md +1 -1
- package/bundled/upstream/ecc/skills/agent-architecture-audit/SKILL.md +256 -0
- package/bundled/upstream/ecc/skills/agent-payment-x402/SKILL.md +49 -3
- package/bundled/upstream/ecc/skills/agentic-os/SKILL.md +387 -0
- package/bundled/upstream/ecc/skills/angular-developer/SKILL.md +154 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/angular-animations.md +160 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/angular-aria.md +410 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/cli.md +86 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/component-harnesses.md +59 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/component-styling.md +91 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/components.md +117 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/creating-services.md +97 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/data-resolvers.md +69 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/define-routes.md +67 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/defining-providers.md +72 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/di-fundamentals.md +120 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/e2e-testing.md +56 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/effects.md +83 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/hierarchical-injectors.md +43 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/host-elements.md +80 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/injection-context.md +63 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/inputs.md +101 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/linked-signal.md +59 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/loading-strategies.md +61 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/mcp.md +108 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/navigate-to-routes.md +69 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/outputs.md +86 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/reactive-forms.md +122 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/rendering-strategies.md +44 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/resource.md +77 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/route-animations.md +56 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/route-guards.md +52 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/router-lifecycle.md +45 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/router-testing.md +87 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/signal-forms.md +795 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/signals-overview.md +94 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/tailwind-css.md +69 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/template-driven-forms.md +114 -0
- package/bundled/upstream/ecc/skills/angular-developer/references/testing-fundamentals.md +65 -0
- package/bundled/upstream/ecc/skills/autonomous-agent-harness/SKILL.md +6 -0
- package/bundled/upstream/ecc/skills/backend-patterns/SKILL.md +8 -45
- package/bundled/upstream/ecc/skills/cisco-ios-patterns/SKILL.md +163 -0
- package/bundled/upstream/ecc/skills/configure-ecc/SKILL.md +31 -14
- package/bundled/upstream/ecc/skills/continuous-learning/SKILL.md +10 -2
- package/bundled/upstream/ecc/skills/continuous-learning-v2/SKILL.md +19 -5
- package/bundled/upstream/ecc/skills/continuous-learning-v2/agents/observer-loop.sh +44 -4
- package/bundled/upstream/ecc/skills/continuous-learning-v2/agents/observer.md +4 -4
- package/bundled/upstream/ecc/skills/continuous-learning-v2/agents/start-observer.sh +5 -1
- package/bundled/upstream/ecc/skills/continuous-learning-v2/hooks/observe.sh +21 -5
- package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/detect-project.sh +58 -13
- package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/instinct-cli.py +97 -4
- package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh +31 -0
- package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/migrate-homunculus.sh +62 -0
- package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/test_parse_instinct.py +34 -0
- package/bundled/upstream/ecc/skills/cost-tracking/SKILL.md +147 -0
- package/bundled/upstream/ecc/skills/deep-research/SKILL.md +4 -0
- package/bundled/upstream/ecc/skills/defi-amm-security/SKILL.md +6 -0
- package/bundled/upstream/ecc/skills/django-celery/SKILL.md +457 -0
- package/bundled/upstream/ecc/skills/ecc-guide/SKILL.md +189 -0
- package/bundled/upstream/ecc/skills/error-handling/SKILL.md +376 -0
- package/bundled/upstream/ecc/skills/exa-search/SKILL.md +4 -0
- package/bundled/upstream/ecc/skills/fal-ai-media/SKILL.md +4 -0
- package/bundled/upstream/ecc/skills/fastapi-patterns/SKILL.md +327 -0
- package/bundled/upstream/ecc/skills/flox-environments/SKILL.md +496 -0
- package/bundled/upstream/ecc/skills/frontend-design-direction/SKILL.md +92 -0
- package/bundled/upstream/ecc/skills/frontend-slides/animation-patterns.md +122 -0
- package/bundled/upstream/ecc/skills/frontend-slides/html-template.md +419 -0
- package/bundled/upstream/ecc/skills/frontend-slides/scripts/export-pdf.sh +418 -0
- package/bundled/upstream/ecc/skills/frontend-slides/scripts/extract-pptx.py +96 -0
- package/bundled/upstream/ecc/skills/frontend-slides/viewport-base.css +153 -0
- package/bundled/upstream/ecc/skills/fsharp-testing/SKILL.md +280 -0
- package/bundled/upstream/ecc/skills/gateguard/SKILL.md +4 -0
- package/bundled/upstream/ecc/skills/hermes-imports/SKILL.md +88 -0
- package/bundled/upstream/ecc/skills/homelab-network-readiness/SKILL.md +169 -0
- package/bundled/upstream/ecc/skills/homelab-network-setup/SKILL.md +129 -0
- package/bundled/upstream/ecc/skills/homelab-pihole-dns/SKILL.md +274 -0
- package/bundled/upstream/ecc/skills/homelab-vlan-segmentation/SKILL.md +311 -0
- package/bundled/upstream/ecc/skills/homelab-wireguard-vpn/SKILL.md +305 -0
- package/bundled/upstream/ecc/skills/ios-icon-gen/SKILL.md +157 -0
- package/bundled/upstream/ecc/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
- package/bundled/upstream/ecc/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
- package/bundled/upstream/ecc/skills/java-coding-standards/SKILL.md +241 -5
- package/bundled/upstream/ecc/skills/make-interfaces-feel-better/SKILL.md +151 -0
- package/bundled/upstream/ecc/skills/mle-workflow/SKILL.md +346 -0
- package/bundled/upstream/ecc/skills/motion-advanced/SKILL.md +596 -0
- package/bundled/upstream/ecc/skills/motion-foundations/SKILL.md +299 -0
- package/bundled/upstream/ecc/skills/motion-patterns/SKILL.md +435 -0
- package/bundled/upstream/ecc/skills/motion-ui/SKILL.md +575 -0
- package/bundled/upstream/ecc/skills/mysql-patterns/SKILL.md +412 -0
- package/bundled/upstream/ecc/skills/netmiko-ssh-automation/SKILL.md +173 -0
- package/bundled/upstream/ecc/skills/network-bgp-diagnostics/SKILL.md +167 -0
- package/bundled/upstream/ecc/skills/network-config-validation/SKILL.md +210 -0
- package/bundled/upstream/ecc/skills/network-interface-health/SKILL.md +152 -0
- package/bundled/upstream/ecc/skills/openclaw-persona-forge/SKILL.md +1 -9
- package/bundled/upstream/ecc/skills/plan-orchestrate/SKILL.md +262 -0
- package/bundled/upstream/ecc/skills/prisma-patterns/SKILL.md +371 -0
- package/bundled/upstream/ecc/skills/production-audit/SKILL.md +206 -0
- package/bundled/upstream/ecc/skills/prompt-optimizer/SKILL.md +24 -400
- package/bundled/upstream/ecc/skills/quarkus-patterns/SKILL.md +722 -0
- package/bundled/upstream/ecc/skills/quarkus-security/SKILL.md +467 -0
- package/bundled/upstream/ecc/skills/quarkus-tdd/SKILL.md +811 -0
- package/bundled/upstream/ecc/skills/quarkus-verification/SKILL.md +479 -0
- package/bundled/upstream/ecc/skills/redis-patterns/SKILL.md +403 -0
- package/bundled/upstream/ecc/skills/scientific-db-pubmed-database/SKILL.md +175 -0
- package/bundled/upstream/ecc/skills/scientific-db-uspto-database/SKILL.md +177 -0
- package/bundled/upstream/ecc/skills/scientific-pkg-gget/SKILL.md +166 -0
- package/bundled/upstream/ecc/skills/scientific-thinking-literature-review/SKILL.md +192 -0
- package/bundled/upstream/ecc/skills/scientific-thinking-scholar-evaluation/SKILL.md +160 -0
- package/bundled/upstream/ecc/skills/search-first/SKILL.md +23 -2
- package/bundled/upstream/ecc/skills/security-review/SKILL.md +10 -2
- package/bundled/upstream/ecc/skills/skill-comply/scripts/runner.py +28 -3
- package/bundled/upstream/ecc/skills/skill-comply/tests/test_runner.py +172 -0
- package/bundled/upstream/ecc/skills/skill-scout/SKILL.md +140 -0
- package/bundled/upstream/ecc/skills/skill-stocktake/SKILL.md +1 -0
- package/bundled/upstream/ecc/skills/strategic-compact/SKILL.md +2 -2
- package/bundled/upstream/ecc/skills/tinystruct-patterns/SKILL.md +203 -0
- package/bundled/upstream/ecc/skills/tinystruct-patterns/references/architecture.md +90 -0
- package/bundled/upstream/ecc/skills/tinystruct-patterns/references/data-handling.md +60 -0
- package/bundled/upstream/ecc/skills/tinystruct-patterns/references/database.md +99 -0
- package/bundled/upstream/ecc/skills/tinystruct-patterns/references/routing.md +64 -0
- package/bundled/upstream/ecc/skills/tinystruct-patterns/references/system-usage.md +97 -0
- package/bundled/upstream/ecc/skills/tinystruct-patterns/references/testing.md +72 -0
- package/bundled/upstream/ecc/skills/ui-to-vue/SKILL.md +134 -0
- package/bundled/upstream/ecc/skills/vite-patterns/SKILL.md +449 -0
- package/bundled/upstream/ecc/skills/windows-desktop-e2e/SKILL.md +788 -0
- package/bundled/upstream/ecc/skills/x-api/SKILL.md +4 -0
- package/bundled/upstream/oh-my-claudecode/.omc-source/bundle.json +20 -0
- package/bundled/upstream/oh-my-claudecode/agents/analyst.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/architect.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/code-reviewer.md +26 -7
- package/bundled/upstream/oh-my-claudecode/agents/critic.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/debugger.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/designer.md +14 -1
- package/bundled/upstream/oh-my-claudecode/agents/document-specialist.md +1 -1
- package/bundled/upstream/oh-my-claudecode/agents/executor.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/explore.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/git-master.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/planner.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/qa-tester.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/scientist.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/security-reviewer.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/test-engineer.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/tracer.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/verifier.md +2 -1
- package/bundled/upstream/oh-my-claudecode/agents/writer.md +2 -1
- package/bundled/upstream/oh-my-claudecode/skills/AGENTS.md +4 -3
- package/bundled/upstream/oh-my-claudecode/skills/ai-slop-cleaner/SKILL.md +12 -0
- package/bundled/upstream/oh-my-claudecode/skills/deep-dive/SKILL.md +69 -13
- package/bundled/upstream/oh-my-claudecode/skills/deep-interview/SKILL.md +176 -63
- package/bundled/upstream/oh-my-claudecode/skills/learner/SKILL.md +3 -1
- package/bundled/upstream/oh-my-claudecode/skills/omc-doctor/SKILL.md +22 -3
- package/bundled/upstream/oh-my-claudecode/skills/omc-reference/SKILL.md +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/01-install-claude-md.md +2 -2
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/02-configure.md +45 -2
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/03-integrations.md +46 -5
- package/bundled/upstream/oh-my-claudecode/skills/plan/SKILL.md +19 -17
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/config.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/parse.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/azure-devops.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/bitbucket.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/gitea.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/github.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/gitlab.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/interface.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/jira.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/session.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/tmux.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/worktree.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/psm.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/tests/test-psm-prompt-injection.sh +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/ralph/SKILL.md +8 -8
- package/bundled/upstream/oh-my-claudecode/skills/ralplan/SKILL.md +9 -5
- package/bundled/upstream/oh-my-claudecode/skills/skill/SKILL.md +6 -6
- package/bundled/upstream/oh-my-claudecode/skills/skillify/SKILL.md +19 -3
- package/bundled/upstream/oh-my-claudecode/skills/trace/SKILL.md +3 -1
- package/bundled/upstream/oh-my-claudecode/skills/ultrawork/SKILL.md +20 -7
- package/bundled/upstream/superpowers/.omc-source/bundle.json +21 -0
- package/bundled/upstream/superpowers/hooks/hooks-cursor.json +1 -1
- package/bundled/upstream/superpowers/skills/executing-plans/SKILL.md +1 -1
- package/bundled/upstream/superpowers/skills/finishing-a-development-branch/SKILL.md +93 -42
- package/bundled/upstream/superpowers/skills/requesting-code-review/SKILL.md +7 -9
- package/bundled/upstream/superpowers/skills/requesting-code-review/code-reviewer.md +107 -85
- package/bundled/upstream/superpowers/skills/subagent-driven-development/SKILL.md +3 -1
- package/bundled/upstream/superpowers/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -3
- package/bundled/upstream/superpowers/skills/systematic-debugging/CREATION-LOG.md +1 -1
- package/bundled/upstream/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
- package/bundled/upstream/superpowers/skills/using-git-worktrees/SKILL.md +95 -98
- package/bundled/upstream/superpowers/skills/using-superpowers/references/codex-tools.md +7 -48
- package/bundled/upstream/superpowers/skills/using-superpowers/references/copilot-tools.md +1 -11
- package/bundled/upstream/superpowers/skills/using-superpowers/references/gemini-tools.md +21 -3
- package/bundled/upstream/superpowers/skills/writing-plans/SKILL.md +1 -1
- package/package.json +1 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/agent-sdk/README.md +0 -355
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/agent-sdk/patterns.md +0 -359
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/agent-sdk/README.md +0 -297
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/agent-sdk/patterns.md +0 -209
- package/bundled/upstream/ecc/commands/agent-sort.md +0 -23
- package/bundled/upstream/ecc/commands/claw.md +0 -23
- package/bundled/upstream/ecc/commands/context-budget.md +0 -23
- package/bundled/upstream/ecc/commands/devfleet.md +0 -23
- package/bundled/upstream/ecc/commands/docs.md +0 -23
- package/bundled/upstream/ecc/commands/e2e.md +0 -268
- package/bundled/upstream/ecc/commands/eval.md +0 -23
- package/bundled/upstream/ecc/commands/orchestrate.md +0 -135
- package/bundled/upstream/ecc/commands/prompt-optimize.md +0 -23
- package/bundled/upstream/ecc/commands/rules-distill.md +0 -20
- package/bundled/upstream/ecc/commands/tdd.md +0 -231
- package/bundled/upstream/ecc/commands/verify.md +0 -23
- package/bundled/upstream/ecc/skills/claude-api/SKILL.md +0 -337
- package/bundled/upstream/ecc/skills/frontend-design/SKILL.md +0 -145
- package/bundled/upstream/superpowers/agents/code-reviewer.md +0 -48
- package/bundled/upstream/superpowers/commands/brainstorm.md +0 -5
- package/bundled/upstream/superpowers/commands/execute-plan.md +0 -5
- package/bundled/upstream/superpowers/commands/write-plan.md +0 -5
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* evaluate-convergence.mjs β Stage 5: THE sole executable convergence authority
|
|
4
|
+
*
|
|
5
|
+
* This script is the ONLY thing that may emit `convergence/<component>.json`.
|
|
6
|
+
* The LLM orchestrator NEVER hand-writes that artifact β it calls this script,
|
|
7
|
+
* which mechanically decides the verdict and EXITS NON-ZERO on any guard
|
|
8
|
+
* violation so a pipeline cannot ignore a failed gate.
|
|
9
|
+
*
|
|
10
|
+
* Trust model (honest β see spec.md Β§1.1: report the residual, do not pretend
|
|
11
|
+
* it is zero β and apply that to the skill's OWN trust model):
|
|
12
|
+
* This grader is mechanically AUTHORITATIVE *given* inputs whose provenance
|
|
13
|
+
* it now binds in code. Each bullet states ONLY what is mechanically true β
|
|
14
|
+
* the precise bound/residual split, not an over-claim:
|
|
15
|
+
* - the structured calibration GATE is recomputed from `calib.floor`
|
|
16
|
+
* using the published tolerances; a hand-loosened `gate` (tight floor +
|
|
17
|
+
* loose gate) is rejected (`gate-floor-mismatch`). This binds the gate
|
|
18
|
+
* TO the floor β it does not bind the floor's *value*;
|
|
19
|
+
* - `calibration_source` binds the IDENTITY of the bundled twin SOURCE
|
|
20
|
+
* FILES (excluding build output/dotfiles): their source-tree hashes are
|
|
21
|
+
* recomputed here from the actual
|
|
22
|
+
* `assets/calibration/{h5-twin,swiftui-twin}` (mismatch β
|
|
23
|
+
* `calibration-twin-mismatch`). This does NOT re-measure or bind the
|
|
24
|
+
* `floor` *value* itself;
|
|
25
|
+
* - the `floor` VALUE is asserted to satisfy calibrate-render.mjs's OWN
|
|
26
|
+
* sanity envelope (shared `_calib-consts.mjs`: ssim_nontext β₯ 0.95,
|
|
27
|
+
* non-null text_iou β₯ 0.9, metric-valid deltaE) β a floor calibrate-
|
|
28
|
+
* render could not have emitted (it writes blocked.json below this) is
|
|
29
|
+
* rejected `floor-implausible` (exit 1). The grader does NOT re-render
|
|
30
|
+
* to re-measure the floor: a floor *within* this envelope yet looser
|
|
31
|
+
* than the true measured floor is trusted (named residual (2) below);
|
|
32
|
+
* - the judge negative control is bound to the shipped, hash-pinned
|
|
33
|
+
* `assets/calibration/swiftui-twin-divergent` SOURCE files (mismatch /
|
|
34
|
+
* unstructured / not-rejected β any YES is VOID,
|
|
35
|
+
* `negative-control-unbound`);
|
|
36
|
+
* - pHash never short-circuits; null IoU is FAIL; the mask budget is
|
|
37
|
+
* monotone-or-fail; sim-screenshot.sh carries its own no-fake spine.
|
|
38
|
+
* NAMED IRREDUCIBLE RESIDUALS (BOTH stated, neither hidden β a fully
|
|
39
|
+
* zero-trust verdict is impossible because something must run the renders
|
|
40
|
+
* and something must measure the floor):
|
|
41
|
+
* (1) The grader CANNOT re-execute the simulator renders. It trusts the
|
|
42
|
+
* per-iteration pixel-diff JSONs were produced by running the real
|
|
43
|
+
* `pixel-diff.mjs` on real `sim-screenshot.sh` renders (bounded by
|
|
44
|
+
* that script's no-fake build/env spine β no simulator / no build β
|
|
45
|
+
* blocked/needs-human, never converged).
|
|
46
|
+
* (2) The grader CANNOT re-measure the calibration floor. It asserts the
|
|
47
|
+
* supplied `floor` satisfies calibrate-render.mjs's own sanity
|
|
48
|
+
* envelope and recomputes the gate from it, but a `floor` *within*
|
|
49
|
+
* that envelope yet looser than the TRUE measured floor is trusted
|
|
50
|
+
* (it cannot re-render the bundled twins to re-derive the real
|
|
51
|
+
* number). Mitigated by: the orchestrator's contractual obligation
|
|
52
|
+
* to run the real, sanity/flat-image-spined `calibrate-render.mjs`,
|
|
53
|
+
* and the human-readable `calibration_provenance` recorded in the
|
|
54
|
+
* convergence artifact.
|
|
55
|
+
* These are the deliberate, documented boundaries β the deliverable is
|
|
56
|
+
* "maximally provenance-bound + honestly disclosed residual", explicitly
|
|
57
|
+
* NOT "zero-trust". A reviewer reading this header learns BOTH residuals.
|
|
58
|
+
*
|
|
59
|
+
* Usage:
|
|
60
|
+
* node evaluate-convergence.mjs --iterations iterations.json
|
|
61
|
+
* --calibration calibration.json
|
|
62
|
+
* --judge judge.json
|
|
63
|
+
* --component <Name>
|
|
64
|
+
* [--masks masks.json]
|
|
65
|
+
* [--component-area <WxH | px>]
|
|
66
|
+
* [--out convergence/<Name>.json]
|
|
67
|
+
* [--blocked blocked.json]
|
|
68
|
+
*
|
|
69
|
+
* Inputs:
|
|
70
|
+
* --iterations <path> REQUIRED. Either:
|
|
71
|
+
* (a) a JSON file: [ { "i":1, "diff_json_path":"...", "built":true }, ... ]
|
|
72
|
+
* where `diff_json_path` points at a pixel-diff.mjs JSON output and
|
|
73
|
+
* `built` is the caller-supplied build result for that iteration; OR
|
|
74
|
+
* (b) a directory: every *.json inside is treated as a pixel-diff output;
|
|
75
|
+
* iteration index = numeric suffix (iterN) or file order; `built`
|
|
76
|
+
* defaults to true ONLY in directory mode (use form (a) to record
|
|
77
|
+
* non-building iterations).
|
|
78
|
+
* The caller supplies `built`; the caller does NOT (and cannot) supply
|
|
79
|
+
* `gate_passed` β this script computes it.
|
|
80
|
+
* --calibration <path> REQUIRED. calibrate-render.mjs output. Consumes the
|
|
81
|
+
* STRUCTURED numeric gate (gate.converged / gate.close objects), never a
|
|
82
|
+
* string DSL. A legacy string gate is rejected as un-enforceable.
|
|
83
|
+
* schema MUST be "h5-to-swiftui/calibration@1"; `gate` is REJECTED
|
|
84
|
+
* unless it equals the gate recomputed from `floor` with the published
|
|
85
|
+
* tolerances (reason: gate-floor-mismatch); `calibration_source` MUST
|
|
86
|
+
* match the SHIPPED bundled twins' source-tree hashes recomputed here
|
|
87
|
+
* (reason: calibration-twin-mismatch).
|
|
88
|
+
* --judge <path> REQUIRED. {
|
|
89
|
+
* "negative_control": {
|
|
90
|
+
* "stimulus_source_hash": "<sha256 source-tree hash of the
|
|
91
|
+
* bundled assets/calibration/swiftui-twin-divergent>",
|
|
92
|
+
* "divergent_pair": "h5-twin vs swiftui-twin-divergent",
|
|
93
|
+
* "rejected": true,
|
|
94
|
+
* "differences": [ { "desc": "...", "severity": "..." }, ... ]
|
|
95
|
+
* },
|
|
96
|
+
* "framing": "forced-difference-3",
|
|
97
|
+
* "differences": [ { "desc": "...", "severity": "..." }, ... ],
|
|
98
|
+
* "verdict": "YES" | "NO" | "visually-equivalent-residual-subperceptual"
|
|
99
|
+
* }
|
|
100
|
+
* The legacy bare `negative_control:"passed"` STRING form is REJECTED
|
|
101
|
+
* as unbound (it asserts a judge run nothing verified) β any YES VOID.
|
|
102
|
+
* --masks <path> Optional. Array of { x, y, w, h, reason }
|
|
103
|
+
* (component-relative px). Every mask MUST have a non-empty `reason`.
|
|
104
|
+
* --component-area Optional. "WxH" (e.g. "320x140") or a raw pixel
|
|
105
|
+
* integer. Used as the mask-fraction denominator. If omitted, the area
|
|
106
|
+
* is taken from the first diff's region coverage / global frame if
|
|
107
|
+
* derivable; if it cannot be derived AND masks are present, that is a
|
|
108
|
+
* guard violation (cannot verify the mask budget) β needs-human.
|
|
109
|
+
* --blocked <path> Optional. If this file exists, the component is
|
|
110
|
+
* `blocked` regardless of metrics (never converged).
|
|
111
|
+
*
|
|
112
|
+
* Mechanically enforced guards (each violation β tier downgrade + non-zero
|
|
113
|
+
* exit, with a machine-readable reason β NEVER a silent pass):
|
|
114
|
+
* 1. Mask budget: sum(mask area)/component area > 0.10 β needs-human.
|
|
115
|
+
* Any mask missing a non-empty `reason` β reject (needs-human).
|
|
116
|
+
* 2. Gate eval in code: read calibration structured thresholds; compute
|
|
117
|
+
* `gate_passed` PER ITERATION from that iteration's pixel-diff metrics.
|
|
118
|
+
* A text-region IoU of `null` counts as FAIL (never pass). pHash is
|
|
119
|
+
* necessary-not-sufficient β phash_fast_candidate never short-circuits
|
|
120
|
+
* to converged; the region gate must still pass.
|
|
121
|
+
* 3. Monotone-or-fail best-of-N: best_iteration is THIS script's choice,
|
|
122
|
+
* only among iterations that are BOTH built==true AND gate_passed==true.
|
|
123
|
+
* Caller-supplied best_iteration is ignored. None β needs-human.
|
|
124
|
+
* 4. Negative control voids judge β BOUND to the bundled divergent twin:
|
|
125
|
+
* judge.negative_control MUST be a structured object whose
|
|
126
|
+
* stimulus_source_hash equals the source-tree hash of the SHIPPED
|
|
127
|
+
* assets/calibration/swiftui-twin-divergent (recomputed here),
|
|
128
|
+
* rejected===true, differences a non-empty structured list, AND
|
|
129
|
+
* judge.framing === "forced-difference-3". The legacy bare string form
|
|
130
|
+
* is rejected as unbound. Any deviation β any YES VOID; cannot be
|
|
131
|
+
* converged (needs-human), recorded negative_control:failed,
|
|
132
|
+
* reason: negative-control-unbound.
|
|
133
|
+
* 5. Tiered verdict (visual-diff-loop-protocol.md):
|
|
134
|
+
* converged = gate_passed(best) AND judge YES (valid neg-control);
|
|
135
|
+
* close = within the calibration `close` band AND judge
|
|
136
|
+
* "visually-equivalent-residual-subperceptual";
|
|
137
|
+
* else needs-human.
|
|
138
|
+
* 6. Build accounting: no built+gate-passing iteration, or a blocked.json
|
|
139
|
+
* present β needs-human / blocked, NEVER converged.
|
|
140
|
+
*
|
|
141
|
+
* Output: writes the `h5-to-swiftui/convergence@1` artifact (pinned-version
|
|
142
|
+
* header pass-through from calibration, full per-iteration history, masks,
|
|
143
|
+
* mask_fraction, negative_control result, best_iteration, tier, residual).
|
|
144
|
+
*
|
|
145
|
+
* Exit codes:
|
|
146
|
+
* 0 β tier is `converged` OR `close` (pipeline may proceed)
|
|
147
|
+
* 1 β fatal error (bad args, file not found, malformed input,
|
|
148
|
+
* legacy string gate, unverifiable mask budget)
|
|
149
|
+
* 3 β tier is `needs-human` OR a guard was violated (mask budget,
|
|
150
|
+
* negative control, no gate-passing iteration)
|
|
151
|
+
* 4 β tier is `blocked` (blocked.json present for this component)
|
|
152
|
+
* (--help exits 0)
|
|
153
|
+
*
|
|
154
|
+
* No npm dependencies β pure JSON; pngjs is NOT required.
|
|
155
|
+
*
|
|
156
|
+
* Examples:
|
|
157
|
+
* node evaluate-convergence.mjs --iterations iters.json \
|
|
158
|
+
* --calibration calibration.json --judge judge.json --component ProductCard
|
|
159
|
+
* node evaluate-convergence.mjs --iterations .h5-to-swiftui/diff/ProductCard \
|
|
160
|
+
* --calibration calibration.json --judge judge.json --masks masks.json \
|
|
161
|
+
* --component-area 320x140 --component ProductCard \
|
|
162
|
+
* --out .h5-to-swiftui/convergence/ProductCard.json
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
import {
|
|
166
|
+
existsSync,
|
|
167
|
+
readFileSync,
|
|
168
|
+
writeFileSync,
|
|
169
|
+
mkdirSync,
|
|
170
|
+
readdirSync,
|
|
171
|
+
statSync,
|
|
172
|
+
} from 'node:fs';
|
|
173
|
+
import { resolve, join, dirname, basename } from 'node:path';
|
|
174
|
+
import { fileURLToPath } from 'node:url';
|
|
175
|
+
|
|
176
|
+
import { sourceTreeHash } from './_provenance.mjs';
|
|
177
|
+
// Shared sanity-envelope constants β the SAME module calibrate-render.mjs
|
|
178
|
+
// imports, so a `floor` this grader accepts is one calibrate-render could
|
|
179
|
+
// actually have emitted (vs writing blocked.json). Single source of truth.
|
|
180
|
+
import { floorWithinCalibrateEnvelope } from './_calib-consts.mjs';
|
|
181
|
+
|
|
182
|
+
// Skill root = parent of this script's directory (scripts/ -> skill root),
|
|
183
|
+
// resolved from the script's own location so bundled-asset provenance is
|
|
184
|
+
// correct no matter what cwd the grader runs from.
|
|
185
|
+
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
186
|
+
const BUNDLED_H5_TWIN = join(SKILL_ROOT, 'assets/calibration/h5-twin');
|
|
187
|
+
const BUNDLED_SWIFTUI_TWIN = join(SKILL_ROOT, 'assets/calibration/swiftui-twin');
|
|
188
|
+
const BUNDLED_DIVERGENT_TWIN = join(SKILL_ROOT, 'assets/calibration/swiftui-twin-divergent');
|
|
189
|
+
|
|
190
|
+
// ββ CLI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
191
|
+
|
|
192
|
+
const args = process.argv.slice(2);
|
|
193
|
+
|
|
194
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
195
|
+
console.log(`evaluate-convergence.mjs β Stage 5 sole executable convergence authority
|
|
196
|
+
|
|
197
|
+
Usage:
|
|
198
|
+
node evaluate-convergence.mjs --iterations <iters.json | dir>
|
|
199
|
+
--calibration <calibration.json>
|
|
200
|
+
--judge <judge.json>
|
|
201
|
+
--component <Name>
|
|
202
|
+
[--masks <masks.json>]
|
|
203
|
+
[--component-area <WxH | px>]
|
|
204
|
+
[--out <convergence/Name.json>]
|
|
205
|
+
[--blocked <blocked.json>]
|
|
206
|
+
|
|
207
|
+
Required:
|
|
208
|
+
--iterations <path> iterations.json [{i,diff_json_path,built}, ...] OR a
|
|
209
|
+
directory of pixel-diff.mjs JSON outputs. The caller
|
|
210
|
+
supplies 'built'; this script computes 'gate_passed'.
|
|
211
|
+
--calibration <path> calibrate-render.mjs output. STRUCTURED numeric gate
|
|
212
|
+
(legacy string gate rejected). schema must be
|
|
213
|
+
h5-to-swiftui/calibration@1; gate must equal the
|
|
214
|
+
gate recomputed from floor (gate-floor-mismatch);
|
|
215
|
+
calibration_source must match the bundled twins'
|
|
216
|
+
source-tree hashes (calibration-twin-mismatch).
|
|
217
|
+
--judge <path> { negative_control:{stimulus_source_hash,
|
|
218
|
+
divergent_pair,rejected,differences}, framing,
|
|
219
|
+
differences, verdict }. The legacy bare
|
|
220
|
+
negative_control:"passed" string is rejected
|
|
221
|
+
(unbound) β any YES VOID.
|
|
222
|
+
--component <Name> Component name (artifact + best-of-N scope)
|
|
223
|
+
|
|
224
|
+
Optional:
|
|
225
|
+
--masks <path> [{x,y,w,h,reason}]; every mask needs a non-empty reason
|
|
226
|
+
--component-area "WxH" or px integer; mask-fraction denominator
|
|
227
|
+
--out <path> Where to write convergence/<Name>.json
|
|
228
|
+
(default: stdout only)
|
|
229
|
+
--blocked <path> If present, component is 'blocked' (never converged)
|
|
230
|
+
|
|
231
|
+
Mechanically enforced guards (violation β downgrade + non-zero exit):
|
|
232
|
+
0 provenance: schema ok; gate == recompute(floor); calibration_source ==
|
|
233
|
+
bundled-twin source-tree hashes (else exit 1 gate-floor-mismatch /
|
|
234
|
+
calibration-twin-mismatch)
|
|
235
|
+
1 mask budget > 10% β needs-human; mask w/o reason β reject
|
|
236
|
+
2 gate eval in code, per iteration; iou null = FAIL; pHash never converges
|
|
237
|
+
3 monotone-or-fail best-of-N: built && gate_passed only; none β needs-human
|
|
238
|
+
4 negative control BOUND to bundled swiftui-twin-divergent (structured
|
|
239
|
+
{stimulus_source_hash,rejected,differences} + forced-difference-3);
|
|
240
|
+
unbound β any YES VOID, needs-human, negative-control-unbound
|
|
241
|
+
5 tiered verdict per visual-diff-loop-protocol.md
|
|
242
|
+
6 blocked.json present / no built+passing iter β never converged
|
|
243
|
+
|
|
244
|
+
Exit codes:
|
|
245
|
+
0 converged | close (pipeline may proceed)
|
|
246
|
+
1 fatal error (bad input, legacy string gate, schema mismatch,
|
|
247
|
+
gate-floor-mismatch, calibration-twin-mismatch, unverifiable mask)
|
|
248
|
+
3 needs-human | guard violation
|
|
249
|
+
4 blocked
|
|
250
|
+
(--help exits 0)
|
|
251
|
+
|
|
252
|
+
No npm deps (pure JSON; pngjs NOT required).
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
node evaluate-convergence.mjs --iterations iters.json \\
|
|
256
|
+
--calibration calibration.json --judge judge.json --component ProductCard
|
|
257
|
+
node evaluate-convergence.mjs --iterations .h5-to-swiftui/diff/ProductCard \\
|
|
258
|
+
--calibration calibration.json --judge judge.json --masks masks.json \\
|
|
259
|
+
--component-area 320x140 --component ProductCard \\
|
|
260
|
+
--out .h5-to-swiftui/convergence/ProductCard.json
|
|
261
|
+
`);
|
|
262
|
+
process.exit(0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getFlag(flag, fallback = null) {
|
|
266
|
+
const idx = args.indexOf(flag);
|
|
267
|
+
if (idx === -1) return fallback;
|
|
268
|
+
const val = args[idx + 1];
|
|
269
|
+
if (val === undefined || val.startsWith('--')) {
|
|
270
|
+
console.error(`Error: ${flag} requires an argument.`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
return val;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const iterationsArg = getFlag('--iterations');
|
|
277
|
+
const calibArg = getFlag('--calibration');
|
|
278
|
+
const judgeArg = getFlag('--judge');
|
|
279
|
+
const componentName = getFlag('--component');
|
|
280
|
+
const masksArg = getFlag('--masks');
|
|
281
|
+
const areaArg = getFlag('--component-area');
|
|
282
|
+
const outArg = getFlag('--out');
|
|
283
|
+
const blockedArg = getFlag('--blocked');
|
|
284
|
+
|
|
285
|
+
function fatal(msg) {
|
|
286
|
+
console.error(`Error: ${msg}\nRun with --help for usage.`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!iterationsArg) fatal('--iterations is required.');
|
|
291
|
+
if (!calibArg) fatal('--calibration is required.');
|
|
292
|
+
if (!judgeArg) fatal('--judge is required.');
|
|
293
|
+
if (!componentName) fatal('--component is required.');
|
|
294
|
+
|
|
295
|
+
const iterationsPath = resolve(iterationsArg);
|
|
296
|
+
const calibPath = resolve(calibArg);
|
|
297
|
+
const judgePath = resolve(judgeArg);
|
|
298
|
+
const masksPath = masksArg ? resolve(masksArg) : null;
|
|
299
|
+
const blockedPath = blockedArg ? resolve(blockedArg) : null;
|
|
300
|
+
|
|
301
|
+
if (!existsSync(iterationsPath)) fatal(`--iterations not found: ${iterationsPath}`);
|
|
302
|
+
if (!existsSync(calibPath)) fatal(`--calibration not found: ${calibPath}`);
|
|
303
|
+
if (!existsSync(judgePath)) fatal(`--judge not found: ${judgePath}`);
|
|
304
|
+
if (masksPath && !existsSync(masksPath)) fatal(`--masks not found: ${masksPath}`);
|
|
305
|
+
|
|
306
|
+
function readJson(p, label) {
|
|
307
|
+
try {
|
|
308
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
309
|
+
} catch (e) {
|
|
310
|
+
fatal(`cannot parse ${label} (${p}): ${e.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ββ Load calibration + validate STRUCTURED gate βββββββββββββββββββββββββββββββ
|
|
315
|
+
|
|
316
|
+
const calib = readJson(calibPath, '--calibration');
|
|
317
|
+
const gate = calib?.gate;
|
|
318
|
+
|
|
319
|
+
if (!gate || typeof gate !== 'object') {
|
|
320
|
+
fatal('calibration.json has no `gate` object.');
|
|
321
|
+
}
|
|
322
|
+
// Reject the legacy string DSL β it is not mechanically enforceable here.
|
|
323
|
+
if (typeof gate.converged === 'string' || typeof gate.close === 'string') {
|
|
324
|
+
fatal(
|
|
325
|
+
'calibration.json `gate.converged`/`gate.close` is a legacy STRING DSL. ' +
|
|
326
|
+
'evaluate-convergence.mjs requires the STRUCTURED numeric gate emitted by ' +
|
|
327
|
+
'the current calibrate-render.mjs (objects with ssim_nontext_min, ' +
|
|
328
|
+
'deltaE_p95_max, text_iou_min). Re-run calibration to regenerate.'
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const gateConverged = gate.converged;
|
|
332
|
+
const gateClose = gate.close;
|
|
333
|
+
if (!gateConverged || typeof gateConverged !== 'object'
|
|
334
|
+
|| !gateClose || typeof gateClose !== 'object') {
|
|
335
|
+
fatal('calibration.json `gate.converged`/`gate.close` must be numeric objects.');
|
|
336
|
+
}
|
|
337
|
+
for (const [name, g] of [['converged', gateConverged], ['close', gateClose]]) {
|
|
338
|
+
if (typeof g.ssim_nontext_min !== 'number'
|
|
339
|
+
|| typeof g.deltaE_p95_max !== 'number') {
|
|
340
|
+
fatal(`gate.${name} missing numeric ssim_nontext_min / deltaE_p95_max.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ββ FIX 1: provenance β schema + gate-recomputed-from-floor βββββββββββββββββββ
|
|
345
|
+
// A loose `gate` (tight-looking floor + hand-loosened gate) is one forge
|
|
346
|
+
// path. Close it: assert the calibration schema, then RECOMPUTE the
|
|
347
|
+
// expected structured gate from `calib.floor` using the SAME published
|
|
348
|
+
// tolerances calibrate-render.mjs uses, and reject any deviation beyond a
|
|
349
|
+
// tiny rounding epsilon. This binds the GATE to the floor: a hand-loosened
|
|
350
|
+
// gate is rejected unless the FLOOR itself is loosened. FIX 2 binds the
|
|
351
|
+
// twin SOURCE IDENTITY (not the floor value); FIX A asserts the floor value
|
|
352
|
+
// is within calibrate-render's own sanity envelope. A floor *within* that
|
|
353
|
+
// envelope but looser than the true measured floor remains trusted
|
|
354
|
+
// (residual (2), disclosed in the Trust-model header).
|
|
355
|
+
|
|
356
|
+
if (calib?.schema !== 'h5-to-swiftui/calibration@1') {
|
|
357
|
+
fatal(
|
|
358
|
+
`calibration.json schema is "${calib?.schema ?? 'absent'}" β expected ` +
|
|
359
|
+
'"h5-to-swiftui/calibration@1". Refusing to grade against an ' +
|
|
360
|
+
'unrecognized calibration contract.'
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const floor = calib?.floor;
|
|
365
|
+
if (!floor || typeof floor !== 'object'
|
|
366
|
+
|| typeof floor.ssim_nontext !== 'number'
|
|
367
|
+
|| typeof floor.deltaE_p95 !== 'number'
|
|
368
|
+
|| !(floor.text_iou === null || typeof floor.text_iou === 'number')) {
|
|
369
|
+
fatal(
|
|
370
|
+
'calibration.json `floor` must carry numeric ssim_nontext + deltaE_p95 ' +
|
|
371
|
+
'and a numeric|null text_iou β cannot recompute the expected gate from ' +
|
|
372
|
+
'an unverifiable floor.'
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ββ FIX A: floor must satisfy calibrate-render's OWN sanity envelope ββββββββββ
|
|
377
|
+
// `calibration_source` (FIX 2) binds the IDENTITY of the bundled twin source
|
|
378
|
+
// files but NOT the MEASURED floor values. Without this check an attacker
|
|
379
|
+
// could write an absurdly loose floor (e.g. ssim_nontext:0.05,
|
|
380
|
+
// deltaE_p95:200), copy the REAL public bundled-twin source hashes into
|
|
381
|
+
// `calibration_source`, let the gate recompute consistently from that absurd
|
|
382
|
+
// floor, and grade visually-broken output as `converged` with ZERO shipped
|
|
383
|
+
// files altered.
|
|
384
|
+
//
|
|
385
|
+
// calibrate-render.mjs ALREADY refuses to EMIT a floor that fails its sanity
|
|
386
|
+
// bound β it writes blocked.json instead of calibration.json. Therefore a
|
|
387
|
+
// calibration.json whose `floor` violates that SAME bound could not have been
|
|
388
|
+
// produced by an honest calibrate-render.mjs run. We reject it on consistency
|
|
389
|
+
// grounds. The envelope constants (SSIM_NONTEXT_MIN=0.95, TEXT_IOU_MIN=0.9)
|
|
390
|
+
// come from ./_calib-consts.mjs β the single source of truth calibrate-render
|
|
391
|
+
// also imports β so this assertion is provably the same bound the producer
|
|
392
|
+
// enforces; no new lenient constant is invented (see _calib-consts.mjs for
|
|
393
|
+
// the per-bound provenance: calibrate-render lines ~430/431 +
|
|
394
|
+
// render-equivalence-calibration.md Β§"Sanity bound" + findings RQ4).
|
|
395
|
+
//
|
|
396
|
+
// This check runs BEFORE the gate-from-floor recompute below, so a fabricated
|
|
397
|
+
// floor is rejected before any gate is derived from it. It does NOT close the
|
|
398
|
+
// residual entirely: a floor *within* this envelope yet looser than the true
|
|
399
|
+
// measured floor is still trusted (the grader cannot re-render to re-measure
|
|
400
|
+
// it) β that residual is disclosed honestly in the Trust-model header above
|
|
401
|
+
// and in references/*.md / SKILL.md.
|
|
402
|
+
{
|
|
403
|
+
const env = floorWithinCalibrateEnvelope(floor);
|
|
404
|
+
if (!env.ok) {
|
|
405
|
+
fatal(
|
|
406
|
+
'floor-implausible: calibration.json `floor` does not satisfy ' +
|
|
407
|
+
"calibrate-render.mjs's OWN sanity envelope, so it could not have " +
|
|
408
|
+
'been produced by an honest calibrate-render.mjs run (that script ' +
|
|
409
|
+
'writes blocked.json β not calibration.json β when the measured ' +
|
|
410
|
+
'floor fails this bound). Refusing to recompute a gate from a floor ' +
|
|
411
|
+
'no real calibration could emit. Details: ' + env.reasons.join('; ')
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Published tolerances β MUST stay in sync with calibrate-render.mjs.
|
|
417
|
+
const round4 = (v) => Math.round(v * 10000) / 10000;
|
|
418
|
+
const D_SSIM = 0.005;
|
|
419
|
+
const D_DELTAE = 0.4;
|
|
420
|
+
const D_IOU = 0.03;
|
|
421
|
+
const GATE_EPS = 1e-4; // rounding epsilon (round4 granularity)
|
|
422
|
+
|
|
423
|
+
const expectGate = {
|
|
424
|
+
converged: {
|
|
425
|
+
ssim_nontext_min: round4(floor.ssim_nontext - D_SSIM),
|
|
426
|
+
deltaE_p95_max: round4(floor.deltaE_p95 + D_DELTAE),
|
|
427
|
+
text_iou_min: floor.text_iou === null
|
|
428
|
+
? null : round4(floor.text_iou - D_IOU),
|
|
429
|
+
},
|
|
430
|
+
close: {
|
|
431
|
+
ssim_nontext_min: round4(floor.ssim_nontext - 2 * D_SSIM),
|
|
432
|
+
deltaE_p95_max: round4(floor.deltaE_p95 + 2 * D_DELTAE),
|
|
433
|
+
text_iou_min: floor.text_iou === null
|
|
434
|
+
? null : round4(floor.text_iou - 2 * D_IOU),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
function whyNot(v) { return v === undefined ? 'absent' : JSON.stringify(v); }
|
|
439
|
+
|
|
440
|
+
{
|
|
441
|
+
const mismatches = [];
|
|
442
|
+
for (const tier of ['converged', 'close']) {
|
|
443
|
+
const a = gate[tier], e = expectGate[tier];
|
|
444
|
+
for (const f of ['ssim_nontext_min', 'deltaE_p95_max', 'text_iou_min']) {
|
|
445
|
+
const exp = e[f];
|
|
446
|
+
const act = a?.[f];
|
|
447
|
+
if (exp === null) {
|
|
448
|
+
if (act !== null && act !== undefined) {
|
|
449
|
+
mismatches.push(
|
|
450
|
+
`gate.${tier}.${f}: expected null (floor.text_iou null) but ` +
|
|
451
|
+
`got ${whyNot(act)}`);
|
|
452
|
+
}
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (typeof act !== 'number') {
|
|
456
|
+
mismatches.push(
|
|
457
|
+
`gate.${tier}.${f}: expected ${exp} but got ${whyNot(act)}`);
|
|
458
|
+
} else if (Math.abs(act - exp) > GATE_EPS) {
|
|
459
|
+
mismatches.push(
|
|
460
|
+
`gate.${tier}.${f}: gate=${act} but floor implies ${exp} ` +
|
|
461
|
+
`(|Ξ|=${Math.abs(act - exp).toFixed(6)} > ${GATE_EPS})`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (mismatches.length > 0) {
|
|
466
|
+
fatal(
|
|
467
|
+
'gate-floor-mismatch: calibration.json `gate` does not match the ' +
|
|
468
|
+
'gate recomputed from `floor` with the published tolerances ' +
|
|
469
|
+
'(ssim β0.005, deltaE +0.4, text_iou β0.03; close = 2Γ band). A ' +
|
|
470
|
+
'hand-loosened gate is rejected. Re-run calibrate-render.mjs to ' +
|
|
471
|
+
'regenerate a consistent calibration. Details: ' + mismatches.join('; ')
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ββ FIX 2: provenance β calibration_source binds the bundled twin IDENTITY ββββ
|
|
477
|
+
// This binds the IDENTITY of the bundled twin SOURCE FILES (excluding build
|
|
478
|
+
// output/dotfiles, see _provenance.mjs): recompute the source-tree hash of
|
|
479
|
+
// the actual bundled assets and fail closed if calibration.json claims a
|
|
480
|
+
// different one. It does NOT re-measure or bind the `floor` *value* β an
|
|
481
|
+
// attacker who keeps the real public twin source hashes here but writes a
|
|
482
|
+
// loose floor is caught by FIX A's `floor-implausible` envelope check above
|
|
483
|
+
// only down to calibrate-render's own sanity floor; a floor within that
|
|
484
|
+
// envelope yet looser than the true measured one is residual (2) (disclosed
|
|
485
|
+
// in the Trust-model header β NOT claimed closed here).
|
|
486
|
+
|
|
487
|
+
const calibSource = calib?.calibration_source;
|
|
488
|
+
if (!calibSource || typeof calibSource !== 'object') {
|
|
489
|
+
fatal(
|
|
490
|
+
'calibration.json has no `calibration_source` object β cannot bind the ' +
|
|
491
|
+
'floor to the bundled twins. Re-run the current calibrate-render.mjs ' +
|
|
492
|
+
'(it emits calibration_source).'
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
// FIX E (lighter option chosen: DROP the mandatory-presence requirement).
|
|
496
|
+
// `twin_hashes` is the SHA-256 of the calibration's input ref/gen PNGs.
|
|
497
|
+
// Those PNGs are runtime-rendered and NOT byte-stable across machines, so
|
|
498
|
+
// the grader cannot and does not verify `twin_hashes` β it is non-security
|
|
499
|
+
// provenance metadata only (the enforced security binding is
|
|
500
|
+
// `calibration_source` source-tree hashes + FIX A's sanity envelope).
|
|
501
|
+
// Requiring its mere presence verified nothing and falsely inflated the
|
|
502
|
+
// apparent binding surface (a reviewer could infer it was load-bearing), so
|
|
503
|
+
// the presence gate is removed. It is still passed through to the artifact
|
|
504
|
+
// below when present, labelled as non-security metadata.
|
|
505
|
+
|
|
506
|
+
{
|
|
507
|
+
const actualH5 = sourceTreeHash(BUNDLED_H5_TWIN);
|
|
508
|
+
const actualSwiftUI = sourceTreeHash(BUNDLED_SWIFTUI_TWIN);
|
|
509
|
+
const claimedH5 = calibSource.h5_twin_source_sha256;
|
|
510
|
+
const claimedSwiftUI = calibSource.swiftui_twin_source_sha256;
|
|
511
|
+
const tw = [];
|
|
512
|
+
if (typeof claimedH5 !== 'string' || claimedH5 !== actualH5) {
|
|
513
|
+
tw.push(
|
|
514
|
+
`h5-twin: calibration claims ${whyNot(claimedH5)} but the bundled ` +
|
|
515
|
+
`${BUNDLED_H5_TWIN} source tree hashes to ${actualH5}`);
|
|
516
|
+
}
|
|
517
|
+
if (typeof claimedSwiftUI !== 'string' || claimedSwiftUI !== actualSwiftUI) {
|
|
518
|
+
tw.push(
|
|
519
|
+
`swiftui-twin: calibration claims ${whyNot(claimedSwiftUI)} but the ` +
|
|
520
|
+
`bundled ${BUNDLED_SWIFTUI_TWIN} source tree hashes to ${actualSwiftUI}`);
|
|
521
|
+
}
|
|
522
|
+
if (tw.length > 0) {
|
|
523
|
+
fatal(
|
|
524
|
+
'calibration-twin-mismatch: calibration.json `calibration_source` ' +
|
|
525
|
+
'does not match the SHIPPED, hash-pinned bundled calibration twins. ' +
|
|
526
|
+
'The measured floor must derive from a calibration run against the ' +
|
|
527
|
+
'real bundled pair β refusing to grade against an unbound floor. ' +
|
|
528
|
+
tw.join('; ')
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ββ Load judge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
534
|
+
|
|
535
|
+
const judge = readJson(judgePath, '--judge');
|
|
536
|
+
|
|
537
|
+
// ββ Load masks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
538
|
+
|
|
539
|
+
let masks = [];
|
|
540
|
+
if (masksPath) {
|
|
541
|
+
const raw = readJson(masksPath, '--masks');
|
|
542
|
+
if (!Array.isArray(raw)) fatal('--masks must be a JSON array of {x,y,w,h,reason}.');
|
|
543
|
+
masks = raw;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ββ Load iterations βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Normalize the --iterations input into [{ i, diff, built }].
|
|
550
|
+
* Form (a): a JSON file [{i,diff_json_path,built}, ...]
|
|
551
|
+
* Form (b): a directory of pixel-diff JSON files (built defaults true).
|
|
552
|
+
*/
|
|
553
|
+
function loadIterations(p) {
|
|
554
|
+
const st = statSync(p);
|
|
555
|
+
const out = [];
|
|
556
|
+
if (st.isDirectory()) {
|
|
557
|
+
const files = readdirSync(p)
|
|
558
|
+
.filter(f => f.endsWith('.json'))
|
|
559
|
+
.sort();
|
|
560
|
+
let idx = 1;
|
|
561
|
+
for (const f of files) {
|
|
562
|
+
const full = join(p, f);
|
|
563
|
+
const diff = readJson(full, `iteration diff (${f})`);
|
|
564
|
+
const m = f.match(/iter(?:ation)?[-_]?(\d+)/i);
|
|
565
|
+
out.push({
|
|
566
|
+
i: m ? parseInt(m[1], 10) : idx,
|
|
567
|
+
diff,
|
|
568
|
+
diff_json_path: full,
|
|
569
|
+
built: true, // directory mode cannot record non-building iters
|
|
570
|
+
});
|
|
571
|
+
idx++;
|
|
572
|
+
}
|
|
573
|
+
if (out.length === 0) {
|
|
574
|
+
fatal(`--iterations directory has no *.json files: ${p}`);
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// File mode
|
|
580
|
+
const spec = readJson(p, '--iterations');
|
|
581
|
+
if (!Array.isArray(spec)) {
|
|
582
|
+
fatal('--iterations file must be an array of {i,diff_json_path,built}.');
|
|
583
|
+
}
|
|
584
|
+
for (const entry of spec) {
|
|
585
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
586
|
+
fatal('--iterations entries must be objects.');
|
|
587
|
+
}
|
|
588
|
+
if (typeof entry.i !== 'number') {
|
|
589
|
+
fatal('--iterations entry missing numeric `i`.');
|
|
590
|
+
}
|
|
591
|
+
if (typeof entry.built !== 'boolean') {
|
|
592
|
+
fatal(`--iterations entry i=${entry.i} missing boolean \`built\`.`);
|
|
593
|
+
}
|
|
594
|
+
let diff = null;
|
|
595
|
+
if (entry.diff_json_path) {
|
|
596
|
+
const dp = resolve(dirname(p), entry.diff_json_path);
|
|
597
|
+
const dp2 = existsSync(dp) ? dp : resolve(entry.diff_json_path);
|
|
598
|
+
if (!existsSync(dp2)) {
|
|
599
|
+
fatal(`iteration i=${entry.i} diff_json_path not found: ${entry.diff_json_path}`);
|
|
600
|
+
}
|
|
601
|
+
diff = readJson(dp2, `iteration i=${entry.i} diff`);
|
|
602
|
+
} else if (entry.diff && typeof entry.diff === 'object') {
|
|
603
|
+
diff = entry.diff; // inline diff allowed
|
|
604
|
+
} else {
|
|
605
|
+
fatal(`iteration i=${entry.i} has neither diff_json_path nor inline diff.`);
|
|
606
|
+
}
|
|
607
|
+
out.push({ i: entry.i, diff, diff_json_path: entry.diff_json_path ?? null, built: entry.built });
|
|
608
|
+
}
|
|
609
|
+
if (out.length === 0) fatal('--iterations is empty.');
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const iterations = loadIterations(iterationsPath);
|
|
614
|
+
|
|
615
|
+
// ββ Component area (mask-fraction denominator) ββββββββββββββββββββββββββββββββ
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Resolve the component pixel area. Precedence:
|
|
619
|
+
* 1. --component-area "WxH" or px integer
|
|
620
|
+
* 2. derive from a diff's global frame if it exposes width*height
|
|
621
|
+
* Returns { area:number|null, source:string }.
|
|
622
|
+
*/
|
|
623
|
+
function resolveComponentArea() {
|
|
624
|
+
if (areaArg) {
|
|
625
|
+
const wh = areaArg.match(/^(\d+)\s*[xX*]\s*(\d+)$/);
|
|
626
|
+
if (wh) {
|
|
627
|
+
return { area: parseInt(wh[1], 10) * parseInt(wh[2], 10), source: `flag ${areaArg}` };
|
|
628
|
+
}
|
|
629
|
+
const px = areaArg.match(/^\d+$/);
|
|
630
|
+
if (px) return { area: parseInt(areaArg, 10), source: `flag ${areaArg}px` };
|
|
631
|
+
fatal(`--component-area must be "WxH" or a pixel integer (got "${areaArg}").`);
|
|
632
|
+
}
|
|
633
|
+
// Try to derive from a diff frame if any iteration exposes it.
|
|
634
|
+
for (const it of iterations) {
|
|
635
|
+
const d = it.diff;
|
|
636
|
+
if (d && typeof d.frame_w === 'number' && typeof d.frame_h === 'number') {
|
|
637
|
+
return { area: d.frame_w * d.frame_h, source: `diff frame i=${it.i}` };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return { area: null, source: 'unresolved' };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const { area: componentArea, source: areaSource } = resolveComponentArea();
|
|
644
|
+
|
|
645
|
+
// ββ GUARD 1: mask budget ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
646
|
+
|
|
647
|
+
const guardReasons = [];
|
|
648
|
+
let maskFraction = null;
|
|
649
|
+
let maskBudgetViolated = false;
|
|
650
|
+
|
|
651
|
+
for (const m of masks) {
|
|
652
|
+
if (typeof m.x !== 'number' || typeof m.y !== 'number'
|
|
653
|
+
|| typeof m.w !== 'number' || typeof m.h !== 'number') {
|
|
654
|
+
guardReasons.push('mask-malformed: a mask is missing numeric x/y/w/h');
|
|
655
|
+
maskBudgetViolated = true;
|
|
656
|
+
}
|
|
657
|
+
if (typeof m.reason !== 'string' || m.reason.trim() === '') {
|
|
658
|
+
guardReasons.push('mask-no-reason: every mask must declare a non-empty reason');
|
|
659
|
+
maskBudgetViolated = true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (masks.length > 0) {
|
|
664
|
+
if (componentArea === null || componentArea <= 0) {
|
|
665
|
+
// Cannot verify the budget β refuse to silently pass. This is fatal:
|
|
666
|
+
// an unverifiable mask budget is an input error the caller must fix.
|
|
667
|
+
fatal(
|
|
668
|
+
'masks supplied but component area is unknown (pass --component-area ' +
|
|
669
|
+
'"WxH"). Cannot verify the 10% mask budget β refusing to proceed ' +
|
|
670
|
+
'rather than silently pass.'
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const maskedPixels = masks.reduce(
|
|
674
|
+
(s, m) => s + Math.max(0, m.w) * Math.max(0, m.h), 0);
|
|
675
|
+
maskFraction = maskedPixels / componentArea;
|
|
676
|
+
maskFraction = Math.round(maskFraction * 10000) / 10000;
|
|
677
|
+
if (maskFraction > 0.10) {
|
|
678
|
+
maskBudgetViolated = true;
|
|
679
|
+
guardReasons.push(
|
|
680
|
+
`mask-budget: masked fraction ${maskFraction} > 0.10 ` +
|
|
681
|
+
`(${maskedPixels}px / ${componentArea}px area via ${areaSource})`);
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
maskFraction = 0;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ββ GUARD 2: gate eval in code, per iteration βββββββββββββββββββββββββββββββββ
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Evaluate the structured gate against one pixel-diff output.
|
|
691
|
+
* Rules (visual-diff-loop-protocol.md, made executable here):
|
|
692
|
+
* - non-text regions: every region's ssim >= g.ssim_nontext_min AND
|
|
693
|
+
* deltaE_p95 <= g.deltaE_p95_max; if no non-text regions, fall back to
|
|
694
|
+
* diff.global_ssim against ssim_nontext_min.
|
|
695
|
+
* - text regions: if g.text_iou_min is null the IoU sub-gate is SKIPPED
|
|
696
|
+
* (calibration had no bbox map). Otherwise EVERY text region must have a
|
|
697
|
+
* numeric iou >= g.text_iou_min. An iou of `null` is a FAIL, never a pass.
|
|
698
|
+
* - pHash is necessary-not-sufficient: phash_fast_candidate is NEVER a
|
|
699
|
+
* short-circuit to pass. The region gate above is the sole authority.
|
|
700
|
+
* Returns { passed:boolean, detail:string }.
|
|
701
|
+
*/
|
|
702
|
+
function evalGate(diff, g) {
|
|
703
|
+
const reasons = [];
|
|
704
|
+
if (!diff || typeof diff !== 'object') {
|
|
705
|
+
return { passed: false, detail: 'diff missing/not an object' };
|
|
706
|
+
}
|
|
707
|
+
const regions = diff.regions ?? {};
|
|
708
|
+
const nontext = Array.isArray(regions.nontext) ? regions.nontext : [];
|
|
709
|
+
const text = Array.isArray(regions.text) ? regions.text : [];
|
|
710
|
+
|
|
711
|
+
// Non-text sub-gate.
|
|
712
|
+
if (nontext.length > 0) {
|
|
713
|
+
for (const r of nontext) {
|
|
714
|
+
if (typeof r.ssim !== 'number' || r.ssim < g.ssim_nontext_min) {
|
|
715
|
+
reasons.push(
|
|
716
|
+
`nontext mark ${r.mark ?? '?'} ssim ${r.ssim ?? 'n/a'} < ` +
|
|
717
|
+
`${g.ssim_nontext_min}`);
|
|
718
|
+
}
|
|
719
|
+
if (typeof r.deltaE_p95 === 'number' && r.deltaE_p95 > g.deltaE_p95_max) {
|
|
720
|
+
reasons.push(
|
|
721
|
+
`nontext mark ${r.mark ?? '?'} deltaE_p95 ${r.deltaE_p95} > ` +
|
|
722
|
+
`${g.deltaE_p95_max}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
const gs = diff.global_ssim;
|
|
727
|
+
if (typeof gs !== 'number' || gs < g.ssim_nontext_min) {
|
|
728
|
+
reasons.push(
|
|
729
|
+
`no nontext regions; global_ssim ${gs ?? 'n/a'} < ` +
|
|
730
|
+
`${g.ssim_nontext_min}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Text sub-gate β only when calibration provided a text_iou floor.
|
|
735
|
+
if (g.text_iou_min !== null && g.text_iou_min !== undefined) {
|
|
736
|
+
if (text.length === 0) {
|
|
737
|
+
// No text regions measured but gate expects one β cannot confirm pass.
|
|
738
|
+
reasons.push(
|
|
739
|
+
`text_iou gate active (min ${g.text_iou_min}) but diff has no text ` +
|
|
740
|
+
`regions to verify`);
|
|
741
|
+
}
|
|
742
|
+
for (const r of text) {
|
|
743
|
+
if (r.iou === null || r.iou === undefined) {
|
|
744
|
+
reasons.push(
|
|
745
|
+
`text mark ${r.mark ?? '?'} iou is null β counts as FAIL ` +
|
|
746
|
+
`(no generated bbox supplied to pixel-diff)`);
|
|
747
|
+
} else if (typeof r.iou !== 'number' || r.iou < g.text_iou_min) {
|
|
748
|
+
reasons.push(
|
|
749
|
+
`text mark ${r.mark ?? '?'} iou ${r.iou} < ${g.text_iou_min}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
passed: reasons.length === 0,
|
|
756
|
+
detail: reasons.length === 0 ? 'all sub-gates passed' : reasons.join('; '),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const history = [];
|
|
761
|
+
for (const it of iterations) {
|
|
762
|
+
const conv = evalGate(it.diff, gateConverged);
|
|
763
|
+
const cls = evalGate(it.diff, gateClose);
|
|
764
|
+
history.push({
|
|
765
|
+
i: it.i,
|
|
766
|
+
built: it.built,
|
|
767
|
+
gate_passed: it.built && conv.passed, // gate only counts if built
|
|
768
|
+
close_band_passed: it.built && cls.passed,
|
|
769
|
+
phash_fast_candidate: it.diff?.phash_fast_candidate === true,
|
|
770
|
+
gate_detail: conv.detail,
|
|
771
|
+
diff_json_path: it.diff_json_path,
|
|
772
|
+
diff_summary: {
|
|
773
|
+
global_ssim: it.diff?.global_ssim ?? null,
|
|
774
|
+
phash_hamming: it.diff?.phash_hamming ?? null,
|
|
775
|
+
diff_pixel_fraction: it.diff?.diff_pixel_fraction ?? null,
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ββ GUARD 3: monotone-or-fail best-of-N (this script's choice) ββββββββββββββββ
|
|
781
|
+
// Only iterations that are BOTH built==true AND gate_passed==true are eligible.
|
|
782
|
+
// Caller-supplied best_iteration is ignored entirely.
|
|
783
|
+
|
|
784
|
+
const eligible = history.filter(h => h.built === true && h.gate_passed === true);
|
|
785
|
+
let bestIteration = null;
|
|
786
|
+
if (eligible.length > 0) {
|
|
787
|
+
// Pick the eligible iteration with the highest global_ssim, then lowest
|
|
788
|
+
// diff_pixel_fraction, then highest index (latest correction wins ties).
|
|
789
|
+
eligible.sort((a, b) => {
|
|
790
|
+
const sa = a.diff_summary.global_ssim ?? -Infinity;
|
|
791
|
+
const sb = b.diff_summary.global_ssim ?? -Infinity;
|
|
792
|
+
if (sb !== sa) return sb - sa;
|
|
793
|
+
const fa = a.diff_summary.diff_pixel_fraction ?? Infinity;
|
|
794
|
+
const fb = b.diff_summary.diff_pixel_fraction ?? Infinity;
|
|
795
|
+
if (fa !== fb) return fa - fb;
|
|
796
|
+
return b.i - a.i;
|
|
797
|
+
});
|
|
798
|
+
bestIteration = eligible[0].i;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// close-band best (used only if no converged-eligible iteration exists)
|
|
802
|
+
const closeEligible = history.filter(
|
|
803
|
+
h => h.built === true && h.close_band_passed === true);
|
|
804
|
+
let bestCloseIteration = null;
|
|
805
|
+
if (closeEligible.length > 0) {
|
|
806
|
+
closeEligible.sort((a, b) =>
|
|
807
|
+
(b.diff_summary.global_ssim ?? -Infinity) -
|
|
808
|
+
(a.diff_summary.global_ssim ?? -Infinity) || b.i - a.i);
|
|
809
|
+
bestCloseIteration = closeEligible[0].i;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// A built result candidate is either a converged-gate-passing iteration OR a
|
|
813
|
+
// built close-band-passing iteration. `needs-human` for "no passing iteration"
|
|
814
|
+
// only when NEITHER exists β a close-band iteration is still a legitimate
|
|
815
|
+
// (honest-accept) candidate and must not be force-failed before the close tier
|
|
816
|
+
// is even evaluated.
|
|
817
|
+
const hasBuiltPassing = bestIteration !== null;
|
|
818
|
+
const hasAnyBuiltCandidate = bestIteration !== null || bestCloseIteration !== null;
|
|
819
|
+
|
|
820
|
+
// ββ GUARD 4: negative control voids judge β BOUND to the bundled twin βββββββββ
|
|
821
|
+
// FIX 3: the negative control must be a STRUCTURED artifact bound to the
|
|
822
|
+
// shipped, hash-pinned `assets/calibration/swiftui-twin-divergent`. The old
|
|
823
|
+
// bare `negative_control:"passed"` string is no longer accepted (it asserts
|
|
824
|
+
// a judge run that nothing verified) β it is treated as UNBOUND β any YES
|
|
825
|
+
// is VOID. The judge must demonstrate it rejected the KNOWN-divergent pair
|
|
826
|
+
// (whose stimulus hash this script recomputes from the shipped asset) under
|
|
827
|
+
// the adversarial forced-difference-3 framing with structured differences.
|
|
828
|
+
//
|
|
829
|
+
// judge.negative_control = {
|
|
830
|
+
// stimulus_source_hash: "<sha256 source-tree hash of the bundled
|
|
831
|
+
// swiftui-twin-divergent>",
|
|
832
|
+
// divergent_pair: "h5-twin vs swiftui-twin-divergent",
|
|
833
|
+
// rejected: true,
|
|
834
|
+
// differences: [ { desc, severity }, ... ] // non-empty, structured
|
|
835
|
+
// }
|
|
836
|
+
|
|
837
|
+
const expectedDivergentHash = sourceTreeHash(BUNDLED_DIVERGENT_TWIN);
|
|
838
|
+
const nc = judge?.negative_control;
|
|
839
|
+
const ncReasons = [];
|
|
840
|
+
|
|
841
|
+
if (typeof nc === 'string') {
|
|
842
|
+
ncReasons.push(
|
|
843
|
+
`negative-control-unbound: judge.negative_control is the legacy bare ` +
|
|
844
|
+
`string "${nc}" β an unbound assertion of a judge run nothing verified. ` +
|
|
845
|
+
`A STRUCTURED, hash-bound negative_control object is required; any YES ` +
|
|
846
|
+
`is VOID`);
|
|
847
|
+
} else if (!nc || typeof nc !== 'object') {
|
|
848
|
+
ncReasons.push(
|
|
849
|
+
`negative-control-unbound: judge.negative_control is ` +
|
|
850
|
+
`${nc === undefined ? 'absent' : 'not an object'} β a STRUCTURED, ` +
|
|
851
|
+
`hash-bound negative_control object is required; any YES is VOID`);
|
|
852
|
+
} else {
|
|
853
|
+
if (typeof nc.stimulus_source_hash !== 'string'
|
|
854
|
+
|| nc.stimulus_source_hash !== expectedDivergentHash) {
|
|
855
|
+
ncReasons.push(
|
|
856
|
+
`negative-control-unbound: negative_control.stimulus_source_hash=` +
|
|
857
|
+
`${whyNot(nc.stimulus_source_hash)} != the bundled ` +
|
|
858
|
+
`swiftui-twin-divergent source-tree hash ${expectedDivergentHash} ` +
|
|
859
|
+
`(${BUNDLED_DIVERGENT_TWIN}) β the judge was not shown the shipped, ` +
|
|
860
|
+
`hash-pinned divergent stimulus; any YES is VOID`);
|
|
861
|
+
}
|
|
862
|
+
if (nc.rejected !== true) {
|
|
863
|
+
ncReasons.push(
|
|
864
|
+
`negative-control-unbound: negative_control.rejected=` +
|
|
865
|
+
`${whyNot(nc.rejected)} (must be boolean true β the judge MUST have ` +
|
|
866
|
+
`rejected the known-divergent pair); any YES is VOID`);
|
|
867
|
+
}
|
|
868
|
+
if (!Array.isArray(nc.differences) || nc.differences.length === 0
|
|
869
|
+
|| !nc.differences.every(d =>
|
|
870
|
+
d && typeof d === 'object'
|
|
871
|
+
&& typeof d.desc === 'string' && d.desc.trim() !== ''
|
|
872
|
+
&& typeof d.severity === 'string' && d.severity.trim() !== '')) {
|
|
873
|
+
ncReasons.push(
|
|
874
|
+
`negative-control-unbound: negative_control.differences must be a ` +
|
|
875
|
+
`non-empty array of structured {desc,severity} entries (proof the ` +
|
|
876
|
+
`judge enumerated real divergences, not a bare pass); any YES is VOID`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (judge?.framing !== 'forced-difference-3') {
|
|
881
|
+
ncReasons.push(
|
|
882
|
+
`judge-framing: judge.framing="${judge?.framing ?? 'absent'}" != ` +
|
|
883
|
+
`"forced-difference-3" β adversarial framing not confirmed; YES is VOID`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const negControlPassed = ncReasons.length === 0;
|
|
887
|
+
const negControlResult = negControlPassed ? 'passed' : 'failed';
|
|
888
|
+
if (!negControlPassed) {
|
|
889
|
+
for (const r of ncReasons) guardReasons.push(r);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const judgeVerdictRaw = String(judge?.verdict ?? '').trim();
|
|
893
|
+
const judgeYes = negControlPassed && judgeVerdictRaw.toUpperCase() === 'YES';
|
|
894
|
+
const judgeEquiv = negControlPassed &&
|
|
895
|
+
judgeVerdictRaw.toLowerCase() === 'visually-equivalent-residual-subperceptual';
|
|
896
|
+
|
|
897
|
+
// ββ GUARD 6 + tier decision βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
898
|
+
|
|
899
|
+
const blockedPresent = blockedPath !== null && existsSync(blockedPath);
|
|
900
|
+
|
|
901
|
+
let tier;
|
|
902
|
+
let tierReason;
|
|
903
|
+
|
|
904
|
+
if (blockedPresent) {
|
|
905
|
+
tier = 'blocked';
|
|
906
|
+
tierReason = `blocked.json present (${blockedPath}) β never converged`;
|
|
907
|
+
} else if (maskBudgetViolated) {
|
|
908
|
+
tier = 'needs-human';
|
|
909
|
+
tierReason = `mask-guard violated: ${guardReasons.join(' | ')}`;
|
|
910
|
+
} else if (!hasAnyBuiltCandidate) {
|
|
911
|
+
tier = 'needs-human';
|
|
912
|
+
tierReason =
|
|
913
|
+
'no iteration is BOTH built and gate/close-band passing ' +
|
|
914
|
+
'(monotone-or-fail): ' +
|
|
915
|
+
history.map(h =>
|
|
916
|
+
`i${h.i}[built=${h.built},gate=${h.gate_passed},` +
|
|
917
|
+
`close=${h.close_band_passed}]`).join(' ');
|
|
918
|
+
} else if (!negControlPassed) {
|
|
919
|
+
tier = 'needs-human';
|
|
920
|
+
tierReason =
|
|
921
|
+
'negative control not bound / adversarial framing not satisfied β ' +
|
|
922
|
+
`judge verdict VOID: ${ncReasons.join(' | ')}`;
|
|
923
|
+
} else if (hasBuiltPassing && judgeYes) {
|
|
924
|
+
tier = 'converged';
|
|
925
|
+
tierReason =
|
|
926
|
+
`gate_passed(best i=${bestIteration}) AND judge YES with valid ` +
|
|
927
|
+
`negative control`;
|
|
928
|
+
} else if (bestCloseIteration !== null && judgeEquiv) {
|
|
929
|
+
tier = 'close';
|
|
930
|
+
// The close-band iteration is the result candidate for this tier.
|
|
931
|
+
bestIteration = bestCloseIteration;
|
|
932
|
+
tierReason =
|
|
933
|
+
`within calibration close band (best i=${bestCloseIteration}) AND judge ` +
|
|
934
|
+
`visually-equivalent-residual-subperceptual (honest accept)`;
|
|
935
|
+
} else {
|
|
936
|
+
tier = 'needs-human';
|
|
937
|
+
tierReason = hasBuiltPassing
|
|
938
|
+
? `gate-passing iteration exists (i=${bestIteration}) but judge verdict ` +
|
|
939
|
+
`"${judgeVerdictRaw || 'absent'}" is not YES`
|
|
940
|
+
: `only a close-band iteration exists (i=${bestCloseIteration}) but ` +
|
|
941
|
+
`judge verdict "${judgeVerdictRaw || 'absent'}" is not ` +
|
|
942
|
+
`visually-equivalent-residual-subperceptual`;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ββ Residual (from the chosen best iteration's diff) ββββββββββββββββββββββββββ
|
|
946
|
+
|
|
947
|
+
function residualFor(iterIndex) {
|
|
948
|
+
if (iterIndex === null) return null;
|
|
949
|
+
const it = iterations.find(x => x.i === iterIndex);
|
|
950
|
+
if (!it || !it.diff) return null;
|
|
951
|
+
const d = it.diff;
|
|
952
|
+
const nontext = d.regions?.nontext ?? [];
|
|
953
|
+
const text = d.regions?.text ?? [];
|
|
954
|
+
const minSsim = nontext.length
|
|
955
|
+
? Math.min(...nontext.map(r => (typeof r.ssim === 'number' ? r.ssim : 1)))
|
|
956
|
+
: (typeof d.global_ssim === 'number' ? d.global_ssim : null);
|
|
957
|
+
const maxDeltaE = nontext.length
|
|
958
|
+
? Math.max(...nontext.map(r => (typeof r.deltaE_p95 === 'number' ? r.deltaE_p95 : 0)))
|
|
959
|
+
: null;
|
|
960
|
+
const ious = text
|
|
961
|
+
.map(r => r.iou)
|
|
962
|
+
.filter(v => typeof v === 'number');
|
|
963
|
+
const minIou = ious.length ? Math.min(...ious) : null;
|
|
964
|
+
return {
|
|
965
|
+
ssim_nontext: minSsim,
|
|
966
|
+
deltaE_p95: maxDeltaE,
|
|
967
|
+
text_iou: minIou,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// ββ Assemble artifact (schema: h5-to-swiftui/convergence@1) βββββββββββββββββββ
|
|
972
|
+
|
|
973
|
+
const artifact = {
|
|
974
|
+
schema: 'h5-to-swiftui/convergence@1',
|
|
975
|
+
component: componentName,
|
|
976
|
+
// pinned-version header pass-through from calibration (do not re-derive)
|
|
977
|
+
pinned: calib.pinned ?? null,
|
|
978
|
+
calibration_floor: calib.floor
|
|
979
|
+
? {
|
|
980
|
+
ssim_nontext: calib.floor.ssim_nontext ?? null,
|
|
981
|
+
deltaE_p95: calib.floor.deltaE_p95 ?? null,
|
|
982
|
+
text_iou: calib.floor.text_iou ?? null,
|
|
983
|
+
}
|
|
984
|
+
: null,
|
|
985
|
+
gate: { converged: gateConverged, close: gateClose },
|
|
986
|
+
iterations: history.map(h => ({
|
|
987
|
+
i: h.i,
|
|
988
|
+
diff: h.diff_summary,
|
|
989
|
+
built: h.built,
|
|
990
|
+
gate_passed: h.gate_passed,
|
|
991
|
+
close_band_passed: h.close_band_passed,
|
|
992
|
+
phash_fast_candidate: h.phash_fast_candidate,
|
|
993
|
+
gate_detail: h.gate_detail,
|
|
994
|
+
...(h.diff_json_path ? { diff_json_path: h.diff_json_path } : {}),
|
|
995
|
+
})),
|
|
996
|
+
masks: masks.map(m => ({
|
|
997
|
+
x: m.x, y: m.y, w: m.w, h: m.h, reason: m.reason,
|
|
998
|
+
})),
|
|
999
|
+
mask_fraction: maskFraction,
|
|
1000
|
+
mask_budget: 0.10,
|
|
1001
|
+
component_area_px: componentArea,
|
|
1002
|
+
component_area_source: areaSource,
|
|
1003
|
+
judge: {
|
|
1004
|
+
negative_control: negControlResult,
|
|
1005
|
+
negative_control_binding: {
|
|
1006
|
+
expected_divergent_source_sha256: expectedDivergentHash,
|
|
1007
|
+
claimed_stimulus_source_hash:
|
|
1008
|
+
(nc && typeof nc === 'object') ? (nc.stimulus_source_hash ?? null)
|
|
1009
|
+
: null,
|
|
1010
|
+
rejected: (nc && typeof nc === 'object') ? (nc.rejected ?? null) : null,
|
|
1011
|
+
bound: negControlPassed,
|
|
1012
|
+
reasons: ncReasons,
|
|
1013
|
+
},
|
|
1014
|
+
framing: judge?.framing ?? null,
|
|
1015
|
+
differences: Array.isArray(judge?.differences) ? judge.differences : [],
|
|
1016
|
+
verdict: judgeVerdictRaw || null,
|
|
1017
|
+
verdict_honored: negControlPassed,
|
|
1018
|
+
},
|
|
1019
|
+
// Provenance binding asserted by this run. Precisely: the GATE was
|
|
1020
|
+
// recomputed from `floor`; the bundled twin SOURCE IDENTITY was verified;
|
|
1021
|
+
// the `floor` VALUE was asserted within calibrate-render's own sanity
|
|
1022
|
+
// envelope. The floor value itself is NOT re-measured by the grader β see
|
|
1023
|
+
// `residual_disclosure` below for the two named irreducible residuals.
|
|
1024
|
+
calibration_provenance: {
|
|
1025
|
+
schema_ok: true,
|
|
1026
|
+
gate_recomputed_from_floor: true,
|
|
1027
|
+
floor_within_calibrate_sanity_envelope: true, // FIX A: floor-implausible
|
|
1028
|
+
calibration_source: {
|
|
1029
|
+
h5_twin_source_sha256: calibSource.h5_twin_source_sha256 ?? null,
|
|
1030
|
+
swiftui_twin_source_sha256:
|
|
1031
|
+
calibSource.swiftui_twin_source_sha256 ?? null,
|
|
1032
|
+
// binds the twin SOURCE-FILE identity (excl. build output/dotfiles),
|
|
1033
|
+
// NOT the measured floor value
|
|
1034
|
+
twin_source_identity_verified_against_bundled: true,
|
|
1035
|
+
},
|
|
1036
|
+
// FIX E: twin_hashes is the SHA-256 of runtime-rendered ref/gen PNGs
|
|
1037
|
+
// which are NOT byte-stable across machines; it is non-security
|
|
1038
|
+
// provenance metadata only (NOT verified by the grader; the security
|
|
1039
|
+
// binding is calibration_source + the sanity envelope).
|
|
1040
|
+
twin_hashes_note:
|
|
1041
|
+
'non-security provenance metadata (input PNG hashes, not byte-stable ' +
|
|
1042
|
+
'across machines, not verified by the grader)',
|
|
1043
|
+
twin_hashes: (calib && typeof calib.twin_hashes === 'object')
|
|
1044
|
+
? calib.twin_hashes : null,
|
|
1045
|
+
// FIX C: BOTH named irreducible residuals, disclosed in the artifact.
|
|
1046
|
+
residual_disclosure: {
|
|
1047
|
+
residual_1_renders_not_re_executed:
|
|
1048
|
+
'the grader cannot re-execute the simulator renders; it trusts the ' +
|
|
1049
|
+
'per-iteration pixel-diff JSONs came from real pixel-diff.mjs on ' +
|
|
1050
|
+
'real sim-screenshot.sh renders (bounded by that script no-fake spine)',
|
|
1051
|
+
residual_2_floor_not_re_measured:
|
|
1052
|
+
'the grader cannot re-measure the calibration floor; it asserts the ' +
|
|
1053
|
+
"supplied floor is within calibrate-render.mjs's own sanity envelope " +
|
|
1054
|
+
'and recomputes the gate from it, but a floor within that envelope ' +
|
|
1055
|
+
'yet looser than the true measured floor is trusted (mitigated by ' +
|
|
1056
|
+
'the orchestrator obligation to run the real calibrate-render.mjs ' +
|
|
1057
|
+
'and this calibration_provenance record)',
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
guard_violations: guardReasons,
|
|
1061
|
+
best_iteration: bestIteration,
|
|
1062
|
+
tier,
|
|
1063
|
+
tier_reason: tierReason,
|
|
1064
|
+
residual: residualFor(bestIteration),
|
|
1065
|
+
evaluated_at: new Date().toISOString(),
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
const json = JSON.stringify(artifact, null, 2) + '\n';
|
|
1069
|
+
|
|
1070
|
+
if (outArg) {
|
|
1071
|
+
const outPath = resolve(outArg);
|
|
1072
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
1073
|
+
writeFileSync(outPath, json, 'utf8');
|
|
1074
|
+
console.error(`Wrote convergence artifact: ${outPath}`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
process.stdout.write(json);
|
|
1078
|
+
|
|
1079
|
+
// ββ Exit code reflects the verdict (a pipeline cannot ignore a failure) βββββββ
|
|
1080
|
+
|
|
1081
|
+
console.error(
|
|
1082
|
+
`tier=${tier} best_iteration=${bestIteration ?? 'none'} ` +
|
|
1083
|
+
`mask_fraction=${maskFraction} negative_control=${negControlResult}`);
|
|
1084
|
+
console.error(`reason: ${tierReason}`);
|
|
1085
|
+
|
|
1086
|
+
if (tier === 'converged' || tier === 'close') {
|
|
1087
|
+
process.exit(0);
|
|
1088
|
+
}
|
|
1089
|
+
if (tier === 'blocked') {
|
|
1090
|
+
process.exit(4);
|
|
1091
|
+
}
|
|
1092
|
+
// needs-human or any guard violation
|
|
1093
|
+
process.exit(3);
|