@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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a trace newer than the contract it is asked to prove? (DOS-14.6: a trace older than the
|
|
3
|
+
* `.feature` or the lock is refused.) Judged by CONTENT, never by file time alone (#967): a `touch`,
|
|
4
|
+
* a checkout or a fresh clone changes no byte, so it stales nothing; an edit after the run always does.
|
|
5
|
+
*
|
|
6
|
+
* schema 2 record the reporter hashed every `.feature` and `contracts.lock.yaml` under `qa/` when
|
|
7
|
+
* the run started. Each of the unit's contract files must hash NOW to what the run
|
|
8
|
+
* saw: one that changed since, or did not exist yet, is newer than the trace → stale
|
|
9
|
+
* schema 1 record an older reporter recorded no content, so the files are dated against the run's
|
|
10
|
+
* start (the record's `startedAt`): one last written before it is older than the run;
|
|
11
|
+
* one written after it still is when git holds it unchanged and the commit that last
|
|
12
|
+
* changed it is an ancestor of HEAD committed before the run started (its bytes ARE
|
|
13
|
+
* that commit's). Anything else — edited, uncommitted, untracked, committed after the
|
|
14
|
+
* run started, no git, git failed or slow — is stale: fail closed.
|
|
15
|
+
*
|
|
16
|
+
* Limit of the schema-1 path: a commit time is what the committer's clock (or GIT_COMMITTER_DATE) said,
|
|
17
|
+
* so a backdated commit passes the time check — time-based checks cannot see through that. Schema 2
|
|
18
|
+
* compares content and retires this path: re-running the free tests once with the current reporter is
|
|
19
|
+
* the remedy.
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from 'child_process';
|
|
22
|
+
import { createHash } from 'crypto';
|
|
23
|
+
import * as fs from 'fs';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import { lockPathFor } from '../contracts-lock-file';
|
|
26
|
+
import { contractFeatureFiles } from '../normalize-feature-steps';
|
|
27
|
+
import type { RunRecord } from './trace-run-record';
|
|
28
|
+
|
|
29
|
+
interface ContractFile { rel: string; sha: string; mtimeMs: number }
|
|
30
|
+
interface LastCommit { sha: string; ms: number }
|
|
31
|
+
/** A unit's contract as it is NOW — its `.feature` files and lock, read once per verdict. */
|
|
32
|
+
export interface UnitContract {
|
|
33
|
+
projectRoot: string;
|
|
34
|
+
files: ContractFile[];
|
|
35
|
+
/** Schema 1 only, asked of git at most once: project path → the last commit that changed each file git holds unchanged. */
|
|
36
|
+
committed?: Map<string, LastCommit> | null;
|
|
37
|
+
/** Schema 1 only: commit → is it an ancestor of HEAD (asked once per commit). */
|
|
38
|
+
ancestors?: Map<string, boolean>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Inside the Claude Code guard too: a slow git leaves a schema-1 trace stale rather than the hook waiting. */
|
|
42
|
+
const GIT_TIMEOUT_MS = 2_000;
|
|
43
|
+
const posix = (p: string): string => p.split(path.sep).join('/');
|
|
44
|
+
|
|
45
|
+
export function unitContract(projectRoot: string, unit: { name: string; dir: string }): UnitContract {
|
|
46
|
+
const root = path.resolve(projectRoot);
|
|
47
|
+
const lock = path.resolve(lockPathFor(unit.dir));
|
|
48
|
+
const files = [...(fs.existsSync(lock) ? [lock] : []), ...contractFeatureFiles(unit.dir, unit.name)].map((abs) => {
|
|
49
|
+
let sha = 'unreadable'; // never equal to a recorded sha256: an unreadable contract file is stale
|
|
50
|
+
let mtimeMs = Number.POSITIVE_INFINITY;
|
|
51
|
+
try { sha = createHash('sha256').update(fs.readFileSync(abs)).digest('hex'); mtimeMs = fs.statSync(abs).mtimeMs; } catch { /* stale */ }
|
|
52
|
+
return { rel: posix(path.relative(root, abs)), sha, mtimeMs };
|
|
53
|
+
});
|
|
54
|
+
return { projectRoot: root, files };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function git(cwd: string, args: string[]): string | null {
|
|
58
|
+
try { return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: GIT_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }).toString('utf8'); } catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Each contract file git holds unchanged → the last commit (first-parent history, merges included)
|
|
63
|
+
* that changed it, with its committer time. Two git calls for the whole unit; null when git cannot say.
|
|
64
|
+
* Paths are passed as literal pathspecs: no glob or `:(magic)` reading of a unit's file names.
|
|
65
|
+
*/
|
|
66
|
+
function committedTimes(c: UnitContract): Map<string, LastCommit> | null {
|
|
67
|
+
if (c.committed !== undefined) return c.committed;
|
|
68
|
+
const rels = c.files.map((f) => f.rel);
|
|
69
|
+
const changed = git(c.projectRoot, ['--literal-pathspecs', 'diff', '--name-only', '--relative', '-z', 'HEAD', '--', ...rels]);
|
|
70
|
+
const log = git(c.projectRoot, ['-c', 'core.quotePath=false', '--literal-pathspecs', 'log', '--first-parent', '-m', '-n', '500', '--format=%x01%H %ct', '--name-only', '--relative', '--', ...rels]);
|
|
71
|
+
if (changed === null || log === null) return (c.committed = null);
|
|
72
|
+
const dirty = new Set(changed.split('\0').filter(Boolean));
|
|
73
|
+
const times = new Map<string, LastCommit>();
|
|
74
|
+
let at: LastCommit | null = null;
|
|
75
|
+
for (const raw of log.split('\n')) {
|
|
76
|
+
const line = raw.trim();
|
|
77
|
+
if (raw.startsWith('\x01')) {
|
|
78
|
+
const [sha, ct] = raw.slice(1).trim().split(' ');
|
|
79
|
+
at = /^[0-9a-f]{40,64}$/.test(sha ?? '') && Number(ct) > 0 ? { sha, ms: Number(ct) * 1000 } : null;
|
|
80
|
+
} else if (line && at && !times.has(line) && !dirty.has(line)) times.set(line, at);
|
|
81
|
+
}
|
|
82
|
+
return (c.committed = times);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Is `sha` an ancestor of HEAD? First-parent history says so already; asserted, never assumed. */
|
|
86
|
+
function isAncestorOfHead(c: UnitContract, sha: string): boolean {
|
|
87
|
+
if (!c.ancestors) c.ancestors = new Map();
|
|
88
|
+
if (!c.ancestors.has(sha)) c.ancestors.set(sha, git(c.projectRoot, ['merge-base', '--is-ancestor', sha, 'HEAD']) !== null);
|
|
89
|
+
return c.ancestors.get(sha)!;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Why a trace bound to `record` is older than the unit's contract — null when it is not. */
|
|
93
|
+
export function staleReason(c: UnitContract, record: RunRecord): string | null {
|
|
94
|
+
if (record.contract) {
|
|
95
|
+
const f = c.files.find((x) => record.contract![x.rel] !== x.sha);
|
|
96
|
+
if (!f) return null;
|
|
97
|
+
const why = record.contract[f.rel] === undefined ? 'it did not exist when the run started' : 'its content changed after the run';
|
|
98
|
+
return `the trace is older than ${f.rel}: ${why} (stale) — re-run the test`;
|
|
99
|
+
}
|
|
100
|
+
const runStart = Date.parse(record.startedAt); // unreadable → nothing is "before the run": stale
|
|
101
|
+
const late = c.files.filter((f) => !(f.mtimeMs <= runStart));
|
|
102
|
+
if (!late.length) return null;
|
|
103
|
+
const committed = committedTimes(c);
|
|
104
|
+
const vouched = (rel: string): boolean => {
|
|
105
|
+
const last = committed?.get(rel);
|
|
106
|
+
return !!last && last.ms <= runStart && isAncestorOfHead(c, last.sha);
|
|
107
|
+
};
|
|
108
|
+
const f = late.find((x) => !vouched(x.rel));
|
|
109
|
+
return f ? `the trace is older than ${f.rel}: it was written after the run, and no commit from before the run holds its bytes (stale) — re-run the test` : null;
|
|
110
|
+
}
|
|
@@ -51,13 +51,16 @@ export function identityRequestsFor(trace: ParsedTrace): TraceIdentityRequest[]
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Ask the driver about every not-yet-cached target of these traces; rewrite the cache. */
|
|
54
|
-
|
|
54
|
+
/** `keys`: every target of the given traces (cached or asked) — what the caller reports on. */
|
|
55
|
+
export async function resolveWithDriver(projectRoot: string, traces: ParsedTrace[], resolver: TraceIdentityResolver): Promise<{ asked: number; resolver: string; blockedRequests: number; keys: string[] }> {
|
|
55
56
|
const { cache } = readIdentityCache(projectRoot);
|
|
56
57
|
// The cache is project-wide (every unit's traces): answers are only ever ADDED, never pruned here.
|
|
57
58
|
const keep = new Map<string, CachedIdentity>(cache.entries);
|
|
59
|
+
const all = traces.flatMap(identityRequestsFor);
|
|
60
|
+
const keys = [...new Set(all.map((req) => req.key))];
|
|
58
61
|
const requests = new Map<string, TraceIdentityRequest>();
|
|
59
|
-
for (const req of
|
|
60
|
-
if (!requests.size) return { asked: 0, resolver: cache.resolver, blockedRequests: 0 };
|
|
62
|
+
for (const req of all) if (!keep.has(req.key)) requests.set(req.key, req);
|
|
63
|
+
if (!requests.size) return { asked: 0, resolver: cache.resolver, blockedRequests: 0, keys };
|
|
61
64
|
const answer = await resolver({ requests: [...requests.values()] });
|
|
62
65
|
if (!answer || !Array.isArray(answer.results)) throw new Error('the trace identity resolver returned no results');
|
|
63
66
|
if (!TRUSTED_RESOLVER.test(String(answer.resolver)) || typeof answer.browser !== 'string' || !answer.browser) {
|
|
@@ -70,5 +73,5 @@ export async function resolveWithDriver(projectRoot: string, traces: ParsedTrace
|
|
|
70
73
|
keep.set(key, { ...rest, browser: answer.browser.slice(0, 100) });
|
|
71
74
|
}
|
|
72
75
|
writeIdentityCache(projectRoot, answer.resolver, keep);
|
|
73
|
-
return { asked: requests.size, resolver: String(answer.resolver), blockedRequests: Number(answer.blockedRequests) || 0 };
|
|
76
|
+
return { asked: requests.size, resolver: String(answer.resolver), blockedRequests: Number(answer.blockedRequests) || 0, keys };
|
|
74
77
|
}
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* the code that produced it live, so an unknown key, a wrong type or a malformed hash means the
|
|
5
5
|
* record is not trusted at all (every trace it would vouch for is unbound).
|
|
6
6
|
*
|
|
7
|
-
* Found by convention: `<project>/test-results/sungen-run-record*.json`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Found by convention: `<project>/test-results-free/sungen-run-record*.json` — the free-test config's
|
|
8
|
+
* own outputDir (#941) — or `<project>/test-results/sungen-run-record*.json` (one per run or shard).
|
|
9
|
+
* Playwright wipes its outputDir at the start of every run, so a record and its traces share a
|
|
10
|
+
* lifetime; the free run has its own so the compiled suite's next run cannot delete its traces.
|
|
11
|
+
*
|
|
12
|
+
* Schema 2 (#967) adds `contract`: the sha256 of every `.feature` and `contracts.lock.yaml` under `qa/`
|
|
13
|
+
* when the run started — what trace freshness compares by content. Schema 1 (an older reporter) has
|
|
14
|
+
* none; its traces are dated instead (trace-freshness.ts).
|
|
10
15
|
*/
|
|
11
16
|
import * as fs from 'fs';
|
|
12
17
|
import * as path from 'path';
|
|
@@ -23,21 +28,29 @@ export interface RunRecord {
|
|
|
23
28
|
shard: string | null;
|
|
24
29
|
report: string | null;
|
|
25
30
|
specs: Record<string, string>;
|
|
31
|
+
/** Schema 2: project path → sha256 of each contract file at run start. null: schema 1 (no content recorded). */
|
|
32
|
+
contract: Record<string, string> | null;
|
|
26
33
|
attempts: RunRecordAttempt[];
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
const
|
|
36
|
+
const TOP_V1: ReadonlySet<string> = new Set(['schema', 'tool', 'runId', 'startedAt', 'playwrightVersion', 'shard', 'report', 'specs', 'attempts']);
|
|
37
|
+
const TOP_V2: ReadonlySet<string> = new Set([...TOP_V1, 'contract']);
|
|
30
38
|
const ATTEMPT = new Set(['testId', 'file', 'title', 'retry', 'status', 'startTime', 'duration', 'trace']);
|
|
31
39
|
const SHA = /^[0-9a-f]{64}$/;
|
|
32
40
|
const isObj = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
/** The free / impl:code run's own outputDir (the documented free-test config); compiled runs clear `test-results/`. */
|
|
43
|
+
export const FREE_RUN_DIR = 'test-results-free';
|
|
44
|
+
/** Where run records are read, in this order: the free run's dir, then Playwright's default (older configs). */
|
|
45
|
+
export const RUN_RECORD_DIRS: readonly string[] = [FREE_RUN_DIR, 'test-results'];
|
|
35
46
|
|
|
36
47
|
export function findRunRecords(projectRoot: string): string[] {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
return RUN_RECORD_DIRS.flatMap((rel) => {
|
|
49
|
+
const dir = path.join(projectRoot, rel);
|
|
50
|
+
let names: string[];
|
|
51
|
+
try { names = fs.readdirSync(dir); } catch { return []; }
|
|
52
|
+
return names.filter((n) => /^sungen-run-record(?:\.[A-Za-z0-9._-]+)?\.json$/.test(n)).sort().map((n) => path.join(dir, n));
|
|
53
|
+
});
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
function attemptOf(raw: unknown, i: number): RunRecordAttempt {
|
|
@@ -54,21 +67,30 @@ function attemptOf(raw: unknown, i: number): RunRecordAttempt {
|
|
|
54
67
|
return { testId, file, title, retry: retry as number, status, startTime, duration, trace: trace as RunRecordAttempt['trace'] };
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
/** Read and check one record; throws an Error naming what is wrong. */
|
|
58
|
-
export function readRunRecord(file: string): RunRecord {
|
|
70
|
+
/** Read and check one record (`text`: its bytes, already read once by the caller); throws an Error naming what is wrong. */
|
|
71
|
+
export function readRunRecord(file: string, text?: string): RunRecord {
|
|
59
72
|
let raw: unknown;
|
|
60
|
-
try { raw = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`not readable JSON (${(e as Error).message.split('\n')[0]})`); }
|
|
73
|
+
try { raw = JSON.parse(text ?? fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`not readable JSON (${(e as Error).message.split('\n')[0]})`); }
|
|
61
74
|
if (!isObj(raw)) throw new Error('not an object');
|
|
62
|
-
|
|
75
|
+
if (raw.tool === 'sungen-trace-run-record' && typeof raw.schema === 'number' && raw.schema > 2) {
|
|
76
|
+
// A newer driver-ui wrote it (mixed versions): say what to upgrade, not just "not a record".
|
|
77
|
+
throw new Error(`record schema ${raw.schema} is newer than this sungen — upgrade @sun-asterisk/sungen (core before or together with driver-ui)`);
|
|
78
|
+
}
|
|
79
|
+
const keys = raw.schema === 2 ? TOP_V2 : raw.schema === 1 ? TOP_V1 : null;
|
|
80
|
+
if (!keys || raw.tool !== 'sungen-trace-run-record') throw new Error('not a sungen run record (schema 1 or 2)');
|
|
81
|
+
const extra = Object.keys(raw).find((k) => !keys.has(k));
|
|
63
82
|
if (extra) throw new Error(`unknown key "${extra}"`);
|
|
64
|
-
if (raw.schema !== 1 || raw.tool !== 'sungen-trace-run-record') throw new Error('not a sungen run record (schema 1)');
|
|
65
83
|
if (typeof raw.runId !== 'string' || !raw.runId || typeof raw.startedAt !== 'string' || typeof raw.playwrightVersion !== 'string') throw new Error('runId, startedAt and playwrightVersion must be strings');
|
|
66
84
|
if (raw.shard !== null && typeof raw.shard !== 'string') throw new Error('shard must be a string or null');
|
|
67
85
|
if (raw.report !== null && typeof raw.report !== 'string') throw new Error('report must be a string or null');
|
|
68
86
|
if (!isObj(raw.specs) || !Object.values(raw.specs).every((v) => typeof v === 'string' && (v === '' || SHA.test(v)))) throw new Error('specs must map files to sha256');
|
|
87
|
+
if (raw.schema === 2 && !(isObj(raw.contract) && Object.values(raw.contract).every((v) => typeof v === 'string' && SHA.test(v)))) {
|
|
88
|
+
throw new Error('contract must map contract files to sha256 (schema 2)');
|
|
89
|
+
}
|
|
69
90
|
if (!Array.isArray(raw.attempts)) throw new Error('attempts must be a list');
|
|
70
91
|
return {
|
|
71
92
|
file, runId: raw.runId, startedAt: raw.startedAt, playwrightVersion: raw.playwrightVersion, shard: raw.shard as string | null,
|
|
72
|
-
report: raw.report as string | null, specs: raw.specs as Record<string, string>,
|
|
93
|
+
report: raw.report as string | null, specs: raw.specs as Record<string, string>,
|
|
94
|
+
contract: raw.schema === 2 ? raw.contract as Record<string, string> : null, attempts: raw.attempts.map(attemptOf),
|
|
73
95
|
};
|
|
74
96
|
}
|
|
@@ -70,7 +70,7 @@ export function traceIntentSteps(trace: ParsedTrace): IntentStep[] {
|
|
|
70
70
|
const runner = c.stepId ? steps.get(c.stepId) : undefined;
|
|
71
71
|
out.push({
|
|
72
72
|
kind, method: c.method, callId: c.callId, params: c.params, group: groupOf(steps, c.stepId),
|
|
73
|
-
location: firstFrame(runner?.stack) ?? firstFrame(stacks.get(c.part)?.get(c.
|
|
73
|
+
location: firstFrame(runner?.stack) ?? firstFrame(stacks.get(c.part)?.get(c.callId)),
|
|
74
74
|
...(verbatim ? { verbatim } : {}), startTime: c.startTime,
|
|
75
75
|
});
|
|
76
76
|
}
|
|
@@ -50,8 +50,12 @@ export function traceZipLimits(env: Record<string, string | undefined> = process
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Trace format versions this reader was verified against: 8 = Playwright 1.57, 9 = 1.63, 10 = 1.64
|
|
55
|
+
* (one shared call id for a library call and its runner step; stacks keyed by that id). Anything
|
|
56
|
+
* else — a newer format included — is refused until it has been checked the same way.
|
|
57
|
+
*/
|
|
58
|
+
export const SUPPORTED_TRACE_VERSIONS: ReadonlySet<number> = new Set([8, 9, 10]);
|
|
55
59
|
|
|
56
60
|
export type TraceRejectCode =
|
|
57
61
|
| 'zip.too-large' | 'zip.too-many-entries' | 'zip.entry-too-large' | 'zip.total-too-large' | 'zip.ratio'
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Every approved lock entry must be vouched for by its decision (DOS-13.2, DOS-14.4, review M1):
|
|
3
3
|
* `qa/<kind>/<unit>/decisions/<decisionId>.yaml`, COMMITTED in git HEAD (outside git: on disk),
|
|
4
4
|
* intact (schema, id = content, entry_hash = entry), of kind `contract-approval`, whose payload lists
|
|
5
|
-
* this scenario at this hash
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* this scenario at this hash — with exactly the opaque steps the entry says QA accepted (#923) — and
|
|
6
|
+
* whose `valid_from` is the entry's `approvedAt`. A lock line copied from elsewhere, re-pointed at
|
|
7
|
+
* another decision, backdated, or given an acceptance nobody recorded fails here — `unverified`,
|
|
8
|
+
* reported under contract.lock-uncommitted (hard).
|
|
8
9
|
*
|
|
9
10
|
* Only entries that are otherwise identical to HEAD are checked: an entry already added, changed or
|
|
10
11
|
* tampered is a hard finding on its own.
|
|
@@ -14,6 +15,7 @@ import { computeDecisionId, computeEntryHash } from '../decisions/decision-ident
|
|
|
14
15
|
import { DecisionEntry, storedEntryErrors } from '../decisions/decision-schema';
|
|
15
16
|
import { safeYamlParse } from '../utils/safe-yaml-parse';
|
|
16
17
|
import { HeadSnapshot, headFileText, posixRel, readTextOrNull } from './contract-git';
|
|
18
|
+
import { sameJson } from './contract-hash';
|
|
17
19
|
import { ContractLock, isHumanApproved, LockEntry } from './contract-schema';
|
|
18
20
|
import type { UncommittedEntry } from './lock-vs-git-head';
|
|
19
21
|
|
|
@@ -38,8 +40,10 @@ function loadDecision(projectRoot: string, unitDir: string, id: string, snap: He
|
|
|
38
40
|
|
|
39
41
|
function mismatch(scenario: string, lockEntry: LockEntry, d: DecisionEntry): string | null {
|
|
40
42
|
if (d.kind !== 'contract-approval') return `decision ${d.id} is a ${d.kind}, not a contract-approval`;
|
|
41
|
-
const listed = Array.isArray(d.payload?.scenarios) ? d.payload!.scenarios as Array<{ id?: unknown; hash?: unknown }> : [];
|
|
42
|
-
|
|
43
|
+
const listed = Array.isArray(d.payload?.scenarios) ? d.payload!.scenarios as Array<{ id?: unknown; hash?: unknown; acceptedOpaque?: unknown }> : [];
|
|
44
|
+
const item = listed.find((s) => s && s.id === scenario && s.hash === lockEntry.hash);
|
|
45
|
+
if (!item) return `decision ${d.id} does not approve ${scenario} at hash ${lockEntry.hash}`;
|
|
46
|
+
if (!sameJson(item.acceptedOpaque ?? [], lockEntry.acceptedOpaque ?? [])) return `decision ${d.id} does not accept the opaque step(s) the lock entry of ${scenario} lists (acceptedOpaque)`;
|
|
43
47
|
if (Date.parse(lockEntry.approvedAt) !== Date.parse(d.valid_from)) return `approvedAt ${lockEntry.approvedAt} is not decision ${d.id}'s valid_from ${d.valid_from}`;
|
|
44
48
|
return null;
|
|
45
49
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* CLI runs on process.stdin/stderr is the code a test drives with a simulated TTY.
|
|
4
4
|
*
|
|
5
5
|
* waive `sungen journey --screen <u> --waive <OB> --reason …` phrase: the obligation id
|
|
6
|
+
* signoff `sungen journey --screen <u> --signoff [--by <name>]` phrase: SIGN-OFF (review W3)
|
|
6
7
|
* decide `sungen journey [--screen <u>] --decide <kind> --text … --reason … [--anchor …]` phrase: the kind
|
|
7
8
|
* migration a leftover write-ahead sentinel → REVERT or KEEP (takumi T6)
|
|
8
9
|
*
|
|
@@ -16,13 +17,13 @@
|
|
|
16
17
|
import * as path from 'path';
|
|
17
18
|
import { assertNoPendingMigration, pendingMigrationState, PendingMigration, readPendingMigration, resolvePendingMigration, sentinelPath } from '../basis/migration-sentinel';
|
|
18
19
|
import { parseAnchorId } from '../basis/anchor-grammar';
|
|
19
|
-
import { JourneyReport, runJourney, waive } from '../harness/journey';
|
|
20
|
+
import { assertCanSignOff, assertWaivable, JourneyReport, runJourney, signoff, signoffSigner, waive } from '../harness/journey';
|
|
20
21
|
import { reportSlug } from '../harness/unit-paths';
|
|
21
22
|
import { appendDecision, AppendResult } from './append-decision';
|
|
22
23
|
import { PROJECT_SCOPE, unitDecisionScope } from './decision-file';
|
|
23
24
|
import { DecisionDraft, DecisionKind, draftErrors, JsonValue } from './decision-schema';
|
|
24
25
|
import { gitUserName } from './decision-source';
|
|
25
|
-
import { askMigrationChoice, ConfirmationIO, formatCommand, requireTypedConfirmation } from './human-confirmation';
|
|
26
|
+
import { askMigrationChoice, ConfirmationIO, formatCommand, requireInteractive, requireTypedConfirmation } from './human-confirmation';
|
|
26
27
|
import { readDecisions } from './read-decisions';
|
|
27
28
|
import { overridePayloadErrors } from '../harness/gates/gate-override-validation';
|
|
28
29
|
|
|
@@ -30,8 +31,7 @@ export interface WaiveInput { projectRoot: string; unit: string; obligation: str
|
|
|
30
31
|
|
|
31
32
|
export async function waiveWithConfirmation(i: WaiveInput): Promise<JourneyReport> {
|
|
32
33
|
if (!i.reason || !i.reason.trim()) throw new Error('A reason is required to waive (anti-amnesia: a waiver must record WHY). Use --reason "...".');
|
|
33
|
-
|
|
34
|
-
if (!valid.includes(i.obligation)) throw new Error(`Unknown obligation "${i.obligation}". Valid: ${valid.join(', ')}`);
|
|
34
|
+
assertWaivable(runJourney(i.projectRoot, i.unit), i.unit, i.obligation);
|
|
35
35
|
await requireTypedConfirmation({
|
|
36
36
|
what: `waive ${i.obligation} for ${i.unit}`,
|
|
37
37
|
phrase: i.obligation,
|
|
@@ -42,6 +42,27 @@ export async function waiveWithConfirmation(i: WaiveInput): Promise<JourneyRepor
|
|
|
42
42
|
return waive(i.projectRoot, i.unit, i.obligation, i.reason);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface SignoffInput { projectRoot: string; unit: string; by?: string; io: ConfirmationIO }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The single human gate (DOS §3.4, S5), typed by a person on a terminal: the gate suggests it, so an
|
|
49
|
+
* agent must not be able to give it. Refused before the prompt when the board cannot be signed off.
|
|
50
|
+
*/
|
|
51
|
+
export async function signoffWithConfirmation(i: SignoffInput): Promise<JourneyReport> {
|
|
52
|
+
const what = `sign off ${i.unit} (the single human gate before delivery)`;
|
|
53
|
+
const command = formatCommand(['sungen', 'journey', '--screen', i.unit, '--signoff', ...(i.by ? ['--by', i.by] : [])]);
|
|
54
|
+
requireInteractive(i.io, what, command); // before the board is read: a refused sign-off writes nothing
|
|
55
|
+
const { signer, gitUser } = signoffSigner(i.projectRoot, i.unit, i.by); // nobody to name → refused, no prompt
|
|
56
|
+
const before = runJourney(i.projectRoot, i.unit);
|
|
57
|
+
assertCanSignOff(before);
|
|
58
|
+
await requireTypedConfirmation({
|
|
59
|
+
what, phrase: 'SIGN-OFF', command,
|
|
60
|
+
summary: [`by: ${signer}${signer !== gitUser ? ` (recorded by git user ${gitUser})` : ''}`, `${before.needsYou.length} item(s) in the review queue — you have reviewed them`, 'it goes stale as soon as the audit changes (sign off again then)'],
|
|
61
|
+
io: i.io,
|
|
62
|
+
});
|
|
63
|
+
return signoff(i.projectRoot, i.unit, i.by);
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
/** Kinds `--decide` records. The others have a command of their own that computes their payload. */
|
|
46
67
|
export const DECIDE_KINDS: readonly DecisionKind[] = ['gate-override', 'clarification'];
|
|
47
68
|
const OWN_COMMAND: Partial<Record<DecisionKind, string>> = {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* checked when read — an unknown key (a stored `superseded_by`, say) makes the entry invalid, because
|
|
9
9
|
* a field outside the id's content could carry meaning the id does not vouch for.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
/** `triage-classification`: a red classified by hand, `sungen triage --case --as` (#928, graph/manual-triage-decisions.ts). */
|
|
12
|
+
export const DECISION_KINDS = ['waiver', 'gate-override', 'contract-approval', 'viewpoint-acceptance', 'clarification', 'triage-classification'] as const;
|
|
12
13
|
export type DecisionKind = typeof DECISION_KINDS[number];
|
|
13
14
|
export type DecisionVisibility = 'internal' | 'client';
|
|
14
15
|
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* did not. Outside git, or for a file git does not track, nothing is trusted. Never throws.
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync } from 'child_process';
|
|
9
|
+
import * as fs from 'fs';
|
|
9
10
|
import * as path from 'path';
|
|
10
11
|
|
|
11
12
|
/** stdout of a git command run in `cwd`, or null when git fails (no repo, no HEAD, untracked path…). */
|
|
@@ -17,6 +18,49 @@ export function gitOutput(cwd: string, args: string[]): string | null {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The text of each `<rev>:<path>` spec, read by ONE `git cat-file --batch` in `cwd` (`./path` is relative
|
|
23
|
+
* to it): null where git holds no such file; null for the whole call when git fails.
|
|
24
|
+
*/
|
|
25
|
+
export function gitFileTexts(cwd: string, specs: string[]): Array<string | null> | null {
|
|
26
|
+
if (specs.length === 0) return [];
|
|
27
|
+
if (specs.some((s) => /[\r\n]/.test(s))) return null;
|
|
28
|
+
let out: Buffer;
|
|
29
|
+
try {
|
|
30
|
+
out = execFileSync('git', ['cat-file', '--batch'], { cwd, input: `${specs.join('\n')}\n`, stdio: ['pipe', 'pipe', 'ignore'], maxBuffer: 256 * 1024 * 1024, timeout: 60000 });
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Each answer: `<oid> <type> <size>\n<content>\n`, or `<spec> missing\n` (also `ambiguous`).
|
|
35
|
+
const texts: Array<string | null> = [];
|
|
36
|
+
let at = 0;
|
|
37
|
+
for (let i = 0; i < specs.length; i++) {
|
|
38
|
+
const eol = out.indexOf(0x0a, at);
|
|
39
|
+
if (eol < 0) return null;
|
|
40
|
+
const header = out.subarray(at, eol).toString('utf8').split(' ');
|
|
41
|
+
at = eol + 1;
|
|
42
|
+
if (header.length !== 3 || !/^\d+$/.test(header[2])) { texts.push(null); continue; }
|
|
43
|
+
const size = Number(header[2]);
|
|
44
|
+
texts.push(header[1] === 'blob' ? out.subarray(at, at + size).toString('utf8') : null);
|
|
45
|
+
at += size + 1;
|
|
46
|
+
}
|
|
47
|
+
return texts;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether `cwd` is in a shallow clone. git < 2.15 does not know `--is-shallow-repository` and echoes it
|
|
52
|
+
* back, so only an exact `true` / `false` is an answer; otherwise the `shallow` file in the git directory
|
|
53
|
+
* (common dir, or the git dir) decides, and when not even that can be found the clone is taken as shallow
|
|
54
|
+
* — its history is then unknown, which fails closed for the callers that ask.
|
|
55
|
+
*/
|
|
56
|
+
export function isShallowRepository(cwd: string): boolean {
|
|
57
|
+
const answer = gitOutput(cwd, ['rev-parse', '--is-shallow-repository'])?.trim();
|
|
58
|
+
if (answer === 'true' || answer === 'false') return answer === 'true';
|
|
59
|
+
const dirs = ['--git-common-dir', '--git-dir'].map((flag) => gitOutput(cwd, ['rev-parse', flag])?.trim())
|
|
60
|
+
.filter((d): d is string => !!d && !d.startsWith('-'));
|
|
61
|
+
return dirs.length === 0 || dirs.some((d) => fs.existsSync(path.join(path.resolve(cwd, d), 'shallow')));
|
|
62
|
+
}
|
|
63
|
+
|
|
20
64
|
/** The committed text of `abs` (a file inside `projectRoot`), or null when git HEAD has no such file. */
|
|
21
65
|
export function committedText(projectRoot: string, abs: string): string | null {
|
|
22
66
|
const rel = path.relative(projectRoot, abs).split(path.sep).join('/');
|
|
@@ -79,6 +79,11 @@ function refuseWithoutTty(what: string, command: string): HumanConfirmationRefus
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Refuse up front, before any state is read or written, when no human is at a terminal. */
|
|
83
|
+
export function requireInteractive(io: ConfirmationIO, what: string, command: string): void {
|
|
84
|
+
if (!isInteractive(io)) throw refuseWithoutTty(what, command);
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
/** A fresh, hard-to-guess suffix for one prompt. */
|
|
83
88
|
export const confirmationNonce = (): string => randomBytes(3).toString('hex').toUpperCase();
|
|
84
89
|
|
|
@@ -80,9 +80,13 @@ export function planWaiverSync(projectRoot: string, unit: string, slug: string,
|
|
|
80
80
|
return { scope, waivers, untrusted, stateWaivers, imports, markDone: !!scope && untrusted.length === 0 };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** The command a human types to give (or confirm) a waiver — the board's line and `sungen next`'s "Waiting for you" (#954). */
|
|
84
|
+
export const waiveCommand = (unit: string, obligation: string, reason: string): string =>
|
|
85
|
+
formatCommand(['sungen', 'journey', '--screen', unit, '--waive', obligation, '--reason', reason]);
|
|
86
|
+
|
|
83
87
|
/** The needs-you line for a waiver that is not in git HEAD: not counted, and how a human confirms it. */
|
|
84
88
|
export function untrustedWaiverText(unit: string, slug: string, title: string, u: UntrustedWaiver): string {
|
|
85
|
-
const confirm =
|
|
89
|
+
const confirm = waiveCommand(unit, u.obligation, u.waiver.reason);
|
|
86
90
|
return `⚠️ Waiver on "${title}" is UNTRUSTED — not counted (decision.in-run: it is in .sungen/journey/${slug}.state.json `
|
|
87
91
|
+ `but not in git HEAD). If it is yours, confirm it on a terminal: ${confirm}`;
|
|
88
92
|
}
|
|
@@ -125,11 +129,18 @@ export function commitWaiverSync(projectRoot: string, unit: string, slug: string
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Record a waiver given now on the CLI; supersedes the waiver in force for that obligation, if any.
|
|
134
|
+
* The same waiver again records nothing — unless the one in force was imported (`where: migrated`):
|
|
135
|
+
* re-confirming it on a terminal is how a human vouches for a migrated waiver its legacy record cannot
|
|
136
|
+
* (decision.in-run), so it always writes the `cli` decision that supersedes it (#925).
|
|
137
|
+
*/
|
|
129
138
|
export function recordWaiver(projectRoot: string, unit: string, obligation: string, w: LegacyWaiver): AppendResult {
|
|
130
139
|
const scope = unitDecisionScope(projectRoot, unit);
|
|
131
140
|
if (!scope) throw new Error(`Cannot record a waiver: "${unit}" does not name exactly one unit under qa/screens, qa/flows or qa/api.`);
|
|
132
141
|
const current = effectiveWaiverEntries(readDecisions(projectRoot, scope, { kind: 'waiver' }).entries).get(obligation);
|
|
133
|
-
if (current && sameWaiver(asLegacyWaiver(current), w))
|
|
142
|
+
if (current && current.source?.where !== 'migrated' && sameWaiver(asLegacyWaiver(current), w)) {
|
|
143
|
+
return { status: 'already-recorded', entry: current, path: current.path };
|
|
144
|
+
}
|
|
134
145
|
return appendDecision(projectRoot, scope, waiverDraft(obligation, w, { by: gitUserName(projectRoot), where: 'cli' }, current?.id));
|
|
135
146
|
}
|
|
@@ -439,12 +439,12 @@ export function renderCsv(summary: ScreenSummary, rows: TestCaseRow[], specLink:
|
|
|
439
439
|
* multi-locale delivery aggregator. Pass an empty string to force the
|
|
440
440
|
* base (no suffix) variant.
|
|
441
441
|
*/
|
|
442
|
-
export function writeCsv(cwd: string, screen: string, csvContent: string, locale?: string | null): string {
|
|
442
|
+
export function writeCsv(cwd: string, screen: string, csvContent: string, locale?: string | null, opts: DeliverableFileOptions = {}): string {
|
|
443
443
|
const outDir = path.join(cwd, 'qa', 'deliverables');
|
|
444
444
|
if (!fs.existsSync(outDir)) {
|
|
445
445
|
fs.mkdirSync(outDir, { recursive: true });
|
|
446
446
|
}
|
|
447
|
-
const outPath = path.join(outDir, `${deliverableBasename(screen, locale)}.csv`);
|
|
447
|
+
const outPath = path.join(outDir, `${deliverableBasename(screen, locale, opts)}.csv`);
|
|
448
448
|
fs.writeFileSync(outPath, csvContent, 'utf-8');
|
|
449
449
|
return outPath;
|
|
450
450
|
}
|
|
@@ -455,8 +455,16 @@ export function writeCsv(cwd: string, screen: string, csvContent: string, locale
|
|
|
455
455
|
* locale is given (or `SUNGEN_ENV` is set and no explicit override). Pass
|
|
456
456
|
* `null` / empty string to force the base variant.
|
|
457
457
|
*/
|
|
458
|
-
export function deliverableBasename(screen: string, locale?: string | null): string {
|
|
458
|
+
export function deliverableBasename(screen: string, locale?: string | null, opts: DeliverableFileOptions = {}): string {
|
|
459
459
|
const effective = locale !== undefined ? locale : process.env.SUNGEN_ENV;
|
|
460
|
-
return effective ? `${screen}-testcases.${effective}` : `${screen}-testcases`;
|
|
460
|
+
return `${effective ? `${screen}-testcases.${effective}` : `${screen}-testcases`}${opts.draft ? DRAFT_MARK : ''}`;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/**
|
|
464
|
+
* A DRAFT render (a watermarked `--preview`) is written beside the official deliverable, never onto it
|
|
465
|
+
* (#965): `<screen>-testcases[.env].DRAFT.<ext>`. Readers of `qa/deliverables/` skip it (isDraftDeliverable).
|
|
466
|
+
*/
|
|
467
|
+
export interface DeliverableFileOptions { draft?: boolean }
|
|
468
|
+
export const DRAFT_MARK = '.DRAFT';
|
|
469
|
+
export const isDraftDeliverable = (fileName: string): boolean => /\.DRAFT\.[^.]+$/.test(fileName);
|
|
470
|
+
|
|
@@ -169,7 +169,12 @@ export function approveMatrix(paths: MatrixTargetPaths, groupIds?: string[]): {
|
|
|
169
169
|
|
|
170
170
|
export type MatrixFormat = 'xlsx' | 'csv' | 'both';
|
|
171
171
|
|
|
172
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Render + write the requested format(s). Caller has already enforced the gate policy. A draft (the
|
|
174
|
+
* matrix is not approved for official delivery: it carries the DRAFT banner) goes to its own
|
|
175
|
+
* `…-testcases.DRAFT.<ext>` and never overwrites the official file (#965); the tester's Manual results
|
|
176
|
+
* are still carried from the official workbook when it can be read.
|
|
177
|
+
*/
|
|
173
178
|
export async function writeMatrixDeliverables(
|
|
174
179
|
paths: MatrixTargetPaths,
|
|
175
180
|
model: MatrixModel,
|
|
@@ -179,23 +184,25 @@ export async function writeMatrixDeliverables(
|
|
|
179
184
|
): Promise<{ csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport }> {
|
|
180
185
|
const out: { csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport } = {};
|
|
181
186
|
const xlsx = format === 'xlsx' || format === 'both';
|
|
187
|
+
const draft = model.manifest.approvalState === 'draft';
|
|
182
188
|
// Read the workbook about to be overwritten BEFORE writing anything, so the tester's Manual
|
|
183
189
|
// results survive (#756) and a refusal leaves no half-written target behind.
|
|
184
190
|
// A CSV-only export overwrites no workbook, so an unreadable one is no reason to refuse it — but a
|
|
185
191
|
// readable one still holds the Manual results the CSV should show.
|
|
192
|
+
// A draft overwrites nothing official either, so an unreadable official workbook is no reason to refuse it.
|
|
186
193
|
const prior = priorRead !== undefined ? priorRead
|
|
187
|
-
: xlsx ? await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit))
|
|
194
|
+
: xlsx && !draft ? await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit))
|
|
188
195
|
: await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit)).catch(() => null);
|
|
189
196
|
// Into the model first: the CSV, every cached total and roll-up, and the caller's summary
|
|
190
197
|
// all read it (#891).
|
|
191
198
|
applyManualCarry(model, prior);
|
|
192
199
|
if (format === 'csv' || format === 'both') {
|
|
193
|
-
out.csvPath = writeCsv(paths.cwd, paths.unit, renderMatrixCsv(model));
|
|
200
|
+
out.csvPath = writeCsv(paths.cwd, paths.unit, renderMatrixCsv(model), undefined, { draft });
|
|
194
201
|
}
|
|
195
202
|
if (xlsx) {
|
|
196
203
|
const wb = renderMatrixXlsx(model, getPackageVersion());
|
|
197
204
|
out.manualCarry = carryManualResults(wb, model, prior);
|
|
198
|
-
out.xlsxPath = await writeXlsx(paths.cwd, paths.unit, wb);
|
|
205
|
+
out.xlsxPath = await writeXlsx(paths.cwd, paths.unit, wb, undefined, { draft });
|
|
199
206
|
}
|
|
200
207
|
return out;
|
|
201
208
|
}
|
|
@@ -49,7 +49,7 @@ export function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
|
|
|
49
49
|
if (model.manifest.approvalState !== 'draft') return;
|
|
50
50
|
ws.mergeCells('A5:H5');
|
|
51
51
|
const c = ws.getCell('A5');
|
|
52
|
-
c.value = 'DRAFT — NOT APPROVED FOR OFFICIAL DELIVERY (review-required findings
|
|
52
|
+
c.value = 'DRAFT — NOT APPROVED FOR OFFICIAL DELIVERY (review-required findings, proposed groups, or an audit that does not pass)';
|
|
53
53
|
c.font = { bold: true, size: 11, name: ARIAL_FONT, color: { argb: 'FFFFFFFF' } };
|
|
54
54
|
c.fill = { type: 'pattern', pattern: 'solid', fgColor: DRAFT_RED };
|
|
55
55
|
c.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
@@ -18,7 +18,7 @@ import ExcelJS from 'exceljs';
|
|
|
18
18
|
import JSZip from 'jszip';
|
|
19
19
|
import { ApiCatalogEntry, ApiDetailRow, ScreenSummary, TestCaseRow } from './types';
|
|
20
20
|
import { getPackageVersion } from './package-info';
|
|
21
|
-
import { deliverableBasename } from './csv-exporter';
|
|
21
|
+
import { deliverableBasename, DeliverableFileOptions } from './csv-exporter';
|
|
22
22
|
import {
|
|
23
23
|
extractApiCallOrder,
|
|
24
24
|
extractCasesDataset,
|
|
@@ -114,8 +114,8 @@ export function buildApiDetailRows(
|
|
|
114
114
|
* to force the base (no suffix) variant.
|
|
115
115
|
*/
|
|
116
116
|
/** Where `writeXlsx` puts a unit's workbook — shared so a reader targets the identical file. */
|
|
117
|
-
export function deliverableXlsxPath(cwd: string, screen: string, locale?: string | null): string {
|
|
118
|
-
return path.join(cwd, 'qa', 'deliverables', `${deliverableBasename(screen, locale)}.xlsx`);
|
|
117
|
+
export function deliverableXlsxPath(cwd: string, screen: string, locale?: string | null, opts: DeliverableFileOptions = {}): string {
|
|
118
|
+
return path.join(cwd, 'qa', 'deliverables', `${deliverableBasename(screen, locale, opts)}.xlsx`);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export async function writeXlsx(
|
|
@@ -123,8 +123,9 @@ export async function writeXlsx(
|
|
|
123
123
|
screen: string,
|
|
124
124
|
wb: ExcelJS.Workbook,
|
|
125
125
|
locale?: string | null,
|
|
126
|
+
opts: DeliverableFileOptions = {},
|
|
126
127
|
): Promise<string> {
|
|
127
|
-
const outPath = deliverableXlsxPath(cwd, screen, locale);
|
|
128
|
+
const outPath = deliverableXlsxPath(cwd, screen, locale, opts);
|
|
128
129
|
const outDir = path.dirname(outPath);
|
|
129
130
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
130
131
|
const buffer = await wb.xlsx.writeBuffer();
|