create-yss-spec 3.5.6 → 3.5.8
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/bin/create-yss-spec.js +3 -1
- package/cli-core.lock.json +10 -6
- package/package.json +4 -1
- package/src/api/_sync-service.js +3 -1
- package/src/command-runner.mjs +44 -9
- package/src/content-identity.js +111 -0
- package/src/runtime-store.mjs +650 -0
- package/src/template/asset-runtime.js +4 -1
- package/src/template/instance-runtime.js +23 -7
- package/src/validation/metadata.js +8 -0
- package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
- package/template/.agents/skills/alibaba-java-code-style/SKILL.md +30 -36
- package/template/.agents/skills/archify/SKILL.md +2 -2
- package/template/.agents/skills/archify/scripts/yss-safe-deliver.mjs +10 -6
- package/template/.agents/skills/archify/test/yss-safe-deliver.test.mjs +9 -2
- package/template/.agents/skills/code-review/SKILL.md +19 -11
- package/template/.agents/skills/code-review/references/candidate-capture.md +1 -1
- package/template/.agents/skills/code-review/references/legacy-convergence.md +3 -1
- package/template/.agents/skills/code-review/references/review-axis-checklists.md +2 -0
- package/template/.agents/skills/code-review/references/yss-review-standards.md +4 -2
- package/template/.agents/skills/competitive-intelligence/SKILL.md +32 -74
- package/template/.agents/skills/file-export-download/SKILL.md +4 -73
- package/template/.agents/skills/file-export-download/references/binary-contract-example.md +26 -0
- package/template/.agents/skills/file-export-download/references/download-call-example.md +45 -0
- package/template/.agents/skills/formily-foundation/SKILL.md +6 -105
- package/template/.agents/skills/formily-foundation/references/async-save-example.md +57 -0
- package/template/.agents/skills/formily-foundation/references/query-layout.md +50 -0
- package/template/.agents/skills/formily-linkage-effects/SKILL.md +3 -116
- package/template/.agents/skills/formily-linkage-effects/references/linkage-example.md +116 -0
- package/template/.agents/skills/formily-mode-slot-detail/SKILL.md +3 -81
- package/template/.agents/skills/formily-mode-slot-detail/references/mode-slot-example.md +81 -0
- package/template/.agents/skills/formily-step-flow/SKILL.md +3 -143
- package/template/.agents/skills/formily-step-flow/references/step-flow-example.md +143 -0
- package/template/.agents/skills/handoff/SKILL.md +3 -1
- package/template/.agents/skills/i-have-adhd/SKILL.md +2 -2
- package/template/.agents/skills/i-have-adhd/references/yss-adaptation.md +2 -2
- package/template/.agents/skills/i-have-adhd/tests/verify-integration.mjs +7 -2
- package/template/.agents/skills/implement/SKILL.md +2 -2
- package/template/.agents/skills/llm-wiki/references/transactions.md +2 -2
- package/template/.agents/skills/llm-wiki/scripts/core.mjs +40 -2
- package/template/.agents/skills/llm-wiki/scripts/transaction.mjs +30 -27
- package/template/.agents/skills/llm-wiki/scripts/v2.test.mjs +38 -2
- package/template/.agents/skills/lombok/SKILL.md +2 -0
- package/template/.agents/skills/mapstruct/SKILL.md +2 -0
- package/template/.agents/skills/tdd/SKILL.md +2 -2
- package/template/.agents/skills/theme-token-usage/SKILL.md +6 -5
- package/template/.agents/skills/wayfinder/references/chart-map.md +2 -2
- package/template/.agents/skills/wayfinder/references/ticket-types.md +1 -1
- package/template/.agents/skills/yss-api-integration/SKILL.md +13 -50
- package/template/.agents/skills/yss-api-integration/references/orval-list-example.md +38 -0
- package/template/.agents/skills/yss-application/SKILL.md +11 -5
- package/template/.agents/skills/yss-audit-log/SKILL.md +8 -6
- package/template/.agents/skills/yss-audit-log/references/README.md +5 -5
- package/template/.agents/skills/yss-cache/SKILL.md +4 -4
- package/template/.agents/skills/yss-ddd-scaffold-generator/SKILL.md +7 -8
- package/template/.agents/skills/yss-ddd-scaffold-generator/references/USAGE_EXAMPLES.md +13 -6
- package/template/.agents/skills/yss-ddd-scaffold-generator/references/command-examples.md +2 -0
- package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +28 -1
- package/template/.agents/skills/yss-design-system/SKILL.md +2 -2
- package/template/.agents/skills/yss-distributed-id/SKILL.md +4 -4
- package/template/.agents/skills/yss-dto/SKILL.md +9 -9
- package/template/.agents/skills/yss-dto/references/README.md +4 -4
- package/template/.agents/skills/yss-dto/references/wire-validation-checklist.md +4 -4
- package/template/.agents/skills/yss-excel-mvc/SKILL.md +4 -4
- package/template/.agents/skills/yss-exception/SKILL.md +42 -48
- package/template/.agents/skills/yss-formily-schema-generator/SKILL.md +26 -170
- package/template/.agents/skills/yss-formily-schema-generator/references/workflow.md +49 -193
- package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +14 -10
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -6
- package/template/.agents/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +15 -1
- package/template/.agents/skills/yss-layered-mvc-scaffold-generator/SKILL.md +1 -1
- package/template/.agents/skills/yss-mybatis/SKILL.md +4 -4
- package/template/.agents/skills/yss-openapi-draft-review/SKILL.md +6 -4
- package/template/.agents/skills/yss-openapi-draft-review/references/review-output.md +2 -0
- package/template/.agents/skills/yss-openapi-draft-review/references/semantic-review.md +3 -3
- package/template/.agents/skills/yss-openapi-governance/SKILL.md +5 -7
- package/template/.agents/skills/yss-openapi-governance/references/governance-output.md +2 -0
- package/template/.agents/skills/yss-openapi-governance/references/governance-workflow.md +3 -3
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +23 -21
- package/template/.agents/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +25 -5
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +100 -4
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +35 -12
- package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +3 -1
- package/template/.agents/skills/yss-product-lifecycle/references/user-decisions.md +3 -3
- package/template/.agents/skills/yss-prototype-stage/SKILL.md +1 -5
- package/template/.agents/skills/yss-repository/SKILL.md +2 -2
- package/template/.agents/skills/yss-research/SKILL.md +39 -48
- package/template/.agents/skills/yss-research/assets/competitive-evidence-template.yaml +146 -0
- package/template/.agents/skills/yss-research/references/competitive-analysis.md +76 -0
- package/template/.agents/skills/yss-research/references/competitive-analysis.schema.json +324 -0
- package/template/.agents/skills/yss-research/references/evidence-contract.md +8 -0
- package/template/.agents/skills/yss-research/scripts/lib/competitive-analysis.mjs +185 -0
- package/template/.agents/skills/yss-research/scripts/render-competitive-outputs.mjs +16 -0
- package/template/.agents/skills/yss-research/scripts/validate-research-package.mjs +6 -2
- package/template/.agents/skills/yss-research/tests/competitive-scenarios.test.mjs +270 -0
- package/template/.agents/skills/yss-resilience4j/SKILL.md +25 -31
- package/template/.agents/skills/yss-security-algorithm/SKILL.md +37 -43
- package/template/.agents/skills/yss-skill-source-index-refresh/SKILL.md +29 -36
- package/template/.agents/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
- package/template/.agents/skills/yss-stage-decision/SKILL.md +2 -5
- package/template/.agents/skills/yss-tactical-design/SKILL.md +1 -1
- package/template/.agents/skills/yss-tactical-design/references/output-template.md +3 -3
- package/template/.agents/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
- package/template/.agents/skills/yss-technical-design/SKILL.md +6 -5
- package/template/.agents/skills/yss-ui/SKILL.md +7 -1
- package/template/.agents/skills/yss-ui/assets/docs/guide/gitlab-ci-integration.md +2 -2
- package/template/.agents/skills/yss-ui/references/checklist.md +38 -96
- package/template/.agents/skills/yss-ui/references/component-routing.md +3 -1
- package/template/.agents/skills/yss-ui/references/quick-recipes.md +17 -272
- package/template/.agents/skills/yss-ui/references/specialized-components.md +3 -3
- package/template/.agents/skills/yss-ui-business-page-generation/SKILL.md +1 -1
- package/template/.agents/skills/yss-up-springboot3/SKILL.md +19 -21
- package/template/.agents/skills/yss-userinfo/SKILL.md +27 -33
- package/template/.agents/skills/yss-validation/SKILL.md +26 -32
- package/template/.agents/skills/yss-web-controller/SKILL.md +11 -44
- package/template/.agents/skills/yss-web-controller/references/crud-generation.md +38 -0
- package/template/.agents/skills/ytable-usage/SKILL.md +2 -2
- package/template/.agents/skills/ytree-usage/SKILL.md +2 -80
- package/template/.agents/skills/ytree-usage/references/tree-and-confirmation-example.md +81 -0
- package/template/.codex/skills/alibaba-java-code-style/SKILL.md +30 -36
- package/template/.codex/skills/archify/SKILL.md +2 -2
- package/template/.codex/skills/archify/scripts/yss-safe-deliver.mjs +10 -6
- package/template/.codex/skills/archify/test/yss-safe-deliver.test.mjs +9 -2
- package/template/.codex/skills/code-review/SKILL.md +19 -11
- package/template/.codex/skills/code-review/references/candidate-capture.md +1 -1
- package/template/.codex/skills/code-review/references/legacy-convergence.md +3 -1
- package/template/.codex/skills/code-review/references/review-axis-checklists.md +2 -0
- package/template/.codex/skills/code-review/references/yss-review-standards.md +4 -2
- package/template/.codex/skills/competitive-intelligence/SKILL.md +32 -74
- package/template/.codex/skills/file-export-download/SKILL.md +4 -73
- package/template/.codex/skills/file-export-download/references/binary-contract-example.md +26 -0
- package/template/.codex/skills/file-export-download/references/download-call-example.md +45 -0
- package/template/.codex/skills/formily-foundation/SKILL.md +6 -105
- package/template/.codex/skills/formily-foundation/references/async-save-example.md +57 -0
- package/template/.codex/skills/formily-foundation/references/query-layout.md +50 -0
- package/template/.codex/skills/formily-linkage-effects/SKILL.md +3 -116
- package/template/.codex/skills/formily-linkage-effects/references/linkage-example.md +116 -0
- package/template/.codex/skills/formily-mode-slot-detail/SKILL.md +3 -81
- package/template/.codex/skills/formily-mode-slot-detail/references/mode-slot-example.md +81 -0
- package/template/.codex/skills/formily-step-flow/SKILL.md +3 -143
- package/template/.codex/skills/formily-step-flow/references/step-flow-example.md +143 -0
- package/template/.codex/skills/handoff/SKILL.md +3 -1
- package/template/.codex/skills/i-have-adhd/SKILL.md +2 -2
- package/template/.codex/skills/i-have-adhd/references/yss-adaptation.md +2 -2
- package/template/.codex/skills/i-have-adhd/tests/verify-integration.mjs +7 -2
- package/template/.codex/skills/implement/SKILL.md +2 -2
- package/template/.codex/skills/llm-wiki/references/transactions.md +2 -2
- package/template/.codex/skills/llm-wiki/scripts/core.mjs +40 -2
- package/template/.codex/skills/llm-wiki/scripts/transaction.mjs +30 -27
- package/template/.codex/skills/llm-wiki/scripts/v2.test.mjs +38 -2
- package/template/.codex/skills/lombok/SKILL.md +2 -0
- package/template/.codex/skills/mapstruct/SKILL.md +2 -0
- package/template/.codex/skills/product-design/references/communication-protocol.md +3 -3
- package/template/.codex/skills/product-design/references/critical-overrides.md +4 -5
- package/template/.codex/skills/product-design/skills/audit/SKILL.md +2 -4
- package/template/.codex/skills/product-design/skills/get-context/SKILL.md +6 -8
- package/template/.codex/skills/product-design/skills/ideate/SKILL.md +3 -3
- package/template/.codex/skills/product-design/skills/image-to-code/SKILL.md +3 -5
- package/template/.codex/skills/product-design/skills/index/SKILL.md +1 -1
- package/template/.codex/skills/product-design/skills/prototype/SKILL.md +2 -4
- package/template/.codex/skills/product-design/skills/research/SKILL.md +4 -6
- package/template/.codex/skills/product-design/skills/research/assets/research-brief-template.md +3 -3
- package/template/.codex/skills/product-design/skills/share/SKILL.md +2 -4
- package/template/.codex/skills/product-design/skills/url-to-code/SKILL.md +2 -4
- package/template/.codex/skills/product-design/skills/user-context/SKILL.md +1 -1
- package/template/.codex/skills/tdd/SKILL.md +2 -2
- package/template/.codex/skills/theme-token-usage/SKILL.md +6 -5
- package/template/.codex/skills/wayfinder/references/chart-map.md +2 -2
- package/template/.codex/skills/wayfinder/references/ticket-types.md +1 -1
- package/template/.codex/skills/yss-api-integration/SKILL.md +13 -50
- package/template/.codex/skills/yss-api-integration/references/orval-list-example.md +38 -0
- package/template/.codex/skills/yss-application/SKILL.md +11 -5
- package/template/.codex/skills/yss-audit-log/SKILL.md +8 -6
- package/template/.codex/skills/yss-audit-log/references/README.md +5 -5
- package/template/.codex/skills/yss-cache/SKILL.md +4 -4
- package/template/.codex/skills/yss-ddd-scaffold-generator/SKILL.md +7 -8
- package/template/.codex/skills/yss-ddd-scaffold-generator/references/USAGE_EXAMPLES.md +13 -6
- package/template/.codex/skills/yss-ddd-scaffold-generator/references/command-examples.md +2 -0
- package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +28 -1
- package/template/.codex/skills/yss-design-system/SKILL.md +2 -2
- package/template/.codex/skills/yss-distributed-id/SKILL.md +4 -4
- package/template/.codex/skills/yss-dto/SKILL.md +9 -9
- package/template/.codex/skills/yss-dto/references/README.md +4 -4
- package/template/.codex/skills/yss-dto/references/wire-validation-checklist.md +4 -4
- package/template/.codex/skills/yss-excel-mvc/SKILL.md +4 -4
- package/template/.codex/skills/yss-exception/SKILL.md +42 -48
- package/template/.codex/skills/yss-formily-schema-generator/SKILL.md +26 -170
- package/template/.codex/skills/yss-formily-schema-generator/references/workflow.md +49 -193
- package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +14 -10
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -6
- package/template/.codex/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +15 -1
- package/template/.codex/skills/yss-layered-mvc-scaffold-generator/SKILL.md +1 -1
- package/template/.codex/skills/yss-mybatis/SKILL.md +4 -4
- package/template/.codex/skills/yss-openapi-draft-review/SKILL.md +6 -4
- package/template/.codex/skills/yss-openapi-draft-review/references/review-output.md +2 -0
- package/template/.codex/skills/yss-openapi-draft-review/references/semantic-review.md +3 -3
- package/template/.codex/skills/yss-openapi-governance/SKILL.md +5 -7
- package/template/.codex/skills/yss-openapi-governance/references/governance-output.md +2 -0
- package/template/.codex/skills/yss-openapi-governance/references/governance-workflow.md +3 -3
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +23 -21
- package/template/.codex/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +25 -5
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +100 -4
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +35 -12
- package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +3 -1
- package/template/.codex/skills/yss-product-lifecycle/references/user-decisions.md +3 -3
- package/template/.codex/skills/yss-prototype-stage/SKILL.md +1 -5
- package/template/.codex/skills/yss-repository/SKILL.md +2 -2
- package/template/.codex/skills/yss-research/SKILL.md +39 -48
- package/template/.codex/skills/yss-research/assets/competitive-evidence-template.yaml +146 -0
- package/template/.codex/skills/yss-research/references/competitive-analysis.md +76 -0
- package/template/.codex/skills/yss-research/references/competitive-analysis.schema.json +324 -0
- package/template/.codex/skills/yss-research/references/evidence-contract.md +8 -0
- package/template/.codex/skills/yss-research/scripts/lib/competitive-analysis.mjs +185 -0
- package/template/.codex/skills/yss-research/scripts/render-competitive-outputs.mjs +16 -0
- package/template/.codex/skills/yss-research/scripts/validate-research-package.mjs +6 -2
- package/template/.codex/skills/yss-research/tests/competitive-scenarios.test.mjs +270 -0
- package/template/.codex/skills/yss-resilience4j/SKILL.md +25 -31
- package/template/.codex/skills/yss-security-algorithm/SKILL.md +37 -43
- package/template/.codex/skills/yss-skill-source-index-refresh/SKILL.md +29 -36
- package/template/.codex/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
- package/template/.codex/skills/yss-stage-decision/SKILL.md +2 -5
- package/template/.codex/skills/yss-tactical-design/SKILL.md +1 -1
- package/template/.codex/skills/yss-tactical-design/references/output-template.md +3 -3
- package/template/.codex/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
- package/template/.codex/skills/yss-technical-design/SKILL.md +6 -5
- package/template/.codex/skills/yss-ui/SKILL.md +7 -1
- package/template/.codex/skills/yss-ui/assets/docs/guide/gitlab-ci-integration.md +2 -2
- package/template/.codex/skills/yss-ui/references/checklist.md +38 -96
- package/template/.codex/skills/yss-ui/references/component-routing.md +3 -1
- package/template/.codex/skills/yss-ui/references/quick-recipes.md +17 -272
- package/template/.codex/skills/yss-ui/references/specialized-components.md +3 -3
- package/template/.codex/skills/yss-ui-business-page-generation/SKILL.md +1 -1
- package/template/.codex/skills/yss-up-springboot3/SKILL.md +19 -21
- package/template/.codex/skills/yss-userinfo/SKILL.md +27 -33
- package/template/.codex/skills/yss-validation/SKILL.md +26 -32
- package/template/.codex/skills/yss-web-controller/SKILL.md +11 -44
- package/template/.codex/skills/yss-web-controller/references/crud-generation.md +38 -0
- package/template/.codex/skills/ytable-usage/SKILL.md +2 -2
- package/template/.codex/skills/ytree-usage/SKILL.md +2 -80
- package/template/.codex/skills/ytree-usage/references/tree-and-confirmation-example.md +81 -0
- package/template/.cursor/skills/alibaba-java-code-style/SKILL.md +30 -36
- package/template/.cursor/skills/archify/SKILL.md +2 -2
- package/template/.cursor/skills/archify/scripts/yss-safe-deliver.mjs +10 -6
- package/template/.cursor/skills/archify/test/yss-safe-deliver.test.mjs +9 -2
- package/template/.cursor/skills/code-review/SKILL.md +19 -11
- package/template/.cursor/skills/code-review/references/candidate-capture.md +1 -1
- package/template/.cursor/skills/code-review/references/legacy-convergence.md +3 -1
- package/template/.cursor/skills/code-review/references/review-axis-checklists.md +2 -0
- package/template/.cursor/skills/code-review/references/yss-review-standards.md +4 -2
- package/template/.cursor/skills/competitive-intelligence/SKILL.md +32 -74
- package/template/.cursor/skills/file-export-download/SKILL.md +4 -73
- package/template/.cursor/skills/file-export-download/references/binary-contract-example.md +26 -0
- package/template/.cursor/skills/file-export-download/references/download-call-example.md +45 -0
- package/template/.cursor/skills/formily-foundation/SKILL.md +6 -105
- package/template/.cursor/skills/formily-foundation/references/async-save-example.md +57 -0
- package/template/.cursor/skills/formily-foundation/references/query-layout.md +50 -0
- package/template/.cursor/skills/formily-linkage-effects/SKILL.md +3 -116
- package/template/.cursor/skills/formily-linkage-effects/references/linkage-example.md +116 -0
- package/template/.cursor/skills/formily-mode-slot-detail/SKILL.md +3 -81
- package/template/.cursor/skills/formily-mode-slot-detail/references/mode-slot-example.md +81 -0
- package/template/.cursor/skills/formily-step-flow/SKILL.md +3 -143
- package/template/.cursor/skills/formily-step-flow/references/step-flow-example.md +143 -0
- package/template/.cursor/skills/handoff/SKILL.md +3 -1
- package/template/.cursor/skills/i-have-adhd/SKILL.md +2 -2
- package/template/.cursor/skills/i-have-adhd/references/yss-adaptation.md +2 -2
- package/template/.cursor/skills/i-have-adhd/tests/verify-integration.mjs +7 -2
- package/template/.cursor/skills/implement/SKILL.md +2 -2
- package/template/.cursor/skills/llm-wiki/references/transactions.md +2 -2
- package/template/.cursor/skills/llm-wiki/scripts/core.mjs +40 -2
- package/template/.cursor/skills/llm-wiki/scripts/transaction.mjs +30 -27
- package/template/.cursor/skills/llm-wiki/scripts/v2.test.mjs +38 -2
- package/template/.cursor/skills/lombok/SKILL.md +2 -0
- package/template/.cursor/skills/mapstruct/SKILL.md +2 -0
- package/template/.cursor/skills/tdd/SKILL.md +2 -2
- package/template/.cursor/skills/theme-token-usage/SKILL.md +6 -5
- package/template/.cursor/skills/wayfinder/references/chart-map.md +2 -2
- package/template/.cursor/skills/wayfinder/references/ticket-types.md +1 -1
- package/template/.cursor/skills/yss-api-integration/SKILL.md +13 -50
- package/template/.cursor/skills/yss-api-integration/references/orval-list-example.md +38 -0
- package/template/.cursor/skills/yss-application/SKILL.md +11 -5
- package/template/.cursor/skills/yss-audit-log/SKILL.md +8 -6
- package/template/.cursor/skills/yss-audit-log/references/README.md +5 -5
- package/template/.cursor/skills/yss-cache/SKILL.md +4 -4
- package/template/.cursor/skills/yss-ddd-scaffold-generator/SKILL.md +7 -8
- package/template/.cursor/skills/yss-ddd-scaffold-generator/references/USAGE_EXAMPLES.md +13 -6
- package/template/.cursor/skills/yss-ddd-scaffold-generator/references/command-examples.md +2 -0
- package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +28 -1
- package/template/.cursor/skills/yss-design-system/SKILL.md +2 -2
- package/template/.cursor/skills/yss-distributed-id/SKILL.md +4 -4
- package/template/.cursor/skills/yss-dto/SKILL.md +9 -9
- package/template/.cursor/skills/yss-dto/references/README.md +4 -4
- package/template/.cursor/skills/yss-dto/references/wire-validation-checklist.md +4 -4
- package/template/.cursor/skills/yss-excel-mvc/SKILL.md +4 -4
- package/template/.cursor/skills/yss-exception/SKILL.md +42 -48
- package/template/.cursor/skills/yss-formily-schema-generator/SKILL.md +26 -170
- package/template/.cursor/skills/yss-formily-schema-generator/references/workflow.md +49 -193
- package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +14 -10
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -6
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +15 -1
- package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/SKILL.md +1 -1
- package/template/.cursor/skills/yss-mybatis/SKILL.md +4 -4
- package/template/.cursor/skills/yss-openapi-draft-review/SKILL.md +6 -4
- package/template/.cursor/skills/yss-openapi-draft-review/references/review-output.md +2 -0
- package/template/.cursor/skills/yss-openapi-draft-review/references/semantic-review.md +3 -3
- package/template/.cursor/skills/yss-openapi-governance/SKILL.md +5 -7
- package/template/.cursor/skills/yss-openapi-governance/references/governance-output.md +2 -0
- package/template/.cursor/skills/yss-openapi-governance/references/governance-workflow.md +3 -3
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +23 -21
- package/template/.cursor/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +25 -5
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +100 -4
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +35 -12
- package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +3 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/user-decisions.md +3 -3
- package/template/.cursor/skills/yss-prototype-stage/SKILL.md +1 -5
- package/template/.cursor/skills/yss-repository/SKILL.md +2 -2
- package/template/.cursor/skills/yss-research/SKILL.md +39 -48
- package/template/.cursor/skills/yss-research/assets/competitive-evidence-template.yaml +146 -0
- package/template/.cursor/skills/yss-research/references/competitive-analysis.md +76 -0
- package/template/.cursor/skills/yss-research/references/competitive-analysis.schema.json +324 -0
- package/template/.cursor/skills/yss-research/references/evidence-contract.md +8 -0
- package/template/.cursor/skills/yss-research/scripts/lib/competitive-analysis.mjs +185 -0
- package/template/.cursor/skills/yss-research/scripts/render-competitive-outputs.mjs +16 -0
- package/template/.cursor/skills/yss-research/scripts/validate-research-package.mjs +6 -2
- package/template/.cursor/skills/yss-research/tests/competitive-scenarios.test.mjs +270 -0
- package/template/.cursor/skills/yss-resilience4j/SKILL.md +25 -31
- package/template/.cursor/skills/yss-security-algorithm/SKILL.md +37 -43
- package/template/.cursor/skills/yss-skill-source-index-refresh/SKILL.md +29 -36
- package/template/.cursor/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
- package/template/.cursor/skills/yss-stage-decision/SKILL.md +2 -5
- package/template/.cursor/skills/yss-tactical-design/SKILL.md +1 -1
- package/template/.cursor/skills/yss-tactical-design/references/output-template.md +3 -3
- package/template/.cursor/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
- package/template/.cursor/skills/yss-technical-design/SKILL.md +6 -5
- package/template/.cursor/skills/yss-ui/SKILL.md +7 -1
- package/template/.cursor/skills/yss-ui/assets/docs/guide/gitlab-ci-integration.md +2 -2
- package/template/.cursor/skills/yss-ui/references/checklist.md +38 -96
- package/template/.cursor/skills/yss-ui/references/component-routing.md +3 -1
- package/template/.cursor/skills/yss-ui/references/quick-recipes.md +17 -272
- package/template/.cursor/skills/yss-ui/references/specialized-components.md +3 -3
- package/template/.cursor/skills/yss-ui-business-page-generation/SKILL.md +1 -1
- package/template/.cursor/skills/yss-up-springboot3/SKILL.md +19 -21
- package/template/.cursor/skills/yss-userinfo/SKILL.md +27 -33
- package/template/.cursor/skills/yss-validation/SKILL.md +26 -32
- package/template/.cursor/skills/yss-web-controller/SKILL.md +11 -44
- package/template/.cursor/skills/yss-web-controller/references/crud-generation.md +38 -0
- package/template/.cursor/skills/ytable-usage/SKILL.md +2 -2
- package/template/.cursor/skills/ytree-usage/SKILL.md +2 -80
- package/template/.cursor/skills/ytree-usage/references/tree-and-confirmation-example.md +81 -0
- package/template/.pi/skills/alibaba-java-code-style/SKILL.md +30 -36
- package/template/.pi/skills/archify/SKILL.md +2 -2
- package/template/.pi/skills/archify/scripts/yss-safe-deliver.mjs +10 -6
- package/template/.pi/skills/archify/test/yss-safe-deliver.test.mjs +9 -2
- package/template/.pi/skills/code-review/SKILL.md +19 -11
- package/template/.pi/skills/code-review/references/candidate-capture.md +1 -1
- package/template/.pi/skills/code-review/references/legacy-convergence.md +3 -1
- package/template/.pi/skills/code-review/references/review-axis-checklists.md +2 -0
- package/template/.pi/skills/code-review/references/yss-review-standards.md +4 -2
- package/template/.pi/skills/competitive-intelligence/SKILL.md +32 -74
- package/template/.pi/skills/file-export-download/SKILL.md +4 -73
- package/template/.pi/skills/file-export-download/references/binary-contract-example.md +26 -0
- package/template/.pi/skills/file-export-download/references/download-call-example.md +45 -0
- package/template/.pi/skills/formily-foundation/SKILL.md +6 -105
- package/template/.pi/skills/formily-foundation/references/async-save-example.md +57 -0
- package/template/.pi/skills/formily-foundation/references/query-layout.md +50 -0
- package/template/.pi/skills/formily-linkage-effects/SKILL.md +3 -116
- package/template/.pi/skills/formily-linkage-effects/references/linkage-example.md +116 -0
- package/template/.pi/skills/formily-mode-slot-detail/SKILL.md +3 -81
- package/template/.pi/skills/formily-mode-slot-detail/references/mode-slot-example.md +81 -0
- package/template/.pi/skills/formily-step-flow/SKILL.md +3 -143
- package/template/.pi/skills/formily-step-flow/references/step-flow-example.md +143 -0
- package/template/.pi/skills/handoff/SKILL.md +3 -1
- package/template/.pi/skills/i-have-adhd/SKILL.md +2 -2
- package/template/.pi/skills/i-have-adhd/references/yss-adaptation.md +2 -2
- package/template/.pi/skills/i-have-adhd/tests/verify-integration.mjs +7 -2
- package/template/.pi/skills/implement/SKILL.md +2 -2
- package/template/.pi/skills/llm-wiki/references/transactions.md +2 -2
- package/template/.pi/skills/llm-wiki/scripts/core.mjs +40 -2
- package/template/.pi/skills/llm-wiki/scripts/transaction.mjs +30 -27
- package/template/.pi/skills/llm-wiki/scripts/v2.test.mjs +38 -2
- package/template/.pi/skills/lombok/SKILL.md +2 -0
- package/template/.pi/skills/mapstruct/SKILL.md +2 -0
- package/template/.pi/skills/tdd/SKILL.md +2 -2
- package/template/.pi/skills/theme-token-usage/SKILL.md +6 -5
- package/template/.pi/skills/wayfinder/references/chart-map.md +2 -2
- package/template/.pi/skills/wayfinder/references/ticket-types.md +1 -1
- package/template/.pi/skills/yss-api-integration/SKILL.md +13 -50
- package/template/.pi/skills/yss-api-integration/references/orval-list-example.md +38 -0
- package/template/.pi/skills/yss-application/SKILL.md +11 -5
- package/template/.pi/skills/yss-audit-log/SKILL.md +8 -6
- package/template/.pi/skills/yss-audit-log/references/README.md +5 -5
- package/template/.pi/skills/yss-cache/SKILL.md +4 -4
- package/template/.pi/skills/yss-ddd-scaffold-generator/SKILL.md +7 -8
- package/template/.pi/skills/yss-ddd-scaffold-generator/references/USAGE_EXAMPLES.md +13 -6
- package/template/.pi/skills/yss-ddd-scaffold-generator/references/command-examples.md +2 -0
- package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +28 -1
- package/template/.pi/skills/yss-design-system/SKILL.md +2 -2
- package/template/.pi/skills/yss-distributed-id/SKILL.md +4 -4
- package/template/.pi/skills/yss-dto/SKILL.md +9 -9
- package/template/.pi/skills/yss-dto/references/README.md +4 -4
- package/template/.pi/skills/yss-dto/references/wire-validation-checklist.md +4 -4
- package/template/.pi/skills/yss-excel-mvc/SKILL.md +4 -4
- package/template/.pi/skills/yss-exception/SKILL.md +42 -48
- package/template/.pi/skills/yss-formily-schema-generator/SKILL.md +26 -170
- package/template/.pi/skills/yss-formily-schema-generator/references/workflow.md +49 -193
- package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +14 -10
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -6
- package/template/.pi/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +15 -1
- package/template/.pi/skills/yss-layered-mvc-scaffold-generator/SKILL.md +1 -1
- package/template/.pi/skills/yss-mybatis/SKILL.md +4 -4
- package/template/.pi/skills/yss-openapi-draft-review/SKILL.md +6 -4
- package/template/.pi/skills/yss-openapi-draft-review/references/review-output.md +2 -0
- package/template/.pi/skills/yss-openapi-draft-review/references/semantic-review.md +3 -3
- package/template/.pi/skills/yss-openapi-governance/SKILL.md +5 -7
- package/template/.pi/skills/yss-openapi-governance/references/governance-output.md +2 -0
- package/template/.pi/skills/yss-openapi-governance/references/governance-workflow.md +3 -3
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +23 -21
- package/template/.pi/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +25 -5
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +100 -4
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +35 -12
- package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +3 -1
- package/template/.pi/skills/yss-product-lifecycle/references/user-decisions.md +3 -3
- package/template/.pi/skills/yss-prototype-stage/SKILL.md +1 -5
- package/template/.pi/skills/yss-repository/SKILL.md +2 -2
- package/template/.pi/skills/yss-research/SKILL.md +39 -48
- package/template/.pi/skills/yss-research/assets/competitive-evidence-template.yaml +146 -0
- package/template/.pi/skills/yss-research/references/competitive-analysis.md +76 -0
- package/template/.pi/skills/yss-research/references/competitive-analysis.schema.json +324 -0
- package/template/.pi/skills/yss-research/references/evidence-contract.md +8 -0
- package/template/.pi/skills/yss-research/scripts/lib/competitive-analysis.mjs +185 -0
- package/template/.pi/skills/yss-research/scripts/render-competitive-outputs.mjs +16 -0
- package/template/.pi/skills/yss-research/scripts/validate-research-package.mjs +6 -2
- package/template/.pi/skills/yss-research/tests/competitive-scenarios.test.mjs +270 -0
- package/template/.pi/skills/yss-resilience4j/SKILL.md +25 -31
- package/template/.pi/skills/yss-security-algorithm/SKILL.md +37 -43
- package/template/.pi/skills/yss-skill-source-index-refresh/SKILL.md +29 -36
- package/template/.pi/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
- package/template/.pi/skills/yss-stage-decision/SKILL.md +2 -5
- package/template/.pi/skills/yss-tactical-design/SKILL.md +1 -1
- package/template/.pi/skills/yss-tactical-design/references/output-template.md +3 -3
- package/template/.pi/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
- package/template/.pi/skills/yss-technical-design/SKILL.md +6 -5
- package/template/.pi/skills/yss-ui/SKILL.md +7 -1
- package/template/.pi/skills/yss-ui/assets/docs/guide/gitlab-ci-integration.md +2 -2
- package/template/.pi/skills/yss-ui/references/checklist.md +38 -96
- package/template/.pi/skills/yss-ui/references/component-routing.md +3 -1
- package/template/.pi/skills/yss-ui/references/quick-recipes.md +17 -272
- package/template/.pi/skills/yss-ui/references/specialized-components.md +3 -3
- package/template/.pi/skills/yss-ui-business-page-generation/SKILL.md +1 -1
- package/template/.pi/skills/yss-up-springboot3/SKILL.md +19 -21
- package/template/.pi/skills/yss-userinfo/SKILL.md +27 -33
- package/template/.pi/skills/yss-validation/SKILL.md +26 -32
- package/template/.pi/skills/yss-web-controller/SKILL.md +11 -44
- package/template/.pi/skills/yss-web-controller/references/crud-generation.md +38 -0
- package/template/.pi/skills/ytable-usage/SKILL.md +2 -2
- package/template/.pi/skills/ytree-usage/SKILL.md +2 -80
- package/template/.pi/skills/ytree-usage/references/tree-and-confirmation-example.md +81 -0
- package/template/.template-spec/agents/backend-architecture-profiles.md +1 -1
- package/template/.template-spec/agents/digital-human-roles.md +5 -1
- package/template/.template-spec/agents/digital-human-roles.yaml +49 -1
- package/template/.template-spec/engineering/backend-platforms.json +1 -1
- package/template/.template-spec/plan/templates/competitive-analysis-template.md +38 -36
- package/template/.template-spec/plan/templates/competitive-matrix-template.md +37 -17
- package/template/.template-spec/process/document-writing.md +22 -3
- package/template/.template-spec/process/harness-process-tailoring.md +5 -1
- package/template/.template-spec/process/implementation-repo-integration.md +26 -8
- package/template/.template-spec/process/research-completion.md +2 -0
- package/template/.template-spec/process/runtime-storage.md +78 -0
- package/template/.template-spec/process/schemas/approval-context.schema.json +67 -0
- package/template/.template-spec/process/schemas/approval-record.schema.json +179 -16
- package/template/.template-spec/process/schemas/backend-delivery.schema.json +16 -0
- package/template/.template-spec/process/schemas/delivery-preflight-input.schema.json +4 -0
- package/template/.template-spec/process/schemas/digital-human-task-package.schema.json +236 -1
- package/template/.template-spec/process/schemas/implementation-repository-preparation-result.schema.json +335 -52
- package/template/.template-spec/process/schemas/lifecycle-checkpoint.schema.json +11 -0
- package/template/.template-spec/process/schemas/project-scaffold-contract.schema.json +3 -0
- package/template/.template-spec/process/schemas/review-bundle.schema.json +347 -35
- package/template/.template-spec/process/schemas/strategic-design-handoff-v3.schema.json +504 -0
- package/template/.template-spec/process/schemas/strategic-design-handoff-v4.schema.json +544 -8
- package/template/.template-spec/process/schemas/strategic-design-handoff-v5.schema.json +616 -0
- package/template/.template-spec/process/schemas/strategic-design-handoff.schema.json +616 -0
- package/template/.template-spec/process/schemas/strategic-handoff-domain-strategy-v3.schema.json +712 -109
- package/template/.template-spec/process/schemas/strategic-handoff-domain-strategy.schema.json +595 -90
- package/template/.template-spec/process/schemas/strategic-handoff-export-v2.schema.json +448 -0
- package/template/.template-spec/process/schemas/strategic-handoff-export.schema.json +392 -0
- package/template/.template-spec/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json +556 -83
- package/template/.template-spec/process/schemas/strategic-handoff-stage-decision-package.schema.json +418 -57
- package/template/.template-spec/process/subagent-collaboration.md +7 -1
- package/template/.template-spec/templates/approval-record-template.json +7 -3
- package/template/.template-spec/templates/approval-record-template.yaml +8 -2
- package/template/.template-spec/templates/examples/lifecycle-writing-examples.md +92 -2
- package/template/.template-spec/templates/retro-report-template.md +10 -3
- package/template/.template-spec/templates/review-bundle-template.yaml +19 -10
- package/template/AGENTS.md +4 -3
- package/template/CONTEXT.md +2 -2
- package/template/README.md +2 -2
- package/template/scripts/advance-maintenance-iteration +51 -0
- package/template/scripts/lib/approval-checkpoint-discovery.mjs +26 -0
- package/template/scripts/lib/approval-consumption.mjs +35 -0
- package/template/scripts/lib/approval-current.mjs +111 -0
- package/template/scripts/lib/approval-record-io.mjs +56 -0
- package/template/scripts/lib/approval-record.mjs +79 -184
- package/template/scripts/lib/approved-execution-context.mjs +8 -6
- package/template/scripts/lib/asset-transactions.mjs +5 -1
- package/template/scripts/lib/backend-delivery.mjs +1 -1
- package/template/scripts/lib/backend-review.mjs +2 -1
- package/template/scripts/lib/backend-scaffold-prerequisites.mjs +3 -2
- package/template/scripts/lib/business-ticket-lifecycle.mjs +2 -1
- package/template/scripts/lib/checkpoint-commit.mjs +50 -0
- package/template/scripts/lib/command-runner.mjs +44 -9
- package/template/scripts/lib/content-identity.mjs +273 -0
- package/template/scripts/lib/delivery-preflight.mjs +2 -1
- package/template/scripts/lib/digital-human-roles.mjs +12 -5
- package/template/scripts/lib/drift-report.mjs +110 -0
- package/template/scripts/lib/existing-backend-architecture.mjs +4 -1
- package/template/scripts/lib/lifecycle-context-query.mjs +172 -11
- package/template/scripts/lib/lifecycle-controls.mjs +9 -19
- package/template/scripts/lib/lifecycle-presentation.mjs +5 -0
- package/template/scripts/lib/lifecycle-status.mjs +63 -12
- package/template/scripts/lib/lifecycle-transition.mjs +2 -1
- package/template/scripts/lib/maintenance-candidate.mjs +24 -30
- package/template/scripts/lib/maintenance-research.mjs +52 -8
- package/template/scripts/lib/maintenance-storage.mjs +81 -0
- package/template/scripts/lib/plan-spec-entry.mjs +2 -1
- package/template/scripts/lib/project-governance.mjs +2 -1
- package/template/scripts/lib/read-only-intake.mjs +44 -9
- package/template/scripts/lib/review-capabilities.mjs +234 -0
- package/template/scripts/lib/review-package.mjs +100 -0
- package/template/scripts/lib/runtime-store.mjs +650 -0
- package/template/scripts/lib/skill-registry.mjs +4 -4
- package/template/scripts/lib/strategic-handoff.mjs +22 -9
- package/template/scripts/lib/task-package.mjs +47 -16
- package/template/scripts/lifecycle-status +26 -1
- package/template/scripts/maintenance-path +11 -0
- package/template/scripts/prepare-review-package +8 -0
- package/template/scripts/project-ci +13 -3
- package/template/scripts/query-lifecycle-context +8 -1
- package/template/scripts/run-read-only-intake +2 -2
- package/template/scripts/runtime-store +40 -0
- package/template/scripts/verify-approval-record +21 -13
- package/template/scripts/verify-lifecycle-checkpoint +7 -3
- package/template/scripts/verify-project-instance +21 -1
- package/template/skills-lock.json +62 -61
- package/template.snapshot.json +4 -4
- package/vendor/cli-core/cli.mjs +2 -0
- package/vendor/cli-core/command-runner.mjs +44 -9
- package/vendor/cli-core/package.json +1 -1
- package/vendor/cli-core/runtime-store.mjs +650 -0
- package/vendor/cli-core/scaffold.mjs +4 -2
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const VERSION = 1;
|
|
11
|
+
const DAY = 86_400_000;
|
|
12
|
+
const SUCCESS = new Set(['passed', 'success', 'completed', 'ok']);
|
|
13
|
+
const TERMINAL = new Set([...SUCCESS, 'failed', 'failure', 'cancelled', 'canceled', 'timed-out', 'timeout', 'error']);
|
|
14
|
+
const TABLES = ['schema_migrations', 'runs', 'commands', 'events', 'objects', 'run_objects', 'pins', 'run_files', 'checkpoint_index'];
|
|
15
|
+
const sha = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
16
|
+
const json = value => JSON.stringify(value);
|
|
17
|
+
const bytes = value => Buffer.isBuffer(value) ? value : Buffer.from(typeof value === 'string' ? value : json(value ?? null));
|
|
18
|
+
const inside = (directory, candidate) => { const relative = path.relative(directory, candidate); return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)); };
|
|
19
|
+
const slash = value => value.split(path.sep).join('/');
|
|
20
|
+
|
|
21
|
+
export function assertNodeVersion(version = process.versions.node) {
|
|
22
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
|
|
23
|
+
if (!match || Number(match[1]) < 22 || Number(match[1]) >= 27 || (Number(match[1]) === 22 && Number(match[2]) < 13)) {
|
|
24
|
+
throw new Error(`运行存储需要 Node >=22.13 <27,当前 ${version}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const assertRuntimeVersion = assertNodeVersion;
|
|
28
|
+
|
|
29
|
+
// Resolve the nearest existing ancestor before creating anything: lexical checks
|
|
30
|
+
// alone do not prevent an external-looking symlink from writing into a repository.
|
|
31
|
+
function realTarget(candidate) {
|
|
32
|
+
let cursor = path.resolve(candidate); const tail = [];
|
|
33
|
+
while (!fs.existsSync(cursor)) {
|
|
34
|
+
try { if (fs.lstatSync(cursor).isSymbolicLink()) throw new Error(`不解析悬空符号链接: ${candidate}`); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
35
|
+
const parent = path.dirname(cursor);
|
|
36
|
+
if (parent === cursor) throw new Error(`无法解析路径: ${candidate}`);
|
|
37
|
+
tail.unshift(path.basename(cursor)); cursor = parent;
|
|
38
|
+
}
|
|
39
|
+
return path.join(fs.realpathSync(cursor), ...tail);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function registeredRoots(root) {
|
|
43
|
+
const roots = [];
|
|
44
|
+
const fields = (source, key) => [...source.matchAll(new RegExp(`["']?${key}["']?\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:''|[^'])*'|[^\\r\\n"'#,}]+)|\\|\\s*${key}\\s*\\|\\s*([^|\\r\\n]+)`, 'g'))].map(match => {
|
|
45
|
+
const value = (match[1] ?? match[2]).trim();
|
|
46
|
+
// Decode complete string tokens: raw JSON escapes change UNC roots, and
|
|
47
|
+
// stripping quotes truncates otherwise valid paths containing apostrophes.
|
|
48
|
+
if (value.startsWith('"')) {
|
|
49
|
+
try { return JSON.parse(value); } catch { throw new Error(`已登记仓库路径无法解析双引号值: ${key}`); }
|
|
50
|
+
}
|
|
51
|
+
if (value.startsWith("'")) return value.slice(1, -1).replaceAll("''", "'");
|
|
52
|
+
return value.replace(/^`|`$/g, '');
|
|
53
|
+
});
|
|
54
|
+
const field = (source, key) => fields(source, key)[0];
|
|
55
|
+
const read = filename => {
|
|
56
|
+
if (!fs.existsSync(filename)) return;
|
|
57
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
58
|
+
// Protect every worktree in arrays/multi-registration Markdown, not just the
|
|
59
|
+
// first parsed project. Whole-worktree denial also covers relative projects.
|
|
60
|
+
for (const value of fields(source, 'local_worktree')) {
|
|
61
|
+
if (!value) continue;
|
|
62
|
+
const target = path.isAbsolute(value) ? value : path.resolve(root, value);
|
|
63
|
+
if (fs.existsSync(target)) roots.push(fs.realpathSync(target));
|
|
64
|
+
}
|
|
65
|
+
const local = field(source, 'local_worktree');
|
|
66
|
+
if (local) {
|
|
67
|
+
const base = path.isAbsolute(local) ? local : path.resolve(root, local);
|
|
68
|
+
if (fs.existsSync(base)) roots.push(fs.realpathSync(base));
|
|
69
|
+
const project = field(source, 'project_root');
|
|
70
|
+
if (project) {
|
|
71
|
+
const actual = path.resolve(base, project);
|
|
72
|
+
if (fs.existsSync(actual)) roots.push(fs.realpathSync(actual));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const value of ['project_root', 'repository_root', 'local_path', 'root_path'].flatMap(key => fields(source, key))) {
|
|
76
|
+
if (!value) continue;
|
|
77
|
+
const target = path.isAbsolute(value) ? value : path.resolve(root, value);
|
|
78
|
+
if (fs.existsSync(target)) roots.push(fs.realpathSync(target));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
read(path.join(root, '.gitmodules'));
|
|
82
|
+
// Registrations remain file authority; this conservative extraction is only a
|
|
83
|
+
// deny-list. Callers can provide the schema-validated registered roots as well.
|
|
84
|
+
for (const relative of ['docs', '.scratch', '.template-spec/implementation', '.template-spec/projects', '.template-spec/project']) {
|
|
85
|
+
const directory = path.join(root, relative);
|
|
86
|
+
if (fs.existsSync(directory)) for (const file of walk(directory).files) if (/\.(?:ya?ml|json|md)$/.test(file)) read(file);
|
|
87
|
+
}
|
|
88
|
+
const modules = path.join(root, '.gitmodules');
|
|
89
|
+
if (fs.existsSync(modules)) for (const match of fs.readFileSync(modules, 'utf8').matchAll(/^\s*path\s*=\s*(.+)$/gm)) {
|
|
90
|
+
const target = path.join(root, match[1].trim()); if (fs.existsSync(target)) roots.push(fs.realpathSync(target));
|
|
91
|
+
}
|
|
92
|
+
return roots;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveRuntimeLocation({ root, home, protectedRoots = [] } = {}) {
|
|
96
|
+
assertNodeVersion();
|
|
97
|
+
if (!root) throw new Error('运行存储需要显式项目根目录');
|
|
98
|
+
const realRoot = fs.realpathSync(path.resolve(root));
|
|
99
|
+
const selected = home ?? process.env.YSS_RUNTIME_HOME ?? (process.env.RUNNER_TEMP ? path.join(process.env.RUNNER_TEMP, 'yss-harness-runtime') : path.join(os.homedir(), '.yss-harness', 'runtime'));
|
|
100
|
+
if (!path.isAbsolute(selected) || (process.platform !== 'win32' && /^(?:[A-Za-z]:[\\/]|\\\\)/.test(selected))) throw new Error('运行存储目录必须为本机仓外绝对路径');
|
|
101
|
+
const external = realTarget(selected);
|
|
102
|
+
const excluded = [realRoot, ...registeredRoots(realRoot), ...protectedRoots.map(value => realTarget(path.resolve(value)))];
|
|
103
|
+
for (const candidate of [path.resolve(selected), external]) for (const forbidden of excluded) {
|
|
104
|
+
if (inside(forbidden, candidate)) throw new Error(`运行存储不得进入项目或已登记仓库: ${candidate}`);
|
|
105
|
+
}
|
|
106
|
+
const workspaceId = sha(realRoot);
|
|
107
|
+
const directory = path.join(external, workspaceId);
|
|
108
|
+
// An existing workspace leaf can itself be a symlink.
|
|
109
|
+
const realDirectory = realTarget(directory);
|
|
110
|
+
for (const forbidden of excluded) if (inside(forbidden, realDirectory)) throw new Error(`运行存储目标越界: ${directory}`);
|
|
111
|
+
return { root: realRoot, workspaceId, home: external, directory: realDirectory, excluded };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function walk(directory, { skip = new Set(), strict = false, followWithin = [] } = {}) {
|
|
115
|
+
const files = [], errors = [], projections = [], seen = new Set();
|
|
116
|
+
function visit(current) {
|
|
117
|
+
const actual = fs.realpathSync(current);
|
|
118
|
+
if (seen.has(actual)) return;
|
|
119
|
+
seen.add(actual);
|
|
120
|
+
let entries;
|
|
121
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch (error) { errors.push(`${current}: ${error.message}`); return; }
|
|
122
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
123
|
+
const filename = path.join(current, entry.name);
|
|
124
|
+
if (entry.isSymbolicLink()) {
|
|
125
|
+
try {
|
|
126
|
+
const target = fs.realpathSync(filename);
|
|
127
|
+
if (followWithin.some(root => inside(root, target))) {
|
|
128
|
+
projections.push({ path: filename, target, classification: 'projection-or-alias', archive_candidate: false });
|
|
129
|
+
if (fs.statSync(target).isDirectory()) visit(target);
|
|
130
|
+
else if (fs.statSync(target).isFile() && !seen.has(target)) { seen.add(target); files.push(target); }
|
|
131
|
+
} else if (strict) errors.push(`不读取越界符号链接: ${filename}`);
|
|
132
|
+
} catch (error) { if (strict) errors.push(`${filename}: ${error.message}`); }
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (entry.isDirectory()) { if (!skip.has(entry.name)) visit(filename); }
|
|
136
|
+
else if (entry.isFile() && !seen.has(filename)) { seen.add(filename); files.push(filename); }
|
|
137
|
+
else if (strict) errors.push(`未知文件类型: ${filename}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (fs.existsSync(directory)) visit(directory);
|
|
141
|
+
return { files, errors, projections };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function fileInfo(filename) {
|
|
145
|
+
if (fs.lstatSync(filename).isSymbolicLink() || !fs.statSync(filename).isFile()) throw new Error(`证据必须是普通文件: ${filename}`);
|
|
146
|
+
const data = fs.readFileSync(filename);
|
|
147
|
+
return { path: fs.realpathSync(filename), sha256: sha(data), bytes: data.length };
|
|
148
|
+
}
|
|
149
|
+
function activePid(pid, hostname) {
|
|
150
|
+
if (!pid) return false;
|
|
151
|
+
if (hostname !== os.hostname()) return true;
|
|
152
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code !== 'ESRCH'; }
|
|
153
|
+
}
|
|
154
|
+
function fileOccupancy(directory) {
|
|
155
|
+
if (!fs.existsSync(directory)) return { complete: true, occupied: false };
|
|
156
|
+
if (process.platform === 'win32') {
|
|
157
|
+
const script = "$ErrorActionPreference='Stop';try{Get-ChildItem -LiteralPath $env:YSS_OCCUPANCY_DIR -Recurse -File | ForEach-Object {$f=[System.IO.File]::Open($_.FullName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::ReadWrite,[System.IO.FileShare]::None);$f.Dispose()};exit 0}catch{Write-Error $_;exit 1}";
|
|
158
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { env: { ...process.env, YSS_OCCUPANCY_DIR: directory }, encoding: 'utf8', timeout: 10_000 });
|
|
159
|
+
return { complete: !result.error && result.status === 0, occupied: result.status === 1, error: result.error?.message || result.stderr?.trim() };
|
|
160
|
+
}
|
|
161
|
+
const result = spawnSync('lsof', ['-nP', '+D', directory], { encoding: 'utf8', timeout: 10_000 });
|
|
162
|
+
if (result.error || result.stderr?.trim() || ![0, 1].includes(result.status)) return { complete: false, occupied: false, error: result.error?.message || result.stderr?.trim() || '占用探测未完成' };
|
|
163
|
+
// macOS lsof can emit matching files and still exit 1; any output protects.
|
|
164
|
+
return { complete: true, occupied: result.stdout.trim().length > 0 };
|
|
165
|
+
}
|
|
166
|
+
function verifyBundle(directory) {
|
|
167
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'));
|
|
168
|
+
if (manifest.schema_version !== 1 || !['runtime-export', 'runtime-backup'].includes(manifest.kind) || !Array.isArray(manifest.files)) throw new Error('不支持的运行包清单');
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
const realDirectory = fs.realpathSync(directory);
|
|
171
|
+
for (const row of manifest.files) {
|
|
172
|
+
if (typeof row.relative_path !== 'string' || !row.relative_path || path.isAbsolute(row.relative_path) || path.win32.isAbsolute(row.relative_path) || row.relative_path.split(/[\\/]/).includes('..')) throw new Error('运行包引用越界');
|
|
173
|
+
if (seen.has(row.relative_path) || !/^[a-f0-9]{64}$/.test(row.sha256) || !Number.isInteger(row.bytes) || row.bytes < 0) throw new Error('运行包文件清单非法或重复');
|
|
174
|
+
seen.add(row.relative_path);
|
|
175
|
+
const filename = path.resolve(realDirectory, row.relative_path);
|
|
176
|
+
if (!inside(realDirectory, fs.realpathSync(filename)) || filename !== fs.realpathSync(filename)) throw new Error('运行包真实目标越界或符号链接');
|
|
177
|
+
const info = fileInfo(filename);
|
|
178
|
+
if (info.sha256 !== row.sha256 || info.bytes !== row.bytes) throw new Error(`运行包摘要不匹配: ${row.relative_path}`);
|
|
179
|
+
}
|
|
180
|
+
return manifest;
|
|
181
|
+
}
|
|
182
|
+
export { verifyBundle as verifyRuntimeBundle };
|
|
183
|
+
export function inspectRuntimeBundle(source) {
|
|
184
|
+
const directory = fs.realpathSync(path.resolve(source)), manifest = verifyBundle(directory);
|
|
185
|
+
const files = manifest.files.map(row => ({ ...row, resolved_path: path.resolve(directory, row.relative_path) }));
|
|
186
|
+
const pathMap = Object.fromEntries(files.filter(row => row.source_path).map(row => [row.source_path, row.resolved_path]));
|
|
187
|
+
for (const event of manifest.events ?? []) if (event.type === 'runtime-restored') {
|
|
188
|
+
const { old_directory: oldDirectory, new_directory: newDirectory } = event.value;
|
|
189
|
+
for (const file of files) if (file.source_path && inside(newDirectory, file.source_path)) pathMap[path.join(oldDirectory, path.relative(newDirectory, file.source_path))] = file.resolved_path;
|
|
190
|
+
}
|
|
191
|
+
return { schema_version: 1, read_only: true, independent: true, kind: manifest.kind, directory, manifest, files, path_map: pathMap, reports: files.filter(row => /(?:^|\/)report\.json$/.test(row.relative_path)), logs: files.filter(row => /(?:stdout|stderr|\.log$)/.test(row.relative_path)) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export class RuntimeStore {
|
|
195
|
+
#ownedRuns = new Set();
|
|
196
|
+
constructor(options = {}) {
|
|
197
|
+
const location = resolveRuntimeLocation(options);
|
|
198
|
+
Object.assign(this, location);
|
|
199
|
+
this.readOnly = options.readOnly === true;
|
|
200
|
+
this.databasePath = path.join(this.directory, 'runtime.sqlite');
|
|
201
|
+
for (const filename of [this.databasePath, ...['-journal', '-wal', '-shm'].map(suffix => this.databasePath + suffix)]) {
|
|
202
|
+
let entry; try { entry = fs.lstatSync(filename); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
203
|
+
// The parent was already resolved and checked. DELETE journals can disappear
|
|
204
|
+
// between lstat and realpath during another connection's commit.
|
|
205
|
+
if (entry && (entry.isSymbolicLink() || !entry.isFile())) throw new Error('数据库真实目标越界或符号链接');
|
|
206
|
+
}
|
|
207
|
+
this.db = null;
|
|
208
|
+
if (this.readOnly && !fs.existsSync(this.databasePath)) return;
|
|
209
|
+
if (options.existingOnly && !fs.existsSync(this.databasePath)) throw new Error('运行数据库不存在');
|
|
210
|
+
if (!this.readOnly) fs.mkdirSync(this.directory, { recursive: true });
|
|
211
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
212
|
+
this.db = new DatabaseSync(this.databasePath, { readOnly: this.readOnly });
|
|
213
|
+
try {
|
|
214
|
+
this.db.exec('PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON;');
|
|
215
|
+
this.db.exec('BEGIN;');
|
|
216
|
+
const version = this.db.prepare('PRAGMA user_version').get().user_version;
|
|
217
|
+
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
218
|
+
if (version !== VERSION && !(version === 0 && tables.length === 0 && !this.readOnly)) throw new Error(`不识别的运行数据库 schema: ${version},拒绝写入`);
|
|
219
|
+
if (version === VERSION && TABLES.some(table => !tables.some(row => row.name === table))) throw new Error('数据库schema不完整,拒绝写入');
|
|
220
|
+
if (version === VERSION && !this.db.prepare('SELECT 1 FROM schema_migrations WHERE version=?').get(VERSION)) throw new Error('数据库迁移记录缺失,拒绝写入');
|
|
221
|
+
this.db.exec('COMMIT;');
|
|
222
|
+
if (!this.readOnly) {
|
|
223
|
+
this.db.exec('PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL;');
|
|
224
|
+
if (version === 0) this.#initialize();
|
|
225
|
+
}
|
|
226
|
+
if (this.db.prepare('PRAGMA quick_check').get().quick_check !== 'ok') throw new Error('运行数据库损坏');
|
|
227
|
+
} catch (error) { this.close(); throw error; }
|
|
228
|
+
}
|
|
229
|
+
#initialize() {
|
|
230
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
231
|
+
// A concurrent opener may have initialized it while this opener waited.
|
|
232
|
+
if (this.db.prepare('PRAGMA user_version').get().user_version === VERSION) { this.db.exec('COMMIT'); return; }
|
|
233
|
+
this.db.exec(`
|
|
234
|
+
CREATE TABLE schema_migrations(version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL);
|
|
235
|
+
CREATE TABLE runs(id TEXT PRIMARY KEY, kind TEXT NOT NULL, root TEXT NOT NULL, started_at TEXT NOT NULL, ended_at TEXT, status TEXT NOT NULL, exit_code INTEGER, input_digest TEXT, run_dir TEXT NOT NULL, report_dir TEXT, owner_pid INTEGER, hostname TEXT, logs_expired INTEGER NOT NULL DEFAULT 0);
|
|
236
|
+
CREATE TABLE commands(id INTEGER PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id), recorded_at TEXT NOT NULL, result_json TEXT NOT NULL);
|
|
237
|
+
CREATE TABLE events(id INTEGER PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id), recorded_at TEXT NOT NULL, type TEXT NOT NULL, value_json TEXT NOT NULL);
|
|
238
|
+
CREATE TABLE objects(digest TEXT PRIMARY KEY, bytes INTEGER NOT NULL, compressed_bytes INTEGER NOT NULL, relative_path TEXT NOT NULL, deleting INTEGER NOT NULL DEFAULT 0);
|
|
239
|
+
CREATE TABLE run_objects(run_id TEXT NOT NULL REFERENCES runs(id), digest TEXT NOT NULL REFERENCES objects(digest), PRIMARY KEY(run_id,digest));
|
|
240
|
+
CREATE TABLE pins(run_id TEXT NOT NULL REFERENCES runs(id), reason TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY(run_id,reason));
|
|
241
|
+
CREATE TABLE run_files(run_id TEXT NOT NULL REFERENCES runs(id), path TEXT NOT NULL, sha256 TEXT NOT NULL, bytes INTEGER NOT NULL, PRIMARY KEY(run_id,path));
|
|
242
|
+
CREATE TABLE checkpoint_index(path TEXT PRIMARY KEY, source_digest TEXT NOT NULL, indexed_at TEXT NOT NULL, value_json TEXT NOT NULL);
|
|
243
|
+
PRAGMA user_version=1;
|
|
244
|
+
INSERT INTO schema_migrations VALUES(1, '${new Date().toISOString()}');
|
|
245
|
+
COMMIT;`);
|
|
246
|
+
}
|
|
247
|
+
#write() { if (this.readOnly || !this.db) throw new Error('只读或缺失的运行存储不能写入'); }
|
|
248
|
+
#transaction(action) {
|
|
249
|
+
this.#write(); this.db.exec('BEGIN IMMEDIATE');
|
|
250
|
+
try { const result = action(); this.db.exec('COMMIT'); return result; }
|
|
251
|
+
catch (error) { try { this.db.exec('ROLLBACK'); } catch {} throw error; }
|
|
252
|
+
}
|
|
253
|
+
close() { if (this.db) { this.db.close(); this.db = null; } }
|
|
254
|
+
#rows(table) { return this.db ? this.db.prepare(`SELECT * FROM ${table} ORDER BY rowid`).all().map(row => ({ ...row })) : []; }
|
|
255
|
+
#stateDigest() { return sha(json(TABLES.map(table => [table, this.#rows(table)]))); }
|
|
256
|
+
#run(id) { const row = this.db?.prepare('SELECT * FROM runs WHERE id=?').get(id); if (!row) throw new Error(`未知运行: ${id}`); return row; }
|
|
257
|
+
#mutableRun(id) { const row = this.#run(id); if (row.logs_expired) throw new Error('运行已进入清理状态,拒绝新增引用或修改'); return row; }
|
|
258
|
+
#safeOwned(filename) {
|
|
259
|
+
const target = realTarget(filename);
|
|
260
|
+
if (!inside(this.directory, target) || target === this.directory || target !== path.resolve(filename)) throw new Error(`运行存储文件越界或内部符号链接: ${filename}`);
|
|
261
|
+
let cursor = path.resolve(filename);
|
|
262
|
+
while (cursor !== this.directory) {
|
|
263
|
+
try { if (fs.lstatSync(cursor).isSymbolicLink()) throw new Error(`运行存储内部符号链接: ${filename}`); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
264
|
+
cursor = path.dirname(cursor);
|
|
265
|
+
}
|
|
266
|
+
for (const forbidden of this.excluded) if (inside(forbidden, target)) throw new Error(`运行存储目标越界: ${filename}`);
|
|
267
|
+
return target;
|
|
268
|
+
}
|
|
269
|
+
#ownedRunDirectory(run) {
|
|
270
|
+
const expected = path.join(this.directory, 'runs', run.id);
|
|
271
|
+
if (path.resolve(run.run_dir) !== expected) throw new Error('运行目录与记录身份不一致');
|
|
272
|
+
return this.#safeOwned(expected);
|
|
273
|
+
}
|
|
274
|
+
begin({ kind = 'command', input, reportDir } = {}) {
|
|
275
|
+
this.#write();
|
|
276
|
+
if (typeof kind !== 'string' || !kind) throw new Error('运行kind必须为非空字符串');
|
|
277
|
+
let report;
|
|
278
|
+
if (reportDir) {
|
|
279
|
+
report = realTarget(path.resolve(reportDir));
|
|
280
|
+
for (const forbidden of this.excluded) if (inside(forbidden, report)) throw new Error(`正式报告必须在仓外: ${report}`);
|
|
281
|
+
}
|
|
282
|
+
const id = `${new Date().toISOString().replace(/[-:.]/g, '')}-${crypto.randomUUID()}`;
|
|
283
|
+
const runDir = this.#safeOwned(path.join(this.directory, 'runs', id));
|
|
284
|
+
const inputBytes = input === undefined ? null : bytes(input);
|
|
285
|
+
const digest = inputBytes && sha(inputBytes);
|
|
286
|
+
// The DB is opened/checked before any run output is created. Orphan content
|
|
287
|
+
// after a failed transaction is retained, never implicitly deleted.
|
|
288
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
289
|
+
let object;
|
|
290
|
+
if (inputBytes) {
|
|
291
|
+
const relative = `objects/${digest.slice(0, 2)}/${digest}.gz`;
|
|
292
|
+
const filename = this.#safeOwned(path.join(this.directory, relative));
|
|
293
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
294
|
+
if (!fs.existsSync(filename)) { const compressed = gzipSync(inputBytes); try { fs.writeFileSync(filename, compressed, { flag: 'wx' }); } catch (error) { if (error.code !== 'EEXIST') throw error; } }
|
|
295
|
+
if (sha(gunzipSync(fs.readFileSync(filename))) !== digest) throw new Error('运行输入对象损坏');
|
|
296
|
+
object = { relative, compressed: fs.statSync(filename).size };
|
|
297
|
+
}
|
|
298
|
+
this.#transaction(() => {
|
|
299
|
+
this.db.prepare('INSERT INTO runs(id,kind,root,started_at,status,input_digest,run_dir,report_dir,owner_pid,hostname) VALUES(?,?,?,?,?,?,?,?,?,?)').run(id, kind, this.root, new Date().toISOString(), 'running', digest, runDir, report ?? null, process.pid, os.hostname());
|
|
300
|
+
if (object) {
|
|
301
|
+
this.db.prepare('INSERT OR IGNORE INTO objects(digest,bytes,compressed_bytes,relative_path) VALUES(?,?,?,?)').run(digest, inputBytes.length, object.compressed, object.relative);
|
|
302
|
+
if (this.db.prepare('SELECT deleting FROM objects WHERE digest=?').get(digest).deleting) throw new Error('输入对象正在清理,拒绝新增引用');
|
|
303
|
+
this.db.prepare('INSERT INTO run_objects VALUES(?,?)').run(id, digest);
|
|
304
|
+
}
|
|
305
|
+
if (report) this.db.prepare('INSERT INTO pins VALUES(?,?,?)').run(id, `report-dir:${report}`, new Date().toISOString());
|
|
306
|
+
});
|
|
307
|
+
this.#ownedRuns.add(id);
|
|
308
|
+
const store = this;
|
|
309
|
+
return {
|
|
310
|
+
id, runDir, store,
|
|
311
|
+
recordCommand(row) { store.recordCommand(id, row); },
|
|
312
|
+
recordCommandAndEvent(row, type, value) { store.recordCommandAndEvent(id, row, type, value); },
|
|
313
|
+
recordEvent(type, value) { store.recordEvent(id, type, value); },
|
|
314
|
+
registerFiles(directory) { return store.registerFiles(id, directory); },
|
|
315
|
+
finish(value) { return store.finish(id, value); },
|
|
316
|
+
pin(reason) { store.pin(id, reason); },
|
|
317
|
+
close() { store.close(); },
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
recordCommand(id, row) {
|
|
321
|
+
return this.#persistCommand(id, row);
|
|
322
|
+
}
|
|
323
|
+
recordCommandAndEvent(id, row, type, value) {
|
|
324
|
+
if (typeof type !== 'string' || !type.trim()) throw new Error('事件type必须为非空字符串');
|
|
325
|
+
const valueJson = json(value ?? null);
|
|
326
|
+
if (typeof valueJson !== 'string') throw new Error('事件值必须可序列化为JSON');
|
|
327
|
+
return this.#persistCommand(id, row, { type, valueJson });
|
|
328
|
+
}
|
|
329
|
+
#persistCommand(id, row, event) {
|
|
330
|
+
this.#write(); this.#mutableRun(id);
|
|
331
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) throw new Error('命令结果必须为对象');
|
|
332
|
+
const { stdout, stderr, ...metadata } = row;
|
|
333
|
+
const metadataJson = json(metadata);
|
|
334
|
+
if (typeof metadataJson !== 'string') throw new Error('命令结果必须可序列化为JSON');
|
|
335
|
+
const infos = [row?.stdoutFile, row?.stderrFile].filter(Boolean).flatMap(filename => this.#fileInfos(filename));
|
|
336
|
+
const recordedAt = new Date().toISOString();
|
|
337
|
+
const commandInsert = this.db.prepare('INSERT INTO commands(run_id,recorded_at,result_json) VALUES(?,?,?)');
|
|
338
|
+
const fileInsert = this.db.prepare('INSERT OR REPLACE INTO run_files VALUES(?,?,?,?)');
|
|
339
|
+
const eventInsert = event && this.db.prepare('INSERT INTO events(run_id,recorded_at,type,value_json) VALUES(?,?,?,?)');
|
|
340
|
+
this.#transaction(() => {
|
|
341
|
+
this.#mutableRun(id);
|
|
342
|
+
commandInsert.run(id, recordedAt, metadataJson);
|
|
343
|
+
for (const info of infos) fileInsert.run(id, info.path, info.sha256, info.bytes);
|
|
344
|
+
if (event) eventInsert.run(id, recordedAt, event.type, event.valueJson);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
recordEvent(id, type, value) {
|
|
348
|
+
this.#write(); this.#mutableRun(id);
|
|
349
|
+
if (typeof type !== 'string' || !type) throw new Error('事件type必须为非空字符串');
|
|
350
|
+
this.#transaction(() => { this.#mutableRun(id); this.db.prepare('INSERT INTO events(run_id,recorded_at,type,value_json) VALUES(?,?,?,?)').run(id, new Date().toISOString(), type, json(value ?? null)); });
|
|
351
|
+
}
|
|
352
|
+
pin(id, reason) {
|
|
353
|
+
this.#write();
|
|
354
|
+
if (typeof reason !== 'string' || !reason.trim()) throw new Error('保护必须记录原因');
|
|
355
|
+
this.#transaction(() => { this.#mutableRun(id); this.db.prepare('INSERT OR IGNORE INTO pins VALUES(?,?,?)').run(id, reason, new Date().toISOString()); });
|
|
356
|
+
}
|
|
357
|
+
#fileInfos(directory) {
|
|
358
|
+
const target = path.resolve(directory);
|
|
359
|
+
if (!fs.existsSync(target)) return [];
|
|
360
|
+
if (fs.lstatSync(target).isSymbolicLink()) throw new Error(`不登记符号链接: ${target}`);
|
|
361
|
+
const found = fs.statSync(target).isDirectory() ? walk(target, { strict: true }) : { files: [target], errors: [] };
|
|
362
|
+
if (found.errors.length) throw new Error(found.errors.join('; '));
|
|
363
|
+
return found.files.map(fileInfo);
|
|
364
|
+
}
|
|
365
|
+
registerFiles(id, directory) {
|
|
366
|
+
this.#write(); this.#mutableRun(id);
|
|
367
|
+
const infos = this.#fileInfos(directory);
|
|
368
|
+
this.#transaction(() => { this.#mutableRun(id); const insert = this.db.prepare('INSERT OR REPLACE INTO run_files VALUES(?,?,?,?)'); for (const info of infos) insert.run(id, info.path, info.sha256, info.bytes); });
|
|
369
|
+
return infos;
|
|
370
|
+
}
|
|
371
|
+
finish(id, { status, exitCode, report, endedAt = new Date().toISOString() } = {}) {
|
|
372
|
+
this.#write(); const run = this.#mutableRun(id);
|
|
373
|
+
this.#ownedRunDirectory(run);
|
|
374
|
+
if (run.ended_at) throw new Error('运行已经结束,拒绝重复finish');
|
|
375
|
+
if (!this.#ownedRuns.has(id)) throw new Error('仅创建运行的存储会话可finish,拒绝其它连接终止或覆写报告');
|
|
376
|
+
if (typeof status !== 'string' || !status || status === 'running') throw new Error('finish需要终态status');
|
|
377
|
+
if (!Number.isInteger(exitCode)) throw new Error('finish需要实际整数exitCode');
|
|
378
|
+
const ended = new Date(endedAt);
|
|
379
|
+
if (!Number.isFinite(ended.getTime())) throw new Error('结束时间非法');
|
|
380
|
+
if (report !== undefined) {
|
|
381
|
+
if (typeof report === 'string') { if (!fs.existsSync(report)) throw new Error('完整运行报告不存在'); }
|
|
382
|
+
else {
|
|
383
|
+
const filename = this.#safeOwned(path.join(run.run_dir, 'report.json'));
|
|
384
|
+
fs.writeFileSync(filename, `${json(report)}\n`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const infos = [run.run_dir, run.report_dir, typeof report === 'string' ? report : null].filter(Boolean).flatMap(filename => this.#fileInfos(filename));
|
|
388
|
+
this.#transaction(() => {
|
|
389
|
+
const current = this.#mutableRun(id); if (current.ended_at) throw new Error('运行已经结束,拒绝重复finish');
|
|
390
|
+
const insert = this.db.prepare('INSERT OR REPLACE INTO run_files VALUES(?,?,?,?)');
|
|
391
|
+
for (const info of infos) insert.run(id, info.path, info.sha256, info.bytes);
|
|
392
|
+
this.db.prepare('UPDATE runs SET status=?,exit_code=?,ended_at=?,owner_pid=NULL WHERE id=?').run(status, exitCode, ended.toISOString(), id);
|
|
393
|
+
});
|
|
394
|
+
return { id, status, exitCode, runDir: run.run_dir };
|
|
395
|
+
}
|
|
396
|
+
#scanReferences() {
|
|
397
|
+
const found = walk(this.root, { skip: new Set(['.git', 'node_modules', '.codegraph', '.graphify']), strict: true, followWithin: this.excluded });
|
|
398
|
+
const hashes = [], contents = [], errors = [...found.errors];
|
|
399
|
+
for (const filename of found.files) {
|
|
400
|
+
if (!/\.(?:json|jsonl|ya?ml|md|txt|toml|xml|html|mjs|js|cjs|ts|sh|bash|zsh|py|ps1)$/.test(filename) && path.extname(filename)) continue;
|
|
401
|
+
try {
|
|
402
|
+
const data = fs.readFileSync(filename); hashes.push([slash(path.relative(this.root, filename)), sha(data)]);
|
|
403
|
+
if (data.includes(0)) { errors.push(`未知文本分类: ${filename}`); continue; }
|
|
404
|
+
const content = data.toString('utf8');
|
|
405
|
+
if (/\brun:(?:\$|\{|<)|(?:run_dir|runtime_dir|stdoutFile|stderrFile)\s*["']?\s*[:=]\s*[^\r\n]{0,160}(?:\$\{|\{\{|<[^>]+>)/.test(content)) errors.push(`未解析动态运行引用: ${filename}`);
|
|
406
|
+
contents.push(content);
|
|
407
|
+
} catch (error) { errors.push(`${filename}: ${error.message}`); }
|
|
408
|
+
}
|
|
409
|
+
return { complete: errors.length === 0, errors, digest: sha(json(hashes)), contents };
|
|
410
|
+
}
|
|
411
|
+
#eligibility(run, now, scan, { recovering = false } = {}) {
|
|
412
|
+
const reasons = [];
|
|
413
|
+
if (!run.ended_at || run.status === 'running') reasons.push('unfinished');
|
|
414
|
+
if (!TERMINAL.has(run.status)) reasons.push('unknown-classification');
|
|
415
|
+
if (run.logs_expired && !recovering) reasons.push('logs-expired');
|
|
416
|
+
if (!scan.complete) reasons.push('reference-scan-incomplete');
|
|
417
|
+
if (this.db.prepare('SELECT 1 FROM pins WHERE run_id=? LIMIT 1').get(run.id)) reasons.push('protected');
|
|
418
|
+
if (activePid(run.owner_pid, run.hostname)) reasons.push('process-occupied');
|
|
419
|
+
const ttl = SUCCESS.has(run.status) && run.exit_code === 0 ? 7 : 30;
|
|
420
|
+
if (!run.ended_at || now - Date.parse(run.ended_at) < ttl * DAY) reasons.push('retained');
|
|
421
|
+
const tokens = [run.id, run.run_dir, ...(run.input_digest ? [run.input_digest] : [])];
|
|
422
|
+
const files = this.db.prepare('SELECT path FROM run_files WHERE run_id=?').all(run.id);
|
|
423
|
+
tokens.push(...files.map(row => row.path));
|
|
424
|
+
if (scan.contents.some(content => tokens.some(token => content.includes(token)))) reasons.push('formally-referenced');
|
|
425
|
+
// External files can belong to callers and must never be retention targets.
|
|
426
|
+
if (files.some(row => !inside(run.run_dir, row.path))) reasons.push('external-evidence');
|
|
427
|
+
if (reasons.length === 0) {
|
|
428
|
+
const occupancy = fileOccupancy(run.run_dir);
|
|
429
|
+
if (!occupancy.complete) reasons.push('occupancy-scan-incomplete');
|
|
430
|
+
if (occupancy.occupied) reasons.push('file-occupied');
|
|
431
|
+
}
|
|
432
|
+
return { eligible: reasons.length === 0, reasons, retention_days: ttl };
|
|
433
|
+
}
|
|
434
|
+
inspect({ now = Date.now(), limit = 100 } = {}) {
|
|
435
|
+
const scan = this.#scanReferences();
|
|
436
|
+
const runs = this.#rows('runs').slice(-limit).reverse().map(run => ({ ...run, cleanup_pending: Boolean(run.logs_expired && this.db.prepare("SELECT 1 FROM events WHERE run_id=? AND type='gc-pending' LIMIT 1").get(run.id)), ...this.#eligibility(run, Number(now), scan), protection: this.db.prepare('SELECT reason FROM pins WHERE run_id=? ORDER BY reason').all(run.id).map(row => row.reason) }));
|
|
437
|
+
const storage = walk(this.directory).files.map(filename => ({ filename, bytes: fs.statSync(filename).size }));
|
|
438
|
+
return { schema_version: 1, read_only: true, workspace_id: this.workspaceId, directory: this.directory, database_exists: Boolean(this.db), storage_bytes: storage.reduce((sum, row) => sum + row.bytes, 0), file_count: storage.length, objects: this.#rows('objects').length, reference_scan: { complete: scan.complete, digest: scan.digest, errors: scan.errors }, runs };
|
|
439
|
+
}
|
|
440
|
+
inventory() {
|
|
441
|
+
const found = walk(this.root, { skip: new Set(['.git', 'node_modules', '.codegraph', '.graphify']), strict: true, followWithin: this.excluded });
|
|
442
|
+
const rows = [], groups = new Map();
|
|
443
|
+
for (const filename of found.files.filter(value => /\.(?:json|jsonl|ya?ml)$/.test(value))) {
|
|
444
|
+
const info = fileInfo(filename), relative = slash(path.relative(this.root, filename));
|
|
445
|
+
let classification = 'unknown';
|
|
446
|
+
if (/(?:checkpoint|approval|user-decision|transaction|receipt|contract|freeze)/i.test(relative)) classification = 'formal-authority';
|
|
447
|
+
else if (/(?:schema|registry|lock|manifest|package|config|yss-project)/i.test(relative)) classification = 'configuration';
|
|
448
|
+
else if (/(?:evidence\/maintenance|runs?\/|logs?\/)/.test(relative)) classification = 'execution-evidence';
|
|
449
|
+
else if (/(?:snapshot|cache|derived|views?\/)/i.test(relative)) classification = 'derived';
|
|
450
|
+
const row = { path: relative, sha256: info.sha256, bytes: info.bytes, classification, archive_candidate: classification === 'execution-evidence', action: 'inventory-only' };
|
|
451
|
+
rows.push(row); const group = groups.get(info.sha256) ?? []; group.push(relative); groups.set(info.sha256, group);
|
|
452
|
+
}
|
|
453
|
+
return { schema_version: 1, read_only: true, root: this.root, total_files: rows.length, total_bytes: rows.reduce((sum, row) => sum + row.bytes, 0), files: rows, projections: found.projections, duplicates: [...groups].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files })), scan_complete: found.errors.length === 0, errors: found.errors };
|
|
454
|
+
}
|
|
455
|
+
planGc({ now = Date.now() } = {}) {
|
|
456
|
+
const scan = this.#scanReferences();
|
|
457
|
+
const candidates = [], retained = [];
|
|
458
|
+
const pendingObjects = new Map();
|
|
459
|
+
for (const run of this.#rows('runs')) {
|
|
460
|
+
const pendingRow = this.db.prepare("SELECT value_json FROM events WHERE run_id=? AND type='gc-pending' ORDER BY id DESC LIMIT 1").get(run.id);
|
|
461
|
+
const pending = pendingRow ? JSON.parse(pendingRow.value_json) : null;
|
|
462
|
+
const recovering = Boolean(run.logs_expired && pending);
|
|
463
|
+
const decision = this.#eligibility(run, Number(now), scan, { recovering });
|
|
464
|
+
if (!decision.eligible) { retained.push({ id: run.id, ...decision }); continue; }
|
|
465
|
+
const directory = this.#ownedRunDirectory(run);
|
|
466
|
+
if (!fs.existsSync(directory) && !recovering) { retained.push({ id: run.id, eligible: false, reasons: ['missing-output'] }); continue; }
|
|
467
|
+
const found = walk(directory, { strict: true });
|
|
468
|
+
if (found.errors.length) { retained.push({ id: run.id, eligible: false, reasons: ['unknown-output'], errors: found.errors }); continue; }
|
|
469
|
+
const actual = found.files.map(fileInfo);
|
|
470
|
+
const registered = this.db.prepare('SELECT path,sha256,bytes FROM run_files WHERE run_id=? ORDER BY path').all(run.id);
|
|
471
|
+
if ((!recovering && actual.length !== registered.length) || actual.some(info => !registered.some(row => row.path === info.path && row.sha256 === info.sha256 && row.bytes === info.bytes))) {
|
|
472
|
+
retained.push({ id: run.id, eligible: false, reasons: ['unregistered-or-drifted-output'] }); continue;
|
|
473
|
+
}
|
|
474
|
+
if (recovering) for (const object of pending.objects ?? []) pendingObjects.set(object.digest, object);
|
|
475
|
+
candidates.push({ id: run.id, run_dir: directory, files: recovering ? registered.map(row => actual.find(info => info.path === row.path) ?? { ...row, missing: true }) : actual, ...(recovering ? { cleanup_pending: true } : {}) });
|
|
476
|
+
}
|
|
477
|
+
const ids = new Set(candidates.map(row => row.id));
|
|
478
|
+
const objects = this.#rows('objects').filter(object => {
|
|
479
|
+
const refs = this.db.prepare('SELECT run_id FROM run_objects WHERE digest=?').all(object.digest);
|
|
480
|
+
return refs.length > 0 && refs.every(row => ids.has(row.run_id));
|
|
481
|
+
}).map(row => {
|
|
482
|
+
const filename = this.#safeOwned(path.join(this.directory, row.relative_path));
|
|
483
|
+
if (fs.existsSync(filename)) return { digest: row.digest, ...fileInfo(filename) };
|
|
484
|
+
const pending = pendingObjects.get(row.digest);
|
|
485
|
+
if (!row.deleting || !pending) throw new Error('输入对象异常缺失,拒绝清理');
|
|
486
|
+
return { ...pending, path: filename, missing: true };
|
|
487
|
+
});
|
|
488
|
+
return { schema_version: 1, workspace_id: this.workspaceId, planned_at: new Date(Number(now)).toISOString(), state_digest: this.#stateDigest(), reference_scan: { complete: scan.complete, digest: scan.digest, errors: scan.errors }, candidates, objects, retained };
|
|
489
|
+
}
|
|
490
|
+
applyGc(plan) {
|
|
491
|
+
this.#write();
|
|
492
|
+
if (plan?.schema_version !== 1 || plan.workspace_id !== this.workspaceId || !plan.reference_scan?.complete) throw new Error('清理计划非法或引用扫描不完整');
|
|
493
|
+
const current = this.planGc({ now: Date.now() });
|
|
494
|
+
if (current.state_digest !== plan.state_digest || current.reference_scan.digest !== plan.reference_scan.digest || !current.reference_scan.complete) throw new Error('清理计划输入漂移,拒绝执行');
|
|
495
|
+
if (json(current.candidates) !== json(plan.candidates)) throw new Error('清理候选、占用或文件摘要漂移');
|
|
496
|
+
if (json(current.objects) !== json(plan.objects)) throw new Error('共享对象引用或摘要漂移');
|
|
497
|
+
// Mark first: interruption during removal leaves retained summaries and files
|
|
498
|
+
// for explicit recovery, rather than making an absent log appear complete.
|
|
499
|
+
this.#transaction(() => {
|
|
500
|
+
if (this.#stateDigest() !== plan.state_digest) throw new Error('清理计划数据库状态漂移');
|
|
501
|
+
for (const row of plan.candidates) {
|
|
502
|
+
this.db.prepare('UPDATE runs SET logs_expired=1 WHERE id=?').run(row.id);
|
|
503
|
+
this.db.prepare('INSERT INTO events(run_id,recorded_at,type,value_json) VALUES(?,?,?,?)').run(row.id, new Date().toISOString(), 'gc-pending', json({ files: row.files, objects: plan.objects }));
|
|
504
|
+
}
|
|
505
|
+
for (const row of plan.objects) this.db.prepare('UPDATE objects SET deleting=1 WHERE digest=?').run(row.digest);
|
|
506
|
+
});
|
|
507
|
+
const lastScan = this.#scanReferences();
|
|
508
|
+
if (!lastScan.complete || lastScan.digest !== plan.reference_scan.digest) throw new Error('删除前正式引用漂移;清理保持待恢复');
|
|
509
|
+
for (const row of plan.candidates) {
|
|
510
|
+
const occupancy = fileOccupancy(row.run_dir);
|
|
511
|
+
if (!occupancy.complete || occupancy.occupied) throw new Error('删除前日志已被占用或探测不完整;清理保持待恢复');
|
|
512
|
+
const output = walk(row.run_dir, { strict: true });
|
|
513
|
+
const expected = row.files.filter(file => !file.missing);
|
|
514
|
+
if (output.errors.length || output.files.length !== expected.length || output.files.some(filename => !expected.some(file => file.path === filename && file.sha256 === fileInfo(filename).sha256))) throw new Error('删除前文件摘要或输出集合漂移;清理保持待恢复');
|
|
515
|
+
}
|
|
516
|
+
for (const row of plan.objects) if (!row.missing && fileInfo(row.path).sha256 !== row.sha256) throw new Error('删除前输入对象漂移;清理保持待恢复');
|
|
517
|
+
for (const row of plan.candidates) fs.rmSync(this.#safeOwned(row.run_dir), { recursive: true, force: true });
|
|
518
|
+
for (const row of plan.objects) if (!row.missing) fs.unlinkSync(this.#safeOwned(row.path));
|
|
519
|
+
this.#transaction(() => {
|
|
520
|
+
for (const row of plan.candidates) {
|
|
521
|
+
this.db.prepare('DELETE FROM run_files WHERE run_id=?').run(row.id);
|
|
522
|
+
this.db.prepare('DELETE FROM commands WHERE run_id=?').run(row.id);
|
|
523
|
+
this.db.prepare('DELETE FROM events WHERE run_id=?').run(row.id);
|
|
524
|
+
this.db.prepare('DELETE FROM run_objects WHERE run_id=?').run(row.id);
|
|
525
|
+
}
|
|
526
|
+
for (const row of plan.objects) this.db.prepare('DELETE FROM objects WHERE digest=?').run(row.digest);
|
|
527
|
+
});
|
|
528
|
+
return { schema_version: 1, deleted_runs: plan.candidates.map(row => row.id), deleted_objects: plan.objects.map(row => row.digest), summaries_retained: true };
|
|
529
|
+
}
|
|
530
|
+
#externalDestination(destination) {
|
|
531
|
+
if (!path.isAbsolute(destination)) throw new Error('导出目标必须为绝对路径');
|
|
532
|
+
const target = realTarget(destination);
|
|
533
|
+
for (const forbidden of [...this.excluded, this.directory]) if (inside(forbidden, target)) throw new Error('导出或备份必须在仓库和运行存储之外');
|
|
534
|
+
if (fs.existsSync(target)) throw new Error('目标已存在,拒绝覆盖');
|
|
535
|
+
return target;
|
|
536
|
+
}
|
|
537
|
+
exportRun(id, destination) {
|
|
538
|
+
this.#write(); const run = this.#run(id);
|
|
539
|
+
if (!run.ended_at || run.logs_expired) throw new Error('运行未结束或日志已过期,不能导出完整证据');
|
|
540
|
+
const target = this.#externalDestination(destination);
|
|
541
|
+
const rows = this.db.prepare('SELECT * FROM run_files WHERE run_id=? ORDER BY path').all(id);
|
|
542
|
+
const files = [];
|
|
543
|
+
// Protect before copying: a concurrent GC can no longer select this run.
|
|
544
|
+
// A failed export intentionally leaves a protection reason for inspection.
|
|
545
|
+
this.pin(id, `export:${target}`);
|
|
546
|
+
// Validate all original bytes before creating the bundle.
|
|
547
|
+
for (const row of rows) if (fileInfo(row.path).sha256 !== row.sha256) throw new Error(`原始运行证据漂移: ${row.path}`);
|
|
548
|
+
fs.mkdirSync(target, { recursive: true });
|
|
549
|
+
for (const row of rows) {
|
|
550
|
+
const relative = inside(run.run_dir, row.path) ? `run/${slash(path.relative(run.run_dir, row.path))}` : `evidence/${sha(path.dirname(row.path)).slice(0, 16)}/${path.basename(row.path)}`;
|
|
551
|
+
const destinationFile = path.join(target, relative); fs.mkdirSync(path.dirname(destinationFile), { recursive: true }); fs.copyFileSync(row.path, destinationFile);
|
|
552
|
+
files.push({ relative_path: relative, source_path: row.path, sha256: row.sha256, bytes: row.bytes });
|
|
553
|
+
}
|
|
554
|
+
for (const object of this.db.prepare('SELECT o.* FROM objects o JOIN run_objects r ON r.digest=o.digest WHERE r.run_id=? ORDER BY o.digest').all(id)) {
|
|
555
|
+
const data = gunzipSync(fs.readFileSync(this.#safeOwned(path.join(this.directory, object.relative_path))));
|
|
556
|
+
if (sha(data) !== object.digest) throw new Error('输入对象摘要不匹配');
|
|
557
|
+
const relative = `inputs/${object.digest}.bin`; fs.mkdirSync(path.join(target, 'inputs'), { recursive: true }); fs.writeFileSync(path.join(target, relative), data);
|
|
558
|
+
files.push({ relative_path: relative, sha256: object.digest, bytes: data.length });
|
|
559
|
+
}
|
|
560
|
+
const manifest = { schema_version: 1, kind: 'runtime-export', workspace_id: this.workspaceId, run: { ...run }, commands: this.db.prepare('SELECT recorded_at,result_json FROM commands WHERE run_id=? ORDER BY id').all(id).map(row => ({ recorded_at: row.recorded_at, result: JSON.parse(row.result_json) })), events: this.db.prepare('SELECT recorded_at,type,value_json FROM events WHERE run_id=? ORDER BY id').all(id).map(row => ({ recorded_at: row.recorded_at, type: row.type, value: JSON.parse(row.value_json) })), files };
|
|
561
|
+
fs.writeFileSync(path.join(target, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
562
|
+
verifyBundle(target);
|
|
563
|
+
return { schema_version: 1, run_id: id, directory: target, manifest: path.join(target, 'manifest.json'), protected: true };
|
|
564
|
+
}
|
|
565
|
+
backup(destination) {
|
|
566
|
+
if (!this.db) throw new Error('没有运行数据库可备份');
|
|
567
|
+
const target = this.#externalDestination(destination);
|
|
568
|
+
const state = this.#stateDigest();
|
|
569
|
+
if (this.#rows('runs').some(run => activePid(run.owner_pid, run.hostname))) throw new Error('运行仍被进程占用,备份需要静止输出');
|
|
570
|
+
fs.mkdirSync(target, { recursive: true });
|
|
571
|
+
const database = path.join(target, 'runtime.sqlite');
|
|
572
|
+
this.db.exec(`VACUUM INTO '${database.replaceAll("'", "''")}'`);
|
|
573
|
+
const files = [{ relative_path: 'runtime.sqlite', ...fileInfo(database) }];
|
|
574
|
+
const found = walk(this.directory, { strict: true });
|
|
575
|
+
if (found.errors.length) throw new Error(found.errors.join('; '));
|
|
576
|
+
for (const filename of found.files) {
|
|
577
|
+
if (filename === this.databasePath || /runtime\.sqlite-(?:journal|wal|shm)$/.test(filename)) continue;
|
|
578
|
+
const info = fileInfo(filename), relative = slash(path.relative(this.directory, filename)), copy = path.join(target, relative);
|
|
579
|
+
fs.mkdirSync(path.dirname(copy), { recursive: true }); fs.copyFileSync(filename, copy);
|
|
580
|
+
if (fileInfo(copy).sha256 !== info.sha256 || fileInfo(filename).sha256 !== info.sha256) throw new Error('备份期间文件漂移');
|
|
581
|
+
files.push({ relative_path: relative, sha256: info.sha256, bytes: info.bytes });
|
|
582
|
+
}
|
|
583
|
+
if (this.#stateDigest() !== state) throw new Error('备份期间数据库漂移');
|
|
584
|
+
const externalDependencies = this.#rows('run_files').filter(row => !inside(this.directory, row.path)).map(row => ({ ...row, available: fs.existsSync(row.path) && fileInfo(row.path).sha256 === row.sha256 }));
|
|
585
|
+
const manifest = { schema_version: 1, kind: 'runtime-backup', workspace_id: this.workspaceId, root: this.root, runtime_directory: this.directory, external_dependencies: externalDependencies, files };
|
|
586
|
+
fs.writeFileSync(path.join(target, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
587
|
+
verifyBundle(target);
|
|
588
|
+
return { directory: target, manifest: path.join(target, 'manifest.json') };
|
|
589
|
+
}
|
|
590
|
+
static restore({ root, home, protectedRoots, source }) {
|
|
591
|
+
const location = resolveRuntimeLocation({ root, home, protectedRoots });
|
|
592
|
+
const manifest = verifyBundle(source);
|
|
593
|
+
if (manifest.kind !== 'runtime-backup' || manifest.workspace_id !== location.workspaceId || manifest.root !== location.root) throw new Error('备份不属于当前工作区');
|
|
594
|
+
if (typeof manifest.runtime_directory !== 'string' || !path.isAbsolute(manifest.runtime_directory) || !manifest.files.some(row => row.relative_path === 'runtime.sqlite')) throw new Error('备份运行位置或数据库清单缺失');
|
|
595
|
+
if (manifest.files.some(row => !/^(?:runtime\.sqlite|runs\/[^/]+\/.+|objects\/[a-f0-9]{2}\/[a-f0-9]{64}\.gz)$/.test(row.relative_path))) throw new Error('备份包含未知存储文件');
|
|
596
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
597
|
+
const check = new DatabaseSync(path.join(source, 'runtime.sqlite'), { readOnly: true });
|
|
598
|
+
try { if (check.prepare('PRAGMA user_version').get().user_version !== VERSION || check.prepare('PRAGMA quick_check').get().quick_check !== 'ok') throw new Error('备份数据库非法'); } finally { check.close(); }
|
|
599
|
+
// Existing identical targets are accepted, differing bytes are never replaced.
|
|
600
|
+
for (const row of manifest.files) {
|
|
601
|
+
const target = path.join(location.directory, row.relative_path);
|
|
602
|
+
if (!inside(location.directory, realTarget(target))) throw new Error('恢复真实目标越界');
|
|
603
|
+
if (fs.existsSync(target) && fileInfo(target).sha256 !== row.sha256) throw new Error(`恢复目标已有不同内容: ${row.relative_path}`);
|
|
604
|
+
}
|
|
605
|
+
for (const row of manifest.files) { const target = path.join(location.directory, row.relative_path); fs.mkdirSync(path.dirname(target), { recursive: true }); if (!fs.existsSync(target)) fs.copyFileSync(path.join(source, row.relative_path), target, fs.constants.COPYFILE_EXCL); }
|
|
606
|
+
if (manifest.runtime_directory !== location.directory) {
|
|
607
|
+
const restored = new DatabaseSync(path.join(location.directory, 'runtime.sqlite'));
|
|
608
|
+
try {
|
|
609
|
+
restored.exec('PRAGMA busy_timeout=5000; BEGIN IMMEDIATE');
|
|
610
|
+
for (const row of restored.prepare('SELECT id,run_dir FROM runs').all()) {
|
|
611
|
+
const current = path.join(location.directory, 'runs', row.id);
|
|
612
|
+
if (row.run_dir === current) continue;
|
|
613
|
+
if (!inside(manifest.runtime_directory, row.run_dir)) throw new Error('备份运行目录越界');
|
|
614
|
+
restored.prepare('UPDATE runs SET run_dir=?,owner_pid=NULL WHERE id=?').run(current, row.id);
|
|
615
|
+
restored.prepare('INSERT INTO events(run_id,recorded_at,type,value_json) VALUES(?,?,?,?)').run(row.id, new Date().toISOString(), 'runtime-restored', json({ old_directory: row.run_dir, new_directory: current }));
|
|
616
|
+
}
|
|
617
|
+
for (const row of restored.prepare('SELECT run_id,path FROM run_files').all()) if (inside(manifest.runtime_directory, row.path)) restored.prepare('UPDATE run_files SET path=? WHERE run_id=? AND path=?').run(path.join(location.directory, path.relative(manifest.runtime_directory, row.path)), row.run_id, row.path);
|
|
618
|
+
for (const row of restored.prepare("SELECT id,value_json FROM events WHERE type='gc-pending'").all()) {
|
|
619
|
+
const pending = JSON.parse(row.value_json);
|
|
620
|
+
for (const entry of [...(pending.files ?? []), ...(pending.objects ?? [])]) if (inside(manifest.runtime_directory, entry.path)) entry.path = path.join(location.directory, path.relative(manifest.runtime_directory, entry.path));
|
|
621
|
+
restored.prepare('UPDATE events SET value_json=? WHERE id=?').run(json(pending), row.id);
|
|
622
|
+
}
|
|
623
|
+
restored.exec('COMMIT');
|
|
624
|
+
} catch (error) { try { restored.exec('ROLLBACK'); } catch {} throw error; }
|
|
625
|
+
finally { restored.close(); }
|
|
626
|
+
}
|
|
627
|
+
const dependencies = (manifest.external_dependencies ?? []).map(row => ({ path: row.path, sha256: row.sha256, available: fs.existsSync(row.path) && fileInfo(row.path).sha256 === row.sha256 }));
|
|
628
|
+
return { schema_version: 1, directory: location.directory, restored_files: manifest.files.length, external_dependencies: dependencies, formal_evidence_complete: dependencies.every(row => row.available) };
|
|
629
|
+
}
|
|
630
|
+
refreshCheckpoint(filename, value) {
|
|
631
|
+
this.#write();
|
|
632
|
+
const info = fileInfo(path.resolve(filename));
|
|
633
|
+
this.db.prepare('INSERT OR REPLACE INTO checkpoint_index VALUES(?,?,?,?)').run(info.path, info.sha256, new Date().toISOString(), json(value));
|
|
634
|
+
return { source_digest: info.sha256, indexed_at: new Date().toISOString(), authority: false };
|
|
635
|
+
}
|
|
636
|
+
readCheckpoint(filename) {
|
|
637
|
+
const resolved = realTarget(path.resolve(filename));
|
|
638
|
+
const row = this.db?.prepare('SELECT * FROM checkpoint_index WHERE path=?').get(resolved);
|
|
639
|
+
if (!row) return { freshness: 'missing', authority: false };
|
|
640
|
+
const fresh = fs.existsSync(resolved) && fileInfo(resolved).sha256 === row.source_digest;
|
|
641
|
+
return { freshness: fresh ? 'current' : 'stale', source_digest: row.source_digest, indexed_at: row.indexed_at, authority: false, ...(fresh ? { value: JSON.parse(row.value_json) } : {}) };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function beginRuntimeRun(options = {}) {
|
|
646
|
+
if (options.mode === 'off') return null;
|
|
647
|
+
if (options.mode && options.mode !== 'sqlite') throw new Error('runtime-store仅接受sqlite或off');
|
|
648
|
+
const store = new RuntimeStore(options);
|
|
649
|
+
try { return store.begin(options); } catch (error) { store.close(); throw error; }
|
|
650
|
+
}
|