@sun-asterisk/sungen 3.3.0-alpha.1 → 3.3.0-alpha.10
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/dist/capabilities/discover.d.ts +4 -0
- package/dist/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +16 -2
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/case-triage-hint.d.ts +14 -0
- package/dist/cli/case-triage-hint.d.ts.map +1 -0
- package/dist/cli/case-triage-hint.js +23 -0
- package/dist/cli/case-triage-hint.js.map +1 -0
- package/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/contract-derive-command.d.ts +4 -2
- package/dist/cli/commands/contract-derive-command.d.ts.map +1 -1
- package/dist/cli/commands/contract-derive-command.js +19 -5
- package/dist/cli/commands/contract-derive-command.js.map +1 -1
- package/dist/cli/commands/contract.d.ts +3 -0
- package/dist/cli/commands/contract.d.ts.map +1 -1
- package/dist/cli/commands/contract.js +40 -7
- package/dist/cli/commands/contract.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +59 -18
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/gate.d.ts.map +1 -1
- package/dist/cli/commands/gate.js +2 -1
- package/dist/cli/commands/gate.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +14 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/journey.d.ts.map +1 -1
- package/dist/cli/commands/journey.js +6 -4
- package/dist/cli/commands/journey.js.map +1 -1
- package/dist/cli/commands/next.d.ts +7 -2
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +41 -10
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/triage.d.ts +2 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +45 -3
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +6 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/json-error-on-exit.d.ts +34 -0
- package/dist/cli/json-error-on-exit.d.ts.map +1 -0
- package/dist/cli/json-error-on-exit.js +63 -0
- package/dist/cli/json-error-on-exit.js.map +1 -0
- package/dist/cli/project-sungen-invocation.d.ts +13 -0
- package/dist/cli/project-sungen-invocation.d.ts.map +1 -0
- package/dist/cli/project-sungen-invocation.js +95 -0
- package/dist/cli/project-sungen-invocation.js.map +1 -0
- package/dist/cli/run-command-hints.d.ts +9 -0
- package/dist/cli/run-command-hints.d.ts.map +1 -0
- package/dist/cli/run-command-hints.js +84 -0
- package/dist/cli/run-command-hints.js.map +1 -0
- package/dist/cli/waiting-for-you-block.d.ts +18 -0
- package/dist/cli/waiting-for-you-block.d.ts.map +1 -0
- package/dist/cli/waiting-for-you-block.js +22 -0
- package/dist/cli/waiting-for-you-block.js.map +1 -0
- package/dist/contract/accept-opaque-steps.d.ts +3 -0
- package/dist/contract/accept-opaque-steps.d.ts.map +1 -0
- package/dist/contract/accept-opaque-steps.js +114 -0
- package/dist/contract/accept-opaque-steps.js.map +1 -0
- package/dist/contract/approve-contract.d.ts +8 -1
- package/dist/contract/approve-contract.d.ts.map +1 -1
- package/dist/contract/approve-contract.js +6 -0
- package/dist/contract/approve-contract.js.map +1 -1
- package/dist/contract/build-contract-verdict.js +2 -2
- package/dist/contract/build-contract-verdict.js.map +1 -1
- package/dist/contract/build-trace-conformance.d.ts +5 -1
- package/dist/contract/build-trace-conformance.d.ts.map +1 -1
- package/dist/contract/build-trace-conformance.js +12 -14
- package/dist/contract/build-trace-conformance.js.map +1 -1
- package/dist/contract/classify-contract-change.d.ts +3 -2
- package/dist/contract/classify-contract-change.d.ts.map +1 -1
- package/dist/contract/classify-contract-change.js.map +1 -1
- package/dist/contract/classify-trace-conformance.d.ts +18 -3
- package/dist/contract/classify-trace-conformance.d.ts.map +1 -1
- package/dist/contract/classify-trace-conformance.js +64 -17
- package/dist/contract/classify-trace-conformance.js.map +1 -1
- package/dist/contract/contract-schema.d.ts +21 -0
- package/dist/contract/contract-schema.d.ts.map +1 -1
- package/dist/contract/contract-schema.js +25 -3
- package/dist/contract/contract-schema.js.map +1 -1
- package/dist/contract/contracts-lock-file.d.ts.map +1 -1
- package/dist/contract/contracts-lock-file.js +1 -0
- package/dist/contract/contracts-lock-file.js.map +1 -1
- package/dist/contract/derive-free-feature.d.ts +19 -0
- package/dist/contract/derive-free-feature.d.ts.map +1 -1
- package/dist/contract/derive-free-feature.js +40 -2
- package/dist/contract/derive-free-feature.js.map +1 -1
- package/dist/contract/project-contract-blockers.d.ts.map +1 -1
- package/dist/contract/project-contract-blockers.js +4 -2
- package/dist/contract/project-contract-blockers.js.map +1 -1
- package/dist/contract/record-contract-approval.d.ts +16 -1
- package/dist/contract/record-contract-approval.d.ts.map +1 -1
- package/dist/contract/record-contract-approval.js +29 -1
- package/dist/contract/record-contract-approval.js.map +1 -1
- package/dist/contract/render-free-gherkin.d.ts +2 -0
- package/dist/contract/render-free-gherkin.d.ts.map +1 -1
- package/dist/contract/render-free-gherkin.js +6 -0
- package/dist/contract/render-free-gherkin.js.map +1 -1
- package/dist/contract/resolve-unit-trace-identities.d.ts +12 -0
- package/dist/contract/resolve-unit-trace-identities.d.ts.map +1 -1
- package/dist/contract/resolve-unit-trace-identities.js +33 -49
- package/dist/contract/resolve-unit-trace-identities.js.map +1 -1
- package/dist/contract/trace/collect-trace-evidence.d.ts +16 -5
- package/dist/contract/trace/collect-trace-evidence.d.ts.map +1 -1
- package/dist/contract/trace/collect-trace-evidence.js +101 -43
- package/dist/contract/trace/collect-trace-evidence.js.map +1 -1
- package/dist/contract/trace/read-file-once.d.ts +20 -0
- package/dist/contract/trace/read-file-once.d.ts.map +1 -0
- package/dist/contract/trace/read-file-once.js +135 -0
- package/dist/contract/trace/read-file-once.js.map +1 -0
- package/dist/contract/trace/scenario-id-claims.d.ts +11 -0
- package/dist/contract/trace/scenario-id-claims.d.ts.map +1 -0
- package/dist/contract/trace/scenario-id-claims.js +89 -0
- package/dist/contract/trace/scenario-id-claims.js.map +1 -0
- package/dist/contract/trace/select-final-attempt.d.ts +4 -2
- package/dist/contract/trace/select-final-attempt.d.ts.map +1 -1
- package/dist/contract/trace/select-final-attempt.js +7 -4
- package/dist/contract/trace/select-final-attempt.js.map +1 -1
- package/dist/contract/trace/test-step-join.d.ts +3 -3
- package/dist/contract/trace/test-step-join.d.ts.map +1 -1
- package/dist/contract/trace/test-step-join.js +2 -2
- package/dist/contract/trace/test-step-join.js.map +1 -1
- package/dist/contract/trace/trace-binding.d.ts +3 -2
- package/dist/contract/trace/trace-binding.d.ts.map +1 -1
- package/dist/contract/trace/trace-binding.js +6 -3
- package/dist/contract/trace/trace-binding.js.map +1 -1
- package/dist/contract/trace/trace-file-reader.d.ts +14 -2
- package/dist/contract/trace/trace-file-reader.d.ts.map +1 -1
- package/dist/contract/trace/trace-file-reader.js +27 -45
- package/dist/contract/trace/trace-file-reader.js.map +1 -1
- package/dist/contract/trace/trace-freshness.d.ts +27 -0
- package/dist/contract/trace/trace-freshness.d.ts.map +1 -0
- package/dist/contract/trace/trace-freshness.js +146 -0
- package/dist/contract/trace/trace-freshness.js.map +1 -0
- package/dist/contract/trace/trace-identity-resolution.d.ts +2 -0
- package/dist/contract/trace/trace-identity-resolution.d.ts.map +1 -1
- package/dist/contract/trace/trace-identity-resolution.js +6 -3
- package/dist/contract/trace/trace-identity-resolution.js.map +1 -1
- package/dist/contract/trace/trace-run-record.d.ts +8 -3
- package/dist/contract/trace/trace-run-record.d.ts.map +1 -1
- package/dist/contract/trace/trace-run-record.js +42 -22
- package/dist/contract/trace/trace-run-record.js.map +1 -1
- package/dist/contract/trace/trace-steps.js +1 -1
- package/dist/contract/trace/trace-steps.js.map +1 -1
- package/dist/contract/trace/trace-zip-limits.d.ts +5 -1
- package/dist/contract/trace/trace-zip-limits.d.ts.map +1 -1
- package/dist/contract/trace/trace-zip-limits.js +6 -2
- package/dist/contract/trace/trace-zip-limits.js.map +1 -1
- package/dist/contract/verify-approval-decisions.d.ts.map +1 -1
- package/dist/contract/verify-approval-decisions.js +9 -4
- package/dist/contract/verify-approval-decisions.js.map +1 -1
- package/dist/decisions/confirmed-decision-commands.d.ts +11 -0
- package/dist/decisions/confirmed-decision-commands.d.ts.map +1 -1
- package/dist/decisions/confirmed-decision-commands.js +21 -3
- package/dist/decisions/confirmed-decision-commands.js.map +1 -1
- package/dist/decisions/decision-schema.d.ts +2 -1
- package/dist/decisions/decision-schema.d.ts.map +1 -1
- package/dist/decisions/decision-schema.js +2 -1
- package/dist/decisions/decision-schema.js.map +1 -1
- package/dist/decisions/git-head-trust.d.ts +12 -0
- package/dist/decisions/git-head-trust.d.ts.map +1 -1
- package/dist/decisions/git-head-trust.js +52 -0
- package/dist/decisions/git-head-trust.js.map +1 -1
- package/dist/decisions/human-confirmation.d.ts +2 -0
- package/dist/decisions/human-confirmation.d.ts.map +1 -1
- package/dist/decisions/human-confirmation.js +6 -0
- package/dist/decisions/human-confirmation.js.map +1 -1
- package/dist/decisions/legacy-waiver-migration.d.ts +8 -1
- package/dist/decisions/legacy-waiver-migration.d.ts.map +1 -1
- package/dist/decisions/legacy-waiver-migration.js +13 -4
- package/dist/decisions/legacy-waiver-migration.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +11 -2
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +8 -4
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +6 -1
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +11 -4
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/xlsx-cells.js +1 -1
- package/dist/exporters/matrix/xlsx-cells.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +3 -2
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +4 -4
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/graph/board-human-actions.d.ts +5 -0
- package/dist/graph/board-human-actions.d.ts.map +1 -0
- package/dist/graph/board-human-actions.js +83 -0
- package/dist/graph/board-human-actions.js.map +1 -0
- package/dist/graph/evaluate-graph.d.ts +21 -1
- package/dist/graph/evaluate-graph.d.ts.map +1 -1
- package/dist/graph/evaluate-graph.js +37 -9
- package/dist/graph/evaluate-graph.js.map +1 -1
- package/dist/graph/human-action.d.ts +21 -0
- package/dist/graph/human-action.d.ts.map +1 -0
- package/dist/graph/human-action.js +3 -0
- package/dist/graph/human-action.js.map +1 -0
- package/dist/graph/manual-triage-decisions.d.ts +19 -0
- package/dist/graph/manual-triage-decisions.d.ts.map +1 -0
- package/dist/graph/manual-triage-decisions.js +53 -0
- package/dist/graph/manual-triage-decisions.js.map +1 -0
- package/dist/graph/node-decl.d.ts +16 -0
- package/dist/graph/node-decl.d.ts.map +1 -1
- package/dist/graph/node-decl.js +4 -1
- package/dist/graph/node-decl.js.map +1 -1
- package/dist/graph/nodes/test-case-contract-nodes.d.ts +3 -1
- package/dist/graph/nodes/test-case-contract-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/test-case-contract-nodes.js +22 -6
- package/dist/graph/nodes/test-case-contract-nodes.js.map +1 -1
- package/dist/graph/nodes/test-case-nodes.d.ts +3 -0
- package/dist/graph/nodes/test-case-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/test-case-nodes.js +33 -4
- package/dist/graph/nodes/test-case-nodes.js.map +1 -1
- package/dist/graph/nodes/unit-audit-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/unit-audit-nodes.js +1 -0
- package/dist/graph/nodes/unit-audit-nodes.js.map +1 -1
- package/dist/graph/nodes/unit-contract-node.d.ts +2 -1
- package/dist/graph/nodes/unit-contract-node.d.ts.map +1 -1
- package/dist/graph/nodes/unit-contract-node.js +16 -1
- package/dist/graph/nodes/unit-contract-node.js.map +1 -1
- package/dist/graph/nodes/unit-design-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/unit-design-nodes.js +2 -1
- package/dist/graph/nodes/unit-design-nodes.js.map +1 -1
- package/dist/graph/test-case-results.d.ts +1 -0
- package/dist/graph/test-case-results.d.ts.map +1 -1
- package/dist/graph/test-case-results.js +27 -3
- package/dist/graph/test-case-results.js.map +1 -1
- package/dist/graph/test-case-satisfied.d.ts +5 -4
- package/dist/graph/test-case-satisfied.d.ts.map +1 -1
- package/dist/graph/test-case-satisfied.js +4 -0
- package/dist/graph/test-case-satisfied.js.map +1 -1
- package/dist/graph/test-case-state-shape.d.ts +7 -1
- package/dist/graph/test-case-state-shape.d.ts.map +1 -1
- package/dist/graph/unit-artifact-probes.d.ts.map +1 -1
- package/dist/graph/unit-artifact-probes.js +3 -1
- package/dist/graph/unit-artifact-probes.js.map +1 -1
- package/dist/graph/unit-results-freshness.d.ts +29 -0
- package/dist/graph/unit-results-freshness.d.ts.map +1 -0
- package/dist/graph/unit-results-freshness.js +213 -0
- package/dist/graph/unit-results-freshness.js.map +1 -0
- package/dist/harness/audit.d.ts +3 -2
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +4 -17
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/catalog/gate-rules.yaml +3 -3
- package/dist/harness/defect-draft.d.ts.map +1 -1
- package/dist/harness/defect-draft.js +12 -2
- package/dist/harness/defect-draft.js.map +1 -1
- package/dist/harness/defects.d.ts +14 -0
- package/dist/harness/defects.d.ts.map +1 -1
- package/dist/harness/defects.js +14 -10
- package/dist/harness/defects.js.map +1 -1
- package/dist/harness/flow-check.d.ts +9 -0
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +11 -0
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/gates/audit-gate-verdict.d.ts.map +1 -1
- package/dist/harness/gates/audit-gate-verdict.js +2 -1
- package/dist/harness/gates/audit-gate-verdict.js.map +1 -1
- package/dist/harness/gates/audit-input-hash.d.ts +2 -0
- package/dist/harness/gates/audit-input-hash.d.ts.map +1 -1
- package/dist/harness/gates/audit-input-hash.js +30 -2
- package/dist/harness/gates/audit-input-hash.js.map +1 -1
- package/dist/harness/gates/audit-report-verification.d.ts +17 -6
- package/dist/harness/gates/audit-report-verification.d.ts.map +1 -1
- package/dist/harness/gates/audit-report-verification.js +55 -21
- package/dist/harness/gates/audit-report-verification.js.map +1 -1
- package/dist/harness/gates/delivery-checklist.d.ts +3 -0
- package/dist/harness/gates/delivery-checklist.d.ts.map +1 -1
- package/dist/harness/gates/delivery-checklist.js +39 -6
- package/dist/harness/gates/delivery-checklist.js.map +1 -1
- package/dist/harness/gates/delivery-decision-gate.d.ts +4 -0
- package/dist/harness/gates/delivery-decision-gate.d.ts.map +1 -1
- package/dist/harness/gates/delivery-decision-gate.js +26 -1
- package/dist/harness/gates/delivery-decision-gate.js.map +1 -1
- package/dist/harness/gates/delivery-decision-step.d.ts +4 -1
- package/dist/harness/gates/delivery-decision-step.d.ts.map +1 -1
- package/dist/harness/gates/delivery-decision-step.js +17 -2
- package/dist/harness/gates/delivery-decision-step.js.map +1 -1
- package/dist/harness/gates/in-run-decision-check.d.ts.map +1 -1
- package/dist/harness/gates/in-run-decision-check.js +40 -12
- package/dist/harness/gates/in-run-decision-check.js.map +1 -1
- package/dist/harness/gates/migrated-waiver-import-bounds.d.ts +13 -0
- package/dist/harness/gates/migrated-waiver-import-bounds.d.ts.map +1 -0
- package/dist/harness/gates/migrated-waiver-import-bounds.js +141 -0
- package/dist/harness/gates/migrated-waiver-import-bounds.js.map +1 -0
- package/dist/harness/gates/migrated-waiver-review.d.ts +14 -4
- package/dist/harness/gates/migrated-waiver-review.d.ts.map +1 -1
- package/dist/harness/gates/migrated-waiver-review.js +78 -80
- package/dist/harness/gates/migrated-waiver-review.js.map +1 -1
- package/dist/harness/journey.d.ts +40 -0
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +155 -22
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/next-step.d.ts +19 -0
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +21 -0
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/provenance.d.ts +29 -0
- package/dist/harness/provenance.d.ts.map +1 -1
- package/dist/harness/provenance.js +109 -9
- package/dist/harness/provenance.js.map +1 -1
- package/dist/harness/triage-manual-classification.d.ts +32 -0
- package/dist/harness/triage-manual-classification.d.ts.map +1 -0
- package/dist/harness/triage-manual-classification.js +56 -0
- package/dist/harness/triage-manual-classification.js.map +1 -0
- package/dist/harness/unit-paths.d.ts +8 -0
- package/dist/harness/unit-paths.d.ts.map +1 -1
- package/dist/harness/unit-paths.js +23 -1
- package/dist/harness/unit-paths.js.map +1 -1
- package/dist/hooks/bash-read-only-segment.d.ts +9 -0
- package/dist/hooks/bash-read-only-segment.d.ts.map +1 -0
- package/dist/hooks/bash-read-only-segment.js +168 -0
- package/dist/hooks/bash-read-only-segment.js.map +1 -0
- package/dist/hooks/bash-write-targets.d.ts +12 -2
- package/dist/hooks/bash-write-targets.d.ts.map +1 -1
- package/dist/hooks/bash-write-targets.js +122 -29
- package/dist/hooks/bash-write-targets.js.map +1 -1
- package/dist/hooks/guard-protected-paths.d.ts +4 -0
- package/dist/hooks/guard-protected-paths.d.ts.map +1 -1
- package/dist/hooks/guard-protected-paths.js +13 -3
- package/dist/hooks/guard-protected-paths.js.map +1 -1
- package/dist/hooks/guard-rules.d.ts +0 -17
- package/dist/hooks/guard-rules.d.ts.map +1 -1
- package/dist/hooks/guard-rules.js +79 -10
- package/dist/hooks/guard-rules.js.map +1 -1
- package/dist/hooks/guard-tty-spoof-rule.d.ts +3 -1
- package/dist/hooks/guard-tty-spoof-rule.d.ts.map +1 -1
- package/dist/hooks/guard-tty-spoof-rule.js +3 -2
- package/dist/hooks/guard-tty-spoof-rule.js.map +1 -1
- package/dist/hooks/hand-back-core.d.ts +9 -4
- package/dist/hooks/hand-back-core.d.ts.map +1 -1
- package/dist/hooks/hand-back-core.js +24 -11
- package/dist/hooks/hand-back-core.js.map +1 -1
- package/dist/hooks/session-start-unit-state-hook.d.ts +2 -1
- package/dist/hooks/session-start-unit-state-hook.d.ts.map +1 -1
- package/dist/hooks/session-start-unit-state-hook.js +9 -4
- package/dist/hooks/session-start-unit-state-hook.js.map +1 -1
- package/dist/hooks/stop-next-step-hook.d.ts.map +1 -1
- package/dist/hooks/stop-next-step-hook.js +1 -0
- package/dist/hooks/stop-next-step-hook.js.map +1 -1
- package/dist/orchestrator/assets-drift.d.ts +5 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -1
- package/dist/orchestrator/assets-drift.js +12 -3
- package/dist/orchestrator/assets-drift.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +1 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +6 -3
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +2 -0
- package/dist/orchestrator/templates/ai-src/commands/review.md +2 -0
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
- package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
- package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
- package/package.json +3 -3
- package/src/capabilities/discover.ts +15 -2
- package/src/cli/case-triage-hint.ts +31 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/contract-derive-command.ts +20 -8
- package/src/cli/commands/contract.ts +41 -7
- package/src/cli/commands/delivery.ts +56 -18
- package/src/cli/commands/gate.ts +2 -1
- package/src/cli/commands/generate.ts +13 -2
- package/src/cli/commands/journey.ts +8 -6
- package/src/cli/commands/next.ts +41 -12
- package/src/cli/commands/triage.ts +41 -3
- package/src/cli/commands/update.ts +6 -2
- package/src/cli/index.ts +5 -1
- package/src/cli/json-error-on-exit.ts +72 -0
- package/src/cli/project-sungen-invocation.ts +59 -0
- package/src/cli/run-command-hints.ts +47 -0
- package/src/cli/waiting-for-you-block.ts +27 -0
- package/src/contract/accept-opaque-steps.ts +114 -0
- package/src/contract/approve-contract.ts +10 -3
- package/src/contract/build-contract-verdict.ts +2 -2
- package/src/contract/build-trace-conformance.ts +11 -8
- package/src/contract/classify-contract-change.ts +3 -3
- package/src/contract/classify-trace-conformance.ts +76 -22
- package/src/contract/contract-schema.ts +31 -2
- package/src/contract/contracts-lock-file.ts +1 -0
- package/src/contract/derive-free-feature.ts +46 -3
- package/src/contract/project-contract-blockers.ts +4 -2
- package/src/contract/record-contract-approval.ts +41 -1
- package/src/contract/render-free-gherkin.ts +5 -0
- package/src/contract/resolve-unit-trace-identities.ts +35 -12
- package/src/contract/trace/collect-trace-evidence.ts +105 -36
- package/src/contract/trace/read-file-once.ts +95 -0
- package/src/contract/trace/scenario-id-claims.ts +60 -0
- package/src/contract/trace/select-final-attempt.ts +12 -5
- package/src/contract/trace/test-step-join.ts +6 -5
- package/src/contract/trace/trace-binding.ts +7 -4
- package/src/contract/trace/trace-file-reader.ts +34 -13
- package/src/contract/trace/trace-freshness.ts +110 -0
- package/src/contract/trace/trace-identity-resolution.ts +7 -4
- package/src/contract/trace/trace-run-record.ts +37 -15
- package/src/contract/trace/trace-steps.ts +1 -1
- package/src/contract/trace/trace-zip-limits.ts +6 -2
- package/src/contract/verify-approval-decisions.ts +9 -5
- package/src/decisions/confirmed-decision-commands.ts +25 -4
- package/src/decisions/decision-schema.ts +2 -1
- package/src/decisions/git-head-trust.ts +44 -0
- package/src/decisions/human-confirmation.ts +5 -0
- package/src/decisions/legacy-waiver-migration.ts +14 -3
- package/src/exporters/csv-exporter.ts +12 -4
- package/src/exporters/matrix/export.ts +11 -4
- package/src/exporters/matrix/xlsx-cells.ts +1 -1
- package/src/exporters/xlsx-exporter.ts +5 -4
- package/src/graph/board-human-actions.ts +80 -0
- package/src/graph/evaluate-graph.ts +45 -10
- package/src/graph/human-action.ts +33 -0
- package/src/graph/manual-triage-decisions.ts +55 -0
- package/src/graph/node-decl.ts +18 -0
- package/src/graph/nodes/test-case-contract-nodes.ts +28 -7
- package/src/graph/nodes/test-case-nodes.ts +37 -5
- package/src/graph/nodes/unit-audit-nodes.ts +2 -1
- package/src/graph/nodes/unit-contract-node.ts +20 -2
- package/src/graph/nodes/unit-design-nodes.ts +2 -1
- package/src/graph/test-case-results.ts +25 -4
- package/src/graph/test-case-satisfied.ts +7 -4
- package/src/graph/test-case-state-shape.ts +7 -2
- package/src/graph/unit-artifact-probes.ts +3 -1
- package/src/graph/unit-results-freshness.ts +165 -0
- package/src/harness/audit.ts +4 -13
- package/src/harness/catalog/gate-rules.yaml +3 -3
- package/src/harness/defect-draft.ts +13 -2
- package/src/harness/defects.ts +22 -9
- package/src/harness/flow-check.ts +11 -0
- package/src/harness/gates/audit-gate-verdict.ts +2 -1
- package/src/harness/gates/audit-input-hash.ts +27 -3
- package/src/harness/gates/audit-report-verification.ts +57 -24
- package/src/harness/gates/delivery-checklist.ts +39 -7
- package/src/harness/gates/delivery-decision-gate.ts +29 -1
- package/src/harness/gates/delivery-decision-step.ts +19 -3
- package/src/harness/gates/in-run-decision-check.ts +38 -13
- package/src/harness/gates/migrated-waiver-import-bounds.ts +103 -0
- package/src/harness/gates/migrated-waiver-review.ts +90 -37
- package/src/harness/journey.ts +174 -21
- package/src/harness/next-step.ts +34 -1
- package/src/harness/provenance.ts +99 -11
- package/src/harness/triage-manual-classification.ts +76 -0
- package/src/harness/unit-paths.ts +19 -0
- package/src/hooks/bash-read-only-segment.ts +130 -0
- package/src/hooks/bash-write-targets.ts +109 -23
- package/src/hooks/guard-protected-paths.ts +13 -3
- package/src/hooks/guard-rules.ts +37 -18
- package/src/hooks/guard-tty-spoof-rule.ts +6 -3
- package/src/hooks/hand-back-core.ts +24 -12
- package/src/hooks/session-start-unit-state-hook.ts +9 -4
- package/src/hooks/stop-next-step-hook.ts +1 -0
- package/src/orchestrator/assets-drift.ts +12 -5
- package/src/orchestrator/project-initializer.ts +1 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +6 -3
- package/src/orchestrator/templates/ai-src/commands/delivery.md +2 -0
- package/src/orchestrator/templates/ai-src/commands/review.md +2 -0
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/src/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
- package/src/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
- package/src/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
|
@@ -7,26 +7,43 @@
|
|
|
7
7
|
* with that step — identically (fine), weaker (hard), stronger (strengthened), changed
|
|
8
8
|
* (review); an approved check with no group or no check in its group → assertion-lost
|
|
9
9
|
* overlays a step whose target identity is `low` → opaque (review, never match, red-team #4);
|
|
10
|
-
* an opaque step → opaque with its verbatim text and file:line (contract.opaque-step)
|
|
10
|
+
* an opaque step → opaque with its verbatim text and file:line (contract.opaque-step) —
|
|
11
|
+
* unless the approved entry lists it as accepted by QA at its sha (#923, opaqueStepSha):
|
|
12
|
+
* then a note, not a finding. impl:code: a test.step whose only candidates are opaque is
|
|
13
|
+
* left to the overlay (judged on the ladder it could only read "changed"), which names the
|
|
14
|
+
* approved step the opaque one replaces
|
|
11
15
|
* runs no admissible trace → unproven (all inconclusive → inconclusive) when APPROVED, a
|
|
12
16
|
* warning while proposed (M5, M6); a trace refused beside an admissible one → a warning;
|
|
13
17
|
* several admissible traces that fingerprint differently → nondeterministic (worst wins)
|
|
14
18
|
*/
|
|
15
19
|
import { classifyScenario, ContractFinding, finding, ScenarioResult, severityOf } from './classify-contract-change';
|
|
16
|
-
import { sameJson } from './contract-hash';
|
|
17
|
-
import type { ContractStep, Implementation, LockEntry, ScenarioClass } from './contract-schema';
|
|
20
|
+
import { canonicalJson, sameJson, sha256Hex } from './contract-hash';
|
|
21
|
+
import type { AcceptedOpaqueStep, ContractStep, Implementation, LockEntry, OpaqueStep, ScenarioClass } from './contract-schema';
|
|
18
22
|
import { compareAsserts } from './oracle-strength';
|
|
23
|
+
import { clip, RenderedStep } from './render-free-gherkin';
|
|
19
24
|
import type { TraceRun } from './trace/collect-trace-evidence';
|
|
20
25
|
import type { TraceFingerprint } from './trace/trace-fingerprint';
|
|
21
26
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
27
|
+
/**
|
|
28
|
+
* `warnings` never change the verdict: a not-yet-approved scenario's trace problems, unused traces, an
|
|
29
|
+
* accepted opaque step. `opaque`: every opaque step of the admissible traces, accepted or not.
|
|
30
|
+
*/
|
|
31
|
+
export interface TraceOutcome { class: ScenarioClass; findings: ContractFinding[]; hashes: string[]; runs: string[]; warnings: string[]; opaque: OpaqueStep[] }
|
|
24
32
|
|
|
25
33
|
/** What an unjudgeable scenario becomes: hard when QA locked it (approved), a warning while it is proposed. */
|
|
26
34
|
export function notJudged(id: string, approved: boolean, cls: 'unproven' | 'inconclusive', reasons: string[]): TraceOutcome {
|
|
27
35
|
return approved
|
|
28
|
-
? { class: cls, findings: reasons.map((r) => finding(cls, r)), hashes: [], runs: [], warnings: [] }
|
|
29
|
-
: { class: 'proposed', findings: [], hashes: [], runs: [], warnings: reasons.map((r) => `${id} (not approved yet, so a warning): ${r}`) };
|
|
36
|
+
? { class: cls, findings: reasons.map((r) => finding(cls, r)), hashes: [], runs: [], warnings: [], opaque: [] }
|
|
37
|
+
: { class: 'proposed', findings: [], hashes: [], runs: [], warnings: reasons.map((r) => `${id} (not approved yet, so a warning): ${r}`), opaque: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The sha a QA acceptance binds to (#923): the opaque step as rendered — its verbatim line or request,
|
|
42
|
+
* check or step, the `test.step` it sits in — and as normalized, with the implementation path. Any
|
|
43
|
+
* change to the step gives another sha, so the old acceptance no longer covers it.
|
|
44
|
+
*/
|
|
45
|
+
export function opaqueStepSha(implementation: Implementation, r: RenderedStep, step: ContractStep | undefined): string {
|
|
46
|
+
return sha256Hex(canonicalJson({ implementation, kind: r.kind, group: r.group ?? null, verbatim: r.opaque?.verbatim ?? null, step: step ?? null })).slice(0, 16);
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
/** How a low target identity opens its finding — `next` reads it to offer `contract check --trace`. */
|
|
@@ -35,19 +52,41 @@ export const LOW_IDENTITY_FINDING = 'target identity is low';
|
|
|
35
52
|
const worstOf = (findings: ContractFinding[], start: ScenarioClass = 'match'): ScenarioClass =>
|
|
36
53
|
findings.reduce<ScenarioClass>((acc, f) => (severityOf(f.class as ScenarioClass) > severityOf(acc) ? f.class as ScenarioClass : acc), start);
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
type Judged = { findings: ContractFinding[]; opaque: OpaqueStep[] };
|
|
56
|
+
|
|
57
|
+
/** An approved step as a reader sees it: `see [My Account] heading with text "My Account"`. */
|
|
58
|
+
function approvedStepText(s: ContractStep): string {
|
|
59
|
+
const refs = [s.target?.ref, ...(s.target?.also ?? [])].filter((r): r is string => r !== undefined);
|
|
60
|
+
const values = (s.kind === 'assert' ? s.expected : s.value)?.split(' | ') ?? [];
|
|
61
|
+
let r = 0;
|
|
62
|
+
let v = 0;
|
|
63
|
+
return s.verb.replace(/\[\]/g, () => `[${refs[r++] ?? ''}]`).replace(/\{\}/g, () => (v < values.length ? `"${values[v++]}"` : '{}'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function overlays(
|
|
67
|
+
fp: TraceFingerprint, implementation: Implementation, accepted: readonly AcceptedOpaqueStep[], used?: Set<number>, replaces?: ReadonlyMap<number, string>,
|
|
68
|
+
): Judged {
|
|
69
|
+
const findings: ContractFinding[] = [];
|
|
70
|
+
const opaque: OpaqueStep[] = [];
|
|
40
71
|
fp.rendered.forEach((r, i) => {
|
|
41
72
|
if (used && !used.has(i)) return;
|
|
42
|
-
if (r.opaque)
|
|
43
|
-
|
|
73
|
+
if (r.opaque) {
|
|
74
|
+
const sha = opaqueStepSha(implementation, r, fp.steps[i]);
|
|
75
|
+
const isAccepted = accepted.some((a) => a.step === i + 1 && a.sha === sha);
|
|
76
|
+
const instead = replaces?.get(i);
|
|
77
|
+
opaque.push({ step: i + 1, sha, kind: r.kind, verbatim: r.opaque.verbatim, ...(r.opaque.where ? { where: r.opaque.where } : {}), ...(instead ? { replaces: instead } : {}), accepted: isAccepted });
|
|
78
|
+
if (!isAccepted) {
|
|
79
|
+
findings.push(finding('opaque', `"${r.opaque.verbatim}"${r.opaque.where ? ` at ${r.opaque.where}` : ''} — ${r.kind === 'assert' ? 'a check' : 'a step'} sungen cannot compare; QA reviews it${instead ? ` — replaces approved ${instead}` : ''}`, i + 1));
|
|
80
|
+
}
|
|
81
|
+
} else if (r.low) findings.push(finding('opaque', `${LOW_IDENTITY_FINDING} (${r.low}) — cannot prove "${r.sentence}" acts on the approved element`, i + 1));
|
|
44
82
|
});
|
|
45
|
-
return
|
|
83
|
+
return { findings, opaque };
|
|
46
84
|
}
|
|
47
85
|
|
|
48
|
-
function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: ContractFinding[]; used: Set<number> } {
|
|
86
|
+
function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: ContractFinding[]; used: Set<number>; replaces: Map<number, string> } {
|
|
49
87
|
const findings: ContractFinding[] = [];
|
|
50
88
|
const used = new Set<number>();
|
|
89
|
+
const replaces = new Map<number, string>();
|
|
51
90
|
const taken = new Set<number>();
|
|
52
91
|
entry.steps.forEach((locked, i) => {
|
|
53
92
|
const g = fp.groups.findIndex((x, gi) => !taken.has(gi) && sameJson(x.titleStep, locked));
|
|
@@ -68,6 +107,13 @@ function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: C
|
|
|
68
107
|
return;
|
|
69
108
|
}
|
|
70
109
|
candidates.forEach((m) => used.add(m));
|
|
110
|
+
// Only steps sungen cannot compare implement this approved step: the overlay sends them to QA's
|
|
111
|
+
// review (#923), naming the approved step. On the ladder they could only read "changed", which
|
|
112
|
+
// re-approving never clears.
|
|
113
|
+
if (candidates.every((m) => fp.rendered[m].opaque)) {
|
|
114
|
+
for (const m of candidates) replaces.set(m, `step ${i + 1} "${clip(approvedStepText(locked))}"`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
71
117
|
if (locked.kind === 'action') { findings.push(finding('hash-changed', `the code for "${locked.verb}" does something else: "${fp.rendered[candidates[0]].sentence}"`, i + 1)); return; }
|
|
72
118
|
const judged = candidates.map((m) => compareAsserts(locked, fp.steps[m] as ContractStep));
|
|
73
119
|
const rank = (k: string): number => (k === 'oracle-weakened' ? 0 : k === 'review' ? 1 : 2);
|
|
@@ -75,18 +121,21 @@ function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: C
|
|
|
75
121
|
findings.push(finding(best.class === 'review' ? 'hash-changed' : best.class, best.detail, i + 1));
|
|
76
122
|
});
|
|
77
123
|
fp.groups.forEach((g, gi) => { if (!taken.has(gi)) findings.push(finding('hash-changed', `test.step "${g.title}" implements no approved step`)); });
|
|
78
|
-
return { findings, used };
|
|
124
|
+
return { findings, used, replaces };
|
|
79
125
|
}
|
|
80
126
|
|
|
81
|
-
function classifyOne(id: string, implementation: Implementation, entry: LockEntry, fp: TraceFingerprint):
|
|
127
|
+
function classifyOne(id: string, implementation: Implementation, entry: LockEntry, fp: TraceFingerprint): Judged {
|
|
128
|
+
const accepted = entry.acceptedOpaque ?? [];
|
|
82
129
|
if (implementation === 'impl:code') {
|
|
83
|
-
const { findings, used } = implCodeFindings(entry, fp);
|
|
84
|
-
|
|
130
|
+
const { findings, used, replaces } = implCodeFindings(entry, fp);
|
|
131
|
+
const o = overlays(fp, implementation, accepted, used, replaces);
|
|
132
|
+
return { findings: [...findings, ...o.findings], opaque: o.opaque };
|
|
85
133
|
}
|
|
86
134
|
const ladder: ScenarioResult = classifyScenario(id, entry, {
|
|
87
135
|
id, implementation, steps: fp.steps, hash: fp.hash, source: { file: '', name: id, hasId: true, lines: [], outline: false },
|
|
88
136
|
});
|
|
89
|
-
|
|
137
|
+
const o = overlays(fp, implementation, accepted);
|
|
138
|
+
return { findings: [...ladder.findings, ...o.findings], opaque: o.opaque };
|
|
90
139
|
}
|
|
91
140
|
|
|
92
141
|
const runLabel = (r: TraceRun): string => `${r.record?.runId ?? '?'}#${r.result?.retry ?? '?'}`;
|
|
@@ -104,13 +153,18 @@ export function classifyTraceRuns(
|
|
|
104
153
|
}
|
|
105
154
|
// A trace that may not speak, beside one that may: the admissible one judges, the other is a warning.
|
|
106
155
|
const warnings = others.map((r) => `${id}: another trace of this scenario was not used — ${r.reason ?? r.status}`);
|
|
107
|
-
if (!entry) return { class: 'proposed', findings: [], hashes, runs: labels, warnings };
|
|
156
|
+
if (!entry) return { class: 'proposed', findings: [], hashes, runs: labels, warnings, opaque: [] };
|
|
108
157
|
const perRun = admissible.map((r) => classifyOne(id, implementation, entry, fingerprints.get(r)!));
|
|
109
|
-
const findings = perRun.
|
|
110
|
-
|
|
158
|
+
const findings = perRun.flatMap((p) => p.findings);
|
|
159
|
+
const opaque = [...new Map(perRun.flatMap((p) => p.opaque).map((o) => [`${o.step}:${o.sha}`, o] as const)).values()]
|
|
160
|
+
.sort((a, b) => a.step - b.step || a.sha.localeCompare(b.sha));
|
|
161
|
+
for (const o of opaque.filter((x) => x.accepted)) {
|
|
162
|
+
warnings.push(`${id}: step ${o.step} — opaque ${o.kind === 'assert' ? 'check' : 'step'} "${clip(o.verbatim)}" accepted by QA (decision ${entry.decisionId}), not compared`);
|
|
163
|
+
}
|
|
164
|
+
let cls = worstOf(findings);
|
|
111
165
|
if (new Set(hashes).size > 1) {
|
|
112
166
|
findings.push(finding('nondeterministic', `${new Set(hashes).size} different fingerprints across ${admissible.length} runs — the runs took different branches`));
|
|
113
167
|
if (severityOf(cls) <= severityOf('strengthened')) cls = 'nondeterministic';
|
|
114
168
|
}
|
|
115
|
-
return { class: cls, findings, hashes, runs: labels, warnings };
|
|
169
|
+
return { class: cls, findings, hashes, runs: labels, warnings, opaque };
|
|
116
170
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* step {kind: action|assert, verb, target?:{ref, also?, parent?, nth?}, value?, matcher?, expected?, negated?, table?}
|
|
5
5
|
* lock qa/<kind>/<unit>/contracts.lock.yaml → {schema: 1, scenarios: {<scenario id>: LockEntry}}
|
|
6
|
-
* entry {hash, steps, implementation, approvedAt, decisionId, humanApproved?}
|
|
6
|
+
* entry {hash, steps, implementation, approvedAt, decisionId, humanApproved?, acceptedOpaque?}
|
|
7
7
|
*
|
|
8
8
|
* A stored entry is checked the way the decision layer checks its records: a wrong type or an
|
|
9
9
|
* UNKNOWN key is an error, because a field outside the hash could carry meaning the hash does not
|
|
@@ -49,6 +49,9 @@ export interface ContractScenario {
|
|
|
49
49
|
source: { file: string; name: string; hasId: boolean; lines: number[]; outline: boolean };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** A reviewed opaque trace step QA accepted (#923): its 1-based position in the trace's steps and its sha (opaqueStepSha). */
|
|
53
|
+
export interface AcceptedOpaqueStep { step: number; sha: string }
|
|
54
|
+
|
|
52
55
|
export interface LockEntry {
|
|
53
56
|
hash: string;
|
|
54
57
|
steps: ContractStep[];
|
|
@@ -56,8 +59,16 @@ export interface LockEntry {
|
|
|
56
59
|
approvedAt: string;
|
|
57
60
|
decisionId: string;
|
|
58
61
|
humanApproved?: true;
|
|
62
|
+
/** Opaque trace steps a QA accepted on a terminal (`contract approve --accept-opaque`) — vouched for by `decisionId`. */
|
|
63
|
+
acceptedOpaque?: AcceptedOpaqueStep[];
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
* One opaque step of a scenario's trace(s), as `contract check` reports it and `--accept-opaque` shows it.
|
|
68
|
+
* `replaces` (impl:code): the approved step it stands in for — `step 5 "see [Title] heading"`.
|
|
69
|
+
*/
|
|
70
|
+
export interface OpaqueStep { step: number; sha: string; kind: 'action' | 'assert'; verbatim: string; where?: string; replaces?: string; accepted: boolean }
|
|
71
|
+
|
|
61
72
|
export const LOCK_SCHEMA = 1;
|
|
62
73
|
export interface ContractLock { schema: typeof LOCK_SCHEMA; scenarios: Record<string, LockEntry> }
|
|
63
74
|
|
|
@@ -88,8 +99,10 @@ export const RULE_OF: Readonly<Partial<Record<FindingClass, string>>> = {
|
|
|
88
99
|
|
|
89
100
|
const STEP_KEYS = new Set(['kind', 'verb', 'target', 'value', 'matcher', 'expected', 'negated', 'table']);
|
|
90
101
|
const TARGET_KEYS = new Set(['ref', 'also', 'parent', 'nth']);
|
|
91
|
-
const ENTRY_KEYS = new Set(['hash', 'steps', 'implementation', 'approvedAt', 'decisionId', 'humanApproved']);
|
|
102
|
+
const ENTRY_KEYS = new Set(['hash', 'steps', 'implementation', 'approvedAt', 'decisionId', 'humanApproved', 'acceptedOpaque']);
|
|
103
|
+
const ACCEPTED_KEYS = new Set(['step', 'sha']);
|
|
92
104
|
const HASH_RE = /^[0-9a-f]{12}$/;
|
|
105
|
+
export const OPAQUE_SHA_RE = /^[0-9a-f]{16}$/;
|
|
93
106
|
|
|
94
107
|
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
95
108
|
const isStringList = (v: unknown): v is string[] => Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
@@ -118,6 +131,18 @@ export function stepErrors(s: unknown, at: string): string[] {
|
|
|
118
131
|
return e;
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
function acceptedOpaqueErrors(v: unknown, at: string): string[] {
|
|
135
|
+
if (!Array.isArray(v) || !v.length) return [`${at}: acceptedOpaque must be a non-empty list`];
|
|
136
|
+
return v.flatMap((a, i) => {
|
|
137
|
+
const where = `${at} acceptedOpaque ${i + 1}`;
|
|
138
|
+
if (!isObject(a)) return [`${where}: not a mapping`];
|
|
139
|
+
const e = unknownKeys(a, ACCEPTED_KEYS, where);
|
|
140
|
+
if (!(Number.isInteger(a.step) && (a.step as number) > 0)) e.push(`${where}: step must be a positive integer`);
|
|
141
|
+
if (typeof a.sha !== 'string' || !OPAQUE_SHA_RE.test(a.sha)) e.push(`${where}: sha must be 16 hex characters`);
|
|
142
|
+
return e;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
/** Errors in one stored lock entry (empty = well-formed). The hash itself is checked by the reader. */
|
|
122
147
|
export function lockEntryErrors(raw: unknown, id: string): string[] {
|
|
123
148
|
const at = `scenario ${id}`;
|
|
@@ -128,6 +153,10 @@ export function lockEntryErrors(raw: unknown, id: string): string[] {
|
|
|
128
153
|
if (typeof raw.approvedAt !== 'string' || Number.isNaN(Date.parse(raw.approvedAt))) e.push(`${at}: approvedAt must be an ISO-8601 timestamp`);
|
|
129
154
|
if (typeof raw.decisionId !== 'string' || !DECISION_ID_RE.test(raw.decisionId)) e.push(`${at}: decisionId must be D-<16 hex>`);
|
|
130
155
|
if (raw.humanApproved !== undefined && raw.humanApproved !== true) e.push(`${at}: humanApproved is either true or absent`);
|
|
156
|
+
if (raw.acceptedOpaque !== undefined) {
|
|
157
|
+
e.push(...acceptedOpaqueErrors(raw.acceptedOpaque, at));
|
|
158
|
+
if (raw.humanApproved !== true) e.push(`${at}: acceptedOpaque needs humanApproved: true`);
|
|
159
|
+
}
|
|
131
160
|
if (!Array.isArray(raw.steps)) e.push(`${at}: steps must be a list`);
|
|
132
161
|
else raw.steps.forEach((s, i) => e.push(...stepErrors(s, `${at} step ${i + 1}`)));
|
|
133
162
|
return e;
|
|
@@ -80,6 +80,7 @@ export function serializeLock(lock: ContractLock): string {
|
|
|
80
80
|
scenarios[id] = {
|
|
81
81
|
hash: e.hash, implementation: e.implementation, approvedAt: e.approvedAt, decisionId: e.decisionId,
|
|
82
82
|
...(e.humanApproved === true ? { humanApproved: true as const } : {}),
|
|
83
|
+
...(e.acceptedOpaque?.length ? { acceptedOpaque: e.acceptedOpaque.map((a) => ({ step: a.step, sha: a.sha })) } : {}),
|
|
83
84
|
steps: e.steps.map(orderedStep),
|
|
84
85
|
};
|
|
85
86
|
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* identities asked of the registered `traceIdentity` resolver (driver-ui) when one is given,
|
|
9
9
|
* otherwise the cache, otherwise core's heuristic — `# identity low` marks those steps
|
|
10
10
|
* approved a scenario QA already approved (no `@proposed`) is never rewritten
|
|
11
|
+
* one id, one two recorded tests claiming one id (#962) refuse the derive — both named with
|
|
12
|
+
* test file:line, NOTHING written for the unit: keeping one would drop the other's contract
|
|
11
13
|
* safety redaction by field meaning, machine paths scrubbed, then the secret scanner and the
|
|
12
14
|
* semantic credential check over the exact text: any hit → NOTHING is written
|
|
13
15
|
*/
|
|
@@ -22,8 +24,10 @@ import { scrubKnownSecrets, scrubMachinePaths, secretScanHits, SecretHit } from
|
|
|
22
24
|
import { idOfTitle, splitFeatureBlocks } from './free-feature-blocks';
|
|
23
25
|
import { sensitiveLiteralHits } from './lock-secret-guard';
|
|
24
26
|
import { scenariosOfFeatureFile } from './normalize-feature-steps';
|
|
27
|
+
import { featureFilesFor } from '../harness/unit-paths';
|
|
25
28
|
import { CONTROL_CHARS, freeFeatureText, renderTraceSteps, scenarioBlockText } from './render-free-gherkin';
|
|
26
29
|
import { collectRuns, TraceRun } from './trace/collect-trace-evidence';
|
|
30
|
+
import { idsClaimedByTwoTests } from './trace/scenario-id-claims';
|
|
27
31
|
import { readIdentityCache } from './trace/identity-cache';
|
|
28
32
|
import type { TraceIdentityResolver } from './trace/target-identity';
|
|
29
33
|
import { identitiesFor, resolveWithDriver } from './trace/trace-identity-resolution';
|
|
@@ -35,9 +39,17 @@ export interface DeriveResult {
|
|
|
35
39
|
file: string;
|
|
36
40
|
written: boolean;
|
|
37
41
|
scenarios: Array<{ id: string; title: string; steps: number; low: number; opaque: number }>;
|
|
38
|
-
|
|
42
|
+
/** `approved: true` — skipped because QA already approved it (derive never rewrites one), not for want of a trace. */
|
|
43
|
+
skipped: Array<{ id: string; reason: string; approved?: true }>;
|
|
39
44
|
refused?: SecretHit[];
|
|
45
|
+
/**
|
|
46
|
+
* Ids two or more recorded tests claim (#962), each test as `file:line "title" (run record <path>)` —
|
|
47
|
+
* nothing was written. `staleRecords`: older test-results/ records that are a claim's only source.
|
|
48
|
+
*/
|
|
49
|
+
duplicateIds?: Array<{ id: string; tests: string[]; staleRecords?: string[] }>;
|
|
40
50
|
note?: string;
|
|
51
|
+
/** A derived id another `.feature` of the unit already uses (#939) — `contract check` and approve would trip on it later. */
|
|
52
|
+
warnings?: string[];
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
const MACHINE_PATH = /(?:\/Users|\/home)\/[^/\s"'`]+\/|[A-Za-z]:\\Users\\/;
|
|
@@ -58,13 +70,42 @@ function semanticHits(text: string, label: string): SecretHit[] {
|
|
|
58
70
|
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
59
71
|
}
|
|
60
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The derived ids another `.feature` of the unit already carries (#939): `contract check` reports an id
|
|
75
|
+
* used twice and approve refuses it, so say it now — when it is still one rename away — not at check.
|
|
76
|
+
*/
|
|
77
|
+
function reusedIdWarnings(unit: { name: string; dir: string }, freeFile: string, ids: string[]): string[] {
|
|
78
|
+
const owners = new Map<string, string>();
|
|
79
|
+
for (const f of featureFilesFor(unit.dir, unit.name)) {
|
|
80
|
+
if (path.resolve(f) === path.resolve(freeFile) || !fs.existsSync(f)) continue;
|
|
81
|
+
try {
|
|
82
|
+
for (const sc of scenariosOfFeatureFile(f)) if (!owners.has(sc.id)) owners.set(sc.id, path.basename(f));
|
|
83
|
+
} catch { /* an unparseable sibling is `contract check`'s to report */ }
|
|
84
|
+
}
|
|
85
|
+
return ids.filter((id) => owners.has(id)).map((id) =>
|
|
86
|
+
`${id} is already the ID of a scenario in ${owners.get(id)} — the derived scenario reuses it; give the free test (its title's leading ID, or its @tag) or that scenario its own ID, or \`sungen contract check\` reports it twice and approval refuses it`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Nothing was derived and nothing explains it away: no test of the unit left an admissible trace
|
|
91
|
+
* (#932). Not when the scenarios are approved (derive leaves those alone by design), up to date,
|
|
92
|
+
* refused, or the project is mobile (`note`).
|
|
93
|
+
*/
|
|
94
|
+
export function nothingDerived(r: DeriveResult): boolean {
|
|
95
|
+
return !r.note && !r.refused?.length && !r.duplicateIds?.length && r.scenarios.length === 0 && !r.skipped.some((s) => s.approved);
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
|
|
62
99
|
const unitBase = path.basename(i.unit.name);
|
|
63
100
|
const file = path.join(i.unit.dir, 'features', `${unitBase}.free.feature`);
|
|
64
101
|
const result: DeriveResult = { file, written: false, scenarios: [], skipped: [] };
|
|
65
102
|
if ((readCapabilities(i.projectRoot).platform ?? '').toLowerCase() === 'mobile') return { ...result, note: MOBILE_NOT_SUPPORTED };
|
|
66
103
|
const grep = i.grep ? new RegExp(i.grep, 'i') : null;
|
|
67
|
-
const found = collectRuns(i.projectRoot, (id, t) => (grep ? grep.test(`${id} ${t.title} ${t.file}`) : belongsToUnit(t.file, unitBase.toLowerCase())),
|
|
104
|
+
const found = collectRuns(i.projectRoot, (id, t) => (grep ? grep.test(`${id} ${t.title} ${t.file}`) : belongsToUnit(t.file, unitBase.toLowerCase())), null);
|
|
105
|
+
const claimed = idsClaimedByTwoTests(i.projectRoot, found.runs);
|
|
106
|
+
if (claimed.size) {
|
|
107
|
+
return { ...result, duplicateIds: [...claimed].sort(([a], [b]) => a.localeCompare(b, 'en')).map(([id, c]) => ({ id, tests: c.tests, ...(c.staleRecords.length ? { staleRecords: c.staleRecords } : {}) })) };
|
|
108
|
+
}
|
|
68
109
|
const byId = new Map<string, TraceRun[]>();
|
|
69
110
|
for (const r of found.runs) byId.set(r.scenarioId, [...(byId.get(r.scenarioId) ?? []), r]);
|
|
70
111
|
const admissible = (runs: TraceRun[]): TraceRun[] => runs.filter((r) => r.status === 'admissible' && r.trace)
|
|
@@ -79,7 +120,7 @@ export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
|
|
|
79
120
|
for (const id of [...byId.keys()].sort((a, b) => a.localeCompare(b, 'en'))) {
|
|
80
121
|
const runs = admissible(byId.get(id)!);
|
|
81
122
|
if (!runs.length) { result.skipped.push({ id, reason: byId.get(id)![0].reason ?? 'no admissible trace' }); continue; }
|
|
82
|
-
if (kept.some((b) => b.id === id && !b.proposed)) { result.skipped.push({ id, reason: 'approved — derive never rewrites an approved scenario; use `sungen contract check --trace`' }); continue; }
|
|
123
|
+
if (kept.some((b) => b.id === id && !b.proposed)) { result.skipped.push({ id, reason: 'approved — derive never rewrites an approved scenario; use `sungen contract check --trace`', approved: true }); continue; }
|
|
83
124
|
const secrets = new Set<string>();
|
|
84
125
|
const rendered = runs.map((r) => {
|
|
85
126
|
const steps = traceIntentSteps(r.trace!);
|
|
@@ -98,6 +139,8 @@ export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
|
|
|
98
139
|
result.scenarios.push({ id, title, steps: rendered[0].length, low: rendered[0].filter((s) => s.low).length, opaque: rendered[0].filter((s) => s.opaque).length });
|
|
99
140
|
}
|
|
100
141
|
if (!result.scenarios.length) return result;
|
|
142
|
+
const reused = reusedIdWarnings(i.unit, file, result.scenarios.map((x) => x.id));
|
|
143
|
+
if (reused.length) result.warnings = reused;
|
|
101
144
|
|
|
102
145
|
const text = freeFeatureText(unitBase, [...blocks.values()]);
|
|
103
146
|
const label = path.basename(file);
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* HEAD is read once (one `git ls-tree`, bounded by the caller's timeout). A unit with no lock in the
|
|
5
5
|
* working tree AND none in HEAD has no contract to break and is skipped without parsing anything.
|
|
6
6
|
* Every other unit gets its verdict rebuilt by buildContractVerdict — the same function `contract
|
|
7
|
-
* check` runs — and anything but `pass` blocks.
|
|
7
|
+
* check` runs — and anything but `pass` blocks. Its trace part gets the guard's ~3 s at most:
|
|
8
|
+
* SUNGEN_TRACE_VERDICT_BUDGET_MS can shorten that here, never lengthen it (#926). A lock deleted from the working tree still counts
|
|
8
9
|
* while HEAD holds it. If git FAILS in a checkout that has .git while a delivered unit has a lock,
|
|
9
10
|
* this throws (GitUnavailableError): the guard asks rather than calling an unverifiable lock clean.
|
|
10
11
|
*/
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import { buildContractVerdict, ContractUnit, VerdictValue } from './build-contract-verdict';
|
|
14
|
+
import { guardTraceBudgetMs } from './build-trace-conformance';
|
|
13
15
|
import { CLI_GIT_TIMEOUT_MS, GitUnavailableError, headSnapshot, posixRel } from './contract-git';
|
|
14
16
|
import { HARD_CLASSES } from './contract-schema';
|
|
15
17
|
import { lockPathFor } from './contracts-lock-file';
|
|
@@ -24,7 +26,7 @@ export function contractBlockers(projectRoot: string, units: ContractUnit[], opt
|
|
|
24
26
|
const out: ContractBlocker[] = [];
|
|
25
27
|
for (const unit of unique) {
|
|
26
28
|
if (!hasWorkingLock(unit) && !snap.blobs?.has(posixRel(projectRoot, lockPathFor(unit.dir)))) continue;
|
|
27
|
-
const v = buildContractVerdict(projectRoot, unit, { snapshot: snap });
|
|
29
|
+
const v = buildContractVerdict(projectRoot, unit, { snapshot: snap, traceBudgetMs: guardTraceBudgetMs() });
|
|
28
30
|
if (v.verdict === 'pass') continue;
|
|
29
31
|
const worst = v.scenarios.find((s) => HARD_CLASSES.has(s.class)) ?? v.scenarios.find((s) => s.class === 'hash-changed');
|
|
30
32
|
const headline = worst
|
|
@@ -9,13 +9,17 @@
|
|
|
9
9
|
* The decision goes first: a crash between the two leaves a record without a lock entry, never an
|
|
10
10
|
* entry pointing at nothing. Only `sungen contract approve` calls this after its TTY confirmation;
|
|
11
11
|
* the confirmation itself lives there (approve-contract.ts).
|
|
12
|
+
*
|
|
13
|
+
* `--accept-opaque` (#923, accept-opaque-steps.ts) writes the same kind of decision for ONE scenario,
|
|
14
|
+
* listing it at its hash WITH the opaque steps QA accepted (step + sha); its lock entry is rewritten to
|
|
15
|
+
* point at that decision and carries the same list, which verify-approval-decisions.ts cross-checks.
|
|
12
16
|
*/
|
|
13
17
|
import * as path from 'path';
|
|
14
18
|
import { appendDecision } from '../decisions/append-decision';
|
|
15
19
|
import { isValidScope } from '../decisions/decision-file';
|
|
16
20
|
import type { JsonValue } from '../decisions/decision-schema';
|
|
17
21
|
import { gitUserName } from '../decisions/decision-source';
|
|
18
|
-
import { ContractLock, ContractScenario, LOCK_SCHEMA } from './contract-schema';
|
|
22
|
+
import { AcceptedOpaqueStep, ContractLock, ContractScenario, LOCK_SCHEMA } from './contract-schema';
|
|
19
23
|
import { lockEntryFor, lockPathFor, serializeLock, writeLockFile } from './contracts-lock-file';
|
|
20
24
|
import { assertNoLockSecrets } from './lock-secret-guard';
|
|
21
25
|
|
|
@@ -30,6 +34,18 @@ export interface ApprovalWrite {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export const DEFAULT_APPROVAL_REASON = 'QA reviewed the normalized Gherkin contract';
|
|
37
|
+
export const DEFAULT_OPAQUE_REASON = 'QA reviewed the opaque step(s) sungen cannot compare';
|
|
38
|
+
|
|
39
|
+
export interface OpaqueAcceptanceWrite {
|
|
40
|
+
projectRoot: string;
|
|
41
|
+
unitDir: string;
|
|
42
|
+
unit: string;
|
|
43
|
+
/** The scenario as it reads now — its hash equals its approved lock entry's. */
|
|
44
|
+
scenario: ContractScenario;
|
|
45
|
+
accepted: AcceptedOpaqueStep[];
|
|
46
|
+
reason?: string;
|
|
47
|
+
lock: ContractLock;
|
|
48
|
+
}
|
|
33
49
|
|
|
34
50
|
/** The decision scope of a unit directory (`screens/login`, `api/flows/pay`…). */
|
|
35
51
|
export function scopeOfUnitDir(projectRoot: string, unitDir: string): string {
|
|
@@ -68,3 +84,27 @@ export function recordContractApproval(w: ApprovalWrite): { decisionId: string;
|
|
|
68
84
|
writeLockFile(lockPathFor(w.unitDir), lock);
|
|
69
85
|
return { decisionId: decision.entry.id, lock };
|
|
70
86
|
}
|
|
87
|
+
|
|
88
|
+
function lockWithAcceptance(w: OpaqueAcceptanceWrite, decisionId: string, approvedAt: string): ContractLock {
|
|
89
|
+
const entry = { ...lockEntryFor(w.scenario, decisionId, approvedAt), acceptedOpaque: w.accepted.map((a) => ({ step: a.step, sha: a.sha })) };
|
|
90
|
+
return { schema: LOCK_SCHEMA, scenarios: { ...w.lock.scenarios, [w.scenario.id]: entry } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function recordOpaqueAcceptance(w: OpaqueAcceptanceWrite): { decisionId: string; lock: ContractLock } {
|
|
94
|
+
const scope = scopeOfUnitDir(w.projectRoot, w.unitDir);
|
|
95
|
+
assertNoLockSecrets([w.scenario], serializeLock(lockWithAcceptance(w, 'D-0000000000000000', new Date().toISOString())));
|
|
96
|
+
const sc = w.scenario;
|
|
97
|
+
const accepted = w.accepted.map((a) => ({ step: a.step, sha: a.sha }));
|
|
98
|
+
const payload: Record<string, JsonValue> = {
|
|
99
|
+
unit: w.unit, scenarios: [{ id: sc.id, hash: sc.hash, implementation: sc.implementation, acceptedOpaque: accepted }], removed: [],
|
|
100
|
+
};
|
|
101
|
+
const decision = appendDecision(w.projectRoot, scope, {
|
|
102
|
+
kind: 'contract-approval', anchors: [], visibility: 'internal', valid_from: new Date().toISOString(), payload,
|
|
103
|
+
text: `Accepted the reviewed opaque step(s) ${[...new Set(accepted.map((a) => a.step))].join(', ')} of ${sc.id} (${w.unit})`,
|
|
104
|
+
reason: w.reason?.trim() || DEFAULT_OPAQUE_REASON,
|
|
105
|
+
source: { by: gitUserName(w.projectRoot), where: 'cli' },
|
|
106
|
+
});
|
|
107
|
+
const lock = lockWithAcceptance(w, decision.entry.id, decision.entry.valid_from);
|
|
108
|
+
writeLockFile(lockPathFor(w.unitDir), lock);
|
|
109
|
+
return { decisionId: decision.entry.id, lock };
|
|
110
|
+
}
|
|
@@ -22,6 +22,11 @@ import { isPageLevel } from './trace/trace-identity-resolution';
|
|
|
22
22
|
import type { IntentStep } from './trace/trace-steps';
|
|
23
23
|
|
|
24
24
|
export const CONTROL_CHARS = /[\u0000-\u001f\u007f\u2028\u2029]/g;
|
|
25
|
+
/** Text from a trace or report shown in one line of a finding: control characters dropped, at most `max` characters. */
|
|
26
|
+
export function clip(s: string, max = 120): string {
|
|
27
|
+
const t = s.replace(CONTROL_CHARS, '');
|
|
28
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
29
|
+
}
|
|
25
30
|
|
|
26
31
|
export interface ResolvedTarget { identity: TargetIdentity; facts?: TargetFacts }
|
|
27
32
|
export interface RenderedStep {
|
|
@@ -3,33 +3,56 @@
|
|
|
3
3
|
* resolver (driver-ui — a real browser) about every target of the unit's admissible traces and
|
|
4
4
|
* cache the answers (DOS-14.5). The verdict itself never calls a browser; it reads the cache, so
|
|
5
5
|
* the CLI and the Claude Code guard keep building the same verdict from the same files.
|
|
6
|
+
*
|
|
7
|
+
* What it reports is split for QA (#966): a WARNING only when something needs attention — no
|
|
8
|
+
* resolver, a mobile project, a target the browser could not name (its steps stay opaque). "Every
|
|
9
|
+
* target was already known" and "N new targets identified" are information, in plain words.
|
|
6
10
|
*/
|
|
7
|
-
import * as fs from 'fs';
|
|
8
11
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
9
12
|
import { readCapabilities } from '../harness/capability';
|
|
10
13
|
import { MOBILE_NOT_SUPPORTED } from './build-trace-conformance';
|
|
11
|
-
import {
|
|
12
|
-
import { contractFeatureFiles, loadContractScenarios } from './normalize-feature-steps';
|
|
14
|
+
import { loadContractScenarios } from './normalize-feature-steps';
|
|
13
15
|
import { collectScenarioEvidence } from './trace/collect-trace-evidence';
|
|
16
|
+
import { readIdentityCache } from './trace/identity-cache';
|
|
17
|
+
import { unitContract } from './trace/trace-freshness';
|
|
14
18
|
import type { TraceIdentityResolver } from './trace/target-identity';
|
|
15
19
|
import { resolveWithDriver } from './trace/trace-identity-resolution';
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
export interface TraceIdentityOutcome { warnings: string[]; info: string[] }
|
|
18
22
|
|
|
19
23
|
/** The first registered capability that can resolve trace identities (UI: driver-ui), if any. */
|
|
20
24
|
export function registeredTraceResolver(): TraceIdentityResolver | undefined {
|
|
21
25
|
return capabilityRegistry.all().find((c) => typeof c.traceIdentity === 'function')?.traceIdentity;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
export async function
|
|
28
|
+
export async function traceIdentityOutcome(projectRoot: string, unit: { name: string; dir: string }, resolver = registeredTraceResolver()): Promise<TraceIdentityOutcome> {
|
|
29
|
+
const none: TraceIdentityOutcome = { warnings: [], info: [] };
|
|
25
30
|
const traced = loadContractScenarios(unit.dir, unit.name).filter((s) => s.implementation !== 'compiled');
|
|
26
|
-
if (!traced.length) return
|
|
27
|
-
if ((readCapabilities(projectRoot).platform ?? '').toLowerCase() === 'mobile') return [MOBILE_NOT_SUPPORTED];
|
|
28
|
-
if (!resolver) return ['no trace identity resolver is registered (install @sungen/driver-ui) — every target identity stays low, so nothing can match'];
|
|
29
|
-
const
|
|
30
|
-
const evidence = collectScenarioEvidence(projectRoot, new Set(traced.map((s) => s.id)), notBefore);
|
|
31
|
+
if (!traced.length) return none;
|
|
32
|
+
if ((readCapabilities(projectRoot).platform ?? '').toLowerCase() === 'mobile') return { warnings: [MOBILE_NOT_SUPPORTED], info: [] };
|
|
33
|
+
if (!resolver) return { warnings: ['no trace identity resolver is registered (install @sungen/driver-ui) — every target identity stays low, so nothing can match'], info: [] };
|
|
34
|
+
const evidence = collectScenarioEvidence(projectRoot, new Set(traced.map((s) => s.id)), unitContract(projectRoot, unit));
|
|
31
35
|
const traces = [...evidence.runs.values()].flat().filter((r) => r.status === 'admissible' && r.trace).map((r) => r.trace!);
|
|
32
|
-
if (!traces.length) return
|
|
36
|
+
if (!traces.length) return none;
|
|
33
37
|
const r = await resolveWithDriver(projectRoot, traces, resolver);
|
|
34
|
-
|
|
38
|
+
const { cache } = readIdentityCache(projectRoot);
|
|
39
|
+
const total = r.keys.length;
|
|
40
|
+
const unnamed = r.keys.filter((k) => cache.entries.get(k)?.identityConfidence !== 'high').length;
|
|
41
|
+
const blocked = r.blockedRequests ? `; the replayed pages tried ${r.blockedRequests} request(s), all blocked` : '';
|
|
42
|
+
const known = total - r.asked;
|
|
43
|
+
const info = r.asked
|
|
44
|
+
? `trace identity: ${r.asked} new target(s) identified in a real browser (${r.resolver})${known ? `, ${known} already known` : ''}${blocked}`
|
|
45
|
+
: `trace identity: all ${total} target(s) were already identified (identity cache) — nothing new to resolve`;
|
|
46
|
+
const warnings = unnamed
|
|
47
|
+
? [`trace identity: ${unnamed} of ${total} target(s) could not be identified (no unique accessible name) — the steps on them stay opaque, for QA to review, until the test targets a unique, named element`]
|
|
48
|
+
: [];
|
|
49
|
+
return { warnings, info: [info] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What `contract check --trace` adds to the verdict's warnings: only what needs attention (#966).
|
|
54
|
+
* The plain-words information is `traceIdentityOutcome(...).info`.
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveUnitTraceIdentities(projectRoot: string, unit: { name: string; dir: string }, resolver = registeredTraceResolver()): Promise<string[]> {
|
|
57
|
+
return (await traceIdentityOutcome(projectRoot, unit, resolver)).warnings;
|
|
35
58
|
}
|