opencode-skills-collection 4.0.68 → 4.0.70
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/bundled-skills/.antigravity-install-manifest.json +304 -1
- package/bundled-skills/access-review/SKILL.md +394 -0
- package/bundled-skills/access-review/references/details.md +121 -0
- package/bundled-skills/agent-evals/SKILL.md +420 -0
- package/bundled-skills/agent-observability/SKILL.md +346 -0
- package/bundled-skills/agent-observability/references/details.md +786 -0
- package/bundled-skills/ai-agent-security/SKILL.md +393 -0
- package/bundled-skills/ai-agent-security/references/details.md +912 -0
- package/bundled-skills/ai-coding-agent-guardrails/SKILL.md +442 -0
- package/bundled-skills/ai-coding-agent-guardrails/references/details.md +753 -0
- package/bundled-skills/ai-inference-service-mesh/SKILL.md +449 -0
- package/bundled-skills/ai-pipeline-orchestration/SKILL.md +287 -0
- package/bundled-skills/ai-red-teaming/SKILL.md +409 -0
- package/bundled-skills/ai-security-hardening/SKILL.md +343 -0
- package/bundled-skills/ai-sre-incident-response/SKILL.md +336 -0
- package/bundled-skills/alerting-oncall/SKILL.md +458 -0
- package/bundled-skills/alerting-oncall/references/details.md +84 -0
- package/bundled-skills/api-integration-architect/SKILL.md +241 -0
- package/bundled-skills/apify-generate-output-schema/SKILL.md +438 -0
- package/bundled-skills/apify-integration-development/SKILL.md +168 -0
- package/bundled-skills/apify-integration-development/references/ai-framework-package.md +158 -0
- package/bundled-skills/apify-integration-development/references/ai-harness-plugin.md +192 -0
- package/bundled-skills/apify-integration-development/references/sdk-integration.md +236 -0
- package/bundled-skills/apify-integration-development/references/workflow-automation.md +163 -0
- package/bundled-skills/apk-redteam-pipeline/SKILL.md +446 -0
- package/bundled-skills/architecture-review/README.md +42 -0
- package/bundled-skills/architecture-review/SKILL.md +77 -0
- package/bundled-skills/architecture-review/examples.md +11 -0
- package/bundled-skills/architecture-review/reference/best-practices.md +7 -0
- package/bundled-skills/architecture-review/reference/capabilities.md +20 -0
- package/bundled-skills/architecture-review/reference/fallbacks.md +11 -0
- package/bundled-skills/architecture-review/reference/graph.md +15 -0
- package/bundled-skills/architecture-review/reference/mcp.md +14 -0
- package/bundled-skills/architecture-review/reference/workflow.md +15 -0
- package/bundled-skills/architecture-review/templates/architecture-review.md +21 -0
- package/bundled-skills/argocd-gitops/SKILL.md +469 -0
- package/bundled-skills/arm-templates/SKILL.md +438 -0
- package/bundled-skills/arm-templates/references/details.md +64 -0
- package/bundled-skills/asset-inventory/SKILL.md +412 -0
- package/bundled-skills/asset-inventory/references/details.md +127 -0
- package/bundled-skills/audit-logging/SKILL.md +476 -0
- package/bundled-skills/aws-cloudtrail/SKILL.md +486 -0
- package/bundled-skills/aws-cost-optimization/SKILL.md +331 -0
- package/bundled-skills/aws-ec2/SKILL.md +426 -0
- package/bundled-skills/aws-ecs-fargate/SKILL.md +388 -0
- package/bundled-skills/aws-iam/SKILL.md +463 -0
- package/bundled-skills/aws-lambda/SKILL.md +428 -0
- package/bundled-skills/aws-rds/SKILL.md +380 -0
- package/bundled-skills/aws-s3/SKILL.md +434 -0
- package/bundled-skills/aws-secrets-manager/SKILL.md +486 -0
- package/bundled-skills/aws-vpc/SKILL.md +436 -0
- package/bundled-skills/azure-ai-document-intelligence-ts/SKILL.md +1 -1
- package/bundled-skills/azure-aks/SKILL.md +423 -0
- package/bundled-skills/azure-devops/SKILL.md +457 -0
- package/bundled-skills/azure-functions-devsec/SKILL.md +436 -0
- package/bundled-skills/azure-keyvault/SKILL.md +455 -0
- package/bundled-skills/azure-keyvault/references/details.md +83 -0
- package/bundled-skills/azure-monitor-audit/SKILL.md +379 -0
- package/bundled-skills/azure-networking/SKILL.md +448 -0
- package/bundled-skills/azure-networking/references/details.md +135 -0
- package/bundled-skills/azure-sql/SKILL.md +413 -0
- package/bundled-skills/azure-sql/references/details.md +113 -0
- package/bundled-skills/azure-vms/SKILL.md +402 -0
- package/bundled-skills/azure-vms/references/details.md +134 -0
- package/bundled-skills/backup-recovery/SKILL.md +388 -0
- package/bundled-skills/bb-methodology/SKILL.md +451 -0
- package/bundled-skills/bb-methodology/references/details.md +120 -0
- package/bundled-skills/block-storage/SKILL.md +371 -0
- package/bundled-skills/blue-green-deploy/SKILL.md +453 -0
- package/bundled-skills/blue-green-deploy/references/details.md +90 -0
- package/bundled-skills/bug-bounty/SKILL.md +447 -0
- package/bundled-skills/bug-bounty/references/details.md +1316 -0
- package/bundled-skills/bugcrowd-reporting/SKILL.md +351 -0
- package/bundled-skills/business-continuity/SKILL.md +463 -0
- package/bundled-skills/career-ops/SKILL.md +186 -0
- package/bundled-skills/cdn-setup/SKILL.md +374 -0
- package/bundled-skills/change-management/SKILL.md +438 -0
- package/bundled-skills/change-management/references/details.md +105 -0
- package/bundled-skills/circleci/SKILL.md +475 -0
- package/bundled-skills/cis-benchmarks/SKILL.md +150 -0
- package/bundled-skills/cloudflare-pages/SKILL.md +318 -0
- package/bundled-skills/cloudflare-r2/SKILL.md +353 -0
- package/bundled-skills/cloudflare-workers/SKILL.md +415 -0
- package/bundled-skills/cloudflare-zero-trust/SKILL.md +361 -0
- package/bundled-skills/cloudformation/SKILL.md +461 -0
- package/bundled-skills/code-review-sensei/SKILL.md +177 -0
- package/bundled-skills/codebase-onboarding/README.md +42 -0
- package/bundled-skills/codebase-onboarding/SKILL.md +77 -0
- package/bundled-skills/codebase-onboarding/examples.md +11 -0
- package/bundled-skills/codebase-onboarding/reference/best-practices.md +7 -0
- package/bundled-skills/codebase-onboarding/reference/capabilities.md +20 -0
- package/bundled-skills/codebase-onboarding/reference/fallbacks.md +11 -0
- package/bundled-skills/codebase-onboarding/reference/graph.md +15 -0
- package/bundled-skills/codebase-onboarding/reference/mcp.md +14 -0
- package/bundled-skills/codebase-onboarding/reference/workflow.md +15 -0
- package/bundled-skills/codebase-onboarding/templates/repository-onboarding.md +21 -0
- package/bundled-skills/connection-auth-rules/SKILL.md +199 -0
- package/bundled-skills/connection-auth-rules/fetch_schema.py +320 -0
- package/bundled-skills/constraint-driven-development/SKILL.md +335 -0
- package/bundled-skills/constraint-driven-development/references/floor-guard.md +99 -0
- package/bundled-skills/container-hardening/SKILL.md +126 -0
- package/bundled-skills/container-registries/SKILL.md +435 -0
- package/bundled-skills/container-scanning/SKILL.md +416 -0
- package/bundled-skills/convex-backend/SKILL.md +338 -0
- package/bundled-skills/dast-scanning/SKILL.md +437 -0
- package/bundled-skills/database-backups/SKILL.md +425 -0
- package/bundled-skills/datadog/SKILL.md +487 -0
- package/bundled-skills/dependency-analysis/README.md +42 -0
- package/bundled-skills/dependency-analysis/SKILL.md +76 -0
- package/bundled-skills/dependency-analysis/examples.md +11 -0
- package/bundled-skills/dependency-analysis/reference/best-practices.md +7 -0
- package/bundled-skills/dependency-analysis/reference/capabilities.md +20 -0
- package/bundled-skills/dependency-analysis/reference/fallbacks.md +11 -0
- package/bundled-skills/dependency-analysis/reference/graph.md +15 -0
- package/bundled-skills/dependency-analysis/reference/mcp.md +14 -0
- package/bundled-skills/dependency-analysis/reference/workflow.md +15 -0
- package/bundled-skills/dependency-analysis/templates/dependency-review.md +21 -0
- package/bundled-skills/dependency-scanning/SKILL.md +457 -0
- package/bundled-skills/devcontainers-nix/SKILL.md +416 -0
- package/bundled-skills/devops-pipeline-builder/SKILL.md +200 -0
- package/bundled-skills/disaster-recovery/SKILL.md +374 -0
- package/bundled-skills/disaster-recovery/references/details.md +219 -0
- package/bundled-skills/dns-management/SKILL.md +375 -0
- package/bundled-skills/docker-compose/SKILL.md +482 -0
- package/bundled-skills/docker-management/SKILL.md +426 -0
- package/bundled-skills/eas-app-stores/SKILL.md +197 -0
- package/bundled-skills/eas-app-stores/agents/openai.yaml +4 -0
- package/bundled-skills/eas-app-stores/references/app-store-metadata.md +497 -0
- package/bundled-skills/eas-app-stores/references/ios-app-store.md +376 -0
- package/bundled-skills/eas-app-stores/references/native-ios.md +167 -0
- package/bundled-skills/eas-app-stores/references/play-store.md +244 -0
- package/bundled-skills/eas-app-stores/references/testflight.md +62 -0
- package/bundled-skills/eas-app-stores/references/workflows.md +120 -0
- package/bundled-skills/eas-hosting/SKILL.md +448 -0
- package/bundled-skills/eas-hosting/agents/openai.yaml +4 -0
- package/bundled-skills/eas-observe/SKILL.md +75 -0
- package/bundled-skills/eas-observe/agents/openai.yaml +4 -0
- package/bundled-skills/eas-observe/references/metrics.md +98 -0
- package/bundled-skills/eas-observe/references/queries.md +403 -0
- package/bundled-skills/eas-observe/references/setup.md +476 -0
- package/bundled-skills/eas-observe/references/third-party.md +136 -0
- package/bundled-skills/eas-simulator/SKILL.md +251 -0
- package/bundled-skills/eas-simulator/agents/openai.yaml +4 -0
- package/bundled-skills/eas-simulator/references/controllers.md +135 -0
- package/bundled-skills/eas-simulator/references/run-your-app.md +240 -0
- package/bundled-skills/eas-simulator/references/troubleshooting.md +47 -0
- package/bundled-skills/eas-workflows/SKILL.md +119 -0
- package/bundled-skills/eas-workflows/agents/openai.yaml +4 -0
- package/bundled-skills/eas-workflows/scripts/fetch.js +109 -0
- package/bundled-skills/ebpf-observability/SKILL.md +436 -0
- package/bundled-skills/ebpf-observability/references/details.md +542 -0
- package/bundled-skills/elk-stack/SKILL.md +487 -0
- package/bundled-skills/enterprise-vpn-attack/SKILL.md +395 -0
- package/bundled-skills/evidence-hygiene/SKILL.md +404 -0
- package/bundled-skills/expo-animation/LICENSE +21 -0
- package/bundled-skills/expo-animation/RECIPES.md +385 -0
- package/bundled-skills/expo-animation/SKILL.md +295 -0
- package/bundled-skills/expo-animation/agents/openai.yaml +4 -0
- package/bundled-skills/fact-check-x-unified/SKILL.md +178 -0
- package/bundled-skills/fact-check-x-unified/agents/openai.yaml +4 -0
- package/bundled-skills/fact-check-x-unified/references/acceptance-criteria.md +44 -0
- package/bundled-skills/fact-check-x-unified/references/contracts.md +39 -0
- package/bundled-skills/fact-check-x-unified/scripts/common.py +31 -0
- package/bundled-skills/fact-check-x-unified/scripts/fact_check_x.py +1832 -0
- package/bundled-skills/fact-check-x-unified/scripts/trusted_search_config.py +324 -0
- package/bundled-skills/fact-check-x-unified/tests/anchor_downgrade_test.py +90 -0
- package/bundled-skills/fact-check-x-unified/tests/multi_platform_test.py +369 -0
- package/bundled-skills/fact-check-x-unified/tests/smoke_test.py +740 -0
- package/bundled-skills/fact-check-x-unified/tests/stage_checkpoint_test.py +103 -0
- package/bundled-skills/fact-check-x-unified/tests/trusted_search_config_test.py +156 -0
- package/bundled-skills/feature-flags/SKILL.md +426 -0
- package/bundled-skills/feature-flags/references/details.md +86 -0
- package/bundled-skills/fedramp-compliance/SKILL.md +453 -0
- package/bundled-skills/firebase-app-platform/SKILL.md +381 -0
- package/bundled-skills/firewall-config/SKILL.md +479 -0
- package/bundled-skills/gcp-audit-logs/SKILL.md +452 -0
- package/bundled-skills/gcp-audit-logs/references/details.md +56 -0
- package/bundled-skills/gcp-cloud-functions/SKILL.md +284 -0
- package/bundled-skills/gcp-cloud-sql/SKILL.md +277 -0
- package/bundled-skills/gcp-compute/SKILL.md +319 -0
- package/bundled-skills/gcp-gke/SKILL.md +307 -0
- package/bundled-skills/gcp-networking/SKILL.md +293 -0
- package/bundled-skills/gcp-secret-manager/SKILL.md +421 -0
- package/bundled-skills/gcp-secret-manager/references/details.md +131 -0
- package/bundled-skills/gdpr-compliance/SKILL.md +451 -0
- package/bundled-skills/gdpr-compliance/references/details.md +145 -0
- package/bundled-skills/geo-audit/SKILL.md +368 -0
- package/bundled-skills/geo-brand-mentions/SKILL.md +68 -0
- package/bundled-skills/geo-brand-mentions/references/details.md +471 -0
- package/bundled-skills/geo-citability/SKILL.md +350 -0
- package/bundled-skills/geo-compare/SKILL.md +340 -0
- package/bundled-skills/geo-content/SKILL.md +383 -0
- package/bundled-skills/geo-crawlers/SKILL.md +408 -0
- package/bundled-skills/geo-llmstxt/SKILL.md +464 -0
- package/bundled-skills/geo-platform-optimizer/SKILL.md +314 -0
- package/bundled-skills/geo-proposal/SKILL.md +378 -0
- package/bundled-skills/geo-prospect/SKILL.md +225 -0
- package/bundled-skills/geo-report/SKILL.md +436 -0
- package/bundled-skills/geo-report-pdf/SKILL.md +157 -0
- package/bundled-skills/geo-schema/SKILL.md +408 -0
- package/bundled-skills/geo-technical/SKILL.md +78 -0
- package/bundled-skills/geo-technical/references/details.md +543 -0
- package/bundled-skills/git-workflow/SKILL.md +460 -0
- package/bundled-skills/github-actions/SKILL.md +368 -0
- package/bundled-skills/gitlab-ci/SKILL.md +340 -0
- package/bundled-skills/gpt-taste/SKILL.md +8 -1
- package/bundled-skills/gpu-kubernetes-operations/SKILL.md +468 -0
- package/bundled-skills/gpu-server-management/SKILL.md +236 -0
- package/bundled-skills/hashicorp-vault/SKILL.md +408 -0
- package/bundled-skills/helm-charts/SKILL.md +469 -0
- package/bundled-skills/hf-cli/SKILL.md +263 -0
- package/bundled-skills/hipaa-compliance/SKILL.md +451 -0
- package/bundled-skills/huggingface-community-evals/SKILL.md +228 -0
- package/bundled-skills/huggingface-community-evals/examples/.env.example +3 -0
- package/bundled-skills/huggingface-community-evals/examples/USAGE_EXAMPLES.md +101 -0
- package/bundled-skills/huggingface-community-evals/scripts/inspect_eval_uv.py +104 -0
- package/bundled-skills/huggingface-community-evals/scripts/inspect_vllm_uv.py +306 -0
- package/bundled-skills/huggingface-community-evals/scripts/lighteval_vllm_uv.py +297 -0
- package/bundled-skills/huggingface-datasets/SKILL.md +130 -0
- package/bundled-skills/hunt-aspnet/SKILL.md +321 -0
- package/bundled-skills/hunt-ato/SKILL.md +184 -0
- package/bundled-skills/hunt-auth-bypass/SKILL.md +426 -0
- package/bundled-skills/hunt-auth-bypass/references/details.md +80 -0
- package/bundled-skills/hunt-brute-force/SKILL.md +341 -0
- package/bundled-skills/hunt-business-logic/SKILL.md +281 -0
- package/bundled-skills/hunt-cache-poison/SKILL.md +382 -0
- package/bundled-skills/hunt-captcha-bypass/SKILL.md +136 -0
- package/bundled-skills/hunt-cicd/SKILL.md +311 -0
- package/bundled-skills/hunt-clickjacking/SKILL.md +110 -0
- package/bundled-skills/hunt-cors/SKILL.md +335 -0
- package/bundled-skills/hunt-dom/SKILL.md +323 -0
- package/bundled-skills/hunt-exceptional-conditions/SKILL.md +111 -0
- package/bundled-skills/hunt-file-upload/SKILL.md +202 -0
- package/bundled-skills/hunt-fintech-graphql/SKILL.md +289 -0
- package/bundled-skills/hunt-forgot-password/SKILL.md +114 -0
- package/bundled-skills/hunt-grpc/SKILL.md +317 -0
- package/bundled-skills/hunt-host-header/SKILL.md +309 -0
- package/bundled-skills/hunt-html-injection/SKILL.md +106 -0
- package/bundled-skills/hunt-http-smuggling/SKILL.md +129 -0
- package/bundled-skills/hunt-http-smuggling/references/phase2h-smuggling-cachepoison.md +177 -0
- package/bundled-skills/hunt-idor/SKILL.md +434 -0
- package/bundled-skills/hunt-jwt-crypto/SKILL.md +221 -0
- package/bundled-skills/hunt-k8s/SKILL.md +337 -0
- package/bundled-skills/hunt-laravel/SKILL.md +255 -0
- package/bundled-skills/hunt-ldap/SKILL.md +351 -0
- package/bundled-skills/hunt-lfi/SKILL.md +311 -0
- package/bundled-skills/hunt-llm-ai/SKILL.md +289 -0
- package/bundled-skills/hunt-mfa-bypass/SKILL.md +177 -0
- package/bundled-skills/hunt-misc/SKILL.md +378 -0
- package/bundled-skills/hunt-nextjs/SKILL.md +299 -0
- package/bundled-skills/hunt-nodejs/SKILL.md +263 -0
- package/bundled-skills/hunt-nosqli/SKILL.md +210 -0
- package/bundled-skills/hunt-ntlm-info/SKILL.md +314 -0
- package/bundled-skills/hunt-oauth/SKILL.md +459 -0
- package/bundled-skills/hunt-open-redirect/SKILL.md +223 -0
- package/bundled-skills/hunt-race-condition/SKILL.md +381 -0
- package/bundled-skills/hunt-race-condition/references/details.md +159 -0
- package/bundled-skills/hunt-rag-vector/SKILL.md +212 -0
- package/bundled-skills/hunt-rce/SKILL.md +444 -0
- package/bundled-skills/hunt-rce/references/details.md +110 -0
- package/bundled-skills/hunt-saml/SKILL.md +156 -0
- package/bundled-skills/hunt-session/SKILL.md +342 -0
- package/bundled-skills/hunt-shadow-api/SKILL.md +198 -0
- package/bundled-skills/hunt-source-leak/SKILL.md +345 -0
- package/bundled-skills/hunt-spa-api/SKILL.md +163 -0
- package/bundled-skills/hunt-springboot/SKILL.md +285 -0
- package/bundled-skills/hunt-sqli/SKILL.md +466 -0
- package/bundled-skills/hunt-ssrf/SKILL.md +396 -0
- package/bundled-skills/hunt-ssrf/references/details.md +179 -0
- package/bundled-skills/hunt-ssti/SKILL.md +163 -0
- package/bundled-skills/hunt-subdomain/SKILL.md +379 -0
- package/bundled-skills/hunt-tls-network/SKILL.md +399 -0
- package/bundled-skills/hunt-xxe/SKILL.md +466 -0
- package/bundled-skills/i-have-adhd/SKILL.md +170 -0
- package/bundled-skills/identity-access-management/SKILL.md +382 -0
- package/bundled-skills/identity-access-management/references/details.md +524 -0
- package/bundled-skills/incident-management/SKILL.md +484 -0
- package/bundled-skills/incident-response/SKILL.md +448 -0
- package/bundled-skills/incident-response/references/details.md +113 -0
- package/bundled-skills/interview-me/SKILL.md +248 -0
- package/bundled-skills/iso27001-compliance/SKILL.md +460 -0
- package/bundled-skills/jenkins/SKILL.md +462 -0
- package/bundled-skills/jev-social/SKILL.md +182 -0
- package/bundled-skills/jev-use/SKILL.md +158 -0
- package/bundled-skills/kubernetes-hardening/SKILL.md +154 -0
- package/bundled-skills/kubernetes-ops/SKILL.md +449 -0
- package/bundled-skills/kubernetes-ops/references/details.md +108 -0
- package/bundled-skills/kustomize/SKILL.md +478 -0
- package/bundled-skills/linux-administration/SKILL.md +367 -0
- package/bundled-skills/linux-hardening/SKILL.md +154 -0
- package/bundled-skills/llm-app-security/SKILL.md +389 -0
- package/bundled-skills/llm-app-security/references/details.md +674 -0
- package/bundled-skills/llm-caching/SKILL.md +334 -0
- package/bundled-skills/llm-cost-optimization/SKILL.md +311 -0
- package/bundled-skills/llm-fine-tuning/SKILL.md +329 -0
- package/bundled-skills/llm-gateway/SKILL.md +282 -0
- package/bundled-skills/llm-inference-scaling/SKILL.md +286 -0
- package/bundled-skills/llmops-platform-engineering/SKILL.md +472 -0
- package/bundled-skills/load-balancing/SKILL.md +403 -0
- package/bundled-skills/loki-logging/SKILL.md +479 -0
- package/bundled-skills/longbridge-derivatives/SKILL.md +117 -0
- package/bundled-skills/longbridge-derivatives/references/option.md +36 -0
- package/bundled-skills/longbridge-derivatives/references/options-advanced.md +101 -0
- package/bundled-skills/longbridge-derivatives/references/options-pnl.md +74 -0
- package/bundled-skills/longbridge-derivatives/references/options-strategy.md +82 -0
- package/bundled-skills/longbridge-derivatives/references/options-volatility.md +70 -0
- package/bundled-skills/longbridge-derivatives/references/warrant.md +12 -0
- package/bundled-skills/longbridge-quant/SKILL.md +151 -0
- package/bundled-skills/longbridge-quant/references/correlation.md +51 -0
- package/bundled-skills/longbridge-quant/references/execution-model.md +68 -0
- package/bundled-skills/longbridge-quant/references/factor-research.md +95 -0
- package/bundled-skills/longbridge-quant/references/factor-screen.md +101 -0
- package/bundled-skills/longbridge-quant/references/hedging.md +136 -0
- package/bundled-skills/longbridge-quant/references/ml-strategy.md +77 -0
- package/bundled-skills/longbridge-quant/references/multifactor.md +68 -0
- package/bundled-skills/longbridge-quant/references/pairs-trading.md +61 -0
- package/bundled-skills/longbridge-quant/references/quant-cli.md +133 -0
- package/bundled-skills/longbridge-quant/references/quant-stats.md +150 -0
- package/bundled-skills/longbridge-quant/references/seasonality.md +50 -0
- package/bundled-skills/longbridge-quant/references/strategy-optimizer.md +68 -0
- package/bundled-skills/longbridge-quant/references/volatility-strategy.md +52 -0
- package/bundled-skills/longbridge-research/SKILL.md +187 -0
- package/bundled-skills/longbridge-research/references/company-profile.md +96 -0
- package/bundled-skills/longbridge-research/references/company-tearsheet.md +82 -0
- package/bundled-skills/longbridge-research/references/competitive-analysis.md +81 -0
- package/bundled-skills/longbridge-research/references/consensus.md +92 -0
- package/bundled-skills/longbridge-research/references/coverage-initiation.md +76 -0
- package/bundled-skills/longbridge-research/references/defi-yield.md +60 -0
- package/bundled-skills/longbridge-research/references/finance-calendar.md +165 -0
- package/bundled-skills/longbridge-research/references/financial-planning.md +77 -0
- package/bundled-skills/longbridge-research/references/forecast-eps.md +39 -0
- package/bundled-skills/longbridge-research/references/fund-holder.md +44 -0
- package/bundled-skills/longbridge-research/references/hkipo-analysis.md +101 -0
- package/bundled-skills/longbridge-research/references/industry-peers.md +46 -0
- package/bundled-skills/longbridge-research/references/industry-rank.md +62 -0
- package/bundled-skills/longbridge-research/references/insider-trades.md +48 -0
- package/bundled-skills/longbridge-research/references/institution-rating.md +62 -0
- package/bundled-skills/longbridge-research/references/investment-ideas.md +69 -0
- package/bundled-skills/longbridge-research/references/investment-proposal.md +95 -0
- package/bundled-skills/longbridge-research/references/investors.md +87 -0
- package/bundled-skills/longbridge-research/references/onchain.md +70 -0
- package/bundled-skills/longbridge-research/references/post-investment.md +76 -0
- package/bundled-skills/longbridge-research/references/shareholder.md +72 -0
- package/bundled-skills/longbridge-research/references/short-positions.md +50 -0
- package/bundled-skills/longbridge-research/references/short-trades.md +50 -0
- package/bundled-skills/longbridge-research/references/stock-research.md +61 -0
- package/bundled-skills/longbridge-research/references/thesis-tracker.md +64 -0
- package/bundled-skills/m365-entra-attack/SKILL.md +423 -0
- package/bundled-skills/mac-mini-llm-lab/SKILL.md +350 -0
- package/bundled-skills/makepad-2-0-animation/SKILL.md +318 -0
- package/bundled-skills/makepad-2-0-animation/references/animator-reference.md +433 -0
- package/bundled-skills/makepad-2-0-dsl/SKILL.md +492 -0
- package/bundled-skills/makepad-2-0-dsl/references/dsl-syntax-reference.md +511 -0
- package/bundled-skills/makepad-2-0-dsl/references/extended-guide.md +56 -0
- package/bundled-skills/makepad-2-0-dsl/references/property-system.md +757 -0
- package/bundled-skills/makepad-2-0-events/SKILL.md +497 -0
- package/bundled-skills/makepad-2-0-events/references/event-patterns.md +802 -0
- package/bundled-skills/makepad-2-0-events/references/extended-guide.md +590 -0
- package/bundled-skills/makepad-2-0-layout/SKILL.md +499 -0
- package/bundled-skills/makepad-2-0-layout/references/extended-guide.md +243 -0
- package/bundled-skills/makepad-2-0-layout/references/layout-patterns.md +881 -0
- package/bundled-skills/makepad-2-0-widgets/SKILL.md +261 -0
- package/bundled-skills/makepad-2-0-widgets/references/widget-advanced.md +648 -0
- package/bundled-skills/makepad-2-0-widgets/references/widget-catalog.md +547 -0
- package/bundled-skills/mcp-server-security/SKILL.md +356 -0
- package/bundled-skills/mcp-server-security/references/details.md +745 -0
- package/bundled-skills/mdm-device-management/SKILL.md +404 -0
- package/bundled-skills/mdm-device-management/references/details.md +410 -0
- package/bundled-skills/meeting-distiller-pro/SKILL.md +120 -0
- package/bundled-skills/meme-coin-audit/SKILL.md +402 -0
- package/bundled-skills/mid-engagement-ir-detection/SKILL.md +377 -0
- package/bundled-skills/model-registry-governance/SKILL.md +452 -0
- package/bundled-skills/model-serving-kubernetes/SKILL.md +339 -0
- package/bundled-skills/model-supply-chain-security/SKILL.md +427 -0
- package/bundled-skills/mongodb/SKILL.md +436 -0
- package/bundled-skills/monte-carlo-analyze-root-cause/SKILL.md +12 -1
- package/bundled-skills/monte-carlo-asset-health/SKILL.md +12 -1
- package/bundled-skills/monte-carlo-context-detection/SKILL.md +170 -0
- package/bundled-skills/monte-carlo-context-detection/references/signal-definitions.md +46 -0
- package/bundled-skills/multi-tenant-llm-hosting/SKILL.md +435 -0
- package/bundled-skills/multi-tenant-llm-hosting/references/details.md +211 -0
- package/bundled-skills/mysql/SKILL.md +390 -0
- package/bundled-skills/new-relic/SKILL.md +472 -0
- package/bundled-skills/nfs-storage/SKILL.md +356 -0
- package/bundled-skills/object-storage/SKILL.md +378 -0
- package/bundled-skills/offensive-osint/SKILL.md +443 -0
- package/bundled-skills/okta-attack/SKILL.md +436 -0
- package/bundled-skills/ollama-stack/SKILL.md +379 -0
- package/bundled-skills/openclaw-deployment-hardening/SKILL.md +135 -0
- package/bundled-skills/openclaw-local-mac-mini/SKILL.md +426 -0
- package/bundled-skills/openclaw-local-mac-mini/references/details.md +221 -0
- package/bundled-skills/openclaw-security-hardening/SKILL.md +135 -0
- package/bundled-skills/openshift/SKILL.md +485 -0
- package/bundled-skills/opentelemetry/SKILL.md +438 -0
- package/bundled-skills/opentelemetry/references/details.md +78 -0
- package/bundled-skills/opentofu-migration/SKILL.md +349 -0
- package/bundled-skills/osint-methodology/SKILL.md +460 -0
- package/bundled-skills/osint-methodology/references/details.md +1350 -0
- package/bundled-skills/pci-dss-compliance/SKILL.md +446 -0
- package/bundled-skills/penetration-testing/SKILL.md +152 -0
- package/bundled-skills/performance-tuning/SKILL.md +381 -0
- package/bundled-skills/planetscale/SKILL.md +297 -0
- package/bundled-skills/platform-engineering/SKILL.md +348 -0
- package/bundled-skills/platform-engineering/references/details.md +944 -0
- package/bundled-skills/podman/SKILL.md +405 -0
- package/bundled-skills/policy-as-code/SKILL.md +434 -0
- package/bundled-skills/policy-as-code/references/details.md +204 -0
- package/bundled-skills/postgresql-devsec/SKILL.md +378 -0
- package/bundled-skills/prometheus-grafana/SKILL.md +469 -0
- package/bundled-skills/prompt-injection-defense/SKILL.md +483 -0
- package/bundled-skills/rag-infrastructure/SKILL.md +269 -0
- package/bundled-skills/rag-observability-evals/SKILL.md +444 -0
- package/bundled-skills/rag-observability-evals/references/details.md +92 -0
- package/bundled-skills/recon-scope-triage/SKILL.md +128 -0
- package/bundled-skills/redis/SKILL.md +421 -0
- package/bundled-skills/redteam-report-template/SKILL.md +370 -0
- package/bundled-skills/remotion-captions/SKILL.md +57 -0
- package/bundled-skills/remotion-captions/agents/openai.yaml +7 -0
- package/bundled-skills/remotion-captions/assets/remotion-icon.svg +4 -0
- package/bundled-skills/remotion-captions/display-captions.md +190 -0
- package/bundled-skills/remotion-captions/import-srt-captions.md +73 -0
- package/bundled-skills/remotion-captions/transcribe-captions.md +70 -0
- package/bundled-skills/remotion-create/SKILL.md +106 -0
- package/bundled-skills/remotion-create/agents/openai.yaml +7 -0
- package/bundled-skills/remotion-create/assets/remotion-icon.svg +4 -0
- package/bundled-skills/remotion-create/tailwind.md +11 -0
- package/bundled-skills/remotion-create/video-layout.md +9 -0
- package/bundled-skills/remotion-docs/SKILL.md +67 -0
- package/bundled-skills/remotion-docs/agents/openai.yaml +7 -0
- package/bundled-skills/remotion-docs/assets/remotion-icon.svg +4 -0
- package/bundled-skills/remotion-interactivity/SKILL.md +270 -0
- package/bundled-skills/remotion-interactivity/agents/openai.yaml +7 -0
- package/bundled-skills/remotion-interactivity/assets/remotion-icon.svg +4 -0
- package/bundled-skills/remotion-render/SKILL.md +48 -0
- package/bundled-skills/remotion-render/agents/openai.yaml +7 -0
- package/bundled-skills/remotion-render/assets/remotion-icon.svg +4 -0
- package/bundled-skills/remotion-render/transparent-videos.md +106 -0
- package/bundled-skills/report-writing/SKILL.md +426 -0
- package/bundled-skills/report-writing/references/details.md +187 -0
- package/bundled-skills/reverse-proxy/SKILL.md +420 -0
- package/bundled-skills/runbook-creation/SKILL.md +438 -0
- package/bundled-skills/runbook-creation/references/details.md +71 -0
- package/bundled-skills/saas-pricing-strategist/SKILL.md +169 -0
- package/bundled-skills/saas-security-posture/SKILL.md +415 -0
- package/bundled-skills/sast-scanning/SKILL.md +444 -0
- package/bundled-skills/sbom-supply-chain/SKILL.md +433 -0
- package/bundled-skills/score-eval/SKILL.md +35 -0
- package/bundled-skills/security-arsenal/SKILL.md +446 -0
- package/bundled-skills/security-arsenal/references/details.md +540 -0
- package/bundled-skills/security-automation/SKILL.md +146 -0
- package/bundled-skills/semantic-versioning/SKILL.md +434 -0
- package/bundled-skills/semantic-versioning/references/details.md +83 -0
- package/bundled-skills/service-mesh/SKILL.md +422 -0
- package/bundled-skills/soc2-compliance/SKILL.md +409 -0
- package/bundled-skills/sops-encryption/SKILL.md +124 -0
- package/bundled-skills/sre-dashboards/SKILL.md +143 -0
- package/bundled-skills/ssh-configuration/SKILL.md +324 -0
- package/bundled-skills/ssl-tls-management/SKILL.md +428 -0
- package/bundled-skills/ssl-tls-management/references/details.md +99 -0
- package/bundled-skills/startup-it-troubleshooting/SKILL.md +415 -0
- package/bundled-skills/supply-chain-attack-recon/SKILL.md +453 -0
- package/bundled-skills/supply-chain-attack-recon/references/details.md +258 -0
- package/bundled-skills/systemd-services/SKILL.md +379 -0
- package/bundled-skills/terraform-aws/SKILL.md +125 -0
- package/bundled-skills/terraform-azure/SKILL.md +415 -0
- package/bundled-skills/terraform-azure/references/details.md +231 -0
- package/bundled-skills/terraform-gcp/SKILL.md +369 -0
- package/bundled-skills/threat-modeling/SKILL.md +487 -0
- package/bundled-skills/user-management/SKILL.md +383 -0
- package/bundled-skills/using-agent-skills/SKILL.md +220 -0
- package/bundled-skills/vector-database-ops/SKILL.md +300 -0
- package/bundled-skills/vendor-management/SKILL.md +439 -0
- package/bundled-skills/vendor-management/references/details.md +109 -0
- package/bundled-skills/vercel-deployments/SKILL.md +296 -0
- package/bundled-skills/vllm-server/SKILL.md +236 -0
- package/bundled-skills/vmware-vcenter-attack/SKILL.md +412 -0
- package/bundled-skills/vpn-setup/SKILL.md +452 -0
- package/bundled-skills/vulnerability-scanning/SKILL.md +448 -0
- package/bundled-skills/waf-setup/SKILL.md +354 -0
- package/bundled-skills/waf-setup/references/details.md +211 -0
- package/bundled-skills/web2-recon/SKILL.md +440 -0
- package/bundled-skills/web2-recon/references/details.md +319 -0
- package/bundled-skills/web3-audit/SKILL.md +445 -0
- package/bundled-skills/web3-audit/references/details.md +224 -0
- package/bundled-skills/windows-hardening/SKILL.md +454 -0
- package/bundled-skills/windows-hardening/references/details.md +204 -0
- package/bundled-skills/windows-server/SKILL.md +318 -0
- package/bundled-skills/writing-guidelines/SKILL.md +60 -0
- package/bundled-skills/zero-trust/SKILL.md +461 -0
- package/package.json +1 -1
- package/skills_index.json +7874 -277
|
@@ -0,0 +1,1832 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import hashlib
|
|
6
|
+
import html
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import secrets
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import zipfile
|
|
15
|
+
from pathlib import Path, PureWindowsPath
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
from common import PipelineError, dump_json, load_json, now_iso
|
|
19
|
+
from trusted_search_config import (
|
|
20
|
+
PROVIDER_URL,
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
load_trusted_search_key,
|
|
23
|
+
trusted_search_key_for_execution,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DEPENDENCIES = {
|
|
28
|
+
"collector": "llm-answer-reference-compare",
|
|
29
|
+
"comparison": "fact-check-x-knowledge-compare",
|
|
30
|
+
"authority": "fact-check-x-authoritative-verify",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
DEPENDENCY_ENTRYPOINTS = {
|
|
34
|
+
"collector": Path("assets/tool/dist/cli.js"),
|
|
35
|
+
"comparison": Path("scripts/knowledge_compare.py"),
|
|
36
|
+
"authority": Path("scripts/authority_verify.py"),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
COLLECTOR_RUNTIME_PACKAGES = (
|
|
40
|
+
Path("node_modules/@playwright/test/package.json"),
|
|
41
|
+
Path("node_modules/commander/package.json"),
|
|
42
|
+
Path("node_modules/zod/package.json"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
TRUSTED_SEARCH_CONFIGURATION_PROMPT = (
|
|
46
|
+
"部分知识点还需要调用可信搜索。首次使用时,我会打开深知智能 MaaS 平台"
|
|
47
|
+
f"({PROVIDER_URL})。您只需完成登录;技能会自动读取已有的可用 Key,"
|
|
48
|
+
"没有时创建 Fact-Check-X 专用 Key,验证后保存到本机共享配置,并自动继续。"
|
|
49
|
+
"后续在 Codex、Claude Code、WorkBuddy 等载体中都会直接复用,已有配置时"
|
|
50
|
+
"不会再次打开登录页。Key 不会进入对话、命令输出或核验报告。"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
PORTABLE_REPORT_PACKAGE = "05-complete-report-package.zip"
|
|
54
|
+
PORTABLE_REPORT_ROOT = Path("fact-check-x-report")
|
|
55
|
+
PORTABLE_REPORT_README = """Fact-Check-X 完整事实核验报告包
|
|
56
|
+
|
|
57
|
+
建议按以下顺序打开:
|
|
58
|
+
1. 01-capture-report.html:各方答案汇总
|
|
59
|
+
2. 02-comparison-report.html:各方答案聚合(未核验)
|
|
60
|
+
3. 03-authority-report.html:权威核验后的最终答案
|
|
61
|
+
4. 04-final-report.html:各方答案测评报告
|
|
62
|
+
|
|
63
|
+
请先解压整个压缩包,再用浏览器打开 HTML 文件,以保留截图、页面存证和报告导航。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
REPORT_NAV_ITEMS = (
|
|
67
|
+
("01-capture-report.html", "各方答案汇总"),
|
|
68
|
+
("02-comparison-report.html", "各方答案聚合"),
|
|
69
|
+
("03-authority-report.html", "权威核验答案"),
|
|
70
|
+
("04-final-report.html", "各方答案测评报告"),
|
|
71
|
+
)
|
|
72
|
+
REPORT_NAV_STYLE = """
|
|
73
|
+
.fcx-report-nav{display:flex;flex-wrap:wrap;gap:8px;margin:14px auto 18px;max-width:1440px;padding:10px 24px;border-top:1px solid #d7dde7;border-bottom:1px solid #d7dde7;background:#fff}
|
|
74
|
+
.fcx-report-nav a,.fcx-report-nav span{display:inline-flex;align-items:center;min-height:32px;padding:5px 10px;border-radius:6px;font-size:13px;text-decoration:none}
|
|
75
|
+
.fcx-report-nav a{border:1px solid #c8d1dc;background:#fff;color:#285a9f}
|
|
76
|
+
.fcx-report-nav a:hover{border-color:#285a9f;color:#173f74}
|
|
77
|
+
.fcx-report-nav span{border:1px solid #187454;background:#e8f5f0;color:#125b42;font-weight:700}
|
|
78
|
+
@media(max-width:760px){.fcx-report-nav{padding:9px 12px;overflow-x:auto;flex-wrap:nowrap}.fcx-report-nav a,.fcx-report-nav span{white-space:nowrap}}
|
|
79
|
+
.fcx-run-notice{max-width:1440px;margin:0 auto 18px;padding:12px 24px;border:1px solid #f0b429;border-left:5px solid #f0b429;border-radius:6px;background:#fffaf0;color:#6b4e07;font-size:13px;line-height:1.6}
|
|
80
|
+
.fcx-run-notice strong{display:block;margin-bottom:6px;font-size:14px;color:#8a5a00}
|
|
81
|
+
.fcx-run-notice ul{margin:0 0 6px;padding-left:20px}
|
|
82
|
+
.fcx-run-notice li{margin:2px 0}
|
|
83
|
+
.fcx-run-notice .fcx-run-notice-tail{color:#7a6320}
|
|
84
|
+
@media(max-width:760px){.fcx-run-notice{padding:11px 12px}}
|
|
85
|
+
""".strip()
|
|
86
|
+
INTERACTION_GATE_FILE = "stage-checkpoints.json"
|
|
87
|
+
INTERACTION_STAGE_ORDER = ("capture", "comparison", "authority", "evaluation")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def interaction_gate_path(run_dir: Path) -> Path:
|
|
91
|
+
return run_dir / INTERACTION_GATE_FILE
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def initialize_interaction_gate(run_dir: Path, execution_mode: str) -> None:
|
|
95
|
+
dump_json(interaction_gate_path(run_dir), {
|
|
96
|
+
"schemaVersion": "fact-check-x/stage-checkpoints@1",
|
|
97
|
+
"executionMode": execution_mode,
|
|
98
|
+
"createdAt": now_iso(),
|
|
99
|
+
"stages": {},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_interaction_gate(run_dir: Path) -> dict:
|
|
104
|
+
path = interaction_gate_path(run_dir)
|
|
105
|
+
if not path.exists():
|
|
106
|
+
raise PipelineError(
|
|
107
|
+
"缺少阶段交付门禁;请从 prepare-comparison 重新开始"
|
|
108
|
+
)
|
|
109
|
+
gate = load_json(path)
|
|
110
|
+
if gate.get("schemaVersion") != "fact-check-x/stage-checkpoints@1":
|
|
111
|
+
raise PipelineError("阶段交付门禁版本不正确")
|
|
112
|
+
return gate
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def record_stage_checkpoint(
|
|
116
|
+
run_dir: Path,
|
|
117
|
+
stage: str,
|
|
118
|
+
label: str,
|
|
119
|
+
path: Path,
|
|
120
|
+
next_stage: str | None,
|
|
121
|
+
) -> dict:
|
|
122
|
+
if stage not in INTERACTION_STAGE_ORDER:
|
|
123
|
+
raise PipelineError(f"未知阶段:{stage}")
|
|
124
|
+
gate = load_interaction_gate(run_dir)
|
|
125
|
+
resolved = path.resolve()
|
|
126
|
+
if not resolved.is_file():
|
|
127
|
+
raise PipelineError(f"阶段产物不存在:{resolved}")
|
|
128
|
+
artifact = deliverable_descriptor(label, resolved)
|
|
129
|
+
status = "completed" if not next_stage else "awaiting_user"
|
|
130
|
+
token = f"fcx_{secrets.token_urlsafe(18)}" if status == "awaiting_user" else None
|
|
131
|
+
entry = {
|
|
132
|
+
"stage": stage,
|
|
133
|
+
"label": label,
|
|
134
|
+
"path": str(resolved),
|
|
135
|
+
"fileUri": artifact["fileUri"],
|
|
136
|
+
"sha256": capture_digest(resolved),
|
|
137
|
+
"status": status,
|
|
138
|
+
"nextStage": next_stage,
|
|
139
|
+
"token": token,
|
|
140
|
+
"createdAt": now_iso(),
|
|
141
|
+
}
|
|
142
|
+
gate.setdefault("stages", {})[stage] = entry
|
|
143
|
+
dump_json(interaction_gate_path(run_dir), gate)
|
|
144
|
+
checkpoint = {
|
|
145
|
+
"required": True,
|
|
146
|
+
"mustPresentBeforeNextStage": True,
|
|
147
|
+
"stage": stage,
|
|
148
|
+
"status": status,
|
|
149
|
+
"label": label,
|
|
150
|
+
"path": str(resolved),
|
|
151
|
+
"fileUri": artifact["fileUri"],
|
|
152
|
+
"markdownLink": artifact["markdownLink"],
|
|
153
|
+
"message": artifact["markdownLink"],
|
|
154
|
+
"gate": str(interaction_gate_path(run_dir).resolve()),
|
|
155
|
+
}
|
|
156
|
+
if next_stage:
|
|
157
|
+
checkpoint.update({
|
|
158
|
+
"nextStage": next_stage,
|
|
159
|
+
"choices": ["继续下一步", "修正当前结果", "到此结束并保留产物"],
|
|
160
|
+
})
|
|
161
|
+
if token:
|
|
162
|
+
checkpoint["acknowledgement"] = {
|
|
163
|
+
"required": True,
|
|
164
|
+
"token": token,
|
|
165
|
+
"command": (
|
|
166
|
+
"acknowledge-stage --run-dir "
|
|
167
|
+
f"{resolved.parent} --stage {stage} --token {token} --decision continue"
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
return checkpoint
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def require_stage_acknowledged(run_dir: Path, stage: str) -> dict:
|
|
174
|
+
gate = load_interaction_gate(run_dir)
|
|
175
|
+
entry = (gate.get("stages") or {}).get(stage)
|
|
176
|
+
if not entry:
|
|
177
|
+
raise PipelineError(f"缺少 {stage} 阶段产物交付记录,禁止继续")
|
|
178
|
+
path = Path(str(entry.get("path") or ""))
|
|
179
|
+
if not path.is_file() or capture_digest(path) != entry.get("sha256"):
|
|
180
|
+
raise PipelineError(f"{stage} 阶段产物已缺失或被修改,禁止继续")
|
|
181
|
+
if entry.get("status") != "acknowledged":
|
|
182
|
+
raise PipelineError(
|
|
183
|
+
f"{stage} 阶段产物尚未得到‘继续下一步’确认,禁止进入后续流程"
|
|
184
|
+
)
|
|
185
|
+
return entry
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def acknowledge_stage(args: argparse.Namespace) -> dict:
|
|
189
|
+
run_dir = require_run(args.run_dir)
|
|
190
|
+
gate = load_interaction_gate(run_dir)
|
|
191
|
+
entry = (gate.get("stages") or {}).get(args.stage)
|
|
192
|
+
if not entry:
|
|
193
|
+
raise PipelineError(f"未找到可确认的 {args.stage} 阶段")
|
|
194
|
+
if gate.get("executionMode") != "interactive":
|
|
195
|
+
raise PipelineError("阶段交付门禁配置无效;请重新执行 prepare-comparison")
|
|
196
|
+
if entry.get("status") != "awaiting_user" or not secrets.compare_digest(
|
|
197
|
+
str(entry.get("token") or ""), str(args.token or "")
|
|
198
|
+
):
|
|
199
|
+
raise PipelineError("阶段确认令牌无效或已使用")
|
|
200
|
+
path = Path(str(entry.get("path") or ""))
|
|
201
|
+
if not path.is_file() or capture_digest(path) != entry.get("sha256"):
|
|
202
|
+
raise PipelineError("待确认的阶段产物已缺失或被修改")
|
|
203
|
+
entry.update({
|
|
204
|
+
"status": "acknowledged" if args.decision == "continue" else args.decision,
|
|
205
|
+
"decision": args.decision,
|
|
206
|
+
"acknowledgedAt": now_iso(),
|
|
207
|
+
"token": None,
|
|
208
|
+
})
|
|
209
|
+
dump_json(interaction_gate_path(run_dir), gate)
|
|
210
|
+
return {
|
|
211
|
+
"status": "completed",
|
|
212
|
+
"stage": args.stage,
|
|
213
|
+
"decision": args.decision,
|
|
214
|
+
"nextStageAllowed": args.decision == "continue",
|
|
215
|
+
"artifact": str(path.resolve()),
|
|
216
|
+
"fileUri": file_uri_for_path(path.resolve()),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def skill_candidates() -> list[Path]:
|
|
221
|
+
values = []
|
|
222
|
+
if os.getenv("FACTCHECK_SKILLS_DIR"):
|
|
223
|
+
values.append(Path(os.environ["FACTCHECK_SKILLS_DIR"]).expanduser())
|
|
224
|
+
# The WorkBuddy single-upload package carries the three business layers here.
|
|
225
|
+
values.append(Path(__file__).resolve().parents[1] / "modules")
|
|
226
|
+
values.append(Path(__file__).resolve().parents[2])
|
|
227
|
+
codex_home = Path(os.getenv("CODEX_HOME", Path.home() / ".codex")).expanduser()
|
|
228
|
+
values.append(codex_home / "skills")
|
|
229
|
+
output = []
|
|
230
|
+
for value in values:
|
|
231
|
+
resolved = value.resolve()
|
|
232
|
+
if resolved not in output:
|
|
233
|
+
output.append(resolved)
|
|
234
|
+
return output
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def locate_skills() -> dict[str, Path]:
|
|
238
|
+
found = {}
|
|
239
|
+
for key, directory in DEPENDENCIES.items():
|
|
240
|
+
for parent in skill_candidates():
|
|
241
|
+
candidate = parent / directory
|
|
242
|
+
if (candidate / "SKILL.md").exists() or (candidate / DEPENDENCY_ENTRYPOINTS[key]).exists():
|
|
243
|
+
found[key] = candidate
|
|
244
|
+
break
|
|
245
|
+
if key not in found:
|
|
246
|
+
searched = ", ".join(str(path / directory) for path in skill_candidates())
|
|
247
|
+
raise PipelineError(f"未找到依赖技能 {directory};已检查:{searched}")
|
|
248
|
+
return found
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def installed_version() -> dict:
|
|
252
|
+
"""读取当前实际运行的技能包版本。
|
|
253
|
+
|
|
254
|
+
载体界面显示的可能是应用市场上的可用版本而不是本机已安装的版本,两者不一致时
|
|
255
|
+
很难判断问题出在版本还是代码,所以每次准备运行时都把真实版本连同来源一并回传。
|
|
256
|
+
版本取自正在执行的这份脚本所在的技能目录,不依赖载体的安装记录。
|
|
257
|
+
"""
|
|
258
|
+
root = Path(__file__).resolve().parent.parent
|
|
259
|
+
manifest = root / "package-manifest.json"
|
|
260
|
+
if manifest.is_file():
|
|
261
|
+
try:
|
|
262
|
+
return {
|
|
263
|
+
"version": str(json.loads(manifest.read_text(encoding="utf-8")).get("version") or "unknown"),
|
|
264
|
+
"source": str(manifest.resolve()),
|
|
265
|
+
}
|
|
266
|
+
except (json.JSONDecodeError, OSError):
|
|
267
|
+
pass
|
|
268
|
+
skill_md = root / "SKILL.md"
|
|
269
|
+
if skill_md.is_file():
|
|
270
|
+
for line in skill_md.read_text(encoding="utf-8").splitlines():
|
|
271
|
+
stripped = line.strip()
|
|
272
|
+
if stripped.startswith("version:"):
|
|
273
|
+
return {
|
|
274
|
+
"version": stripped.split(":", 1)[1].strip().strip('"').strip("'"),
|
|
275
|
+
"source": str(skill_md.resolve()),
|
|
276
|
+
}
|
|
277
|
+
return {"version": "unknown", "source": str(root.resolve())}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def prepare_runtime(skills: dict[str, Path]) -> dict:
|
|
281
|
+
tool_root = skills["collector"] / "assets" / "tool"
|
|
282
|
+
package_json = tool_root / "package.json"
|
|
283
|
+
package_lock = tool_root / "package-lock.json"
|
|
284
|
+
if not package_json.is_file() or not package_lock.is_file():
|
|
285
|
+
raise PipelineError("采集运行时缺少 package.json 或 package-lock.json")
|
|
286
|
+
|
|
287
|
+
node = shutil.which("node")
|
|
288
|
+
if not node:
|
|
289
|
+
raise PipelineError("未找到 Node.js;请安装 Node.js 20 或更高版本后重试")
|
|
290
|
+
|
|
291
|
+
def runtime_ready() -> bool:
|
|
292
|
+
if not all((tool_root / relative).is_file() for relative in COLLECTOR_RUNTIME_PACKAGES):
|
|
293
|
+
return False
|
|
294
|
+
probe = subprocess.run(
|
|
295
|
+
[node, "dist/cli.js", "platforms"],
|
|
296
|
+
cwd=tool_root,
|
|
297
|
+
text=True,
|
|
298
|
+
capture_output=True,
|
|
299
|
+
check=False,
|
|
300
|
+
timeout=30,
|
|
301
|
+
)
|
|
302
|
+
return probe.returncode == 0
|
|
303
|
+
|
|
304
|
+
if runtime_ready():
|
|
305
|
+
return {
|
|
306
|
+
"status": "completed",
|
|
307
|
+
"runtime": "ready",
|
|
308
|
+
"installAction": "skipped",
|
|
309
|
+
"toolRoot": str(tool_root.resolve()),
|
|
310
|
+
"installedVersion": installed_version(),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
npm = shutil.which("npm")
|
|
314
|
+
if not npm:
|
|
315
|
+
raise PipelineError("未找到 npm;请安装 Node.js 20 或更高版本后重试")
|
|
316
|
+
environment = os.environ.copy()
|
|
317
|
+
environment["PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD"] = "1"
|
|
318
|
+
try:
|
|
319
|
+
install = subprocess.run(
|
|
320
|
+
[npm, "ci", "--omit=dev", "--no-audit", "--no-fund"],
|
|
321
|
+
cwd=tool_root,
|
|
322
|
+
text=True,
|
|
323
|
+
capture_output=True,
|
|
324
|
+
check=False,
|
|
325
|
+
env=environment,
|
|
326
|
+
timeout=600,
|
|
327
|
+
)
|
|
328
|
+
except subprocess.TimeoutExpired as exc:
|
|
329
|
+
raise PipelineError("采集运行依赖安装超时,请检查 npm 网络后重试") from exc
|
|
330
|
+
if install.returncode:
|
|
331
|
+
detail = install.stderr.strip() or install.stdout.strip() or "npm ci 执行失败"
|
|
332
|
+
raise PipelineError(f"采集运行依赖安装失败:{detail}")
|
|
333
|
+
if not runtime_ready():
|
|
334
|
+
raise PipelineError("采集运行依赖安装完成,但运行时自检未通过")
|
|
335
|
+
return {
|
|
336
|
+
"status": "completed",
|
|
337
|
+
"runtime": "ready",
|
|
338
|
+
"installAction": "installed",
|
|
339
|
+
"toolRoot": str(tool_root.resolve()),
|
|
340
|
+
"installedVersion": installed_version(),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def run(command: list[str], environment: dict[str, str] | None = None) -> dict:
|
|
345
|
+
process = subprocess.run(
|
|
346
|
+
command,
|
|
347
|
+
text=True,
|
|
348
|
+
capture_output=True,
|
|
349
|
+
check=False,
|
|
350
|
+
env=environment,
|
|
351
|
+
)
|
|
352
|
+
if process.returncode:
|
|
353
|
+
raise PipelineError(process.stdout.strip() or process.stderr.strip() or "子技能执行失败")
|
|
354
|
+
lines = [line for line in process.stdout.splitlines() if line.strip()]
|
|
355
|
+
try:
|
|
356
|
+
return json.loads(lines[-1]) if lines else {"status": "completed"}
|
|
357
|
+
except json.JSONDecodeError:
|
|
358
|
+
return {"status": "completed", "output": process.stdout.strip()}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def trusted_search_key() -> str:
|
|
362
|
+
try:
|
|
363
|
+
return load_trusted_search_key()
|
|
364
|
+
except ConfigurationError as exc:
|
|
365
|
+
raise PipelineError(str(exc)) from exc
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def validated_authority_key() -> str:
|
|
369
|
+
try:
|
|
370
|
+
key, _, _ = trusted_search_key_for_execution()
|
|
371
|
+
return key
|
|
372
|
+
except ConfigurationError as exc:
|
|
373
|
+
raise PipelineError(str(exc)) from exc
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def trusted_search_configuration() -> dict:
|
|
377
|
+
helper = Path(__file__).with_name("trusted_search_config.py").resolve()
|
|
378
|
+
return {
|
|
379
|
+
"providerUrl": PROVIDER_URL,
|
|
380
|
+
"command": [sys.executable, str(helper), "configure"],
|
|
381
|
+
"interaction": "browser_login_only",
|
|
382
|
+
"requiresChatSecret": False,
|
|
383
|
+
"autoResume": True,
|
|
384
|
+
"sharedAcrossCarriers": True,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def require_run(run_dir: str) -> Path:
|
|
389
|
+
path = Path(run_dir).resolve()
|
|
390
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
391
|
+
return path
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def capture_digest(path: Path) -> str:
|
|
395
|
+
digest = hashlib.sha256()
|
|
396
|
+
with path.open("rb") as handle:
|
|
397
|
+
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
398
|
+
digest.update(block)
|
|
399
|
+
return digest.hexdigest()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def normalize_platform_path_text(path: Path | str, platform: str | None = None) -> str:
|
|
403
|
+
raw = str(path)
|
|
404
|
+
if (platform or os.name) == "nt":
|
|
405
|
+
match = re.match(r"^/([A-Za-z])(?:/(.*))?$", raw)
|
|
406
|
+
if match:
|
|
407
|
+
suffix = match.group(2) or ""
|
|
408
|
+
return f"{match.group(1).upper()}:/{suffix}"
|
|
409
|
+
return raw
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def file_uri_for_path(path: Path | str, platform: str | None = None) -> str:
|
|
413
|
+
"""Return a standards-compliant file URI without guessing path separators."""
|
|
414
|
+
raw = normalize_platform_path_text(path, platform)
|
|
415
|
+
if re.match(r"^[A-Za-z]:[\\/]", raw) or raw.startswith("\\\\"):
|
|
416
|
+
posix_path = PureWindowsPath(raw).as_posix()
|
|
417
|
+
encoded = quote(posix_path, safe="/:")
|
|
418
|
+
return f"file:{encoded}" if posix_path.startswith("//") else f"file:///{encoded}"
|
|
419
|
+
return Path(raw).resolve().as_uri()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def deliverable_descriptor(label: str, path: Path | str, **extra) -> dict:
|
|
423
|
+
resolved = Path(normalize_platform_path_text(path)).resolve()
|
|
424
|
+
if not resolved.is_file():
|
|
425
|
+
raise PipelineError(f"阶段产物不存在:{resolved}")
|
|
426
|
+
file_uri = file_uri_for_path(resolved)
|
|
427
|
+
return {
|
|
428
|
+
"label": label,
|
|
429
|
+
"path": str(resolved),
|
|
430
|
+
"fileUri": file_uri,
|
|
431
|
+
"markdownLink": f"[打开{label}](<{file_uri}>)",
|
|
432
|
+
**extra,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def referenced_capture_artifacts(results_path: Path, results: dict) -> dict[str, Path]:
|
|
437
|
+
source_root = results_path.parent.resolve()
|
|
438
|
+
referenced: dict[str, Path] = {}
|
|
439
|
+
for platform in results.get("platforms") or []:
|
|
440
|
+
platform_id = str(platform.get("platform") or "unknown")
|
|
441
|
+
artifacts = platform.get("artifacts") or {}
|
|
442
|
+
if not isinstance(artifacts, dict):
|
|
443
|
+
raise PipelineError(f"{platform_id} 的采集存证格式不正确")
|
|
444
|
+
for artifact_type, value in artifacts.items():
|
|
445
|
+
raw_path = str(value or "").strip()
|
|
446
|
+
if not raw_path:
|
|
447
|
+
continue
|
|
448
|
+
relative = Path(raw_path)
|
|
449
|
+
if (
|
|
450
|
+
relative.is_absolute()
|
|
451
|
+
or ".." in relative.parts
|
|
452
|
+
or not relative.parts
|
|
453
|
+
or relative.parts[0] != "artifacts"
|
|
454
|
+
):
|
|
455
|
+
raise PipelineError(
|
|
456
|
+
f"{platform_id} 的 {artifact_type} 存证路径必须是 artifacts/ 下的相对路径:{raw_path}"
|
|
457
|
+
)
|
|
458
|
+
source = (source_root / relative).resolve()
|
|
459
|
+
try:
|
|
460
|
+
source.relative_to(source_root)
|
|
461
|
+
except ValueError as exc:
|
|
462
|
+
raise PipelineError(
|
|
463
|
+
f"{platform_id} 的 {artifact_type} 存证路径越出采集目录:{raw_path}"
|
|
464
|
+
) from exc
|
|
465
|
+
if not source.is_file():
|
|
466
|
+
raise PipelineError(
|
|
467
|
+
f"{platform_id} 声明的 {artifact_type} 存证文件不存在:{raw_path}"
|
|
468
|
+
)
|
|
469
|
+
normalized = relative.as_posix()
|
|
470
|
+
previous = referenced.get(normalized)
|
|
471
|
+
if previous and previous != source:
|
|
472
|
+
raise PipelineError(f"采集存证路径冲突:{normalized}")
|
|
473
|
+
referenced[normalized] = source
|
|
474
|
+
return referenced
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def capture_artifact_hashes(results_path: Path, results: dict) -> dict[str, str]:
|
|
478
|
+
return {
|
|
479
|
+
relative: capture_digest(source)
|
|
480
|
+
for relative, source in sorted(
|
|
481
|
+
referenced_capture_artifacts(results_path, results).items()
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def sync_capture_evidence(run_dir: Path, results_path: Path, results: dict) -> None:
|
|
487
|
+
capture_dir = (run_dir / "capture").resolve()
|
|
488
|
+
source_dir = results_path.parent.resolve()
|
|
489
|
+
artifacts = referenced_capture_artifacts(results_path, results)
|
|
490
|
+
recovery_source = source_dir / "capture-recovery.json"
|
|
491
|
+
recovery_target = capture_dir / "capture-recovery.json"
|
|
492
|
+
artifact_bytes = {
|
|
493
|
+
relative: source.read_bytes() for relative, source in artifacts.items()
|
|
494
|
+
}
|
|
495
|
+
recovery_bytes = recovery_source.read_bytes() if recovery_source.is_file() else None
|
|
496
|
+
for target_root in (capture_dir, run_dir):
|
|
497
|
+
shutil.rmtree(target_root / "artifacts", ignore_errors=True)
|
|
498
|
+
for relative, content in artifact_bytes.items():
|
|
499
|
+
target = target_root / relative
|
|
500
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
501
|
+
target.write_bytes(content)
|
|
502
|
+
if recovery_bytes is not None:
|
|
503
|
+
recovery_target.write_bytes(recovery_bytes)
|
|
504
|
+
else:
|
|
505
|
+
recovery_target.unlink(missing_ok=True)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def json_file_manifest(directory: Path, expected_names: set[str] | None = None) -> dict[str, str]:
|
|
509
|
+
actual = {path.name for path in directory.glob("*.json")} if directory.exists() else set()
|
|
510
|
+
if expected_names is not None and actual != expected_names:
|
|
511
|
+
missing = sorted(expected_names - actual)
|
|
512
|
+
extra = sorted(actual - expected_names)
|
|
513
|
+
raise PipelineError(f"{directory.name} 文件集合不匹配;缺失={missing},额外/陈旧={extra}")
|
|
514
|
+
return {name: capture_digest(directory / name) for name in sorted(actual)}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def require_digest(path: Path, expected: str, label: str) -> None:
|
|
518
|
+
if not path.exists() or capture_digest(path) != expected:
|
|
519
|
+
raise PipelineError(f"{label} 在门禁后缺失或被修改,禁止继续")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def write_comparison_provenance(run_dir: Path, results_path: Path, analysis_path: Path, comparison_path: Path) -> Path:
|
|
523
|
+
gate = run_dir / "comparison-gate.json"
|
|
524
|
+
dump_json(gate, {
|
|
525
|
+
"schemaVersion": "fact-check-x/comparison-gate@1",
|
|
526
|
+
"status": "completed",
|
|
527
|
+
"createdAt": now_iso(),
|
|
528
|
+
"files": {
|
|
529
|
+
"results": {"path": str(results_path), "sha256": capture_digest(results_path)},
|
|
530
|
+
"analysis": {"path": str(analysis_path), "sha256": capture_digest(analysis_path)},
|
|
531
|
+
"comparison": {"path": str(comparison_path), "sha256": capture_digest(comparison_path)},
|
|
532
|
+
},
|
|
533
|
+
})
|
|
534
|
+
return gate
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def require_comparison_provenance(run_dir: Path) -> dict:
|
|
538
|
+
gate_path = run_dir / "comparison-gate.json"
|
|
539
|
+
if not gate_path.exists():
|
|
540
|
+
raise PipelineError("缺少 1.1 provenance 门禁;必须重新执行 complete-comparison")
|
|
541
|
+
gate = load_json(gate_path)
|
|
542
|
+
if gate.get("schemaVersion") != "fact-check-x/comparison-gate@1" or gate.get("status") != "completed":
|
|
543
|
+
raise PipelineError("1.1 provenance 门禁状态不正确")
|
|
544
|
+
for label, item in (gate.get("files") or {}).items():
|
|
545
|
+
require_digest(Path(str(item.get("path") or "")).resolve(), str(item.get("sha256") or ""), label)
|
|
546
|
+
return gate
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def expected_request_ids(manifest: dict) -> list[str]:
|
|
550
|
+
ids = [str(entry.get("requestId") or "") for entry in manifest.get("requests") or []]
|
|
551
|
+
if not ids or any(not value for value in ids) or len(ids) != len(set(ids)):
|
|
552
|
+
raise PipelineError("权威核验请求清单包含空 ID 或重复 ID")
|
|
553
|
+
return ids
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def scrub_portable_paths(value, run_dir: Path):
|
|
557
|
+
if isinstance(value, dict):
|
|
558
|
+
return {key: scrub_portable_paths(item, run_dir) for key, item in value.items()}
|
|
559
|
+
if isinstance(value, list):
|
|
560
|
+
return [scrub_portable_paths(item, run_dir) for item in value]
|
|
561
|
+
if isinstance(value, str):
|
|
562
|
+
run_uri = file_uri_for_path(run_dir.resolve()).rstrip("/")
|
|
563
|
+
if value == run_uri:
|
|
564
|
+
return "."
|
|
565
|
+
if value.startswith(run_uri + "/"):
|
|
566
|
+
return "../" + value[len(run_uri) + 1 :]
|
|
567
|
+
if run_uri + "/" in value:
|
|
568
|
+
return value.replace(run_uri + "/", "../")
|
|
569
|
+
path = Path(value)
|
|
570
|
+
if path.is_absolute():
|
|
571
|
+
try:
|
|
572
|
+
return path.relative_to(run_dir).as_posix()
|
|
573
|
+
except ValueError:
|
|
574
|
+
return path.name
|
|
575
|
+
return value
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def portable_file_bytes(path: Path, run_dir: Path) -> bytes:
|
|
579
|
+
if path.suffix.lower() == ".json":
|
|
580
|
+
payload = scrub_portable_paths(load_json(path), run_dir)
|
|
581
|
+
if path.name == "pipeline.json":
|
|
582
|
+
artifacts = payload.get("artifacts") or {}
|
|
583
|
+
artifacts.pop("reportPackageSha256", None)
|
|
584
|
+
artifacts.pop("reportPackageBytes", None)
|
|
585
|
+
return (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
586
|
+
return path.read_bytes()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
TECHNICAL_ACQUISITION_STATES = {"blocked", "failed"}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def collect_technical_notices(run_dir: Path) -> list[str]:
|
|
593
|
+
"""收集「本该执行但因技术原因没跑成、流程仍继续」的动作。
|
|
594
|
+
|
|
595
|
+
这些不是内容层面的证据不足,而是采集或调用本身没完成;判定口径不因此改变,
|
|
596
|
+
但必须在每一步报告的显著位置统一提示,供阅读者判断证据完整性。
|
|
597
|
+
"""
|
|
598
|
+
notices: list[str] = []
|
|
599
|
+
results_path = run_dir / "capture" / "results.json"
|
|
600
|
+
if results_path.is_file():
|
|
601
|
+
incomplete: list[str] = []
|
|
602
|
+
for platform in load_json(results_path).get("platforms") or []:
|
|
603
|
+
hits = 0
|
|
604
|
+
for reference in platform.get("references") or []:
|
|
605
|
+
content_state = str(reference.get("contentAcquisition") or "")
|
|
606
|
+
source_state = str(reference.get("sourceAcquisitionStatus") or "")
|
|
607
|
+
if (
|
|
608
|
+
content_state in TECHNICAL_ACQUISITION_STATES
|
|
609
|
+
or source_state in TECHNICAL_ACQUISITION_STATES
|
|
610
|
+
):
|
|
611
|
+
hits += 1
|
|
612
|
+
if hits:
|
|
613
|
+
label = str(platform.get("label") or platform.get("platform") or "")
|
|
614
|
+
incomplete.append(f"{label} {hits} 条")
|
|
615
|
+
if incomplete:
|
|
616
|
+
notices.append(
|
|
617
|
+
"来源正文因访问受阻或采集失败未取得:" + "、".join(incomplete)
|
|
618
|
+
+ "。相关主张只依据平台原文和已取得的证据判定。"
|
|
619
|
+
)
|
|
620
|
+
recovery_path = run_dir / "capture" / "capture-recovery.json"
|
|
621
|
+
if recovery_path.is_file():
|
|
622
|
+
recovery = load_json(recovery_path)
|
|
623
|
+
failed_platforms = [str(item) for item in (recovery.get("failedPlatforms") or [])]
|
|
624
|
+
status = str(recovery.get("status") or "")
|
|
625
|
+
if failed_platforms:
|
|
626
|
+
notices.append(
|
|
627
|
+
"采集恢复流程被触发,涉及:" + "、".join(failed_platforms)
|
|
628
|
+
+ f"(状态 {status or '未知'})。"
|
|
629
|
+
)
|
|
630
|
+
return notices
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def render_run_notices(notices) -> str:
|
|
634
|
+
items = "".join(f"<li>{html.escape(str(item))}</li>" for item in notices)
|
|
635
|
+
if not items:
|
|
636
|
+
return ""
|
|
637
|
+
return (
|
|
638
|
+
'<div class="fcx-run-notice" data-fcx-run-notice="1" role="note">'
|
|
639
|
+
"<strong>本次运行存在未完成的技术动作</strong>"
|
|
640
|
+
f"<ul>{items}</ul>"
|
|
641
|
+
'<span class="fcx-run-notice-tail">判定口径不因此改变;'
|
|
642
|
+
"以上项目可能影响相关结论的证据完整性,受影响且无法核验的主张按“疑似误导”呈现。</span></div>"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def normalize_report_navigation(content: bytes, current_report: str, notices=()) -> bytes:
|
|
647
|
+
html = content.decode("utf-8")
|
|
648
|
+
html = html.replace('href="capture/report.html"', 'href="01-capture-report.html"')
|
|
649
|
+
html = html.replace('href="report.html"', 'href="04-final-report.html"')
|
|
650
|
+
html = re.sub(
|
|
651
|
+
r'<nav\s+class="(?:report-nav|nav|fcx-report-nav)"[^>]*>.*?</nav>',
|
|
652
|
+
"",
|
|
653
|
+
html,
|
|
654
|
+
flags=re.DOTALL,
|
|
655
|
+
)
|
|
656
|
+
items = []
|
|
657
|
+
for filename, label in REPORT_NAV_ITEMS:
|
|
658
|
+
if filename == current_report:
|
|
659
|
+
items.append(f'<span aria-current="page">{label}</span>')
|
|
660
|
+
else:
|
|
661
|
+
items.append(f'<a href="{filename}">{label}</a>')
|
|
662
|
+
navigation = (
|
|
663
|
+
'<nav class="fcx-report-nav" data-fcx-report-nav="1" '
|
|
664
|
+
'aria-label="事实核验四阶段报告导航">'
|
|
665
|
+
+ "".join(items)
|
|
666
|
+
+ "</nav>"
|
|
667
|
+
+ render_run_notices(notices)
|
|
668
|
+
)
|
|
669
|
+
if REPORT_NAV_STYLE not in html:
|
|
670
|
+
if "</style>" in html:
|
|
671
|
+
html = html.replace("</style>", f"\n{REPORT_NAV_STYLE}\n</style>", 1)
|
|
672
|
+
else:
|
|
673
|
+
html = html.replace("</head>", f"<style>{REPORT_NAV_STYLE}</style></head>", 1)
|
|
674
|
+
if "<main" in html:
|
|
675
|
+
html = re.sub(r"(<main(?:\s[^>]*)?>)", navigation + r"\1", html, count=1)
|
|
676
|
+
else:
|
|
677
|
+
html = re.sub(r"(<body(?:\s[^>]*)?>)", r"\1" + navigation, html, count=1)
|
|
678
|
+
return html.encode("utf-8")
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def normalize_report_file(path: Path, notices=()) -> None:
|
|
682
|
+
path.write_bytes(
|
|
683
|
+
normalize_report_navigation(path.read_bytes(), path.name, notices)
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def build_portable_report_package(run_dir: Path) -> dict:
|
|
688
|
+
package_path = run_dir / PORTABLE_REPORT_PACKAGE
|
|
689
|
+
package_path.unlink(missing_ok=True)
|
|
690
|
+
reports = {
|
|
691
|
+
Path("01-capture-report.html"): run_dir / "01-capture-report.html",
|
|
692
|
+
Path("02-comparison-report.html"): run_dir / "02-comparison-report.html",
|
|
693
|
+
Path("03-authority-report.html"): run_dir / "03-authority-report.html",
|
|
694
|
+
Path("04-final-report.html"): run_dir / "04-final-report.html",
|
|
695
|
+
}
|
|
696
|
+
data_files = [
|
|
697
|
+
Path("capture/results.json"),
|
|
698
|
+
Path("capture/report.md"),
|
|
699
|
+
Path("capture/capture-recovery.json"),
|
|
700
|
+
Path("capture-gate.json"),
|
|
701
|
+
Path("comparison-task.json"),
|
|
702
|
+
Path("comparison-analysis.json"),
|
|
703
|
+
Path("comparison.json"),
|
|
704
|
+
Path("comparison-gate.json"),
|
|
705
|
+
Path("authority-gate.json"),
|
|
706
|
+
Path(INTERACTION_GATE_FILE),
|
|
707
|
+
Path("verification.json"),
|
|
708
|
+
Path("pipeline.json"),
|
|
709
|
+
]
|
|
710
|
+
with zipfile.ZipFile(package_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
|
711
|
+
archive.writestr(str(PORTABLE_REPORT_ROOT / "README.txt"), PORTABLE_REPORT_README.encode("utf-8"))
|
|
712
|
+
for relative, source in reports.items():
|
|
713
|
+
if not source.exists():
|
|
714
|
+
raise PipelineError(f"缺少对外交付报告:{source.name}")
|
|
715
|
+
content = normalize_report_navigation(source.read_bytes(), relative.name)
|
|
716
|
+
archive.writestr(str(PORTABLE_REPORT_ROOT / relative), content)
|
|
717
|
+
artifacts_dir = run_dir / "capture" / "artifacts"
|
|
718
|
+
if artifacts_dir.exists():
|
|
719
|
+
for source in sorted(path for path in artifacts_dir.rglob("*") if path.is_file()):
|
|
720
|
+
relative = Path("artifacts") / source.relative_to(artifacts_dir)
|
|
721
|
+
archive.writestr(str(PORTABLE_REPORT_ROOT / relative), source.read_bytes())
|
|
722
|
+
for relative in data_files:
|
|
723
|
+
source = run_dir / relative
|
|
724
|
+
if source.exists():
|
|
725
|
+
archive.writestr(
|
|
726
|
+
str(PORTABLE_REPORT_ROOT / "data" / relative),
|
|
727
|
+
portable_file_bytes(source, run_dir),
|
|
728
|
+
)
|
|
729
|
+
for directory in (run_dir / "authority", run_dir / "report-input"):
|
|
730
|
+
if not directory.exists():
|
|
731
|
+
continue
|
|
732
|
+
for source in sorted(path for path in directory.rglob("*") if path.is_file()):
|
|
733
|
+
relative = Path("data") / source.relative_to(run_dir)
|
|
734
|
+
archive.writestr(
|
|
735
|
+
str(PORTABLE_REPORT_ROOT / relative),
|
|
736
|
+
portable_file_bytes(source, run_dir),
|
|
737
|
+
)
|
|
738
|
+
with zipfile.ZipFile(package_path) as archive:
|
|
739
|
+
broken = archive.testzip()
|
|
740
|
+
if broken:
|
|
741
|
+
raise PipelineError(f"对外报告包校验失败:{broken}")
|
|
742
|
+
return {
|
|
743
|
+
"path": str(package_path.resolve()),
|
|
744
|
+
"sha256": capture_digest(package_path),
|
|
745
|
+
"bytes": package_path.stat().st_size,
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def validate_capture_results(results_path: Path, minimum_platforms: int = 1) -> dict:
|
|
750
|
+
results = load_json(results_path)
|
|
751
|
+
if results.get("schemaVersion") != "1":
|
|
752
|
+
raise PipelineError("采集结果版本不正确,禁止进入知识点对比")
|
|
753
|
+
platforms = results.get("platforms") or []
|
|
754
|
+
if len(platforms) < minimum_platforms:
|
|
755
|
+
raise PipelineError(f"完整事实核验至少需要 {minimum_platforms} 个采集平台,当前只有 {len(platforms)} 个")
|
|
756
|
+
names = [str(item.get("platform") or "").strip() for item in platforms]
|
|
757
|
+
if len(set(names)) != len(names) or any(not name for name in names):
|
|
758
|
+
raise PipelineError("采集结果包含空平台或重复平台,禁止进入知识点对比")
|
|
759
|
+
incomplete = []
|
|
760
|
+
prompt_markers = (
|
|
761
|
+
"为您智能匹配到当前所在区域为",
|
|
762
|
+
"如想咨询其他区域可点击修改",
|
|
763
|
+
"请选择您想咨询的地区",
|
|
764
|
+
"请先完成登录",
|
|
765
|
+
"登录后即可提问",
|
|
766
|
+
)
|
|
767
|
+
def is_prompt_only(answer: str) -> bool:
|
|
768
|
+
compact = "".join(answer.split())
|
|
769
|
+
matches = ["".join(marker.split()) for marker in prompt_markers if "".join(marker.split()) in compact]
|
|
770
|
+
if not matches:
|
|
771
|
+
return False
|
|
772
|
+
return len(compact) <= max(240, sum(len(marker) for marker in matches) + 120)
|
|
773
|
+
|
|
774
|
+
for item in platforms:
|
|
775
|
+
answer = str(item.get("answerMarkdown") or "").strip()
|
|
776
|
+
if item.get("status") != "success" or not answer:
|
|
777
|
+
incomplete.append(
|
|
778
|
+
f"{item.get('label') or item.get('platform')}: "
|
|
779
|
+
f"{item.get('status')} ({item.get('error') or '无完整回答'})"
|
|
780
|
+
)
|
|
781
|
+
elif is_prompt_only(answer):
|
|
782
|
+
incomplete.append(f"{item.get('label') or item.get('platform')}: 捕获内容是登录、地区选择或初始化提示")
|
|
783
|
+
if incomplete:
|
|
784
|
+
raise PipelineError("采集未完成,已禁止进入知识点对比;请先重新采集失败平台:" + ";".join(incomplete))
|
|
785
|
+
recovery_path = results_path.parent / "capture-recovery.json"
|
|
786
|
+
if recovery_path.exists():
|
|
787
|
+
recovery = load_json(recovery_path)
|
|
788
|
+
if recovery.get("status") == "required":
|
|
789
|
+
raise PipelineError("采集恢复状态仍为 required,必须先由 Computer Use 完成浏览器恢复并重新采集")
|
|
790
|
+
return results
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def write_capture_gate(run_dir: Path, results_path: Path, results: dict) -> Path:
|
|
794
|
+
gate = run_dir / "capture-gate.json"
|
|
795
|
+
recovery_path = results_path.parent / "capture-recovery.json"
|
|
796
|
+
dump_json(gate, {
|
|
797
|
+
"schemaVersion": "fact-check-x/capture-gate@1",
|
|
798
|
+
"status": "completed",
|
|
799
|
+
"createdAt": now_iso(),
|
|
800
|
+
"results": str(results_path.resolve()),
|
|
801
|
+
"sha256": capture_digest(results_path),
|
|
802
|
+
"artifactSha256": capture_artifact_hashes(results_path, results),
|
|
803
|
+
"recoverySha256": capture_digest(recovery_path) if recovery_path.is_file() else None,
|
|
804
|
+
"question": results.get("question"),
|
|
805
|
+
"platforms": [item.get("platform") for item in results.get("platforms") or []],
|
|
806
|
+
})
|
|
807
|
+
return gate
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def validate_and_gate_capture(run_dir: Path, results_path: Path) -> dict:
|
|
811
|
+
results = validate_capture_results(results_path)
|
|
812
|
+
write_capture_gate(run_dir, results_path, results)
|
|
813
|
+
return results
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def require_capture_gate(run_dir: Path, results_path: Path | None = None) -> dict:
|
|
817
|
+
gate_path = run_dir / "capture-gate.json"
|
|
818
|
+
if not gate_path.exists():
|
|
819
|
+
raise PipelineError("缺少采集完整性门禁,禁止执行下游步骤;请先完成 1.0 采集并运行 prepare-comparison")
|
|
820
|
+
gate = load_json(gate_path)
|
|
821
|
+
if gate.get("schemaVersion") != "fact-check-x/capture-gate@1" or gate.get("status") != "completed":
|
|
822
|
+
raise PipelineError("采集完整性门禁状态不正确,禁止执行下游步骤")
|
|
823
|
+
checked_path = results_path or Path(str(gate.get("results") or "")).resolve()
|
|
824
|
+
results = validate_capture_results(checked_path)
|
|
825
|
+
if gate.get("sha256") != capture_digest(checked_path):
|
|
826
|
+
raise PipelineError("results.json 在采集门禁后发生变化,禁止继续;请重新执行 prepare-comparison")
|
|
827
|
+
if gate.get("artifactSha256") != capture_artifact_hashes(checked_path, results):
|
|
828
|
+
raise PipelineError("采集存证文件在门禁后缺失或被修改,禁止继续;请重新执行 prepare-comparison")
|
|
829
|
+
recovery_path = checked_path.parent / "capture-recovery.json"
|
|
830
|
+
recovery_sha256 = capture_digest(recovery_path) if recovery_path.is_file() else None
|
|
831
|
+
if gate.get("recoverySha256") != recovery_sha256:
|
|
832
|
+
raise PipelineError("采集恢复记录在门禁后缺失或被修改,禁止继续;请重新执行 prepare-comparison")
|
|
833
|
+
return gate
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def prepare_comparison(args: argparse.Namespace, skills: dict[str, Path]) -> dict:
|
|
837
|
+
run_dir = require_run(args.run_dir)
|
|
838
|
+
results_path = Path(args.results).resolve()
|
|
839
|
+
results = validate_and_gate_capture(run_dir, results_path)
|
|
840
|
+
capture_report = run([
|
|
841
|
+
"node",
|
|
842
|
+
str(skills["collector"] / "assets" / "tool" / "dist" / "report-cli.js"),
|
|
843
|
+
"--input",
|
|
844
|
+
str(results_path),
|
|
845
|
+
"--out",
|
|
846
|
+
str(run_dir / "capture"),
|
|
847
|
+
])
|
|
848
|
+
sync_capture_evidence(run_dir, results_path, results)
|
|
849
|
+
capture_deliverable = run_dir / "01-capture-report.html"
|
|
850
|
+
shutil.copyfile(capture_report["report"], capture_deliverable)
|
|
851
|
+
normalize_report_file(capture_deliverable, collect_technical_notices(run_dir))
|
|
852
|
+
task = run_dir / "comparison-task.json"
|
|
853
|
+
result = run([sys.executable, str(skills["comparison"] / "scripts" / "knowledge_compare.py"), "--input", str(results_path), "--task-output", str(task)])
|
|
854
|
+
initialize_interaction_gate(run_dir, args.execution_mode)
|
|
855
|
+
result.update({
|
|
856
|
+
"stage": "capture_completed",
|
|
857
|
+
"analysisExecution": {
|
|
858
|
+
"mode": "single_pass",
|
|
859
|
+
"maxToolCallsAfterStageAcknowledgement": 3,
|
|
860
|
+
"instructions": [
|
|
861
|
+
"读取 comparison-task.json 一次,不读取技能源码、契约或完整 results.json 探路",
|
|
862
|
+
"直接写入 comparison-analysis.json 一次",
|
|
863
|
+
"执行 complete-comparison 一次,由程序使用完整采集正文校验",
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
"platforms": [
|
|
867
|
+
{
|
|
868
|
+
"platform": item.get("platform"),
|
|
869
|
+
"label": item.get("label") or item.get("platform"),
|
|
870
|
+
"status": item.get("status"),
|
|
871
|
+
"referenceCount": len(item.get("references") or []),
|
|
872
|
+
"sourceMentionCount": len(item.get("sourceMentions") or []),
|
|
873
|
+
"sourceCountAudit": item.get("sourceCountAudit"),
|
|
874
|
+
"durationMs": item.get("durationMs"),
|
|
875
|
+
}
|
|
876
|
+
for item in results.get("platforms") or []
|
|
877
|
+
],
|
|
878
|
+
"artifacts": {
|
|
879
|
+
"results": capture_report["results"],
|
|
880
|
+
"answerReferenceReport": capture_report["report"],
|
|
881
|
+
"answerReferenceMarkdown": capture_report["markdown"],
|
|
882
|
+
"captureDeliverable": str(capture_deliverable.resolve()),
|
|
883
|
+
"captureGate": str((run_dir / "capture-gate.json").resolve()),
|
|
884
|
+
"comparisonTask": str(task.resolve()),
|
|
885
|
+
},
|
|
886
|
+
"deliverables": [deliverable_descriptor("各方答案汇总", capture_deliverable)],
|
|
887
|
+
"checkpoint": record_stage_checkpoint(
|
|
888
|
+
run_dir, "capture", "各方答案汇总", capture_deliverable, "comparison"
|
|
889
|
+
),
|
|
890
|
+
})
|
|
891
|
+
return result
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def complete_comparison(args: argparse.Namespace, skills: dict[str, Path]) -> dict:
|
|
895
|
+
run_dir = require_run(args.run_dir)
|
|
896
|
+
require_stage_acknowledged(run_dir, "capture")
|
|
897
|
+
results_path = Path(args.results).resolve()
|
|
898
|
+
results = validate_and_gate_capture(run_dir, results_path)
|
|
899
|
+
sync_capture_evidence(run_dir, results_path, results)
|
|
900
|
+
analysis_source = (
|
|
901
|
+
Path(args.analysis).resolve()
|
|
902
|
+
if args.analysis
|
|
903
|
+
else run_dir / "comparison-analysis.json"
|
|
904
|
+
)
|
|
905
|
+
analysis = run_dir / "comparison-analysis.json"
|
|
906
|
+
comparison = run_dir / "comparison.json"
|
|
907
|
+
result = run([
|
|
908
|
+
sys.executable,
|
|
909
|
+
str(skills["comparison"] / "scripts" / "knowledge_compare.py"),
|
|
910
|
+
"--input",
|
|
911
|
+
str(results_path),
|
|
912
|
+
"--analysis",
|
|
913
|
+
str(analysis_source),
|
|
914
|
+
"--canonical-analysis-output",
|
|
915
|
+
str(analysis),
|
|
916
|
+
"--output",
|
|
917
|
+
str(comparison),
|
|
918
|
+
])
|
|
919
|
+
comparison_report = run_dir / "comparison.html"
|
|
920
|
+
run([sys.executable, str(skills["comparison"] / "scripts" / "render_comparison.py"), "--results", str(results_path), "--comparison", str(comparison), "--output", str(comparison_report)])
|
|
921
|
+
comparison_deliverable = run_dir / "02-comparison-report.html"
|
|
922
|
+
shutil.copyfile(comparison_report, comparison_deliverable)
|
|
923
|
+
normalize_report_file(comparison_deliverable, collect_technical_notices(run_dir))
|
|
924
|
+
comparison_data = load_json(comparison)
|
|
925
|
+
blocking_analysis_gaps = [
|
|
926
|
+
item
|
|
927
|
+
for item in comparison_data.get("analysisGaps") or []
|
|
928
|
+
if isinstance(item, dict) and item.get("blocking") is True
|
|
929
|
+
]
|
|
930
|
+
if blocking_analysis_gaps:
|
|
931
|
+
details = "; ".join(
|
|
932
|
+
f"{item.get('knowledgePointId') or '?'}·{item.get('platform') or '?'}:{item.get('reason') or '比较分析结构错误'}"
|
|
933
|
+
for item in blocking_analysis_gaps[:5]
|
|
934
|
+
)
|
|
935
|
+
raise PipelineError(
|
|
936
|
+
"比较分析未通过原子性门禁,禁止进入权威核验。"
|
|
937
|
+
f"请补全 answerExcerpt 或拆分过宽主张,并重新执行 complete-comparison:{details}"
|
|
938
|
+
)
|
|
939
|
+
comparison_gate = write_comparison_provenance(run_dir, results_path, analysis, comparison)
|
|
940
|
+
result.update({
|
|
941
|
+
"stage": "comparison_completed",
|
|
942
|
+
"knowledgePointCount": len(comparison_data.get("knowledgePoints") or []),
|
|
943
|
+
"analysisGapCount": len(comparison_data.get("analysisGaps") or []),
|
|
944
|
+
"artifacts": {
|
|
945
|
+
"comparisonAnalysis": str(analysis.resolve()),
|
|
946
|
+
"comparison": str(comparison.resolve()),
|
|
947
|
+
"comparisonReport": str(comparison_report.resolve()),
|
|
948
|
+
"comparisonDeliverable": str(comparison_deliverable.resolve()),
|
|
949
|
+
"comparisonGate": str(comparison_gate.resolve()),
|
|
950
|
+
},
|
|
951
|
+
"deliverables": [deliverable_descriptor("各方答案聚合(未核验)", comparison_deliverable)],
|
|
952
|
+
"checkpoint": record_stage_checkpoint(
|
|
953
|
+
run_dir,
|
|
954
|
+
"comparison",
|
|
955
|
+
"各方答案聚合(未核验)",
|
|
956
|
+
comparison_deliverable,
|
|
957
|
+
"authority",
|
|
958
|
+
),
|
|
959
|
+
})
|
|
960
|
+
return result
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def differing_claims(point: dict) -> list[dict]:
|
|
964
|
+
claims = []
|
|
965
|
+
for platform, claim in (point.get("claims") or {}).items():
|
|
966
|
+
value = str(claim.get("claim") or "").strip()
|
|
967
|
+
if claim.get("covered") and value:
|
|
968
|
+
claims.append({"platform": platform, "claim": value})
|
|
969
|
+
unique = {item["claim"] for item in claims}
|
|
970
|
+
if (point.get("comparison") or {}).get("status") in ("conflict", "partial", "mostly_consensus") and len(unique) >= 2:
|
|
971
|
+
return claims
|
|
972
|
+
return []
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def official_exempt_policy(point: dict) -> str:
|
|
976
|
+
anchor = point.get("trustedAnchor") or {}
|
|
977
|
+
policy = str(anchor.get("sourcePolicy") or "")
|
|
978
|
+
if (
|
|
979
|
+
not policy
|
|
980
|
+
and anchor.get("eligible")
|
|
981
|
+
and anchor.get("platform") in {"dknowc-chat", "dknowc-deep-research"}
|
|
982
|
+
and anchor.get("trustedSearchUsed") is True
|
|
983
|
+
):
|
|
984
|
+
policy = "dknow_official_reference"
|
|
985
|
+
if anchor.get("eligible") and policy in {
|
|
986
|
+
"dknow_official_reference",
|
|
987
|
+
"gov_cn_reference",
|
|
988
|
+
}:
|
|
989
|
+
return policy
|
|
990
|
+
return ""
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def build_requests(comparison: dict, requests_dir: Path) -> dict:
|
|
994
|
+
if comparison.get("schemaVersion") != "fact-check-x/comparison@1":
|
|
995
|
+
raise PipelineError("comparison.json 版本不正确")
|
|
996
|
+
points = comparison.get("knowledgePoints") or []
|
|
997
|
+
if not points:
|
|
998
|
+
raise PipelineError("comparison.json 没有知识点")
|
|
999
|
+
requests_dir.mkdir(parents=True, exist_ok=True)
|
|
1000
|
+
expected = {f"{point['id']}.json" for point in points}
|
|
1001
|
+
stale = [path.name for path in requests_dir.glob("*.json") if path.name not in expected and path.name != "manifest.json"]
|
|
1002
|
+
if stale:
|
|
1003
|
+
raise PipelineError(f"请求目录存在不属于本次任务的旧文件:{stale}")
|
|
1004
|
+
entries = []
|
|
1005
|
+
for point in points:
|
|
1006
|
+
knowledge_point = {
|
|
1007
|
+
key: point.get(key)
|
|
1008
|
+
for key in ("id", "description", "role", "claimType", "core")
|
|
1009
|
+
}
|
|
1010
|
+
payload = {"title": comparison.get("question"), "knowledgePoint": knowledge_point}
|
|
1011
|
+
differences = differing_claims(point)
|
|
1012
|
+
if differences:
|
|
1013
|
+
payload["differingClaims"] = differences
|
|
1014
|
+
request = {
|
|
1015
|
+
"schemaVersion": "fact-check-x/authority-request@1",
|
|
1016
|
+
"requestId": point["id"],
|
|
1017
|
+
"title": comparison.get("question"),
|
|
1018
|
+
"comparisonStatus": (point.get("comparison") or {}).get("status"),
|
|
1019
|
+
"knowledgePoint": knowledge_point,
|
|
1020
|
+
"claims": point.get("claims") or {},
|
|
1021
|
+
"cloudPayload": payload,
|
|
1022
|
+
"trustedAnchor": point.get("trustedAnchor") or {"eligible": False},
|
|
1023
|
+
}
|
|
1024
|
+
path = requests_dir / f"{point['id']}.json"
|
|
1025
|
+
dump_json(path, request)
|
|
1026
|
+
exempt_policy = official_exempt_policy(point)
|
|
1027
|
+
entries.append({
|
|
1028
|
+
"requestId": point["id"],
|
|
1029
|
+
"file": str(path.resolve()),
|
|
1030
|
+
"hasDifferingClaims": bool(differences),
|
|
1031
|
+
"officialExempt": bool(exempt_policy),
|
|
1032
|
+
"dknowExempt": exempt_policy == "dknow_official_reference",
|
|
1033
|
+
"govExempt": exempt_policy == "gov_cn_reference",
|
|
1034
|
+
})
|
|
1035
|
+
manifest = {"schemaVersion": "fact-check-x/authority-requests@1", "createdAt": now_iso(), "question": comparison.get("question"), "taskCount": len(entries), "requests": entries}
|
|
1036
|
+
dump_json(requests_dir / "manifest.json", manifest)
|
|
1037
|
+
return manifest
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def prepare_authority(args: argparse.Namespace) -> dict:
|
|
1041
|
+
run_dir = require_run(args.run_dir)
|
|
1042
|
+
require_stage_acknowledged(run_dir, "comparison")
|
|
1043
|
+
require_capture_gate(run_dir)
|
|
1044
|
+
comparison_provenance = require_comparison_provenance(run_dir)
|
|
1045
|
+
comparison_path = Path(args.comparison).resolve() if args.comparison else run_dir / "comparison.json"
|
|
1046
|
+
recorded_comparison = Path(comparison_provenance["files"]["comparison"]["path"]).resolve()
|
|
1047
|
+
if comparison_path != recorded_comparison:
|
|
1048
|
+
raise PipelineError("prepare-authority 只能使用 complete-comparison 已锁定的 comparison.json")
|
|
1049
|
+
comparison = load_json(comparison_path)
|
|
1050
|
+
manifest = build_requests(comparison, run_dir / "authority" / "requests")
|
|
1051
|
+
request_ids = expected_request_ids(manifest)
|
|
1052
|
+
request_names = {f"{request_id}.json" for request_id in request_ids} | {"manifest.json"}
|
|
1053
|
+
request_hashes = json_file_manifest(run_dir / "authority" / "requests", request_names)
|
|
1054
|
+
required_count = sum(not entry.get("officialExempt") for entry in manifest.get("requests") or [])
|
|
1055
|
+
configured = bool(trusted_search_key())
|
|
1056
|
+
result = {
|
|
1057
|
+
"status": "configuration_required" if required_count and not configured else "prepared",
|
|
1058
|
+
"taskCount": manifest["taskCount"],
|
|
1059
|
+
"trustedSearchRequiredCount": required_count,
|
|
1060
|
+
"trustedSearchConfigured": configured,
|
|
1061
|
+
"requests": str((run_dir / "authority" / "requests").resolve()),
|
|
1062
|
+
}
|
|
1063
|
+
if result["status"] == "configuration_required":
|
|
1064
|
+
result.update({
|
|
1065
|
+
"action": "configure_trusted_search",
|
|
1066
|
+
"userPrompt": TRUSTED_SEARCH_CONFIGURATION_PROMPT,
|
|
1067
|
+
"configuration": trusted_search_configuration(),
|
|
1068
|
+
"blockedReason": "未配置 TRUSTED_SEARCH_KEY,禁止发起可信搜索或使用其他来源绕过",
|
|
1069
|
+
})
|
|
1070
|
+
dump_json(run_dir / "authority-gate.json", {
|
|
1071
|
+
"schemaVersion": "fact-check-x/authority-gate@1",
|
|
1072
|
+
"createdAt": now_iso(),
|
|
1073
|
+
"status": result["status"],
|
|
1074
|
+
"trustedSearchRequiredCount": required_count,
|
|
1075
|
+
"trustedSearchConfigured": configured,
|
|
1076
|
+
"requests": result["requests"],
|
|
1077
|
+
"comparisonSha256": comparison_provenance["files"]["comparison"]["sha256"],
|
|
1078
|
+
"requestHashes": request_hashes,
|
|
1079
|
+
})
|
|
1080
|
+
return result
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def search_authority(args: argparse.Namespace, skills: dict[str, Path]) -> dict:
|
|
1084
|
+
run_dir = require_run(args.run_dir)
|
|
1085
|
+
require_capture_gate(run_dir)
|
|
1086
|
+
authority_gate_path = run_dir / "authority-gate.json"
|
|
1087
|
+
if not authority_gate_path.exists():
|
|
1088
|
+
raise PipelineError("缺少权威核验门禁;必须先执行 prepare-authority")
|
|
1089
|
+
authority_gate = load_json(authority_gate_path)
|
|
1090
|
+
if authority_gate.get("schemaVersion") != "fact-check-x/authority-gate@1":
|
|
1091
|
+
raise PipelineError("权威核验门禁版本不正确")
|
|
1092
|
+
requests_manifest = load_json(run_dir / "authority" / "requests" / "manifest.json")
|
|
1093
|
+
request_ids = expected_request_ids(requests_manifest)
|
|
1094
|
+
current_request_hashes = json_file_manifest(
|
|
1095
|
+
run_dir / "authority" / "requests",
|
|
1096
|
+
{f"{request_id}.json" for request_id in request_ids} | {"manifest.json"},
|
|
1097
|
+
)
|
|
1098
|
+
if current_request_hashes != authority_gate.get("requestHashes"):
|
|
1099
|
+
raise PipelineError("权威核验请求在 prepare-authority 后被修改,禁止搜索")
|
|
1100
|
+
require_comparison_provenance(run_dir)
|
|
1101
|
+
required_count = sum(not entry.get("officialExempt") for entry in requests_manifest.get("requests") or [])
|
|
1102
|
+
key = trusted_search_key()
|
|
1103
|
+
if required_count and not args.fixtures:
|
|
1104
|
+
key = validated_authority_key()
|
|
1105
|
+
configured = bool(key)
|
|
1106
|
+
if required_count and not configured and not args.fixtures:
|
|
1107
|
+
return {
|
|
1108
|
+
"status": "configuration_required",
|
|
1109
|
+
"action": "configure_trusted_search",
|
|
1110
|
+
"trustedSearchRequiredCount": required_count,
|
|
1111
|
+
"trustedSearchConfigured": False,
|
|
1112
|
+
"userPrompt": TRUSTED_SEARCH_CONFIGURATION_PROMPT,
|
|
1113
|
+
"configuration": trusted_search_configuration(),
|
|
1114
|
+
"blockedReason": "未配置 TRUSTED_SEARCH_KEY,禁止发起可信搜索或使用其他来源绕过",
|
|
1115
|
+
}
|
|
1116
|
+
if authority_gate.get("status") == "configuration_required" and not args.fixtures:
|
|
1117
|
+
return {
|
|
1118
|
+
"status": "configuration_required",
|
|
1119
|
+
"action": "configure_trusted_search",
|
|
1120
|
+
"trustedSearchRequiredCount": required_count,
|
|
1121
|
+
"trustedSearchConfigured": configured,
|
|
1122
|
+
"userPrompt": TRUSTED_SEARCH_CONFIGURATION_PROMPT,
|
|
1123
|
+
"configuration": trusted_search_configuration(),
|
|
1124
|
+
"blockedReason": "配置完成后必须重新执行 prepare-authority,禁止绕过权威核验门禁",
|
|
1125
|
+
}
|
|
1126
|
+
if authority_gate.get("status") != "prepared":
|
|
1127
|
+
raise PipelineError("权威核验门禁状态不允许搜索;请重新执行 prepare-authority")
|
|
1128
|
+
command = [
|
|
1129
|
+
sys.executable,
|
|
1130
|
+
str(skills["authority"] / "scripts" / "batch_search.py"),
|
|
1131
|
+
"--requests-dir",
|
|
1132
|
+
str(run_dir / "authority" / "requests"),
|
|
1133
|
+
"--output-dir",
|
|
1134
|
+
str(run_dir / "authority" / "evidence"),
|
|
1135
|
+
"--max-workers",
|
|
1136
|
+
str(args.max_workers),
|
|
1137
|
+
]
|
|
1138
|
+
if args.service_area:
|
|
1139
|
+
command.extend(["--service-area", args.service_area])
|
|
1140
|
+
if args.fixtures:
|
|
1141
|
+
command.extend(["--fixtures", str(Path(args.fixtures).resolve())])
|
|
1142
|
+
environment = None
|
|
1143
|
+
if key:
|
|
1144
|
+
environment = {**os.environ, "TRUSTED_SEARCH_KEY": key}
|
|
1145
|
+
result = run(command, environment)
|
|
1146
|
+
if result.get("status") != "completed":
|
|
1147
|
+
raise PipelineError("可信搜索批次未完成,禁止进入裁决")
|
|
1148
|
+
evidence_hashes = json_file_manifest(
|
|
1149
|
+
run_dir / "authority" / "evidence",
|
|
1150
|
+
{f"{request_id}.json" for request_id in request_ids} | {"batch.json"},
|
|
1151
|
+
)
|
|
1152
|
+
dump_json(authority_gate_path, {
|
|
1153
|
+
**authority_gate,
|
|
1154
|
+
"status": "searched",
|
|
1155
|
+
"searchedAt": now_iso(),
|
|
1156
|
+
"batch": str((run_dir / "authority" / "evidence" / "batch.json").resolve()),
|
|
1157
|
+
"evidenceHashes": evidence_hashes,
|
|
1158
|
+
})
|
|
1159
|
+
return result
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def finalize_authority(args: argparse.Namespace, skills: dict[str, Path]) -> dict:
|
|
1163
|
+
run_dir = require_run(args.run_dir)
|
|
1164
|
+
require_capture_gate(run_dir)
|
|
1165
|
+
authority_gate_path = run_dir / "authority-gate.json"
|
|
1166
|
+
if not authority_gate_path.exists():
|
|
1167
|
+
raise PipelineError("缺少权威核验门禁,禁止裁决")
|
|
1168
|
+
authority_gate = load_json(authority_gate_path)
|
|
1169
|
+
if (
|
|
1170
|
+
authority_gate.get("schemaVersion") != "fact-check-x/authority-gate@1"
|
|
1171
|
+
or authority_gate.get("status") != "searched"
|
|
1172
|
+
):
|
|
1173
|
+
raise PipelineError("可信搜索尚未通过程序门禁,禁止写入裁决或进入最终报告")
|
|
1174
|
+
if not (run_dir / "authority" / "evidence" / "batch.json").exists():
|
|
1175
|
+
raise PipelineError("缺少可信搜索批次证明,禁止裁决")
|
|
1176
|
+
requests_dir = run_dir / "authority" / "requests"
|
|
1177
|
+
manifest = load_json(requests_dir / "manifest.json")
|
|
1178
|
+
request_ids = expected_request_ids(manifest)
|
|
1179
|
+
if json_file_manifest(
|
|
1180
|
+
requests_dir,
|
|
1181
|
+
{f"{request_id}.json" for request_id in request_ids} | {"manifest.json"},
|
|
1182
|
+
) != authority_gate.get("requestHashes"):
|
|
1183
|
+
raise PipelineError("权威核验请求已被修改,禁止裁决")
|
|
1184
|
+
if json_file_manifest(
|
|
1185
|
+
run_dir / "authority" / "evidence",
|
|
1186
|
+
{f"{request_id}.json" for request_id in request_ids} | {"batch.json"},
|
|
1187
|
+
) != authority_gate.get("evidenceHashes"):
|
|
1188
|
+
raise PipelineError("权威证据包已被修改或混入陈旧文件,禁止裁决")
|
|
1189
|
+
require_comparison_provenance(run_dir)
|
|
1190
|
+
evidence_dir = run_dir / "authority" / "evidence"
|
|
1191
|
+
assessments_dir = Path(args.assessments_dir).resolve() if args.assessments_dir else run_dir / "authority" / "assessments"
|
|
1192
|
+
results_dir = run_dir / "authority" / "results"
|
|
1193
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
1194
|
+
existing_results = list(results_dir.glob("*.json"))
|
|
1195
|
+
if existing_results:
|
|
1196
|
+
raise PipelineError("results 目录必须为空;检测到旧文件或手工结果,禁止覆盖式裁决")
|
|
1197
|
+
expected_assessments = set()
|
|
1198
|
+
for request_id in request_ids:
|
|
1199
|
+
evidence = load_json(evidence_dir / f"{request_id}.json")
|
|
1200
|
+
if evidence.get("status") == "verified":
|
|
1201
|
+
expected_assessments.add(f"{request_id}.json")
|
|
1202
|
+
assessment_hashes = json_file_manifest(assessments_dir, expected_assessments)
|
|
1203
|
+
statuses = []
|
|
1204
|
+
evidence_gap_count = 0
|
|
1205
|
+
verification_path = run_dir / "verification.json"
|
|
1206
|
+
authority_report = run_dir / "03-authority-report.html"
|
|
1207
|
+
try:
|
|
1208
|
+
for entry in manifest.get("requests") or []:
|
|
1209
|
+
request_id = entry["requestId"]
|
|
1210
|
+
command = [
|
|
1211
|
+
sys.executable,
|
|
1212
|
+
str(skills["authority"] / "scripts" / "authority_verify.py"),
|
|
1213
|
+
"finalize",
|
|
1214
|
+
"--request",
|
|
1215
|
+
str(requests_dir / f"{request_id}.json"),
|
|
1216
|
+
"--evidence",
|
|
1217
|
+
str(evidence_dir / f"{request_id}.json"),
|
|
1218
|
+
"--output",
|
|
1219
|
+
str(results_dir / f"{request_id}.json"),
|
|
1220
|
+
]
|
|
1221
|
+
assessment = assessments_dir / f"{request_id}.json"
|
|
1222
|
+
if assessment.exists():
|
|
1223
|
+
command.extend(["--assessment", str(assessment)])
|
|
1224
|
+
statuses.append(run(command).get("status"))
|
|
1225
|
+
point_result = load_json(results_dir / f"{request_id}.json")
|
|
1226
|
+
evidence_gap_count += len(point_result.get("evidenceGaps") or [])
|
|
1227
|
+
final_status = "completed"
|
|
1228
|
+
result_hashes = json_file_manifest(
|
|
1229
|
+
results_dir, {f"{request_id}.json" for request_id in request_ids}
|
|
1230
|
+
)
|
|
1231
|
+
comparison = load_json(run_dir / "comparison.json")
|
|
1232
|
+
verification = merge_verification(comparison, results_dir)
|
|
1233
|
+
dump_json(verification_path, verification)
|
|
1234
|
+
run([
|
|
1235
|
+
sys.executable,
|
|
1236
|
+
str(skills["authority"] / "scripts" / "render_authority_report.py"),
|
|
1237
|
+
"--verification",
|
|
1238
|
+
str(verification_path),
|
|
1239
|
+
"--output",
|
|
1240
|
+
str(authority_report),
|
|
1241
|
+
])
|
|
1242
|
+
normalize_report_file(authority_report, collect_technical_notices(run_dir))
|
|
1243
|
+
dump_json(authority_gate_path, {
|
|
1244
|
+
**authority_gate,
|
|
1245
|
+
"status": "finalized",
|
|
1246
|
+
"finalizedAt": now_iso(),
|
|
1247
|
+
"finalizeStatus": final_status,
|
|
1248
|
+
"results": str(results_dir.resolve()),
|
|
1249
|
+
"assessmentHashes": assessment_hashes,
|
|
1250
|
+
"resultHashes": result_hashes,
|
|
1251
|
+
"verificationSha256": capture_digest(verification_path),
|
|
1252
|
+
"authorityReportSha256": capture_digest(authority_report),
|
|
1253
|
+
})
|
|
1254
|
+
except Exception:
|
|
1255
|
+
for result_path in results_dir.glob("*.json"):
|
|
1256
|
+
result_path.unlink()
|
|
1257
|
+
verification_path.unlink(missing_ok=True)
|
|
1258
|
+
authority_report.unlink(missing_ok=True)
|
|
1259
|
+
dump_json(authority_gate_path, authority_gate)
|
|
1260
|
+
raise
|
|
1261
|
+
return {
|
|
1262
|
+
"status": final_status,
|
|
1263
|
+
"stage": "authority_completed",
|
|
1264
|
+
"taskCount": len(statuses),
|
|
1265
|
+
"evidenceGapCount": evidence_gap_count,
|
|
1266
|
+
"results": str(results_dir.resolve()),
|
|
1267
|
+
"artifacts": {
|
|
1268
|
+
"verification": str(verification_path.resolve()),
|
|
1269
|
+
"authorityReport": str(authority_report.resolve()),
|
|
1270
|
+
"authorityGate": str(authority_gate_path.resolve()),
|
|
1271
|
+
},
|
|
1272
|
+
"deliverables": [deliverable_descriptor("权威核验后的最终答案", authority_report)],
|
|
1273
|
+
"checkpoint": record_stage_checkpoint(
|
|
1274
|
+
run_dir,
|
|
1275
|
+
"authority",
|
|
1276
|
+
"权威核验后的最终答案",
|
|
1277
|
+
authority_report,
|
|
1278
|
+
"evaluation",
|
|
1279
|
+
),
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def reopen_authority(args: argparse.Namespace) -> dict:
|
|
1284
|
+
run_dir = require_run(args.run_dir)
|
|
1285
|
+
authority_gate_path = run_dir / "authority-gate.json"
|
|
1286
|
+
if not authority_gate_path.is_file():
|
|
1287
|
+
raise PipelineError("缺少已完成的权威核验门禁,无法重开")
|
|
1288
|
+
authority_gate = load_json(authority_gate_path)
|
|
1289
|
+
if (
|
|
1290
|
+
authority_gate.get("schemaVersion") != "fact-check-x/authority-gate@1"
|
|
1291
|
+
or authority_gate.get("status") != "finalized"
|
|
1292
|
+
or authority_gate.get("finalizeStatus") != "completed"
|
|
1293
|
+
):
|
|
1294
|
+
raise PipelineError("只有已完成且未被修改的权威核验可以受控重开")
|
|
1295
|
+
requests_dir = run_dir / "authority" / "requests"
|
|
1296
|
+
evidence_dir = run_dir / "authority" / "evidence"
|
|
1297
|
+
results_dir = run_dir / "authority" / "results"
|
|
1298
|
+
assessments_dir = run_dir / "authority" / "assessments"
|
|
1299
|
+
manifest = load_json(requests_dir / "manifest.json")
|
|
1300
|
+
request_ids = expected_request_ids(manifest)
|
|
1301
|
+
if json_file_manifest(
|
|
1302
|
+
requests_dir,
|
|
1303
|
+
{f"{request_id}.json" for request_id in request_ids} | {"manifest.json"},
|
|
1304
|
+
) != authority_gate.get("requestHashes"):
|
|
1305
|
+
raise PipelineError("请求文件已被修改,禁止重开")
|
|
1306
|
+
if json_file_manifest(
|
|
1307
|
+
evidence_dir,
|
|
1308
|
+
{f"{request_id}.json" for request_id in request_ids} | {"batch.json"},
|
|
1309
|
+
) != authority_gate.get("evidenceHashes"):
|
|
1310
|
+
raise PipelineError("证据文件已被修改,禁止重开")
|
|
1311
|
+
if json_file_manifest(
|
|
1312
|
+
results_dir,
|
|
1313
|
+
{f"{request_id}.json" for request_id in request_ids},
|
|
1314
|
+
) != authority_gate.get("resultHashes"):
|
|
1315
|
+
raise PipelineError("裁决结果已被修改或混入陈旧文件,禁止重开")
|
|
1316
|
+
if json_file_manifest(
|
|
1317
|
+
assessments_dir,
|
|
1318
|
+
set((authority_gate.get("assessmentHashes") or {}).keys()),
|
|
1319
|
+
) != authority_gate.get("assessmentHashes"):
|
|
1320
|
+
raise PipelineError("assessment 已被修改;请先恢复到锁定版本再重开")
|
|
1321
|
+
verification_path = run_dir / "verification.json"
|
|
1322
|
+
authority_report = run_dir / "03-authority-report.html"
|
|
1323
|
+
if (
|
|
1324
|
+
not verification_path.is_file()
|
|
1325
|
+
or capture_digest(verification_path) != authority_gate.get("verificationSha256")
|
|
1326
|
+
or not authority_report.is_file()
|
|
1327
|
+
or capture_digest(authority_report) != authority_gate.get("authorityReportSha256")
|
|
1328
|
+
):
|
|
1329
|
+
raise PipelineError("第三步锁定产物已缺失或被修改,禁止重开")
|
|
1330
|
+
|
|
1331
|
+
revisions_root = run_dir / "authority" / "revisions"
|
|
1332
|
+
revisions_root.mkdir(parents=True, exist_ok=True)
|
|
1333
|
+
revision_number = len([path for path in revisions_root.iterdir() if path.is_dir()]) + 1
|
|
1334
|
+
revision_dir = revisions_root / f"revision-{revision_number:03d}"
|
|
1335
|
+
revision_dir.mkdir()
|
|
1336
|
+
archive_paths = (
|
|
1337
|
+
Path("authority-gate.json"),
|
|
1338
|
+
Path(INTERACTION_GATE_FILE),
|
|
1339
|
+
Path("verification.json"),
|
|
1340
|
+
Path("03-authority-report.html"),
|
|
1341
|
+
Path("04-final-report.html"),
|
|
1342
|
+
Path("report.html"),
|
|
1343
|
+
Path("pipeline.json"),
|
|
1344
|
+
Path(PORTABLE_REPORT_PACKAGE),
|
|
1345
|
+
Path("report-input"),
|
|
1346
|
+
Path("authority/results"),
|
|
1347
|
+
Path("authority/assessments"),
|
|
1348
|
+
)
|
|
1349
|
+
archived = []
|
|
1350
|
+
for relative_path in archive_paths:
|
|
1351
|
+
source = run_dir / relative_path
|
|
1352
|
+
if not source.exists():
|
|
1353
|
+
continue
|
|
1354
|
+
destination = revision_dir / relative_path
|
|
1355
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1356
|
+
if source.is_dir():
|
|
1357
|
+
shutil.copytree(source, destination)
|
|
1358
|
+
else:
|
|
1359
|
+
shutil.copy2(source, destination)
|
|
1360
|
+
archived.append(str(relative_path).replace("\\", "/"))
|
|
1361
|
+
dump_json(revision_dir / "revision.json", {
|
|
1362
|
+
"schemaVersion": "fact-check-x/authority-revision@1",
|
|
1363
|
+
"revision": revision_number,
|
|
1364
|
+
"createdAt": now_iso(),
|
|
1365
|
+
"reason": str(args.reason).strip(),
|
|
1366
|
+
"archived": archived,
|
|
1367
|
+
"priorFinalizedAt": authority_gate.get("finalizedAt"),
|
|
1368
|
+
"priorVerificationSha256": authority_gate.get("verificationSha256"),
|
|
1369
|
+
"priorAuthorityReportSha256": authority_gate.get("authorityReportSha256"),
|
|
1370
|
+
})
|
|
1371
|
+
|
|
1372
|
+
for result_path in results_dir.glob("*.json"):
|
|
1373
|
+
result_path.unlink()
|
|
1374
|
+
for stale in (
|
|
1375
|
+
verification_path,
|
|
1376
|
+
authority_report,
|
|
1377
|
+
run_dir / "04-final-report.html",
|
|
1378
|
+
run_dir / "report.html",
|
|
1379
|
+
run_dir / "pipeline.json",
|
|
1380
|
+
run_dir / PORTABLE_REPORT_PACKAGE,
|
|
1381
|
+
):
|
|
1382
|
+
stale.unlink(missing_ok=True)
|
|
1383
|
+
shutil.rmtree(run_dir / "report-input", ignore_errors=True)
|
|
1384
|
+
interaction_gate = load_interaction_gate(run_dir)
|
|
1385
|
+
stages = interaction_gate.setdefault("stages", {})
|
|
1386
|
+
stages.pop("authority", None)
|
|
1387
|
+
stages.pop("evaluation", None)
|
|
1388
|
+
dump_json(interaction_gate_path(run_dir), interaction_gate)
|
|
1389
|
+
reopened_at = now_iso()
|
|
1390
|
+
reopen_history = list(authority_gate.get("reopenHistory") or [])
|
|
1391
|
+
reopen_history.append({
|
|
1392
|
+
"revision": revision_number,
|
|
1393
|
+
"reopenedAt": reopened_at,
|
|
1394
|
+
"reason": str(args.reason).strip(),
|
|
1395
|
+
"archive": str(revision_dir.resolve()),
|
|
1396
|
+
})
|
|
1397
|
+
reset_gate = {
|
|
1398
|
+
key: value for key, value in authority_gate.items()
|
|
1399
|
+
if key not in {
|
|
1400
|
+
"finalizedAt", "finalizeStatus", "results", "assessmentHashes",
|
|
1401
|
+
"resultHashes", "verificationSha256", "authorityReportSha256"
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
reset_gate.update({
|
|
1405
|
+
"status": "searched",
|
|
1406
|
+
"reopenedAt": reopened_at,
|
|
1407
|
+
"reopenReason": str(args.reason).strip(),
|
|
1408
|
+
"reopenHistory": reopen_history,
|
|
1409
|
+
})
|
|
1410
|
+
dump_json(authority_gate_path, reset_gate)
|
|
1411
|
+
return {
|
|
1412
|
+
"status": "completed",
|
|
1413
|
+
"stage": "authority_reopened",
|
|
1414
|
+
"revision": revision_number,
|
|
1415
|
+
"archive": str(revision_dir.resolve()),
|
|
1416
|
+
"nextCommand": "finalize-authority",
|
|
1417
|
+
"message": "旧裁决已完整归档;可修改 assessment 后重新 finalize,新的第三步报告仍需重新确认。",
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def merge_verification(comparison: dict, results_dir: Path) -> dict:
|
|
1422
|
+
points = []
|
|
1423
|
+
evidence_gaps = []
|
|
1424
|
+
request_count = 0
|
|
1425
|
+
dknow_exempt_count = 0
|
|
1426
|
+
gov_exempt_count = 0
|
|
1427
|
+
downgraded_anchor_ids: list[str] = []
|
|
1428
|
+
authority_verdicts: dict[tuple[str, str], str] = {}
|
|
1429
|
+
for point in comparison.get("knowledgePoints") or []:
|
|
1430
|
+
path = results_dir / f"{point['id']}.json"
|
|
1431
|
+
if not path.exists():
|
|
1432
|
+
raise PipelineError(f"缺少单点核验结果 {point['id']}")
|
|
1433
|
+
authority = load_json(path)
|
|
1434
|
+
if authority.get("schemaVersion") != "fact-check-x/authority-result@1" or authority.get("requestId") != point["id"]:
|
|
1435
|
+
raise PipelineError(f"{point['id']} 的单点核验结果版本或 ID 不正确")
|
|
1436
|
+
exempt_policy = official_exempt_policy(point)
|
|
1437
|
+
expected_mode = {
|
|
1438
|
+
"dknow_official_reference": "dknow_exempt",
|
|
1439
|
+
"gov_cn_reference": "gov_exempt",
|
|
1440
|
+
}.get(exempt_policy, "trusted_search")
|
|
1441
|
+
expected_count = 0 if exempt_policy else 1
|
|
1442
|
+
actual_mode = str(authority.get("searchMode") or "")
|
|
1443
|
+
actual_count = int(authority.get("requestCount") or 0)
|
|
1444
|
+
# 非对称门禁:比较阶段依据引用池认定「免搜索」,核验阶段可能独立判定该锚点
|
|
1445
|
+
# 撑不住主张而改走可信搜索。多搜一次是保守方向,放行;反过来把本应搜索的
|
|
1446
|
+
# 知识点降级成免搜索属于洗白,必须拦住。
|
|
1447
|
+
if actual_mode == expected_mode:
|
|
1448
|
+
if actual_count != expected_count:
|
|
1449
|
+
raise PipelineError(
|
|
1450
|
+
f"{point['id']} 权威证据模式为 {actual_mode},"
|
|
1451
|
+
f"请求数应为 {expected_count},实际为 {actual_count}"
|
|
1452
|
+
)
|
|
1453
|
+
elif exempt_policy and actual_mode == "trusted_search" and actual_count >= 1:
|
|
1454
|
+
downgraded_anchor_ids.append(str(point["id"]))
|
|
1455
|
+
else:
|
|
1456
|
+
raise PipelineError(
|
|
1457
|
+
f"{point['id']} 权威证据模式应为 {expected_mode},"
|
|
1458
|
+
f"请求数应为 {expected_count},"
|
|
1459
|
+
f"实际为 {actual_mode or '未知'}/{actual_count}"
|
|
1460
|
+
)
|
|
1461
|
+
request_count += int(authority.get("requestCount") or 0)
|
|
1462
|
+
dknow_exempt_count += int(authority.get("searchMode") == "dknow_exempt")
|
|
1463
|
+
gov_exempt_count += int(authority.get("searchMode") == "gov_exempt")
|
|
1464
|
+
for platform, verdict in (authority.get("verdicts") or {}).items():
|
|
1465
|
+
authority_verdicts[(point["id"], platform)] = str(
|
|
1466
|
+
verdict.get("verdict") or ""
|
|
1467
|
+
)
|
|
1468
|
+
finding = str(authority.get("authoritativeFinding") or "").strip()
|
|
1469
|
+
if not finding:
|
|
1470
|
+
finding = "未取得足够权威证据;该知识点保持证据不足。"
|
|
1471
|
+
authority = {**authority, "authoritativeFinding": finding}
|
|
1472
|
+
for gap in authority.get("evidenceGaps") or []:
|
|
1473
|
+
evidence_gaps.append({"stage": "authority", "knowledgePointId": point["id"], **gap})
|
|
1474
|
+
points.append({**point, "authority": authority})
|
|
1475
|
+
for review in comparison.get("analysisGaps") or []:
|
|
1476
|
+
key = (
|
|
1477
|
+
str(review.get("knowledgePointId") or ""),
|
|
1478
|
+
str(review.get("platform") or ""),
|
|
1479
|
+
)
|
|
1480
|
+
if (
|
|
1481
|
+
review.get("stage") == "comparison"
|
|
1482
|
+
and authority_verdicts.get(key) in ("supported", "contradicted")
|
|
1483
|
+
):
|
|
1484
|
+
continue
|
|
1485
|
+
evidence_gaps.append({
|
|
1486
|
+
**review,
|
|
1487
|
+
"reason": str(review.get("reason") or "")
|
|
1488
|
+
or "当前分析信息不足,且权威证据未能完成裁决",
|
|
1489
|
+
})
|
|
1490
|
+
final_answer_lines = []
|
|
1491
|
+
final_answer_items = []
|
|
1492
|
+
final_answer_point_ids = []
|
|
1493
|
+
excluded_point_ids = []
|
|
1494
|
+
supplemental_items = []
|
|
1495
|
+
supplemental_point_ids = []
|
|
1496
|
+
supplemental_excluded_point_ids = []
|
|
1497
|
+
for point in points:
|
|
1498
|
+
authority = point.get("authority") or {}
|
|
1499
|
+
claims = authority.get("claims") or {}
|
|
1500
|
+
verdicts = authority.get("verdicts") or {}
|
|
1501
|
+
resolved = any(
|
|
1502
|
+
claims.get(platform, {}).get("covered")
|
|
1503
|
+
and (
|
|
1504
|
+
verdict.get("verdict") in {"supported", "contradicted"}
|
|
1505
|
+
or verdict.get("category") == "recommendation"
|
|
1506
|
+
)
|
|
1507
|
+
for platform, verdict in verdicts.items()
|
|
1508
|
+
)
|
|
1509
|
+
point_id = str(point.get("id") or "")
|
|
1510
|
+
role = str(point.get("role") or "direct")
|
|
1511
|
+
if not resolved:
|
|
1512
|
+
if role == "direct":
|
|
1513
|
+
excluded_point_ids.append(point_id)
|
|
1514
|
+
else:
|
|
1515
|
+
supplemental_excluded_point_ids.append(point_id)
|
|
1516
|
+
continue
|
|
1517
|
+
finding = str(
|
|
1518
|
+
(authority.get("authoritativeFinding") or "")
|
|
1519
|
+
).strip()
|
|
1520
|
+
title = str(point.get("description") or point.get("id") or "").strip()
|
|
1521
|
+
answer = finding
|
|
1522
|
+
item = {
|
|
1523
|
+
"knowledgePointId": point_id,
|
|
1524
|
+
"title": title,
|
|
1525
|
+
"answer": answer,
|
|
1526
|
+
}
|
|
1527
|
+
if role == "direct":
|
|
1528
|
+
final_answer_point_ids.append(point_id)
|
|
1529
|
+
final_answer_lines.append(answer)
|
|
1530
|
+
final_answer_items.append(item)
|
|
1531
|
+
else:
|
|
1532
|
+
supplemental_point_ids.append(point_id)
|
|
1533
|
+
supplemental_items.append(item)
|
|
1534
|
+
final_answer_status = (
|
|
1535
|
+
"verified"
|
|
1536
|
+
if final_answer_point_ids and not excluded_point_ids
|
|
1537
|
+
else "partially_verified"
|
|
1538
|
+
if final_answer_point_ids
|
|
1539
|
+
else "insufficient_evidence"
|
|
1540
|
+
)
|
|
1541
|
+
return {
|
|
1542
|
+
"schemaVersion": "fact-check-x/verification@2",
|
|
1543
|
+
"question": comparison.get("question"),
|
|
1544
|
+
"coreQuestion": comparison.get("coreQuestion"),
|
|
1545
|
+
"finalAnswer": {
|
|
1546
|
+
"status": final_answer_status,
|
|
1547
|
+
"answer": "\n".join(final_answer_lines)
|
|
1548
|
+
or "当前没有足够权威证据形成确定答案;各项证据边界已在报告中列明。",
|
|
1549
|
+
"items": final_answer_items,
|
|
1550
|
+
"knowledgePointIds": final_answer_point_ids,
|
|
1551
|
+
"excludedKnowledgePointIds": excluded_point_ids,
|
|
1552
|
+
},
|
|
1553
|
+
"supplementalFindings": {
|
|
1554
|
+
"items": supplemental_items,
|
|
1555
|
+
"knowledgePointIds": supplemental_point_ids,
|
|
1556
|
+
"excludedKnowledgePointIds": supplemental_excluded_point_ids,
|
|
1557
|
+
},
|
|
1558
|
+
"createdAt": now_iso(),
|
|
1559
|
+
"platforms": comparison.get("platforms") or [],
|
|
1560
|
+
"knowledgePoints": points,
|
|
1561
|
+
"trustedSearchRequestCount": request_count,
|
|
1562
|
+
"dknowExemptCount": dknow_exempt_count,
|
|
1563
|
+
"govExemptCount": gov_exempt_count,
|
|
1564
|
+
"anchorDowngrades": downgraded_anchor_ids,
|
|
1565
|
+
"officialExemptCount": dknow_exempt_count + gov_exempt_count,
|
|
1566
|
+
"evidenceGaps": evidence_gaps,
|
|
1567
|
+
"evidenceGapCount": len(evidence_gaps),
|
|
1568
|
+
"status": "completed",
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def deliver(args: argparse.Namespace, skills: dict[str, Path]) -> dict:
|
|
1573
|
+
run_dir = require_run(args.run_dir)
|
|
1574
|
+
require_stage_acknowledged(run_dir, "capture")
|
|
1575
|
+
require_stage_acknowledged(run_dir, "comparison")
|
|
1576
|
+
require_stage_acknowledged(run_dir, "authority")
|
|
1577
|
+
results_path = Path(args.results).resolve()
|
|
1578
|
+
require_capture_gate(run_dir, results_path)
|
|
1579
|
+
results = validate_capture_results(results_path)
|
|
1580
|
+
authority_gate_path = run_dir / "authority-gate.json"
|
|
1581
|
+
if not authority_gate_path.exists():
|
|
1582
|
+
raise PipelineError("缺少权威核验门禁,禁止生成最终报告")
|
|
1583
|
+
authority_gate = load_json(authority_gate_path)
|
|
1584
|
+
if (
|
|
1585
|
+
authority_gate.get("schemaVersion") != "fact-check-x/authority-gate@1"
|
|
1586
|
+
or authority_gate.get("status") != "finalized"
|
|
1587
|
+
or authority_gate.get("finalizeStatus") != "completed"
|
|
1588
|
+
):
|
|
1589
|
+
raise PipelineError("权威核验尚未通过取证与裁决门禁,禁止生成最终报告")
|
|
1590
|
+
comparison_gate = require_comparison_provenance(run_dir)
|
|
1591
|
+
comparison_path = Path(args.comparison).resolve() if args.comparison else run_dir / "comparison.json"
|
|
1592
|
+
if comparison_path != Path(comparison_gate["files"]["comparison"]["path"]).resolve():
|
|
1593
|
+
raise PipelineError("deliver 只能使用 provenance 门禁锁定的 comparison.json")
|
|
1594
|
+
requests_manifest = load_json(run_dir / "authority" / "requests" / "manifest.json")
|
|
1595
|
+
request_ids = expected_request_ids(requests_manifest)
|
|
1596
|
+
if json_file_manifest(
|
|
1597
|
+
run_dir / "authority" / "requests",
|
|
1598
|
+
{f"{request_id}.json" for request_id in request_ids} | {"manifest.json"},
|
|
1599
|
+
) != authority_gate.get("requestHashes"):
|
|
1600
|
+
raise PipelineError("请求文件在裁决后被修改,禁止交付")
|
|
1601
|
+
if json_file_manifest(
|
|
1602
|
+
run_dir / "authority" / "evidence",
|
|
1603
|
+
{f"{request_id}.json" for request_id in request_ids} | {"batch.json"},
|
|
1604
|
+
) != authority_gate.get("evidenceHashes"):
|
|
1605
|
+
raise PipelineError("证据文件在裁决后被修改,禁止交付")
|
|
1606
|
+
if json_file_manifest(
|
|
1607
|
+
run_dir / "authority" / "results",
|
|
1608
|
+
{f"{request_id}.json" for request_id in request_ids},
|
|
1609
|
+
) != authority_gate.get("resultHashes"):
|
|
1610
|
+
raise PipelineError("结果文件被修改或混入额外/陈旧 ID,禁止交付")
|
|
1611
|
+
assessments_dir = run_dir / "authority" / "assessments"
|
|
1612
|
+
if json_file_manifest(assessments_dir, set((authority_gate.get("assessmentHashes") or {}).keys())) != authority_gate.get("assessmentHashes"):
|
|
1613
|
+
raise PipelineError("assessment 文件被修改、缺失或混入额外文件,禁止交付")
|
|
1614
|
+
verification_path = run_dir / "verification.json"
|
|
1615
|
+
report_path = run_dir / "report.html"
|
|
1616
|
+
capture_deliverable = run_dir / "01-capture-report.html"
|
|
1617
|
+
comparison_deliverable = run_dir / "02-comparison-report.html"
|
|
1618
|
+
authority_deliverable = run_dir / "03-authority-report.html"
|
|
1619
|
+
final_deliverable = run_dir / "04-final-report.html"
|
|
1620
|
+
report_package = run_dir / PORTABLE_REPORT_PACKAGE
|
|
1621
|
+
if not verification_path.is_file():
|
|
1622
|
+
raise PipelineError("缺少第三步已锁定的 verification.json,禁止重算或交付")
|
|
1623
|
+
if capture_digest(verification_path) != authority_gate.get("verificationSha256"):
|
|
1624
|
+
raise PipelineError("第三步权威核验结果已被修改,禁止第四步重算或交付")
|
|
1625
|
+
if (
|
|
1626
|
+
not authority_deliverable.is_file()
|
|
1627
|
+
or capture_digest(authority_deliverable)
|
|
1628
|
+
!= authority_gate.get("authorityReportSha256")
|
|
1629
|
+
):
|
|
1630
|
+
raise PipelineError("第三步报告已缺失或被修改,禁止第四步继续")
|
|
1631
|
+
verification = load_json(verification_path)
|
|
1632
|
+
for stale in (
|
|
1633
|
+
run_dir / "03-final-report.html",
|
|
1634
|
+
run_dir / "04-complete-report-package.zip",
|
|
1635
|
+
):
|
|
1636
|
+
stale.unlink(missing_ok=True)
|
|
1637
|
+
capture_report = {
|
|
1638
|
+
"results": str((run_dir / "capture" / "results.json").resolve()),
|
|
1639
|
+
"report": str((run_dir / "capture" / "report.html").resolve()),
|
|
1640
|
+
"markdown": str((run_dir / "capture" / "report.md").resolve()),
|
|
1641
|
+
}
|
|
1642
|
+
for required in (
|
|
1643
|
+
capture_deliverable,
|
|
1644
|
+
comparison_deliverable,
|
|
1645
|
+
authority_deliverable,
|
|
1646
|
+
Path(capture_report["results"]),
|
|
1647
|
+
Path(capture_report["report"]),
|
|
1648
|
+
Path(capture_report["markdown"]),
|
|
1649
|
+
):
|
|
1650
|
+
if not required.is_file():
|
|
1651
|
+
raise PipelineError(f"缺少已完成阶段的锁定产物:{required.name}")
|
|
1652
|
+
run([
|
|
1653
|
+
sys.executable,
|
|
1654
|
+
str(skills["authority"] / "scripts" / "render_final_report.py"),
|
|
1655
|
+
"--results",
|
|
1656
|
+
str(results_path),
|
|
1657
|
+
"--comparison",
|
|
1658
|
+
str(comparison_path),
|
|
1659
|
+
"--verification",
|
|
1660
|
+
str(verification_path),
|
|
1661
|
+
"--output",
|
|
1662
|
+
str(report_path),
|
|
1663
|
+
"--intermediate-dir",
|
|
1664
|
+
str(run_dir / "report-input"),
|
|
1665
|
+
])
|
|
1666
|
+
shutil.copyfile(report_path, final_deliverable)
|
|
1667
|
+
normalize_report_file(final_deliverable, collect_technical_notices(run_dir))
|
|
1668
|
+
checkpoint = record_stage_checkpoint(
|
|
1669
|
+
run_dir,
|
|
1670
|
+
"evaluation",
|
|
1671
|
+
"各方答案测评报告",
|
|
1672
|
+
final_deliverable,
|
|
1673
|
+
None,
|
|
1674
|
+
)
|
|
1675
|
+
manifest = {
|
|
1676
|
+
"schemaVersion": "fact-check-x/pipeline@2",
|
|
1677
|
+
"createdAt": now_iso(),
|
|
1678
|
+
"status": verification["status"],
|
|
1679
|
+
"skills": DEPENDENCIES,
|
|
1680
|
+
"knowledgePointCount": len(verification["knowledgePoints"]),
|
|
1681
|
+
"trustedSearchRequestCount": verification["trustedSearchRequestCount"],
|
|
1682
|
+
"dknowExemptCount": verification["dknowExemptCount"],
|
|
1683
|
+
"govExemptCount": verification["govExemptCount"],
|
|
1684
|
+
"officialExemptCount": verification["officialExemptCount"],
|
|
1685
|
+
"artifacts": {
|
|
1686
|
+
"sourceResults": str(results_path),
|
|
1687
|
+
"captureGate": str((run_dir / "capture-gate.json").resolve()),
|
|
1688
|
+
"results": capture_report["results"],
|
|
1689
|
+
"answerReferenceReport": capture_report["report"],
|
|
1690
|
+
"captureDeliverable": str(capture_deliverable.resolve()),
|
|
1691
|
+
"answerReferenceMarkdown": capture_report["markdown"],
|
|
1692
|
+
"comparisonTask": str((run_dir / "comparison-task.json").resolve()),
|
|
1693
|
+
"comparisonAnalysis": str(
|
|
1694
|
+
(run_dir / "comparison-analysis.json").resolve()
|
|
1695
|
+
),
|
|
1696
|
+
"comparison": str(comparison_path.resolve()),
|
|
1697
|
+
"comparisonReport": str((run_dir / "comparison.html").resolve()),
|
|
1698
|
+
"comparisonDeliverable": str(comparison_deliverable.resolve()),
|
|
1699
|
+
"comparisonGate": str((run_dir / "comparison-gate.json").resolve()),
|
|
1700
|
+
"authorityGate": str((run_dir / "authority-gate.json").resolve()),
|
|
1701
|
+
"stageCheckpoints": str((run_dir / INTERACTION_GATE_FILE).resolve()),
|
|
1702
|
+
"authorityDirectory": str((run_dir / "authority").resolve()),
|
|
1703
|
+
"verification": str(verification_path.resolve()),
|
|
1704
|
+
"authorityReport": str(authority_deliverable.resolve()),
|
|
1705
|
+
"authorityDeliverable": str(authority_deliverable.resolve()),
|
|
1706
|
+
"report": str(report_path.resolve()),
|
|
1707
|
+
"finalDeliverable": str(final_deliverable.resolve()),
|
|
1708
|
+
"reportPackage": str(report_package.resolve()),
|
|
1709
|
+
},
|
|
1710
|
+
}
|
|
1711
|
+
dump_json(run_dir / "pipeline.json", manifest)
|
|
1712
|
+
package = build_portable_report_package(run_dir)
|
|
1713
|
+
manifest["artifacts"].update({
|
|
1714
|
+
"reportPackageSha256": package["sha256"],
|
|
1715
|
+
"reportPackageBytes": package["bytes"],
|
|
1716
|
+
})
|
|
1717
|
+
dump_json(run_dir / "pipeline.json", manifest)
|
|
1718
|
+
return {
|
|
1719
|
+
"status": manifest["status"],
|
|
1720
|
+
"manifest": str((run_dir / "pipeline.json").resolve()),
|
|
1721
|
+
"evidenceGapCount": len(verification.get("evidenceGaps") or []),
|
|
1722
|
+
"artifacts": manifest["artifacts"],
|
|
1723
|
+
"answerReferenceReport": manifest["artifacts"]["answerReferenceReport"],
|
|
1724
|
+
"comparisonReport": manifest["artifacts"]["comparisonReport"],
|
|
1725
|
+
"report": str(report_path.resolve()),
|
|
1726
|
+
"deliverables": [
|
|
1727
|
+
deliverable_descriptor("各方答案汇总", capture_deliverable),
|
|
1728
|
+
deliverable_descriptor("各方答案聚合(未核验)", comparison_deliverable),
|
|
1729
|
+
deliverable_descriptor("权威核验后的最终答案", authority_deliverable),
|
|
1730
|
+
deliverable_descriptor("各方答案测评报告", final_deliverable),
|
|
1731
|
+
deliverable_descriptor(
|
|
1732
|
+
"完整可分发报告包",
|
|
1733
|
+
package["path"],
|
|
1734
|
+
sha256=package["sha256"],
|
|
1735
|
+
portable=True,
|
|
1736
|
+
),
|
|
1737
|
+
],
|
|
1738
|
+
"checkpoint": checkpoint,
|
|
1739
|
+
"trustedSearchRequestCount": manifest["trustedSearchRequestCount"],
|
|
1740
|
+
"dknowExemptCount": manifest["dknowExemptCount"],
|
|
1741
|
+
"govExemptCount": manifest["govExemptCount"],
|
|
1742
|
+
"officialExemptCount": manifest["officialExemptCount"],
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
def parser() -> argparse.ArgumentParser:
|
|
1747
|
+
root = argparse.ArgumentParser(description="Fact-Check-X 轻量统一编排入口。")
|
|
1748
|
+
commands = root.add_subparsers(dest="command", required=True)
|
|
1749
|
+
commands.add_parser("locate")
|
|
1750
|
+
commands.add_parser("prepare-runtime")
|
|
1751
|
+
prepare = commands.add_parser("prepare-comparison")
|
|
1752
|
+
prepare.add_argument("--results", required=True)
|
|
1753
|
+
prepare.add_argument("--run-dir", required=True)
|
|
1754
|
+
prepare.add_argument(
|
|
1755
|
+
"--execution-mode",
|
|
1756
|
+
choices=("interactive",),
|
|
1757
|
+
default="interactive",
|
|
1758
|
+
help="固定为 interactive;每阶段均需用户确认后才能继续。",
|
|
1759
|
+
)
|
|
1760
|
+
acknowledge = commands.add_parser("acknowledge-stage")
|
|
1761
|
+
acknowledge.add_argument("--run-dir", required=True)
|
|
1762
|
+
acknowledge.add_argument(
|
|
1763
|
+
"--stage",
|
|
1764
|
+
required=True,
|
|
1765
|
+
choices=("capture", "comparison", "authority"),
|
|
1766
|
+
)
|
|
1767
|
+
acknowledge.add_argument("--token", required=True)
|
|
1768
|
+
acknowledge.add_argument(
|
|
1769
|
+
"--decision",
|
|
1770
|
+
required=True,
|
|
1771
|
+
choices=("continue", "revise", "stop"),
|
|
1772
|
+
)
|
|
1773
|
+
complete = commands.add_parser("complete-comparison")
|
|
1774
|
+
complete.add_argument("--results", required=True)
|
|
1775
|
+
complete.add_argument("--run-dir", required=True)
|
|
1776
|
+
complete.add_argument("--analysis")
|
|
1777
|
+
authority = commands.add_parser("prepare-authority")
|
|
1778
|
+
authority.add_argument("--run-dir", required=True)
|
|
1779
|
+
authority.add_argument("--comparison")
|
|
1780
|
+
search = commands.add_parser("search-authority")
|
|
1781
|
+
search.add_argument("--run-dir", required=True)
|
|
1782
|
+
search.add_argument("--max-workers", type=int, default=12)
|
|
1783
|
+
search.add_argument("--service-area", default="")
|
|
1784
|
+
search.add_argument("--fixtures")
|
|
1785
|
+
final = commands.add_parser("finalize-authority")
|
|
1786
|
+
final.add_argument("--run-dir", required=True)
|
|
1787
|
+
final.add_argument("--assessments-dir")
|
|
1788
|
+
reopen = commands.add_parser("reopen-authority")
|
|
1789
|
+
reopen.add_argument("--run-dir", required=True)
|
|
1790
|
+
reopen.add_argument("--reason", required=True)
|
|
1791
|
+
delivery = commands.add_parser("deliver")
|
|
1792
|
+
delivery.add_argument("--results", required=True)
|
|
1793
|
+
delivery.add_argument("--run-dir", required=True)
|
|
1794
|
+
delivery.add_argument("--comparison")
|
|
1795
|
+
return root
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
def main() -> int:
|
|
1799
|
+
args = parser().parse_args()
|
|
1800
|
+
try:
|
|
1801
|
+
skills = locate_skills()
|
|
1802
|
+
if args.command == "locate":
|
|
1803
|
+
result = {"status": "completed", "skills": {key: str(path) for key, path in skills.items()}}
|
|
1804
|
+
elif args.command == "prepare-runtime":
|
|
1805
|
+
result = prepare_runtime(skills)
|
|
1806
|
+
elif args.command == "prepare-comparison":
|
|
1807
|
+
result = prepare_comparison(args, skills)
|
|
1808
|
+
elif args.command == "acknowledge-stage":
|
|
1809
|
+
result = acknowledge_stage(args)
|
|
1810
|
+
elif args.command == "complete-comparison":
|
|
1811
|
+
result = complete_comparison(args, skills)
|
|
1812
|
+
elif args.command == "prepare-authority":
|
|
1813
|
+
result = prepare_authority(args)
|
|
1814
|
+
elif args.command == "search-authority":
|
|
1815
|
+
result = search_authority(args, skills)
|
|
1816
|
+
elif args.command == "finalize-authority":
|
|
1817
|
+
result = finalize_authority(args, skills)
|
|
1818
|
+
elif args.command == "reopen-authority":
|
|
1819
|
+
result = reopen_authority(args)
|
|
1820
|
+
else:
|
|
1821
|
+
result = deliver(args, skills)
|
|
1822
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
1823
|
+
if result.get("status") == "configuration_required":
|
|
1824
|
+
return 3
|
|
1825
|
+
return 0
|
|
1826
|
+
except (PipelineError, OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1827
|
+
print(json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=False))
|
|
1828
|
+
return 1
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
if __name__ == "__main__":
|
|
1832
|
+
raise SystemExit(main())
|