spectra-cli 4.2.0 → 4.3.0
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/README.md +12 -5
- package/dist/.spectra-build-meta.json +9 -0
- package/dist/adapters/language-adapter.d.ts +7 -12
- package/dist/adapters/language-adapter.d.ts.map +1 -1
- package/dist/adapters/python-adapter.d.ts.map +1 -1
- package/dist/adapters/python-adapter.js +11 -2
- package/dist/adapters/python-adapter.js.map +1 -1
- package/dist/batch/batch-orchestrator.d.ts +44 -1
- package/dist/batch/batch-orchestrator.d.ts.map +1 -1
- package/dist/batch/batch-orchestrator.js +312 -76
- package/dist/batch/batch-orchestrator.js.map +1 -1
- package/dist/batch/delta-regenerator.d.ts.map +1 -1
- package/dist/batch/delta-regenerator.js +21 -28
- package/dist/batch/delta-regenerator.js.map +1 -1
- package/dist/batch/module-grouper.d.ts +7 -0
- package/dist/batch/module-grouper.d.ts.map +1 -1
- package/dist/batch/module-grouper.js +2 -0
- package/dist/batch/module-grouper.js.map +1 -1
- package/dist/batch/regen-plan.d.ts +78 -0
- package/dist/batch/regen-plan.d.ts.map +1 -0
- package/dist/batch/regen-plan.js +93 -0
- package/dist/batch/regen-plan.js.map +1 -0
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +36 -8
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/scaffold-kb.d.ts +9 -0
- package/dist/cli/commands/scaffold-kb.d.ts.map +1 -0
- package/dist/cli/commands/scaffold-kb.js +179 -0
- package/dist/cli/commands/scaffold-kb.js.map +1 -0
- package/dist/cli/index.js +24 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/error-handler.d.ts +0 -6
- package/dist/cli/utils/error-handler.d.ts.map +1 -1
- package/dist/cli/utils/error-handler.js +0 -13
- package/dist/cli/utils/error-handler.js.map +1 -1
- package/dist/cli/utils/parse-args.d.ts +43 -3
- package/dist/cli/utils/parse-args.d.ts.map +1 -1
- package/dist/cli/utils/parse-args.js +98 -12
- package/dist/cli/utils/parse-args.js.map +1 -1
- package/dist/cli/version-meta.d.ts +16 -0
- package/dist/cli/version-meta.d.ts.map +1 -0
- package/dist/cli/version-meta.js +28 -0
- package/dist/cli/version-meta.js.map +1 -0
- package/dist/core/ast-analyzer.d.ts.map +1 -1
- package/dist/core/ast-analyzer.js +7 -11
- package/dist/core/ast-analyzer.js.map +1 -1
- package/dist/core/import-resolver.d.ts +70 -67
- package/dist/core/import-resolver.d.ts.map +1 -1
- package/dist/core/import-resolver.js +341 -200
- package/dist/core/import-resolver.js.map +1 -1
- package/dist/core/single-spec-orchestrator.d.ts +21 -0
- package/dist/core/single-spec-orchestrator.d.ts.map +1 -1
- package/dist/core/single-spec-orchestrator.js +41 -14
- package/dist/core/single-spec-orchestrator.js.map +1 -1
- package/dist/core/skeleton-hash.d.ts +33 -0
- package/dist/core/skeleton-hash.d.ts.map +1 -0
- package/dist/core/skeleton-hash.js +76 -0
- package/dist/core/skeleton-hash.js.map +1 -0
- package/dist/core/tree-sitter-analyzer.d.ts +3 -2
- package/dist/core/tree-sitter-analyzer.d.ts.map +1 -1
- package/dist/core/tree-sitter-analyzer.js +5 -6
- package/dist/core/tree-sitter-analyzer.js.map +1 -1
- package/dist/core/tree-sitter-fallback.d.ts +7 -2
- package/dist/core/tree-sitter-fallback.d.ts.map +1 -1
- package/dist/core/tree-sitter-fallback.js +18 -15
- package/dist/core/tree-sitter-fallback.js.map +1 -1
- package/dist/extraction/image-extractor.d.ts.map +1 -1
- package/dist/extraction/image-extractor.js +3 -1
- package/dist/extraction/image-extractor.js.map +1 -1
- package/dist/extraction/markdown-extractor.d.ts.map +1 -1
- package/dist/extraction/markdown-extractor.js +3 -1
- package/dist/extraction/markdown-extractor.js.map +1 -1
- package/dist/generator/frontmatter.d.ts +5 -0
- package/dist/generator/frontmatter.d.ts.map +1 -1
- package/dist/generator/frontmatter.js +4 -0
- package/dist/generator/frontmatter.js.map +1 -1
- package/dist/kb-mcp/index.d.ts +14 -0
- package/dist/kb-mcp/index.d.ts.map +1 -0
- package/dist/kb-mcp/index.js +24 -0
- package/dist/kb-mcp/index.js.map +1 -0
- package/dist/kb-mcp/lib/kb-error.d.ts +21 -0
- package/dist/kb-mcp/lib/kb-error.d.ts.map +1 -0
- package/dist/kb-mcp/lib/kb-error.js +28 -0
- package/dist/kb-mcp/lib/kb-error.js.map +1 -0
- package/dist/kb-mcp/lib/kb-locator.d.ts +58 -0
- package/dist/kb-mcp/lib/kb-locator.d.ts.map +1 -0
- package/dist/kb-mcp/lib/kb-locator.js +85 -0
- package/dist/kb-mcp/lib/kb-locator.js.map +1 -0
- package/dist/kb-mcp/lib/result-merger.d.ts +35 -0
- package/dist/kb-mcp/lib/result-merger.d.ts.map +1 -0
- package/dist/kb-mcp/lib/result-merger.js +64 -0
- package/dist/kb-mcp/lib/result-merger.js.map +1 -0
- package/dist/kb-mcp/server.d.ts +12 -0
- package/dist/kb-mcp/server.d.ts.map +1 -0
- package/dist/kb-mcp/server.js +29 -0
- package/dist/kb-mcp/server.js.map +1 -0
- package/dist/kb-mcp/tools/kb-api-lookup.d.ts +28 -0
- package/dist/kb-mcp/tools/kb-api-lookup.d.ts.map +1 -0
- package/dist/kb-mcp/tools/kb-api-lookup.js +237 -0
- package/dist/kb-mcp/tools/kb-api-lookup.js.map +1 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.d.ts +20 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.d.ts.map +1 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.js +92 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.js.map +1 -0
- package/dist/kb-mcp/tools/kb-search.d.ts +23 -0
- package/dist/kb-mcp/tools/kb-search.d.ts.map +1 -0
- package/dist/kb-mcp/tools/kb-search.js +126 -0
- package/dist/kb-mcp/tools/kb-search.js.map +1 -0
- package/dist/knowledge-graph/import-resolver.d.ts +11 -86
- package/dist/knowledge-graph/import-resolver.d.ts.map +1 -1
- package/dist/knowledge-graph/import-resolver.js +10 -333
- package/dist/knowledge-graph/import-resolver.js.map +1 -1
- package/dist/knowledge-graph/incremental.d.ts +25 -4
- package/dist/knowledge-graph/incremental.d.ts.map +1 -1
- package/dist/knowledge-graph/incremental.js +37 -15
- package/dist/knowledge-graph/incremental.js.map +1 -1
- package/dist/knowledge-graph/index.d.ts +11 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -1
- package/dist/knowledge-graph/index.js +43 -1
- package/dist/knowledge-graph/index.js.map +1 -1
- package/dist/knowledge-graph/module-derivation.d.ts +14 -0
- package/dist/knowledge-graph/module-derivation.d.ts.map +1 -1
- package/dist/knowledge-graph/module-derivation.js +44 -44
- package/dist/knowledge-graph/module-derivation.js.map +1 -1
- package/dist/knowledge-graph/persistence.d.ts +56 -16
- package/dist/knowledge-graph/persistence.d.ts.map +1 -1
- package/dist/knowledge-graph/persistence.js +86 -32
- package/dist/knowledge-graph/persistence.js.map +1 -1
- package/dist/knowledge-graph/query-helpers.d.ts +39 -12
- package/dist/knowledge-graph/query-helpers.d.ts.map +1 -1
- package/dist/knowledge-graph/query-helpers.js +199 -34
- package/dist/knowledge-graph/query-helpers.js.map +1 -1
- package/dist/knowledge-graph/relativize.d.ts +51 -0
- package/dist/knowledge-graph/relativize.d.ts.map +1 -0
- package/dist/knowledge-graph/relativize.js +104 -0
- package/dist/knowledge-graph/relativize.js.map +1 -0
- package/dist/mcp/agent-context-tools.d.ts +3 -54
- package/dist/mcp/agent-context-tools.d.ts.map +1 -1
- package/dist/mcp/agent-context-tools.js +141 -189
- package/dist/mcp/agent-context-tools.js.map +1 -1
- package/dist/mcp/file-nav-tools.d.ts +42 -0
- package/dist/mcp/file-nav-tools.d.ts.map +1 -0
- package/dist/mcp/file-nav-tools.js +318 -0
- package/dist/mcp/file-nav-tools.js.map +1 -0
- package/dist/mcp/graph-tools.d.ts +2 -0
- package/dist/mcp/graph-tools.d.ts.map +1 -1
- package/dist/mcp/graph-tools.js +134 -123
- package/dist/mcp/graph-tools.js.map +1 -1
- package/dist/mcp/lib/file-nav-helpers.d.ts +128 -0
- package/dist/mcp/lib/file-nav-helpers.d.ts.map +1 -0
- package/dist/mcp/lib/file-nav-helpers.js +305 -0
- package/dist/mcp/lib/file-nav-helpers.js.map +1 -0
- package/dist/mcp/lib/telemetry.d.ts +69 -0
- package/dist/mcp/lib/telemetry.d.ts.map +1 -0
- package/dist/mcp/lib/telemetry.js +113 -0
- package/dist/mcp/lib/telemetry.js.map +1 -0
- package/dist/mcp/lib/tool-response.d.ts +46 -0
- package/dist/mcp/lib/tool-response.d.ts.map +1 -0
- package/dist/mcp/lib/tool-response.js +83 -0
- package/dist/mcp/lib/tool-response.js.map +1 -0
- package/dist/mcp/server.d.ts +13 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +209 -150
- package/dist/mcp/server.js.map +1 -1
- package/dist/models/code-skeleton.d.ts +4 -2
- package/dist/models/code-skeleton.d.ts.map +1 -1
- package/dist/models/code-skeleton.js +2 -1
- package/dist/models/code-skeleton.js.map +1 -1
- package/dist/models/module-spec.d.ts +18 -0
- package/dist/models/module-spec.d.ts.map +1 -1
- package/dist/models/module-spec.js +6 -0
- package/dist/models/module-spec.js.map +1 -1
- package/dist/panoramic/builders/doc-graph-builder.d.ts +13 -0
- package/dist/panoramic/builders/doc-graph-builder.d.ts.map +1 -1
- package/dist/panoramic/builders/doc-graph-builder.js +20 -0
- package/dist/panoramic/builders/doc-graph-builder.js.map +1 -1
- package/dist/panoramic/graph/graph-builder.d.ts +72 -2
- package/dist/panoramic/graph/graph-builder.d.ts.map +1 -1
- package/dist/panoramic/graph/graph-builder.js +257 -51
- package/dist/panoramic/graph/graph-builder.js.map +1 -1
- package/dist/panoramic/graph/graph-query.d.ts +16 -0
- package/dist/panoramic/graph/graph-query.d.ts.map +1 -1
- package/dist/panoramic/graph/graph-query.js +49 -1
- package/dist/panoramic/graph/graph-query.js.map +1 -1
- package/dist/panoramic/graph/index.d.ts +2 -1
- package/dist/panoramic/graph/index.d.ts.map +1 -1
- package/dist/panoramic/graph/index.js +1 -1
- package/dist/panoramic/graph/index.js.map +1 -1
- package/dist/panoramic/pipelines/adr-evidence-verifier.d.ts.map +1 -1
- package/dist/panoramic/pipelines/adr-evidence-verifier.js +1 -29
- package/dist/panoramic/pipelines/adr-evidence-verifier.js.map +1 -1
- package/dist/panoramic/query.d.ts +1 -0
- package/dist/panoramic/query.d.ts.map +1 -1
- package/dist/panoramic/query.js +5 -0
- package/dist/panoramic/query.js.map +1 -1
- package/dist/scaffold-kb/api-entities-serializer.d.ts +12 -0
- package/dist/scaffold-kb/api-entities-serializer.d.ts.map +1 -0
- package/dist/scaffold-kb/api-entities-serializer.js +144 -0
- package/dist/scaffold-kb/api-entities-serializer.js.map +1 -0
- package/dist/scaffold-kb/arbitration.d.ts +44 -0
- package/dist/scaffold-kb/arbitration.d.ts.map +1 -0
- package/dist/scaffold-kb/arbitration.js +144 -0
- package/dist/scaffold-kb/arbitration.js.map +1 -0
- package/dist/scaffold-kb/chunk-splitter.d.ts +20 -0
- package/dist/scaffold-kb/chunk-splitter.d.ts.map +1 -0
- package/dist/scaffold-kb/chunk-splitter.js +309 -0
- package/dist/scaffold-kb/chunk-splitter.js.map +1 -0
- package/dist/scaffold-kb/doc-graph-builder.d.ts +26 -0
- package/dist/scaffold-kb/doc-graph-builder.d.ts.map +1 -0
- package/dist/scaffold-kb/doc-graph-builder.js +72 -0
- package/dist/scaffold-kb/doc-graph-builder.js.map +1 -0
- package/dist/scaffold-kb/entity-extractor.d.ts +40 -0
- package/dist/scaffold-kb/entity-extractor.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-extractor.js +0 -0
- package/dist/scaffold-kb/entity-extractor.js.map +1 -0
- package/dist/scaffold-kb/entity-heuristic.d.ts +14 -0
- package/dist/scaffold-kb/entity-heuristic.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-heuristic.js +107 -0
- package/dist/scaffold-kb/entity-heuristic.js.map +1 -0
- package/dist/scaffold-kb/entity-matcher.d.ts +20 -0
- package/dist/scaffold-kb/entity-matcher.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-matcher.js +53 -0
- package/dist/scaffold-kb/entity-matcher.js.map +1 -0
- package/dist/scaffold-kb/entity-util.d.ts +11 -0
- package/dist/scaffold-kb/entity-util.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-util.js +23 -0
- package/dist/scaffold-kb/entity-util.js.map +1 -0
- package/dist/scaffold-kb/evidence-envelope.d.ts +17 -0
- package/dist/scaffold-kb/evidence-envelope.d.ts.map +1 -0
- package/dist/scaffold-kb/evidence-envelope.js +32 -0
- package/dist/scaffold-kb/evidence-envelope.js.map +1 -0
- package/dist/scaffold-kb/index.d.ts +20 -0
- package/dist/scaffold-kb/index.d.ts.map +1 -0
- package/dist/scaffold-kb/index.js +102 -0
- package/dist/scaffold-kb/index.js.map +1 -0
- package/dist/scaffold-kb/ingest/ingest-core.d.ts +47 -0
- package/dist/scaffold-kb/ingest/ingest-core.d.ts.map +1 -0
- package/dist/scaffold-kb/ingest/ingest-core.js +231 -0
- package/dist/scaffold-kb/ingest/ingest-core.js.map +1 -0
- package/dist/scaffold-kb/ingest/office-parser.d.ts +33 -0
- package/dist/scaffold-kb/ingest/office-parser.d.ts.map +1 -0
- package/dist/scaffold-kb/ingest/office-parser.js +187 -0
- package/dist/scaffold-kb/ingest/office-parser.js.map +1 -0
- package/dist/scaffold-kb/ingest/url-fetcher.d.ts +43 -0
- package/dist/scaffold-kb/ingest/url-fetcher.d.ts.map +1 -0
- package/dist/scaffold-kb/ingest/url-fetcher.js +245 -0
- package/dist/scaffold-kb/ingest/url-fetcher.js.map +1 -0
- package/dist/scaffold-kb/ingester.d.ts +38 -0
- package/dist/scaffold-kb/ingester.d.ts.map +1 -0
- package/dist/scaffold-kb/ingester.js +261 -0
- package/dist/scaffold-kb/ingester.js.map +1 -0
- package/dist/scaffold-kb/injection-format.d.ts +25 -0
- package/dist/scaffold-kb/injection-format.d.ts.map +1 -0
- package/dist/scaffold-kb/injection-format.js +55 -0
- package/dist/scaffold-kb/injection-format.js.map +1 -0
- package/dist/scaffold-kb/kb-writer.d.ts +13 -0
- package/dist/scaffold-kb/kb-writer.d.ts.map +1 -0
- package/dist/scaffold-kb/kb-writer.js +69 -0
- package/dist/scaffold-kb/kb-writer.js.map +1 -0
- package/dist/scaffold-kb/keyword-extract.d.ts +16 -0
- package/dist/scaffold-kb/keyword-extract.d.ts.map +1 -0
- package/dist/scaffold-kb/keyword-extract.js +49 -0
- package/dist/scaffold-kb/keyword-extract.js.map +1 -0
- package/dist/scaffold-kb/query-sanitizer.d.ts +28 -0
- package/dist/scaffold-kb/query-sanitizer.d.ts.map +1 -0
- package/dist/scaffold-kb/query-sanitizer.js +38 -0
- package/dist/scaffold-kb/query-sanitizer.js.map +1 -0
- package/dist/scaffold-kb/recall-eval.d.ts +48 -0
- package/dist/scaffold-kb/recall-eval.d.ts.map +1 -0
- package/dist/scaffold-kb/recall-eval.js +55 -0
- package/dist/scaffold-kb/recall-eval.js.map +1 -0
- package/dist/scaffold-kb/schema-compat.d.ts +22 -0
- package/dist/scaffold-kb/schema-compat.d.ts.map +1 -0
- package/dist/scaffold-kb/schema-compat.js +32 -0
- package/dist/scaffold-kb/schema-compat.js.map +1 -0
- package/dist/scaffold-kb/search-core.d.ts +42 -0
- package/dist/scaffold-kb/search-core.d.ts.map +1 -0
- package/dist/scaffold-kb/search-core.js +89 -0
- package/dist/scaffold-kb/search-core.js.map +1 -0
- package/dist/scaffold-kb/sqlite-engine.d.ts +58 -0
- package/dist/scaffold-kb/sqlite-engine.d.ts.map +1 -0
- package/dist/scaffold-kb/sqlite-engine.js +61 -0
- package/dist/scaffold-kb/sqlite-engine.js.map +1 -0
- package/dist/scaffold-kb/sqlite-writer.d.ts +18 -0
- package/dist/scaffold-kb/sqlite-writer.d.ts.map +1 -0
- package/dist/scaffold-kb/sqlite-writer.js +76 -0
- package/dist/scaffold-kb/sqlite-writer.js.map +1 -0
- package/dist/scaffold-kb/tokenizer.d.ts +26 -0
- package/dist/scaffold-kb/tokenizer.d.ts.map +1 -0
- package/dist/scaffold-kb/tokenizer.js +97 -0
- package/dist/scaffold-kb/tokenizer.js.map +1 -0
- package/dist/scaffold-kb/types.d.ts +157 -0
- package/dist/scaffold-kb/types.d.ts.map +1 -0
- package/dist/scaffold-kb/types.js +5 -0
- package/dist/scaffold-kb/types.js.map +1 -0
- package/dist/utils/file-scanner.d.ts +15 -0
- package/dist/utils/file-scanner.d.ts.map +1 -1
- package/dist/utils/file-scanner.js +17 -0
- package/dist/utils/file-scanner.js.map +1 -1
- package/dist/utils/string-distance.d.ts +20 -0
- package/dist/utils/string-distance.d.ts.map +1 -0
- package/dist/utils/string-distance.js +38 -0
- package/dist/utils/string-distance.js.map +1 -0
- package/package.json +11 -2
- package/plugins/demo-kb-en/.claude-plugin/plugin.json +11 -0
- package/plugins/demo-kb-en/.mcp.json +8 -0
- package/plugins/demo-kb-en/FIXTURE.json +23 -0
- package/plugins/demo-kb-en/FIXTURE.md +53 -0
- package/plugins/demo-kb-en/ingest-samples/meeting-notes.md +13 -0
- package/plugins/demo-kb-en/kb/api-entities.json +3445 -0
- package/plugins/demo-kb-en/kb/chunks.sqlite +0 -0
- package/plugins/demo-kb-en/kb/doc-graph.json +113 -0
- package/plugins/demo-kb-zh/.claude-plugin/plugin.json +11 -0
- package/plugins/demo-kb-zh/.mcp.json +8 -0
- package/plugins/demo-kb-zh/FIXTURE.json +23 -0
- package/plugins/demo-kb-zh/FIXTURE.md +63 -0
- package/plugins/demo-kb-zh/ingest-samples/meeting-notes.md +13 -0
- package/plugins/demo-kb-zh/kb/api-entities.json +990 -0
- package/plugins/demo-kb-zh/kb/chunks.sqlite +0 -0
- package/plugins/demo-kb-zh/kb/doc-graph.json +113 -0
- package/plugins/spec-driver/.claude-plugin/plugin.json +1 -1
- package/plugins/spec-driver/README.md +33 -1
- package/plugins/spec-driver/agents/verify.md +88 -0
- package/plugins/spec-driver/config/orchestration.yaml +39 -31
- package/plugins/spec-driver/contracts/orchestration-overrides-contract.yaml +27 -0
- package/plugins/spec-driver/contracts/orchestration-schema.mjs +272 -227
- package/plugins/spec-driver/contracts/wrapper-source-of-truth.yaml +3 -0
- package/plugins/spec-driver/hooks/hooks.json +9 -0
- package/plugins/spec-driver/hooks/stop-fix-compliance-check.sh +36 -0
- package/plugins/spec-driver/lib/delegation-contract.mjs +84 -0
- package/plugins/spec-driver/lib/orchestration-resolver.mjs +49 -0
- package/plugins/spec-driver/lib/orchestrator.mjs +29 -1
- package/plugins/spec-driver/scripts/codex-skills.sh +28 -28
- package/plugins/spec-driver/scripts/dev/spike-fix-compliance-e2e.mjs +130 -0
- package/plugins/spec-driver/scripts/fix-compliance-judge.mjs +415 -0
- package/plugins/spec-driver/scripts/generate-adoption-insights.mjs +48 -1
- package/plugins/spec-driver/scripts/goal-loop-cli.mjs +307 -0
- package/plugins/spec-driver/scripts/init-project.sh +56 -0
- package/plugins/spec-driver/scripts/kb-prequery.mjs +125 -0
- package/plugins/spec-driver/scripts/lib/config-schema.mjs +200 -94
- package/plugins/spec-driver/scripts/lib/ensure-gitignore.sh +277 -0
- package/plugins/spec-driver/scripts/lib/extract-wrapper-body.mjs +136 -0
- package/plugins/spec-driver/scripts/lib/fix-compliance-core.mjs +434 -0
- package/plugins/spec-driver/scripts/lib/fix-compliance-io.mjs +335 -0
- package/plugins/spec-driver/scripts/lib/goal-loop-core.mjs +783 -0
- package/plugins/spec-driver/scripts/lib/init-project-output.sh +28 -0
- package/plugins/spec-driver/scripts/lib/load-zod.mjs +62 -0
- package/plugins/spec-driver/scripts/lib/project-profile-resolver.mjs +141 -49
- package/plugins/spec-driver/scripts/lib/project-profile-schema.mjs +88 -54
- package/plugins/spec-driver/scripts/postinstall.sh +13 -1
- package/plugins/spec-driver/scripts/record-workflow-run.mjs +75 -0
- package/plugins/spec-driver/scripts/sync-delegation-contract.mjs +187 -0
- package/plugins/spec-driver/scripts/validate-orchestrator-models.mjs +156 -0
- package/plugins/spec-driver/scripts/validate-wrapper-sources.mjs +46 -1
- package/plugins/spec-driver/skills/spec-driver-feature/SKILL.md +292 -0
- package/plugins/spec-driver/skills/spec-driver-fix/SKILL.md +87 -8
- package/plugins/spec-driver/skills/spec-driver-implement/SKILL.md +8 -0
- package/plugins/spec-driver/skills/spec-driver-resume/SKILL.md +12 -1
- package/plugins/spec-driver/skills/spec-driver-story/SKILL.md +20 -0
- package/plugins/spec-driver/templates/delegation-contract.md +27 -0
- package/plugins/spec-driver/templates/goal-loop-override-template.yaml +284 -0
- package/plugins/spec-driver/templates/specify-base/project-context-template.yaml +10 -0
- package/plugins/spectra/.claude-plugin/plugin.json +1 -1
- package/plugins/spectra/README.md +1 -1
- package/templates/module-spec.hbs +1 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-loop-core.mjs
|
|
3
|
+
* Feature 201 — goal_loop agent_mode 确定性 core
|
|
4
|
+
*
|
|
5
|
+
* 设计原则(plan §1.1):本模块的 12 个函数均为**纯函数**(无 I/O,输入→输出),
|
|
6
|
+
* 不读文件、不调 git、不调 MCP、不写日志。git/MCP 调用由编排器在 SKILL.md 散文中发起,
|
|
7
|
+
* core 只负责**规划命令序列**和**解释结果**。单实例文件锁的 I/O 边界助手放在
|
|
8
|
+
* goal-loop-cli.mjs,不在本纯 core 模块。
|
|
9
|
+
*
|
|
10
|
+
* TDD:本文件在 T011 阶段以空桩形式存在(每个函数 throw NotImplemented),
|
|
11
|
+
* 配套测试 goal-loop-core.test.mjs 写真实断言,全部因桩抛错而干净红;
|
|
12
|
+
* 随后 T012~T017 逐函数替换为实现转绿。
|
|
13
|
+
*
|
|
14
|
+
* 运行测试: node --test plugins/spec-driver/tests/goal-loop-core.test.mjs
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* goal_loop 循环配置路径——跨快照保留,不得被 stash/clean 触碰(F203 缺陷 1)
|
|
19
|
+
*
|
|
20
|
+
* goal_loop 验证态会临时写 .specify/orchestration-overrides.yaml(刻意不入 commit),
|
|
21
|
+
* 早期 planSnapshotCommands 的 `git stash push -u` 与 planRollbackCommands 的 `git clean -fd`
|
|
22
|
+
* 会把它当普通工作树变更卷走/删除,导致循环中途配置自毁。该常量驱动命令生成的 pathspec 排除。
|
|
23
|
+
*/
|
|
24
|
+
export const PRESERVED_CONFIG_PATHSPECS = [
|
|
25
|
+
'.specify/orchestration-overrides.yaml',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 分类单条命令结果(FR-009)
|
|
30
|
+
* @param {{ exit_code?: number|null, skipped_reason?: string|null }} cmdResult
|
|
31
|
+
* @returns {'PASS'|'FAIL'|'SKIPPED'|'UNKNOWN'}
|
|
32
|
+
*/
|
|
33
|
+
export function classifyCommand(cmdResult) {
|
|
34
|
+
// skipped_reason 非 null 优先判 SKIPPED(即使填了 exit_code)
|
|
35
|
+
if (cmdResult && cmdResult.skipped_reason != null) {
|
|
36
|
+
return 'SKIPPED';
|
|
37
|
+
}
|
|
38
|
+
const code = cmdResult ? cmdResult.exit_code : undefined;
|
|
39
|
+
// exit_code 缺失或非数字 → UNKNOWN(无法证明真实退出码,职责分离防 reward-hacking)
|
|
40
|
+
if (typeof code !== 'number' || Number.isNaN(code)) {
|
|
41
|
+
return 'UNKNOWN';
|
|
42
|
+
}
|
|
43
|
+
// 0 = PASS;非零(含超时如 124)= FAIL
|
|
44
|
+
return code === 0 ? 'PASS' : 'FAIL';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 判定本轮 report 是否达标(FR-008)
|
|
49
|
+
* @param {Object} report
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function evaluateMetric(report) {
|
|
53
|
+
if (!report || !Array.isArray(report.layer2_commands)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// 防 vacuous-truth(Codex C3):空命令集下 every(...) 返回 true,会让"零命令 + 覆盖 100 +
|
|
57
|
+
// COMPLIANT"被误判达标。空 layer2_commands 表示没有任何可执行验证证据,必须不达标。
|
|
58
|
+
if (report.layer2_commands.length === 0) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// 条件 1:所有 layer2 命令均 PASS(任何 FAIL/SKIPPED/UNKNOWN 即不达标)
|
|
62
|
+
const allPass = report.layer2_commands.every((cmd) => classifyCommand(cmd) === 'PASS');
|
|
63
|
+
if (!allPass) return false;
|
|
64
|
+
// 条件 2:P1 FR 覆盖率 100%
|
|
65
|
+
const cov = report.layer1_fr_coverage;
|
|
66
|
+
if (!cov || cov.p1_coverage_pct !== 100) return false;
|
|
67
|
+
// 条件 3:Layer 1.5 证据 COMPLIANT
|
|
68
|
+
const evidence = report.layer1_5_evidence;
|
|
69
|
+
if (!evidence || evidence.status !== 'COMPLIANT') return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 校验 full 报告的 PASS 命令是否覆盖全部必需 kind 类别(F204)
|
|
75
|
+
*
|
|
76
|
+
* 纯函数:无 I/O、无副作用、幂等。对 verify_mode:full 报告取其 PASS 命令的 kind 集合,
|
|
77
|
+
* 校验是否 ⊇ requiredKinds。requiredKinds 为空 → complete:true(优雅降级到现状)。
|
|
78
|
+
*
|
|
79
|
+
* **保护边界(诚实说明)**:kind 由 verify 子代理自报(与 exit_code 同源、同层级)。能挡遗漏/
|
|
80
|
+
* 截断(漏跑 lint、输出被截断少命令),不能挡对抗性自我误标(把 echo ok 标 kind:'test')。
|
|
81
|
+
* 后者残留与 dist_not_built 校验同层级,由人工 GATE_VERIFY + Codex 对抗审查兜底。
|
|
82
|
+
*
|
|
83
|
+
* **类型守卫(Codex C-3)**:requiredKinds/cmd.kind 可能畸形(payload 由编排器构造、report 由
|
|
84
|
+
* 可错 LLM 产出)。非数组 requiredKinds → 视同 [];非字符串元素/非字符串 cmd.kind → 过滤、不贡献,
|
|
85
|
+
* 绝不 .toLowerCase() 崩——decideStop 须 total/stable,崩溃会破坏"稳定转 GATE"的承诺。
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} report - parseReport 解析后的有效 report 对象(非降级态、verify_mode='full')
|
|
88
|
+
* @param {string[]} requiredKinds - 期望必须出现的 kind 枚举数组,来自 config.full_required_kinds
|
|
89
|
+
* @returns {{ complete: boolean, missing: string[] }}
|
|
90
|
+
*/
|
|
91
|
+
export function validateFullCommandKinds(report, requiredKinds) {
|
|
92
|
+
// kind 归一化(Codex Phase3 W-3):trim + 小写——容忍 LLM 产出的 ' build ' 等前后空白,
|
|
93
|
+
// 否则 ' build '.toLowerCase() !== 'build' 会把合法命令误判缺失 → false INCOMPLETE_FULL_VERIFY。
|
|
94
|
+
const normKind = (k) => k.trim().toLowerCase();
|
|
95
|
+
// requiredKinds 类型守卫(C-3):非数组→[];仅取字符串元素,归一化、去重
|
|
96
|
+
const required = new Set(
|
|
97
|
+
(Array.isArray(requiredKinds) ? requiredKinds : [])
|
|
98
|
+
.filter((k) => typeof k === 'string')
|
|
99
|
+
.map(normKind),
|
|
100
|
+
);
|
|
101
|
+
// 空期望集 → 直接通过(优雅降级,AC-4),不读任何 report 字段
|
|
102
|
+
if (required.size === 0) return { complete: true, missing: [] };
|
|
103
|
+
|
|
104
|
+
// PASS 命令的 kind 集合:仅取 status===PASS 且 kind 为字符串的命令
|
|
105
|
+
// - FAIL/SKIPPED/UNKNOWN 不计入(防 reward-hacking:FAIL 命令不能"代缴"完整性)
|
|
106
|
+
// - 非字符串 kind(123/null/对象)不贡献、绝不 .toLowerCase() 崩(C-3)
|
|
107
|
+
const passKinds = new Set(
|
|
108
|
+
(report && Array.isArray(report.layer2_commands) ? report.layer2_commands : [])
|
|
109
|
+
.filter((cmd) => classifyCommand(cmd) === 'PASS' && typeof cmd.kind === 'string')
|
|
110
|
+
.map((cmd) => normKind(cmd.kind)),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const missing = [...required].filter((k) => !passKinds.has(k));
|
|
114
|
+
return { complete: missing.length === 0, missing };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 判定 smoke 报告是否满足 escalate_full 条件(非权威触发,F203 缺陷 2)
|
|
119
|
+
*
|
|
120
|
+
* 与权威门禁 evaluateMetric 的区别:smoke 轮跑全量 vitest 但不先 build,含 build 依赖的 e2e
|
|
121
|
+
* 会被源头标 SKIPPED。evaluateMetric 严格要求全量 PASS(SKIPPED 即不达标),smoke 永不达标。
|
|
122
|
+
* evaluateSmokeReadiness 放宽——允许 SKIPPED,只要非 SKIPPED 命令全 PASS 且至少有一条非 SKIPPED
|
|
123
|
+
* 命令(vacuous-truth 防护)。满足即触发 escalate_full(升级 full verify 重判,绝非直接 REACHED_GOAL)。
|
|
124
|
+
* @param {Object} report
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
export function evaluateSmokeReadiness(report) {
|
|
128
|
+
if (!report || !Array.isArray(report.layer2_commands)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// 条件 1:P1 FR 覆盖率 100%
|
|
132
|
+
const cov = report.layer1_fr_coverage;
|
|
133
|
+
if (!cov || cov.p1_coverage_pct !== 100) return false;
|
|
134
|
+
// 条件 2:Layer 1.5 证据 COMPLIANT
|
|
135
|
+
const evidence = report.layer1_5_evidence;
|
|
136
|
+
if (!evidence || evidence.status !== 'COMPLIANT') return false;
|
|
137
|
+
// 条件 3 + vacuous 防护:取非 SKIPPED 命令,要求 ≥1 条且全 PASS
|
|
138
|
+
// UNKNOWN(非 SKIPPED 且非 PASS)即不满足;FAIL 即不满足。
|
|
139
|
+
const nonSkipped = report.layer2_commands.filter((cmd) => classifyCommand(cmd) !== 'SKIPPED');
|
|
140
|
+
if (nonSkipped.length === 0) return false; // 全 SKIPPED → vacuous,不放行(C3)
|
|
141
|
+
return nonSkipped.every((cmd) => classifyCommand(cmd) === 'PASS');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 从单行 porcelain v1 输出提取受影响路径集合(F203 修订 #1,DRY helper)
|
|
146
|
+
*
|
|
147
|
+
* porcelain v1 固定列:前两字符 XY(X=index 列,Y=工作区列)+ 空格 + 路径。
|
|
148
|
+
* 由 parsePreservedConfigStates 与 isCleanExcludingPreserved 共用,统一行→路径解析。
|
|
149
|
+
* - `??`(untracked)→ unquote(rest)
|
|
150
|
+
* - rename/copy(X='R'/'C')→ 两端路径(from + to)
|
|
151
|
+
* - 其余 → unquote(rest)(单路径)
|
|
152
|
+
* 空行返回空数组。
|
|
153
|
+
* @param {string} line - 单行 porcelain 文本(不含换行符)
|
|
154
|
+
* @returns {string[]}
|
|
155
|
+
*/
|
|
156
|
+
function extractPorcelainPaths(line) {
|
|
157
|
+
if (!line || line.length === 0) return [];
|
|
158
|
+
const xy = line.slice(0, 2);
|
|
159
|
+
const rest = line.slice(3);
|
|
160
|
+
const x = xy[0];
|
|
161
|
+
if (xy === '??') {
|
|
162
|
+
return [unquotePorcelainPath(rest)];
|
|
163
|
+
}
|
|
164
|
+
if (x === 'R' || x === 'C') {
|
|
165
|
+
const { from, to } = splitRenameArrow(rest);
|
|
166
|
+
return [unquotePorcelainPath(from), unquotePorcelainPath(to)];
|
|
167
|
+
}
|
|
168
|
+
return [unquotePorcelainPath(rest)];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 判定"排除 preserved 路径后"工作区是否干净(F203 修订 #1 / CRITICAL-7)
|
|
173
|
+
*
|
|
174
|
+
* 危险场景:工作区唯一 dirty 是 untracked preserved override 时,若按全仓 porcelain 判
|
|
175
|
+
* isClean=false,则 planSnapshotCommands(false) 的 stash push 排除了 override → "没有要保存的
|
|
176
|
+
* 本地修改"(无新 stash)→ 随后 `git rev-parse stash@{0}` 抓到**仓库里已有的无关旧 stash** →
|
|
177
|
+
* `stash apply --index` 套用无关改动污染工作区。修复:isClean 必须排除 preserved 后再判。
|
|
178
|
+
*
|
|
179
|
+
* 语义:解析**全仓** porcelain 文本,逐行提取路径——若存在任一行其路径**不全属于** preservedPaths
|
|
180
|
+
* (rename 两端只要有一端非 preserved 即算非 preserved 变更)→ 返回 false;所有 dirty 行都是
|
|
181
|
+
* preserved(或无 dirty 行)→ true。
|
|
182
|
+
*
|
|
183
|
+
* 输入契约:porcelainText MUST 来自 `git status --porcelain --untracked-files=all`。默认 porcelain
|
|
184
|
+
* (无 -uall)会把整个 untracked 目录折叠成单行 `?? .specify/`(而非展开到 `?? .specify/orchestration-overrides.yaml`);
|
|
185
|
+
* 折叠形式下 `.specify/` ≠ preserved 文件路径 → 被判为非 preserved 变更 → 误判 false(CRITICAL-7 漏网根因)。
|
|
186
|
+
* 本函数对折叠输入保守判脏(返回 false)——这是正确的防御性行为,但前提是调用方喂入 -uall 展开形式才能拿到 true。
|
|
187
|
+
* @param {string} porcelainText - 全仓 `git status --porcelain --untracked-files=all` 原始 stdout
|
|
188
|
+
* @param {string[]} preservedPaths - 视为可忽略的保留路径(默认 PRESERVED_CONFIG_PATHSPECS)
|
|
189
|
+
* @returns {boolean}
|
|
190
|
+
*/
|
|
191
|
+
export function isCleanExcludingPreserved(porcelainText, preservedPaths = PRESERVED_CONFIG_PATHSPECS) {
|
|
192
|
+
const text = typeof porcelainText === 'string' ? porcelainText : '';
|
|
193
|
+
const preserved = new Set(preservedPaths || []);
|
|
194
|
+
for (const rawLine of text.split('\n')) {
|
|
195
|
+
if (rawLine.length === 0) continue;
|
|
196
|
+
const paths = extractPorcelainPaths(rawLine);
|
|
197
|
+
if (paths.length === 0) continue;
|
|
198
|
+
// 该行只要有任一路径不属于 preserved → 存在非 preserved 变更 → 不干净
|
|
199
|
+
if (paths.some((p) => !preserved.has(p))) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 解析 `git status --porcelain -- <paths>` 文本为 preserved config 状态数组(F203 精化 #2)
|
|
208
|
+
*
|
|
209
|
+
* porcelain v1 固定列:前两字符 XY(X=index 列,Y=工作区列)+ 空格 + 路径。
|
|
210
|
+
* 把脆弱的文本解析放进可单测 core,SKILL.md 散文只负责跑命令并把原文管道给 CLI,不自行解析。
|
|
211
|
+
* @param {string} porcelainText - git status --porcelain 原始 stdout
|
|
212
|
+
* @param {string[]} preservedPaths - 需检查的路径(如 PRESERVED_CONFIG_PATHSPECS)
|
|
213
|
+
* @returns {{ path: string, state: 'absent'|'untracked'|'tracked-clean'|'tracked-modified'|'staged' }[]}
|
|
214
|
+
*/
|
|
215
|
+
export function parsePreservedConfigStates(porcelainText, preservedPaths) {
|
|
216
|
+
const text = typeof porcelainText === 'string' ? porcelainText : '';
|
|
217
|
+
// path → 最强状态(staged > tracked-modified > untracked),同一路径多行时取最不安全的
|
|
218
|
+
const byPath = new Map();
|
|
219
|
+
const rank = { untracked: 1, 'tracked-modified': 2, staged: 3 };
|
|
220
|
+
const assign = (p, state) => {
|
|
221
|
+
if (!p) return;
|
|
222
|
+
const prev = byPath.get(p);
|
|
223
|
+
if (!prev || rank[state] > rank[prev]) byPath.set(p, state);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (const rawLine of text.split('\n')) {
|
|
227
|
+
if (rawLine.length === 0) continue;
|
|
228
|
+
// porcelain v1:第 0-1 字符为 XY 状态码,第 2 字符为空格,其后为路径。
|
|
229
|
+
// 路径提取复用 extractPorcelainPaths(DRY,与 isCleanExcludingPreserved 同源);
|
|
230
|
+
// 本函数额外按 XY 列推断 state(untracked / staged / tracked-modified)。
|
|
231
|
+
const xy = rawLine.slice(0, 2);
|
|
232
|
+
const x = xy[0];
|
|
233
|
+
const y = xy[1];
|
|
234
|
+
const paths = extractPorcelainPaths(rawLine);
|
|
235
|
+
|
|
236
|
+
if (xy === '??') {
|
|
237
|
+
assign(paths[0], 'untracked');
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
// rename/copy 行:`R old -> new`(X='R'/'C')—— 两端路径命中任一即归类 staged
|
|
241
|
+
if (x === 'R' || x === 'C') {
|
|
242
|
+
for (const p of paths) assign(p, 'staged');
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const p = paths[0];
|
|
246
|
+
// index 列(X)非空非空格 → staged(含 M /A /MM/AM 等,index 有暂存即 staged 优先)
|
|
247
|
+
if (x && x !== ' ' && x !== '?') {
|
|
248
|
+
assign(p, 'staged');
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// 仅工作区列(Y)非空、index 列为空格 → tracked-modified(如 " M"/" D")
|
|
252
|
+
if (y && y !== ' ') {
|
|
253
|
+
assign(p, 'tracked-modified');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 对每个 preserved path:命中则用解析出的 state,未命中则 absent(含 tracked-clean,统一按安全归类)
|
|
258
|
+
return (preservedPaths || []).map((path) => ({
|
|
259
|
+
path,
|
|
260
|
+
state: byPath.get(path) ?? 'absent',
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 拆分 porcelain rename/copy 行的 `old -> new` 两端(F203 修订 #3)
|
|
266
|
+
* @param {string} rest - 状态码之后的内容
|
|
267
|
+
* @returns {{ from: string, to: string }}
|
|
268
|
+
*/
|
|
269
|
+
function splitRenameArrow(rest) {
|
|
270
|
+
const idx = rest.indexOf(' -> ');
|
|
271
|
+
if (idx === -1) {
|
|
272
|
+
return { from: rest, to: rest };
|
|
273
|
+
}
|
|
274
|
+
return { from: rest.slice(0, idx), to: rest.slice(idx + 4) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 去除 porcelain 对含特殊字符路径加的双引号 + C-style 转义(F203 修订 #3)
|
|
279
|
+
*
|
|
280
|
+
* porcelain 对含空格/特殊字符的路径用双引号包裹并做 C-style 转义。我们的 preserved path 是简单
|
|
281
|
+
* ASCII 不会被引号化,但 parser 须健壮处理引号包裹形式,避免误判 absent。
|
|
282
|
+
* @param {string} raw
|
|
283
|
+
* @returns {string}
|
|
284
|
+
*/
|
|
285
|
+
function unquotePorcelainPath(raw) {
|
|
286
|
+
const s = (raw || '').trim();
|
|
287
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
288
|
+
const inner = s.slice(1, -1);
|
|
289
|
+
// 反转义常见 C-style 序列(\" \\ \t \n 等);八进制 \nnn 转回字节
|
|
290
|
+
return inner
|
|
291
|
+
.replace(/\\([\\"abfnrtv])/g, (_, c) => {
|
|
292
|
+
const map = { '\\': '\\', '"': '"', a: '\x07', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v' };
|
|
293
|
+
return map[c] ?? c;
|
|
294
|
+
})
|
|
295
|
+
.replace(/\\([0-7]{1,3})/g, (_, oct) => String.fromCharCode(parseInt(oct, 8)));
|
|
296
|
+
}
|
|
297
|
+
return s;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 检查 preserved config 路径是否处于安全状态(preflight 守护,F203 缺陷 1)
|
|
302
|
+
*
|
|
303
|
+
* pathspec 排除只保护 untracked 的 preserved config;staged/tracked-modified 态会被
|
|
304
|
+
* `git reset --hard` 摧毁,pathspec 拦不住,故由本 preflight 提前拦截(硬失败优于静默数据丢失)。
|
|
305
|
+
* @param {{ path: string, state: 'absent'|'untracked'|'tracked-clean'|'tracked-modified'|'staged' }[]} entries
|
|
306
|
+
* 由编排器调 `git status --porcelain -- <paths>` 经 parsePreservedConfigStates 解析后传入
|
|
307
|
+
* @returns {{ safe: boolean, unsafe: { path: string, state: string, reason: string }[] }}
|
|
308
|
+
*/
|
|
309
|
+
export function assessPreservedConfigSafety(entries) {
|
|
310
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
311
|
+
const UNSAFE_STATES = new Set(['staged', 'tracked-modified']);
|
|
312
|
+
const unsafe = [];
|
|
313
|
+
for (const entry of list) {
|
|
314
|
+
if (entry && UNSAFE_STATES.has(entry.state)) {
|
|
315
|
+
unsafe.push({
|
|
316
|
+
path: entry.path,
|
|
317
|
+
state: entry.state,
|
|
318
|
+
reason: `preserved config 处于 ${entry.state} 态,会被 git reset --hard HEAD 摧毁;goal_loop 期望其 untracked,中止防数据丢失`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { safe: unsafe.length === 0, unsafe };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 检测回归——只比较 verify_mode 相同的命令(FR-013)
|
|
327
|
+
* @param {Object|null} prevReport
|
|
328
|
+
* @param {Object} curReport
|
|
329
|
+
* @returns {{ regression: boolean, commands: string[] }}
|
|
330
|
+
*/
|
|
331
|
+
export function detectRegression(prevReport, curReport) {
|
|
332
|
+
// 第一轮无前轮 → 无回归
|
|
333
|
+
if (prevReport === null || prevReport === undefined) {
|
|
334
|
+
return { regression: false, commands: [] };
|
|
335
|
+
}
|
|
336
|
+
// 核心约束:只比较 verify_mode 相同的命令(smoke↔smoke / full↔full)
|
|
337
|
+
// 跨模式(如上轮 smoke、本轮 full)绝不比较,避免把"smoke 未跑 lint"误判成 regression
|
|
338
|
+
if (prevReport.verify_mode !== curReport.verify_mode) {
|
|
339
|
+
return { regression: false, commands: [] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 上轮 PASS 的命令名集合
|
|
343
|
+
const prevPass = new Set(
|
|
344
|
+
(prevReport.layer2_commands || [])
|
|
345
|
+
.filter((c) => classifyCommand(c) === 'PASS')
|
|
346
|
+
.map((c) => c.name),
|
|
347
|
+
);
|
|
348
|
+
// 本轮 FAIL 但上轮 PASS 的命令 = regression
|
|
349
|
+
const regressed = (curReport.layer2_commands || [])
|
|
350
|
+
.filter((c) => classifyCommand(c) === 'FAIL' && prevPass.has(c.name))
|
|
351
|
+
.map((c) => c.name);
|
|
352
|
+
|
|
353
|
+
return { regression: regressed.length > 0, commands: regressed };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 计算五维 delta 向量 + hasProgress(FR-006)
|
|
358
|
+
* @param {Object|null} prevReport
|
|
359
|
+
* @param {Object} curReport
|
|
360
|
+
* @returns {{ delta: number[], hasProgress: boolean }}
|
|
361
|
+
*/
|
|
362
|
+
export function computeDelta(prevReport, curReport) {
|
|
363
|
+
const cur = curReport.delta_inputs || {};
|
|
364
|
+
// prevReport=null(第一轮)→ 以零基线比较(前值视为 0),net_loc 取本轮绝对净变更
|
|
365
|
+
const prev = (prevReport && prevReport.delta_inputs) || {};
|
|
366
|
+
const prevVal = (k) => (prevReport ? prev[k] ?? 0 : 0);
|
|
367
|
+
|
|
368
|
+
// d1 layer2_pass_count、d2 p1_fr_coverage_pct、d3 layer1_5_status_score、d4 regression_count(负=改善)
|
|
369
|
+
const d1 = (cur.layer2_pass_count ?? 0) - prevVal('layer2_pass_count');
|
|
370
|
+
const d2 = (cur.p1_fr_coverage_pct ?? 0) - prevVal('p1_fr_coverage_pct');
|
|
371
|
+
const d3 = (cur.layer1_5_status_score ?? 0) - prevVal('layer1_5_status_score');
|
|
372
|
+
const d4 = (cur.regression_count ?? 0) - prevVal('regression_count');
|
|
373
|
+
// d5 = 本轮净 LOC 变更(绝对值,非差分):仅作日志/可观测信号,**不计入 hasProgress**。
|
|
374
|
+
// 关键修正(Codex C1):若把 net_loc churn 计入 hasProgress,则每轮改代码 → d5≠0 → hasProgress
|
|
375
|
+
// 恒 true → NO_PROGRESS 早停 fallback 永不触发(死逻辑)。真实"无进展"应只看 metric 改善方向。
|
|
376
|
+
const d5 = cur.net_loc_delta ?? 0;
|
|
377
|
+
|
|
378
|
+
const delta = [d1, d2, d3, d4, d5];
|
|
379
|
+
// hasProgress:只看四个 metric 维度的"改善方向"——
|
|
380
|
+
// d1 PASS 数增(>0)、d2 P1 覆盖增(>0)、d3 证据分增(>0)、d4 回归数减(<0)。
|
|
381
|
+
// 任一维朝好的方向变化即视为有进展;d5(LOC churn)不参与判定。
|
|
382
|
+
const hasProgress = d1 > 0 || d2 > 0 || d3 > 0 || d4 < 0;
|
|
383
|
+
return { delta, hasProgress };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 每轮 verify 后按固定优先级判定停止/处置(FR-004)
|
|
388
|
+
* @param {{ report: Object, round: number, config: Object, prevReports: Object[], rollbackResult: ({ success: boolean }|null) }} params
|
|
389
|
+
* @returns {{ stop: boolean, exit_reason: string|null, action: string }}
|
|
390
|
+
*/
|
|
391
|
+
export function decideStop({ report, round, config, prevReports, rollbackResult }) {
|
|
392
|
+
const history = Array.isArray(prevReports) ? prevReports : [];
|
|
393
|
+
const maxIterations = config.max_iterations;
|
|
394
|
+
const noProgressMax = config.no_progress_max_rounds ?? 2;
|
|
395
|
+
|
|
396
|
+
// 优先级 1:回滚失败(最高优先,FR-014)
|
|
397
|
+
if (rollbackResult !== null && rollbackResult !== undefined && !rollbackResult.success) {
|
|
398
|
+
return { stop: true, exit_reason: 'ROLLBACK_FAILED', action: 'goto_gate_verify' };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 本轮是否为 infra-failure(report 降级标记),degraded 报告不参与 regression/达标判定
|
|
402
|
+
const isDegraded = report && report.degraded === 'infra-failure';
|
|
403
|
+
|
|
404
|
+
// 优先级 2:regression(内部自调 detectRegression,不信任外部传入/report 自带字段;FR-013)
|
|
405
|
+
// 修正(Codex W1):回归必须与"上一个 verify_mode 相同的轮次"比较,而非固定取最后一轮。
|
|
406
|
+
// full→smoke→full 的 FAIL 序列里,若拿当前 full 与中间那轮 smoke 比,会因跨模式分桶被漏判。
|
|
407
|
+
if (!isDegraded) {
|
|
408
|
+
const prevSameMode = findPrevSameModeReport(history, report);
|
|
409
|
+
const reg = detectRegression(prevSameMode, report);
|
|
410
|
+
if (reg.regression) {
|
|
411
|
+
// 预算耗尽(已到末轮)则 stop,否则视剩余预算 continue 回滚后重试
|
|
412
|
+
const budgetExhausted = round >= maxIterations;
|
|
413
|
+
return {
|
|
414
|
+
stop: budgetExhausted,
|
|
415
|
+
exit_reason: 'REGRESSION_ROLLBACK',
|
|
416
|
+
action: 'rollback',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 优先级 3:达标(无论是否末轮,达标优先于 max_iterations;FR-004 同轮冲突规则)
|
|
422
|
+
// 修正(Codex C2 / plan §OQ-03):达标退出前必须经过一次 full verify。
|
|
423
|
+
// - full 轮 metric 满足 → REACHED_GOAL(真正退出)
|
|
424
|
+
// - smoke 轮 metric 满足 → 不得 REACHED_GOAL;返回 escalate_full 信号,
|
|
425
|
+
// 由编排器升级到 full verify 重判(tasks T022 步骤 6c)。堵死"smoke 假达标"。
|
|
426
|
+
//
|
|
427
|
+
// **escalate 非递归不变量(Codex C1,最高风险)**:escalate_full 只在
|
|
428
|
+
// `verify_mode !== 'full'` 时返回。因此一旦 forced full verify 产出 verify_mode==='full'
|
|
429
|
+
// 的报告且 metric 满足,本函数**必然**走 REACHED_GOAL 分支,**永不**再次返回 escalate_full
|
|
430
|
+
// —— 从纯函数层面就不存在"full 报告 → escalate_full"的路径,无法递归升级。
|
|
431
|
+
// 散文层(SKILL.md 步骤 6c)另有两道兜底:
|
|
432
|
+
// (1) forced full 后先校验 curReportFull.verify_mode === 'full',否则按 infra-failure 转 GATE_VERIFY;
|
|
433
|
+
// (2) 重 decide 后若仍意外得到 escalate_full(契约违反),MUST NOT 再升级,直接转 GATE_VERIFY。
|
|
434
|
+
//
|
|
435
|
+
// **干净结构(F203 精化 #1)**:full→只调 evaluateMetric;smoke→只调 evaluateSmokeReadiness。
|
|
436
|
+
// full 权威门禁严格(全量 PASS + p1=100 + COMPLIANT,SKIPPED 即不达标,evaluateMetric 不放宽);
|
|
437
|
+
// smoke 用 evaluateSmokeReadiness(允许 SKIPPED,非 SKIPPED 全 PASS 且 ≥1 非 SKIPPED)触发 escalate。
|
|
438
|
+
if (!isDegraded) {
|
|
439
|
+
if (report.verify_mode === 'full') {
|
|
440
|
+
if (evaluateMetric(report)) {
|
|
441
|
+
// F204:metric 满足后、REACHED_GOAL 之前校验命令集完整性(W-1:之后不是之前)。
|
|
442
|
+
// requiredKinds 从 config 读(命令名/类别由项目配置,core 零硬编码命令名);缺省/空 → 跳过。
|
|
443
|
+
const requiredKinds = (config && config.full_required_kinds) || [];
|
|
444
|
+
const kindCheck = validateFullCommandKinds(report, requiredKinds);
|
|
445
|
+
if (!kindCheck.complete) {
|
|
446
|
+
// 命令集缺必需 kind → 拒绝认证,交人工 GATE_VERIFY(fail-loud)。
|
|
447
|
+
// 语义差异:REACHED_GOAL 是机器确认达标;INCOMPLETE_FULL_VERIFY 是"metric 满足但命令集
|
|
448
|
+
// 缺必需类别",由人工决定是否接受。不复用 infra-failure(那会计入 NO_PROGRESS 迭代)。
|
|
449
|
+
return { stop: true, exit_reason: 'INCOMPLETE_FULL_VERIFY', action: 'goto_gate_verify' };
|
|
450
|
+
}
|
|
451
|
+
return { stop: true, exit_reason: 'REACHED_GOAL', action: 'goto_gate_verify' };
|
|
452
|
+
}
|
|
453
|
+
// full 未达标 → 落入后续优先级(绝不 escalate,C1 不变量从结构层面保证)
|
|
454
|
+
} else if (report.verify_mode === 'smoke') {
|
|
455
|
+
// smoke 分支(C1 不变量:escalate_full 仅此路径可能返回)。
|
|
456
|
+
// 防御纵深(F203 修订 #2 / WARNING-1):收紧为显式 'smoke' —— 非 full 非 smoke 的报告
|
|
457
|
+
// 已被 parseReport 降级为 infra-failure,但此处再设一道防线,绝不让非法 verify_mode escalate。
|
|
458
|
+
if (evaluateSmokeReadiness(report)) {
|
|
459
|
+
return { stop: false, exit_reason: null, action: 'escalate_full' };
|
|
460
|
+
}
|
|
461
|
+
// smoke 未满足 → 落入后续优先级
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 优先级 4:达到最大迭代轮数
|
|
466
|
+
if (round >= maxIterations) {
|
|
467
|
+
return { stop: true, exit_reason: 'MAX_ITERATIONS', action: 'goto_gate_verify' };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 优先级 5:infra-failure 或连续 no_progress_max_rounds 轮无进展 → NO_PROGRESS
|
|
471
|
+
if (countConsecutiveNoProgress(report, history) >= noProgressMax) {
|
|
472
|
+
return { stop: true, exit_reason: 'NO_PROGRESS', action: 'goto_gate_verify' };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 优先级 6:继续
|
|
476
|
+
return { stop: false, exit_reason: null, action: 'continue' };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 在历史 report 中从后向前找 verify_mode 与本轮相同的最近一条(Codex W1)
|
|
481
|
+
*
|
|
482
|
+
* regression 只在同模式分桶比较。decideStop 必须把"上一个同模式轮次"传给 detectRegression,
|
|
483
|
+
* 否则 full→smoke→full FAIL 序列里,当前 full 与中间 smoke 跨桶比较被漏判。
|
|
484
|
+
* degraded 报告(无 verify_mode 的 infra-failure)不参与同模式匹配,直接跳过。
|
|
485
|
+
*
|
|
486
|
+
* @param {Object[]} history - 历史 report(旧→新顺序)
|
|
487
|
+
* @param {Object} curReport
|
|
488
|
+
* @returns {Object|null}
|
|
489
|
+
*/
|
|
490
|
+
function findPrevSameModeReport(history, curReport) {
|
|
491
|
+
const mode = curReport && curReport.verify_mode;
|
|
492
|
+
if (!mode) return null;
|
|
493
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
494
|
+
const r = history[i];
|
|
495
|
+
if (r && r.degraded === 'infra-failure') continue;
|
|
496
|
+
if (r && r.verify_mode === mode) return r;
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 计算"含本轮在内、最近连续无进展轮数"
|
|
503
|
+
*
|
|
504
|
+
* 语义(与 no_progress_max_rounds 阈值对齐):从最新一轮起向前回溯,统计处于
|
|
505
|
+
* "卡住"状态的连续轮次数。一轮卡住的判定:
|
|
506
|
+
* - 本轮为 infra-failure(report.degraded)→ 卡住
|
|
507
|
+
* - 本轮相对**上一轮**无进展(computeDelta(prev, cur) 五维全 0)→ 本轮与上一轮均计入卡住
|
|
508
|
+
* 即两轮 delta_inputs 完全相同时,这两轮都视为"无进展"(共 2 轮),以匹配
|
|
509
|
+
* "连续 N 轮无进展即早停"的直觉(N=2 → 两轮指标无变化即触发)。
|
|
510
|
+
*
|
|
511
|
+
* @param {Object} curReport
|
|
512
|
+
* @param {Object[]} history - 历史 report(旧→新顺序)
|
|
513
|
+
* @returns {number}
|
|
514
|
+
*/
|
|
515
|
+
function countConsecutiveNoProgress(curReport, history) {
|
|
516
|
+
// 时间序列(旧→新)
|
|
517
|
+
const series = [...history, curReport];
|
|
518
|
+
// 单轮是否"卡住":infra-failure 直接卡住;否则看相对上一轮是否无进展。
|
|
519
|
+
const isStuck = (idx) => {
|
|
520
|
+
const cur = series[idx];
|
|
521
|
+
if (cur && cur.degraded === 'infra-failure') return true;
|
|
522
|
+
const prev = idx > 0 ? series[idx - 1] : null;
|
|
523
|
+
if (prev === null) return false; // 首轮无基准,不单独判卡住
|
|
524
|
+
return !computeDelta(prev, cur).hasProgress;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// 从最新一轮起回溯统计停滞段长度(以"轮"为单位)。
|
|
528
|
+
// 当某轮相对上一轮无进展时,该轮与其比较基准上一轮共同构成停滞段:
|
|
529
|
+
// 故一段连续相同指标的 K 个 transition 跨 K+1 轮。
|
|
530
|
+
let consecutive = 0;
|
|
531
|
+
for (let i = series.length - 1; i >= 0; i--) {
|
|
532
|
+
if (isStuck(i)) {
|
|
533
|
+
consecutive += 1;
|
|
534
|
+
// 若上一轮是序列首轮且本轮相对它无进展,把首轮也计入停滞段
|
|
535
|
+
if (i - 1 === 0 && !(series[0] && series[0].degraded === 'infra-failure')) {
|
|
536
|
+
consecutive += 1;
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return consecutive;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* 判定 implement phase 分派策略(FR-017)
|
|
547
|
+
* @param {string} phaseId
|
|
548
|
+
* @param {string} agentMode
|
|
549
|
+
* @returns {{ dispatch: string, warning?: string }}
|
|
550
|
+
*/
|
|
551
|
+
export function decideDispatch(phaseId, agentMode) {
|
|
552
|
+
if (agentMode === 'goal_loop') {
|
|
553
|
+
if (phaseId === 'implement') {
|
|
554
|
+
return { dispatch: 'goal_loop' };
|
|
555
|
+
}
|
|
556
|
+
// goal_loop 误配在非 implement phase → 降级 single + warning(FR-017)
|
|
557
|
+
return {
|
|
558
|
+
dispatch: 'single',
|
|
559
|
+
warning: `agent_mode=goal_loop 仅在 implement phase 生效,phase="${phaseId}" 降级为 single`,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
// 其他模式透传
|
|
563
|
+
return { dispatch: agentMode };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 按轮次选择 verify 模式(FR-007)
|
|
568
|
+
* @param {number} round
|
|
569
|
+
* @param {number} maxIterations
|
|
570
|
+
* @param {boolean} aboutToExit
|
|
571
|
+
* @returns {'smoke'|'full'}
|
|
572
|
+
*/
|
|
573
|
+
export function selectVerifyMode(round, maxIterations, aboutToExit) {
|
|
574
|
+
// 最后一轮 或 达标退出前(aboutToExit)→ full(堵死 smoke 假达标)
|
|
575
|
+
if (aboutToExit || round >= maxIterations) {
|
|
576
|
+
return 'full';
|
|
577
|
+
}
|
|
578
|
+
// 第 1 至 (N-1) 轮 → smoke(tsc --noEmit + vitest,快速反馈)
|
|
579
|
+
return 'smoke';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 规划建立 snapshot 的 git 命令序列(FR-013)
|
|
584
|
+
* @param {boolean} isClean
|
|
585
|
+
* @param {string[]} preservedPaths - 跨快照保留、不入 stash 的路径(F203 缺陷 1,默认常量;
|
|
586
|
+
* 多 path 展开为多个独立 ':(exclude)<p>' token,禁止 join 成单字符串)
|
|
587
|
+
* @returns {string[]}
|
|
588
|
+
*/
|
|
589
|
+
export function planSnapshotCommands(isClean, preservedPaths = PRESERVED_CONFIG_PATHSPECS) {
|
|
590
|
+
// 干净工作区:无 stash entry,锚点 = HEAD,无需任何命令
|
|
591
|
+
if (isClean) {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
// F203 修订 #3(WARNING-5):preserved path 会被原样拼进 `'...'` 引号的 shell 命令,
|
|
595
|
+
// 含单引号会破坏引号闭合形成注入面。与 isValidGitSha 防注入风格一致,含单引号即拒绝。
|
|
596
|
+
assertNoSingleQuote(preservedPaths);
|
|
597
|
+
// 非干净:全量捕获 tracked+staged+untracked → 捕获 SHA → 立即原样还原继续本轮
|
|
598
|
+
// {i} / {stash_ref} 为占位符,编排器执行时替换
|
|
599
|
+
// F203:每个 preserved path 展开为独立 ':(exclude)<p>' pathspec token(多 path 不 join),
|
|
600
|
+
// 使 untracked preserved config 不被 stash push -u 卷走。
|
|
601
|
+
const excludeTokens = (preservedPaths || []).map((p) => `':(exclude)${p}'`);
|
|
602
|
+
const stashPush = ['git stash push --include-untracked -m "goal_loop-S{i}" -- .', ...excludeTokens].join(' ');
|
|
603
|
+
return [
|
|
604
|
+
stashPush,
|
|
605
|
+
'git rev-parse stash@{0}',
|
|
606
|
+
'git stash apply --index {stash_ref}',
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* 规划回滚到 S_i 的 git 命令序列(FR-013)
|
|
612
|
+
* @param {{ clean: boolean, ref: string }} S_i
|
|
613
|
+
* @param {string[]} preservedPaths - 跨快照保留、不被 clean 删除的路径(F203 缺陷 1,默认常量;
|
|
614
|
+
* 多 path 展开为多个独立 `-e <p>` token,禁止 join)
|
|
615
|
+
* @returns {string[]}
|
|
616
|
+
*/
|
|
617
|
+
export function planRollbackCommands(S_i, preservedPaths = PRESERVED_CONFIG_PATHSPECS) {
|
|
618
|
+
// F203 修订 #3(WARNING-5):preserved path 拼入 `-e '<p>'` 引号,含单引号会破坏引号闭合。
|
|
619
|
+
assertNoSingleQuote(preservedPaths);
|
|
620
|
+
// base:复位 tracked+index 到 HEAD + 删全部 untracked
|
|
621
|
+
// git clean -fd 安全性:不带 -x(保留 .gitignore 文件)、单 -f 非 -ff(拒删嵌套 git 仓库)
|
|
622
|
+
// F203:每个 preserved path 展开为独立 `-e '<p>'` token(多 path 不 join),保护 untracked preserved config。
|
|
623
|
+
const excludeTokens = (preservedPaths || []).map((p) => `-e '${p}'`);
|
|
624
|
+
const cleanCmd = ['git clean -fd', ...excludeTokens].join(' ');
|
|
625
|
+
const base = ['git reset --hard HEAD', cleanCmd];
|
|
626
|
+
if (S_i && S_i.clean) {
|
|
627
|
+
// 干净基线:reset + clean 即回到 S_i
|
|
628
|
+
return base;
|
|
629
|
+
}
|
|
630
|
+
// 非干净:全量还原 S_i(含既有 untracked,stash 已捕获)
|
|
631
|
+
// 安全(Codex W2):S_i.ref 会被原样拼进 shell 命令字符串,必须先校验为 40 位 hex SHA-1,
|
|
632
|
+
// 否则恶意/损坏的 ref(如 "x; rm -rf /")会形成命令注入面。非法 ref → 抛错,由编排器标回滚失败。
|
|
633
|
+
if (!isValidGitSha(S_i.ref)) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`非法 snapshot ref(非 40 位 hex SHA): ${JSON.stringify(S_i.ref)},拒绝拼入回滚命令`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return [...base, `git stash apply --index ${S_i.ref}`];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* 校验是否为 40 位十六进制 git SHA-1(FR-013 安全防线,Codex W2)
|
|
643
|
+
* @param {unknown} ref
|
|
644
|
+
* @returns {boolean}
|
|
645
|
+
*/
|
|
646
|
+
function isValidGitSha(ref) {
|
|
647
|
+
return typeof ref === 'string' && /^[0-9a-f]{40}$/.test(ref);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* 校验 preserved path 列表不含单引号(F203 修订 #3 / WARNING-5 防注入)
|
|
652
|
+
*
|
|
653
|
+
* 默认常量安全,但 injectable 第二参可能传入含单引号的路径,会破坏 planSnapshot/planRollback
|
|
654
|
+
* 生成命令里的 `'...'` 引号闭合形成 shell 注入面。含单引号即抛错,拒绝拼入命令。
|
|
655
|
+
* @param {string[]} preservedPaths
|
|
656
|
+
*/
|
|
657
|
+
function assertNoSingleQuote(preservedPaths) {
|
|
658
|
+
for (const p of preservedPaths || []) {
|
|
659
|
+
if (typeof p === 'string' && p.includes("'")) {
|
|
660
|
+
throw new Error('preserved path 含非法字符(单引号),拒绝拼入 shell 命令');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* 解析 verify 子代理产出的 JSON 文本(FR-010)
|
|
667
|
+
* @param {string} jsonText
|
|
668
|
+
* @returns {{ report: Object }|{ degraded: 'infra-failure', reason: string }}
|
|
669
|
+
*/
|
|
670
|
+
export function parseReport(jsonText) {
|
|
671
|
+
let report;
|
|
672
|
+
try {
|
|
673
|
+
report = JSON.parse(jsonText);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
return { degraded: 'infra-failure', reason: `JSON 解析失败: ${err.message}` };
|
|
676
|
+
}
|
|
677
|
+
// schema 必填字段校验(缺一即降级,不静默当达标)
|
|
678
|
+
if (!report || typeof report !== 'object') {
|
|
679
|
+
return { degraded: 'infra-failure', reason: 'report 非对象' };
|
|
680
|
+
}
|
|
681
|
+
if (!Array.isArray(report.layer2_commands)) {
|
|
682
|
+
return { degraded: 'infra-failure', reason: 'schema 缺必填字段 layer2_commands' };
|
|
683
|
+
}
|
|
684
|
+
// 空命令集降级(Codex C3):verify 子代理一条命令都没产出,说明 verify 基础设施未真正执行,
|
|
685
|
+
// 不可当作"全部通过"。降级为 infra-failure,由编排器计入无进展/早停,绝不静默当达标。
|
|
686
|
+
if (report.layer2_commands.length === 0) {
|
|
687
|
+
return {
|
|
688
|
+
degraded: 'infra-failure',
|
|
689
|
+
reason: 'layer2_commands 为空:verify 未产出任何命令结果,无法证明验证已执行',
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (!report.layer1_fr_coverage || !report.layer1_5_evidence) {
|
|
693
|
+
return {
|
|
694
|
+
degraded: 'infra-failure',
|
|
695
|
+
reason: 'schema 缺必填字段 layer1_fr_coverage / layer1_5_evidence',
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
// 任一命令缺 exit_code(且非 SKIPPED)→ 无法证明真实退出码,强制降级(职责分离)
|
|
699
|
+
for (const cmd of report.layer2_commands) {
|
|
700
|
+
const isSkipped = cmd && cmd.skipped_reason != null;
|
|
701
|
+
if (!isSkipped && typeof cmd.exit_code !== 'number') {
|
|
702
|
+
return {
|
|
703
|
+
degraded: 'infra-failure',
|
|
704
|
+
reason: `命令 "${cmd && cmd.name}" 缺 exit_code,无法证明真实退出码`,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// verify_mode 合法性校验(F203 修订 #2 / WARNING-1):decideStop 重构后"非 full 一律走 smoke
|
|
709
|
+
// readiness",verify_mode=undefined/typo 的报告(可能带 SKIPPED)会被误 escalate。在 full 契约
|
|
710
|
+
// 校验之前先拦截非法 verify_mode,降级 infra-failure,绝不进入达标/escalate 判定。
|
|
711
|
+
if (report.verify_mode !== 'smoke' && report.verify_mode !== 'full') {
|
|
712
|
+
return {
|
|
713
|
+
degraded: 'infra-failure',
|
|
714
|
+
reason: 'verify_mode 非法(须 smoke|full)',
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
// full 轮契约校验(F203 修订 #2):full verify 必须先 build,dist 已就位故不应出现
|
|
718
|
+
// dist_not_built SKIPPED。一旦出现即 verify 契约违反——降级 infra-failure,绝不当普通 continue。
|
|
719
|
+
// smoke 轮的 dist_not_built SKIPPED 是预期行为(不 build),正常放行。
|
|
720
|
+
if (report.verify_mode === 'full') {
|
|
721
|
+
const distSkipped = report.layer2_commands.some(
|
|
722
|
+
(cmd) => cmd && cmd.skipped_reason === 'dist_not_built',
|
|
723
|
+
);
|
|
724
|
+
if (distSkipped) {
|
|
725
|
+
return {
|
|
726
|
+
degraded: 'infra-failure',
|
|
727
|
+
reason: 'full verify 不应出现 dist_not_built SKIPPED(full 必须先 build)',
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return { report };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* 解释 Spectra MCP impact 工具返回(FR-011/012)
|
|
736
|
+
* @param {Object} mcpResult
|
|
737
|
+
* @returns {{ injected: true, summary: string }|{ injected: false, skipped: true, warning: string }}
|
|
738
|
+
*/
|
|
739
|
+
export function interpretImpactResult(mcpResult) {
|
|
740
|
+
// 空结果 → 跳过注入,不中止(FR-012)
|
|
741
|
+
if (mcpResult == null) {
|
|
742
|
+
return {
|
|
743
|
+
injected: false,
|
|
744
|
+
skipped: true,
|
|
745
|
+
warning: 'Spectra impact 不可用:空结果,本轮跳过注入',
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
// 错误 / graph-not-built → 跳过注入 + warning(含原因)
|
|
749
|
+
if (mcpResult.error) {
|
|
750
|
+
return {
|
|
751
|
+
injected: false,
|
|
752
|
+
skipped: true,
|
|
753
|
+
warning: `Spectra impact 不可用:${mcpResult.error},本轮跳过注入`,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
// 有效 impact 数据 → 注入摘要
|
|
757
|
+
const affected = Array.isArray(mcpResult.affected) ? mcpResult.affected : [];
|
|
758
|
+
if (affected.length === 0 && !mcpResult.summary) {
|
|
759
|
+
return {
|
|
760
|
+
injected: false,
|
|
761
|
+
skipped: true,
|
|
762
|
+
warning: 'Spectra impact 不可用:无 affected 数据,本轮跳过注入',
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const riskLevel = mcpResult.summary && mcpResult.summary.riskLevel;
|
|
766
|
+
const summary = `Spectra impact:受影响 symbol ${affected.length} 个${
|
|
767
|
+
riskLevel ? `,风险等级 ${riskLevel}` : ''
|
|
768
|
+
}`;
|
|
769
|
+
return { injected: true, summary };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* 格式化迭代日志条目为含内嵌 ```json 围栏的 markdown 块(FR-019)
|
|
774
|
+
* @param {Object} entry
|
|
775
|
+
* @returns {string}
|
|
776
|
+
*/
|
|
777
|
+
export function formatIterationLogEntry(entry) {
|
|
778
|
+
// markdown 标题(轮次人可读)+ 内嵌 ```json 围栏(机器可解析)
|
|
779
|
+
const round = entry && entry.round;
|
|
780
|
+
const heading = `### 轮次 ${round}(round ${round})`;
|
|
781
|
+
const json = JSON.stringify(entry, null, 2);
|
|
782
|
+
return `${heading}\n\n\`\`\`json\n${json}\n\`\`\`\n`;
|
|
783
|
+
}
|