@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
|
@@ -6,50 +6,79 @@
|
|
|
6
6
|
* status `gateStatus` is PASS|FAIL and agrees with the integer `exitCode` (0 | 2)
|
|
7
7
|
* inputs `inputsHash` equals the hash of today's inputs + decisions (audit-input-hash.ts)
|
|
8
8
|
* decisions `decisionsHash` equals today's — otherwise the audit is stale: re-audit
|
|
9
|
+
* rules `provenance.catalogHash` and `provenance.rulesHash` are the running sungen's — other
|
|
10
|
+
* catalogs or another AUDIT_RULES_VERSION scored it (a 3.2 PASS can be a 3.3 FAIL on the
|
|
11
|
+
* same files): stale, re-audit. Missing → stale (fail closed). The package VERSION
|
|
12
|
+
* alone is not a reason: a newer sungen with the same rules gives the same verdict (W4)
|
|
9
13
|
* verdict gateStatus re-evaluated from the report's own terms (audit-gate-terms.ts) under the
|
|
10
14
|
* overrides in force — a flipped gateStatus + exitCode is `verdict-mismatch` (tester m1)
|
|
11
15
|
* Anything else counts as NEVER AUDITED, not as clean; only an explicit `gateStatus: PASS` passes.
|
|
12
16
|
* These checks are anti-STALENESS and anti-casual-tamper, NOT anti-forgery: a writer of audit.json can
|
|
13
17
|
* recompute the hashes, and one who rewrites every finding, gap and verdict is out of scope here.
|
|
14
18
|
*
|
|
15
|
-
* Legacy (DOS-11.4): a report written before these fields existed carries none of exitCode /
|
|
16
|
-
* inputsHash / decisionsHash / gateOverrides
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* Legacy (DOS-11.4, #933): a report written before these fields existed carries none of exitCode /
|
|
20
|
+
* inputsHash / decisionsHash / gateOverrides, so nothing in it can vouch for today's inputs. It reads
|
|
21
|
+
* STALE — one `sungen audit` records a current one. (It used to count while no input file was newer:
|
|
22
|
+
* a fresh checkout gives every file the checkout's mtime, so that proved nothing and an old PASS was
|
|
23
|
+
* trusted.) A legacy report claiming a PASS its own fields contradict is invalid. A report carrying
|
|
24
|
+
* SOME of the fields is malformed.
|
|
19
25
|
*/
|
|
20
26
|
import * as fs from 'fs';
|
|
21
27
|
import * as path from 'path';
|
|
22
28
|
import { resolveUnitCandidates } from '../../cli/resolve-unit';
|
|
23
29
|
import { reportSlug } from '../unit-paths';
|
|
24
|
-
import {
|
|
30
|
+
import { getProvenance } from '../provenance';
|
|
31
|
+
import { auditInputsChangedReason, auditInputsHash } from './audit-input-hash';
|
|
25
32
|
import { AUDIT_EXIT_CODE } from './audit-gate-verdict';
|
|
26
33
|
import { recomputeAuditGateStatus, termSourcesOf } from './audit-gate-terms';
|
|
27
34
|
import { EMPTY_DECISIONS_HASH } from './decisions-hash';
|
|
28
35
|
import { loadUnitGateOverrides, unitScopeOfDir, UnitGateOverrides } from './unit-gate-overrides';
|
|
29
36
|
|
|
30
|
-
export type AuditState = 'absent' | 'verified' | '
|
|
37
|
+
export type AuditState = 'absent' | 'verified' | 'stale' | 'invalid';
|
|
31
38
|
export type AuditProblemCode = 'unreadable' | 'unknown-key' | 'bad-type' | 'partial-fields' | 'status-exit-mismatch'
|
|
32
|
-
| 'unit-unresolved' | 'decisions-changed' | 'inputs-changed' | 'legacy-with-overrides' | 'legacy-
|
|
39
|
+
| 'unit-unresolved' | 'decisions-changed' | 'inputs-changed' | 'rules-changed' | 'legacy-with-overrides' | 'legacy-unverified' | 'verdict-mismatch';
|
|
33
40
|
|
|
34
41
|
export interface AuditVerification {
|
|
35
42
|
state: AuditState;
|
|
36
43
|
problems: Array<{ code: AuditProblemCode; message: string }>;
|
|
37
|
-
/** Caveats on a report that counts
|
|
44
|
+
/** Caveats on a report that counts. */
|
|
38
45
|
notes: string[];
|
|
39
|
-
/** The parsed report — only when it counts (verified
|
|
46
|
+
/** The parsed report — only when it counts (verified). */
|
|
40
47
|
report: Record<string, unknown> | null;
|
|
41
48
|
path: string;
|
|
42
49
|
/** Today's decisionsHash for the unit (what a fresh audit would record). */
|
|
43
50
|
currentDecisionsHash: string;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
/** A string field of the report's provenance, or null when it does not say. */
|
|
54
|
+
function provenanceField(r: Record<string, unknown>, key: 'sungenVersion' | 'catalogHash' | 'rulesHash'): string | null {
|
|
55
|
+
const v = r.provenance && typeof r.provenance === 'object' ? (r.provenance as Record<string, unknown>)[key] : undefined;
|
|
56
|
+
return typeof v === 'string' && v ? v : null;
|
|
57
|
+
}
|
|
58
|
+
const writtenBy = (r: Record<string, unknown>): string | null => provenanceField(r, 'sungenVersion');
|
|
59
|
+
|
|
60
|
+
export interface RulesStamp { catalogHash: string | null; rulesHash: string | null }
|
|
61
|
+
/** rulesProblem's answer for a report that records no rules (`provenance.catalogHash` / `rulesHash`): an older sungen wrote it. */
|
|
62
|
+
export const NO_RULES_STAMP = 'audited by an older sungen, which did not record the rules it scored with';
|
|
63
|
+
/** What getProvenance writes when it could not read a catalog — never a match, on either side. */
|
|
64
|
+
const UNKNOWN = 'unknown';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Why the rules a report was scored by (`was`) cannot be taken as this sungen's (`now`), or null when
|
|
68
|
+
* they are the same KNOWN rules. Missing or `unknown` on either side is a problem (fail closed):
|
|
69
|
+
* unknown == unknown proves nothing.
|
|
70
|
+
*/
|
|
71
|
+
export function rulesProblem(was: RulesStamp, now: RulesStamp): string | null {
|
|
72
|
+
if (!was.catalogHash || !was.rulesHash) return NO_RULES_STAMP;
|
|
73
|
+
if ([was.catalogHash, was.rulesHash].includes(UNKNOWN)) return 'the rules that scored it are unknown (provenance says "unknown")';
|
|
74
|
+
if (!now.catalogHash || !now.rulesHash || [now.catalogHash, now.rulesHash].includes(UNKNOWN)) return 'this sungen cannot read its own rules (catalog or rules hash unknown), so it trusts no report';
|
|
75
|
+
if (was.catalogHash === now.catalogHash && was.rulesHash === now.rulesHash) return null;
|
|
76
|
+
return `scored by other rules (catalog ${was.catalogHash} · rules ${was.rulesHash}) than this sungen's (catalog ${now.catalogHash} · rules ${now.rulesHash})`;
|
|
77
|
+
}
|
|
47
78
|
|
|
48
|
-
/**
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
const at = fs.statSync(reportFile).mtimeMs;
|
|
52
|
-
return auditInputFiles(unitDir, unit).find((f) => fs.statSync(f).mtimeMs > at) ?? null;
|
|
79
|
+
/** Why a current-format report was judged by other rules than this sungen's, or null when the same. */
|
|
80
|
+
function otherRules(r: Record<string, unknown>): string | null {
|
|
81
|
+
return rulesProblem({ catalogHash: provenanceField(r, 'catalogHash'), rulesHash: provenanceField(r, 'rulesHash') }, getProvenance());
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
type JsonKind = 'string' | 'number' | 'boolean' | 'object' | 'array';
|
|
@@ -124,21 +153,25 @@ export function verifyAuditReport(projectRoot: string, unit: string): AuditVerif
|
|
|
124
153
|
if (schema.length) return result('invalid', schema);
|
|
125
154
|
|
|
126
155
|
if (!P0_KEYS.some((k) => k in r)) {
|
|
127
|
-
if (current !== EMPTY_DECISIONS_HASH) {
|
|
128
|
-
return result('stale', [{ code: 'legacy-with-overrides', message: 'written by an earlier sungen (no decisionsHash) and a gate-override now applies to this unit' }]);
|
|
129
|
-
}
|
|
130
|
-
const newer = newerLegacyInput(file, unitDir, unit);
|
|
131
|
-
if (newer) return result('stale', [{ code: 'legacy-inputs-newer', message: `written by an earlier sungen and ${path.basename(newer)} changed after it` }]);
|
|
132
156
|
// A legacy report predates gateSensorError, so only a stored PASS its own fields contradict is caught.
|
|
133
157
|
const sources = termSourcesOf(r, false);
|
|
134
158
|
if (r.gateStatus === 'PASS' && sources && recomputeAuditGateStatus(sources, overrides) === 'FAIL') return result('invalid', [verdictMismatch(r)]);
|
|
135
|
-
|
|
159
|
+
const by = `written by ${writtenBy(r) ? `sungen ${writtenBy(r)}` : 'an earlier sungen'}`;
|
|
160
|
+
if (current !== EMPTY_DECISIONS_HASH) {
|
|
161
|
+
return result('stale', [{ code: 'legacy-with-overrides', message: `${by} (no decisionsHash) and a gate-override now applies to this unit` }]);
|
|
162
|
+
}
|
|
163
|
+
return result('stale', [{ code: 'legacy-unverified', message: `${by}, before inputsHash existed — its inputs cannot be verified` }]);
|
|
136
164
|
}
|
|
137
165
|
const stale: AuditVerification['problems'] = [];
|
|
166
|
+
const rules = otherRules(r);
|
|
167
|
+
// A report from before the rules stamp reads in plain words, with the one command that clears it (#966).
|
|
168
|
+
if (rules) stale.push({ code: 'rules-changed', message: rules === NO_RULES_STAMP
|
|
169
|
+
? `audited by an older sungen${writtenBy(r) ? ` (${writtenBy(r)})` : ''} — run \`sungen audit --screen ${unit}\` once`
|
|
170
|
+
: `${writtenBy(r) ? `written by sungen ${writtenBy(r)}, ` : ''}${rules}` });
|
|
138
171
|
if (r.decisionsHash !== current) stale.push({ code: 'decisions-changed', message: 'the gate-override decisions changed since this audit' });
|
|
139
172
|
if (!unitDir) return result('invalid', [{ code: 'unit-unresolved', message: `"${unit}" does not name exactly one unit, so the audit's inputs cannot be checked` }]);
|
|
140
173
|
if (r.inputsHash !== auditInputsHash(unitDir, unit, r.decisionsHash as string)) {
|
|
141
|
-
stale.push({ code: 'inputs-changed', message:
|
|
174
|
+
stale.push({ code: 'inputs-changed', message: auditInputsChangedReason(unitDir) });
|
|
142
175
|
}
|
|
143
176
|
if (stale.length) return result('stale', stale);
|
|
144
177
|
const tampered = verdictProblem(r, overrides);
|
|
@@ -157,8 +190,8 @@ function verdictProblem(r: Record<string, unknown>, overrides: UnitGateOverrides
|
|
|
157
190
|
return recomputeAuditGateStatus(sources, overrides) === r.gateStatus ? null : verdictMismatch(r);
|
|
158
191
|
}
|
|
159
192
|
|
|
160
|
-
/** Whether the report may be used at all
|
|
161
|
-
export const auditCounts = (v: AuditVerification): boolean => v.state === 'verified'
|
|
193
|
+
/** Whether the report may be used at all: only a verified one. */
|
|
194
|
+
export const auditCounts = (v: AuditVerification): boolean => v.state === 'verified';
|
|
162
195
|
|
|
163
196
|
/** Only an explicit `gateStatus: PASS` on a report that counts passes; a score never does. */
|
|
164
197
|
export const auditPassed = (v: AuditVerification): boolean => auditCounts(v) && v.report?.gateStatus === 'PASS';
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* script-check runScriptCheck: OK | FAIL (`sungen script-check` 0 | 2)
|
|
7
7
|
* contract check buildContractVerdict: pass | review | fail (`sungen contract check` 0 | 0 | 2)
|
|
8
8
|
* decisions evaluateDeliveryDecisions: problems, in-run decisions (what this delivery checks)
|
|
9
|
-
*
|
|
9
|
+
* results resultsFreshness: the run the counts come from, not older than the .feature,
|
|
10
|
+
* compiled spec or spec.md, and present for SUNGEN_ENV (#942)
|
|
11
|
+
* triage testCaseStates: every red case classified, candidates decided — naming the classified
|
|
12
|
+
* reds still awaiting a test fix or an environment re-run
|
|
10
13
|
* A box that cannot apply is SKIPPED with its reason — never absent, never ticked. The checklist only
|
|
11
14
|
* reports: the gates before it decide whether the delivery runs, so no exit code moves because of it.
|
|
12
15
|
*/
|
|
@@ -15,6 +18,7 @@ import { readTextOrNull } from '../../contract/contract-git';
|
|
|
15
18
|
import { lockPathFor } from '../../contract/contracts-lock-file';
|
|
16
19
|
import { testCaseNeed } from '../../graph/test-case-satisfied';
|
|
17
20
|
import { testCaseStates } from '../../graph/test-case-states';
|
|
21
|
+
import { describeResultsRun, resultsFreshness } from '../../graph/unit-results-freshness';
|
|
18
22
|
import { runScriptCheck, UnitKind } from '../script-check';
|
|
19
23
|
import { auditCounts, auditNotCountedReason, verifyAuditReport } from './audit-report-verification';
|
|
20
24
|
import { evaluateDeliveryDecisions } from './delivery-decision-gate';
|
|
@@ -33,10 +37,10 @@ function auditBox(projectRoot: string, u: ChecklistUnit): ChecklistBox {
|
|
|
33
37
|
const v = verifyAuditReport(projectRoot, u.unitName);
|
|
34
38
|
if (!auditCounts(v)) return box('audit', 'fail', auditNotCountedReason(v, u.unitName));
|
|
35
39
|
const overall = (v.report?.score as { overall?: number } | undefined)?.overall;
|
|
36
|
-
const how =
|
|
40
|
+
const how = "verified against today's inputs";
|
|
37
41
|
return v.report?.gateStatus === 'PASS'
|
|
38
42
|
? box('audit', 'pass', `PASS ${overall ?? '?'}/10 — ${how}`)
|
|
39
|
-
: box('audit', 'fail', `FAIL ${overall ?? '?'}/10 — \`sungen audit\` exits 2 (${how})`);
|
|
43
|
+
: box('audit', 'fail', `FAIL ${overall ?? '?'}/10 — \`sungen audit\` exits 2 (${how}); an official delivery is blocked until it passes — --preview renders a draft`);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/** The compiler prints progress on stdout; the checklist keeps the delivery output to itself. */
|
|
@@ -46,13 +50,23 @@ async function quietly<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
46
50
|
try { return await fn(); } finally { Object.assign(console, saved); }
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
/** Findings a fresh `sungen generate` cures: the spec is out of step with the .feature, not hollow. */
|
|
54
|
+
const REGENERATE_CURES = /^(?:DRIFT|MISSING in spec|EXTRA in spec|Count mismatch)\b/;
|
|
55
|
+
|
|
56
|
+
/** The first finding's first sentence — its cause after "→" kept (#943) — and, when regenerating cures it, the command. */
|
|
57
|
+
function scriptCheckFailure(u: ChecklistUnit, finding = 'not faithful'): string {
|
|
58
|
+
const first = finding.split(/\. /)[0].slice(0, 240);
|
|
59
|
+
const generate = `sungen generate --${u.kind === 'flow' ? 'flow' : u.kind === 'api' ? 'api' : 'screen'} ${u.unitName}`;
|
|
60
|
+
return `FAIL — ${first}${REGENERATE_CURES.test(finding) ? ` — run \`${generate}\` after fixing the scenario` : ''} (\`sungen script-check\` exits 2)`;
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
async function scriptCheckBox(u: ChecklistUnit): Promise<ChecklistBox> {
|
|
50
64
|
try {
|
|
51
65
|
const r = await quietly(() => runScriptCheck(u.unitDir, u.unitName, u.kind));
|
|
52
66
|
if (!r.specPath) return box('script-check', 'skipped', 'no compiled spec to compare: nothing was compiled for this unit');
|
|
53
67
|
return r.status === 'OK'
|
|
54
68
|
? box('script-check', 'pass', `OK — the spec is a 1:1 of the .feature, no drift (${r.automatedScenarios} automated)`)
|
|
55
|
-
: box('script-check', 'fail',
|
|
69
|
+
: box('script-check', 'fail', scriptCheckFailure(u, r.findings[0]));
|
|
56
70
|
} catch (e) {
|
|
57
71
|
return box('script-check', 'fail', `could not run: ${why(e)}`);
|
|
58
72
|
}
|
|
@@ -87,7 +101,21 @@ function decisionsBox(projectRoot: string, u: ChecklistUnit, accepted?: InRunAcc
|
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
|
|
104
|
+
/** Which run the delivered counts come from — and whether the suite moved on since (#942). */
|
|
105
|
+
export function resultsBox(projectRoot: string, u: ChecklistUnit): ChecklistBox {
|
|
106
|
+
try {
|
|
107
|
+
const f = resultsFreshness(projectRoot, u.unitDir, u.unitName);
|
|
108
|
+
if (!f) return box('results', 'skipped', 'no run results: the unit has not run, every case reads Pending');
|
|
109
|
+
const run = `${describeResultsRun(f)}${f.files.length > 1 ? `, ${f.files.length} files` : ''}`;
|
|
110
|
+
return f.warning
|
|
111
|
+
? box('results', 'fail', `${run} — ${f.warning}`)
|
|
112
|
+
: box('results', 'pass', `${run} — not older than the .feature, compiled spec or spec.md`);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return box('results', 'fail', `could not be dated: ${why(e)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function triageBox(projectRoot: string, u: ChecklistUnit): ChecklistBox {
|
|
91
119
|
try {
|
|
92
120
|
const cases = testCaseStates(projectRoot, u.unitName, { unitDir: u.unitDir });
|
|
93
121
|
if (!cases.some((c) => c.result !== 'never-run')) return box('triage', 'skipped', 'no run results to classify');
|
|
@@ -97,14 +125,18 @@ function triageBox(projectRoot: string, u: ChecklistUnit): ChecklistBox {
|
|
|
97
125
|
if (owed.length) return box('triage', 'fail', `${owed.length} red test case(s) not classified — run \`sungen triage ${flag} ${u.unitName}\``);
|
|
98
126
|
if (decide.length) return box('triage', 'fail', `${decide.length} product-defect candidate(s) await a QA decision (\`sungen defect draft ${flag} ${u.unitName}\`)`);
|
|
99
127
|
const reds = cases.filter((c) => c.result === 'failed').length;
|
|
100
|
-
|
|
128
|
+
// Classified is not done: a test defect still needs its fix, an environment red its re-run (#928).
|
|
129
|
+
const waiting = cases.filter((c) => testCaseNeed(c) === 'fix-test' || testCaseNeed(c) === 'rerun-environment');
|
|
130
|
+
const named = waiting.length
|
|
131
|
+
? ` — ${waiting.length} awaiting a fix or a re-run (${waiting.slice(0, 4).map((c) => `${c.id}: ${c.triage?.class}`).join(', ')}${waiting.length > 4 ? ', …' : ''})` : '';
|
|
132
|
+
return box('triage', 'pass', reds ? `${reds} red test case(s), each classified or exempt${named}` : 'no red test case');
|
|
101
133
|
} catch (e) {
|
|
102
134
|
return box('triage', 'fail', `could not be read: ${why(e)}`);
|
|
103
135
|
}
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
export async function unitChecklist(projectRoot: string, u: ChecklistUnit, accepted?: InRunAcceptance): Promise<ChecklistBox[]> {
|
|
107
|
-
return [auditBox(projectRoot, u), await scriptCheckBox(u), contractBox(projectRoot, u), decisionsBox(projectRoot, u, accepted), triageBox(projectRoot, u)];
|
|
139
|
+
return [auditBox(projectRoot, u), await scriptCheckBox(u), contractBox(projectRoot, u), decisionsBox(projectRoot, u, accepted), resultsBox(projectRoot, u), triageBox(projectRoot, u)];
|
|
108
140
|
}
|
|
109
141
|
|
|
110
142
|
const MARK: Record<BoxState, string> = { pass: '[x]', fail: '[ ]', skipped: '[-]' };
|
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
* no trusted no audit report, or one that does not count (stale, invalid, verdict-mismatch):
|
|
9
9
|
* audit a real delivery stops and asks for `sungen audit` (owner decision 2026-10-22);
|
|
10
10
|
* `--preview` / `--check` go on with a warning — the caller decides (`untrustedAudits`)
|
|
11
|
+
* failing a trusted audit whose verdict is FAIL (`sungen audit` exits 2): the same — a real
|
|
12
|
+
* audit delivery stops, naming the failing gate rules (#935, owner decision); `--preview`
|
|
13
|
+
* renders a DRAFT, `--check` reports it (`failingAudits`)
|
|
11
14
|
* It also hands each target its gate rules (catalog + overrides) for the matrix gates, and the
|
|
12
15
|
* override rows the XLSX may show. With no decision in reach nothing here changes the delivery.
|
|
13
16
|
*/
|
|
14
17
|
import { checkDecisionIntegrity, DecisionFinding } from '../../decisions/decision-integrity-check';
|
|
15
18
|
import { PROJECT_SCOPE } from '../../decisions/decision-file';
|
|
16
19
|
import { auditCounts, verifyAuditReport } from './audit-report-verification';
|
|
20
|
+
import { auditGateTerms, termSourcesOf } from './audit-gate-terms';
|
|
17
21
|
import { GateOverrideReport, gateOverrideReports, gateOverrideSummaryLine, InRunAcceptance } from './gate-override-report';
|
|
18
22
|
import { unitsCoveredByScope } from './gate-override-scope';
|
|
19
23
|
import { committedDecisionPaths, hasInRunKindDecisions, inRunDecisions, InRunDecision, IN_RUN_RULE, runStartOf } from './in-run-decision-check';
|
|
@@ -35,6 +39,10 @@ export interface DeliveryDecisionGate {
|
|
|
35
39
|
blocking: string[];
|
|
36
40
|
/** Units with no trusted audit — a real delivery stops on them, --preview / --check warn. */
|
|
37
41
|
untrustedAudits: string[];
|
|
42
|
+
/** Units whose trusted audit FAILs (#935) — a real delivery stops on them, --preview / --check warn. */
|
|
43
|
+
failingAudits: string[];
|
|
44
|
+
/** The unit's audit does not let it be delivered officially (untrusted or FAIL): its render is a DRAFT. */
|
|
45
|
+
auditDraft(unitDir: string): boolean;
|
|
38
46
|
notes: string[];
|
|
39
47
|
inRun: Array<InRunDecision & { unit: string }>;
|
|
40
48
|
/** The unit's tracked gate rules, for the matrix gates of its targets. */
|
|
@@ -48,9 +56,22 @@ export interface DeliveryDecisionGate {
|
|
|
48
56
|
|
|
49
57
|
const reachOf = (scope: string | null): string[] => [`qa/decisions/`, ...(scope ? [`qa/${scope}/decisions/`] : [])];
|
|
50
58
|
|
|
59
|
+
/** The audit gate rules a FAIL report's own terms violate under the unit's overrides, gaps named. */
|
|
60
|
+
function failingGateRules(report: Record<string, unknown>, overrides: UnitGateOverrides): string {
|
|
61
|
+
const sources = termSourcesOf(report, true);
|
|
62
|
+
if (!sources) return 'its gate terms';
|
|
63
|
+
const gaps = (sources.gate.gaps as Array<{ theme?: unknown; status?: unknown }>)
|
|
64
|
+
.map((g) => `${g.status === 'shallow' ? 'SHALLOW' : 'MISSING'}: ${String(g.theme)}`);
|
|
65
|
+
const rules = auditGateTerms(sources).filter((t) => !t.holds && resolveRule(t.rule, overrides.context).blocking)
|
|
66
|
+
.map((t) => (t.rule === 'audit.viewpoint-coverage' && gaps.length ? `${t.rule} (${gaps.join(', ')})` : t.rule));
|
|
67
|
+
return rules.join(', ') || 'its gate terms';
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
export function evaluateDeliveryDecisions(projectRoot: string, refs: DeliveryUnitRef[]): DeliveryDecisionGate {
|
|
52
71
|
const blocking: string[] = [];
|
|
53
72
|
const untrustedAudits: string[] = [];
|
|
73
|
+
const failingAudits: string[] = [];
|
|
74
|
+
const draftDirs = new Set<string>();
|
|
54
75
|
const notes: string[] = [];
|
|
55
76
|
const inRun: DeliveryDecisionGate['inRun'] = [];
|
|
56
77
|
const units = new Map<string, UnitState>();
|
|
@@ -63,10 +84,15 @@ export function evaluateDeliveryDecisions(projectRoot: string, refs: DeliveryUni
|
|
|
63
84
|
? (audit.report!.gateOverrides as GateOverrideReport[]).filter((g) => g?.triggered).map((g) => g.rule) : []);
|
|
64
85
|
units.set(ref.unitDir, { ref, resultsPaths: [ref.resultsPath], overrides, rules: trackGateRules(overrides.context), auditTriggered });
|
|
65
86
|
for (const r of overrides.rejected) blocking.push(`${ref.label}: gate.override-rejected — ${r.decisionId} (${r.path}): ${r.reason}`);
|
|
87
|
+
const flag = /[\\/]qa[\\/]api[\\/]/.test(ref.unitDir) ? '--api' : '--screen';
|
|
66
88
|
if (!auditCounts(audit)) {
|
|
67
89
|
const why = audit.state === 'absent' ? 'no audit report' : `the audit is ${audit.state === 'stale' ? 'stale' : 'not trusted'}: ${audit.problems.map((p) => p.message).join('; ')}`;
|
|
68
|
-
const flag = /[\\/]qa[\\/]api[\\/]/.test(ref.unitDir) ? '--api' : '--screen';
|
|
69
90
|
untrustedAudits.push(`${ref.label}: ${why} — re-run \`sungen audit ${flag} ${ref.unitName}\``);
|
|
91
|
+
draftDirs.add(ref.unitDir);
|
|
92
|
+
} else if (audit.report?.gateStatus !== 'PASS') {
|
|
93
|
+
const overall = (audit.report?.score as { overall?: number } | undefined)?.overall;
|
|
94
|
+
failingAudits.push(`${ref.label}: FAIL ${overall ?? '?'}/10 on ${failingGateRules(audit.report!, overrides)} — fix the suite, then re-run \`sungen audit ${flag} ${ref.unitName}\``);
|
|
95
|
+
draftDirs.add(ref.unitDir);
|
|
70
96
|
}
|
|
71
97
|
for (const n of audit.notes) notes.push(`${ref.label}: ${n}`);
|
|
72
98
|
}
|
|
@@ -95,6 +121,8 @@ export function evaluateDeliveryDecisions(projectRoot: string, refs: DeliveryUni
|
|
|
95
121
|
return {
|
|
96
122
|
blocking,
|
|
97
123
|
untrustedAudits,
|
|
124
|
+
failingAudits,
|
|
125
|
+
auditDraft: (dir) => draftDirs.has(dir),
|
|
98
126
|
notes,
|
|
99
127
|
inRun,
|
|
100
128
|
rulesFor: (dir) => units.get(dir)?.rules,
|
|
@@ -16,7 +16,10 @@ export interface DeliveryDecisionStepInput {
|
|
|
16
16
|
argv: string[];
|
|
17
17
|
io: ConfirmationIO;
|
|
18
18
|
log: (line: string) => void;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* A real export needs, per unit, a trusted audit (owner decision 2026-10-22) that PASSes (#935);
|
|
21
|
+
* false for --preview / --check, which go on with a warning (a DRAFT render / a report).
|
|
22
|
+
*/
|
|
20
23
|
requireTrustedAudit: boolean;
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -29,6 +32,8 @@ export async function runDeliveryDecisionStep(i: DeliveryDecisionStepInput): Pro
|
|
|
29
32
|
for (const b of gate.blocking) i.log(` - ${b}`);
|
|
30
33
|
return null;
|
|
31
34
|
}
|
|
35
|
+
// Every reason to stop is listed before stopping, so one run names all of them (audit and in-run).
|
|
36
|
+
let blocked = false;
|
|
32
37
|
if (gate.untrustedAudits.length) {
|
|
33
38
|
i.log(i.requireTrustedAudit
|
|
34
39
|
? `✗ Delivery blocked — ${gate.untrustedAudits.length} unit(s) without a trusted audit (a delivery stands on an audit that matches today's suite):`
|
|
@@ -36,10 +41,20 @@ export async function runDeliveryDecisionStep(i: DeliveryDecisionStepInput): Pro
|
|
|
36
41
|
for (const u of gate.untrustedAudits) i.log(` - ${u}`);
|
|
37
42
|
if (i.requireTrustedAudit) {
|
|
38
43
|
i.log(' Or render a watermarked draft with --preview.');
|
|
39
|
-
|
|
44
|
+
blocked = true;
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
|
-
if (gate.
|
|
47
|
+
if (gate.failingAudits.length) {
|
|
48
|
+
i.log(i.requireTrustedAudit
|
|
49
|
+
? `✗ Delivery blocked — ${gate.failingAudits.length} unit(s) whose audit FAILs (\`sungen audit\` exits 2; an official delivery stands on a passing audit):`
|
|
50
|
+
: `⚠ Rendering ${gate.failingAudits.length} unit(s) whose audit FAILs — a draft, not an official delivery:`);
|
|
51
|
+
for (const u of gate.failingAudits) i.log(` - ${u}`);
|
|
52
|
+
if (i.requireTrustedAudit) {
|
|
53
|
+
i.log(' Fix the suite and re-run the audit, or render a watermarked draft with --preview.');
|
|
54
|
+
blocked = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (gate.inRun.length === 0) return blocked ? null : gate;
|
|
43
58
|
const acceptCommand = formatCommand([...i.argv, '--accept-in-run']);
|
|
44
59
|
if (!i.acceptInRun) {
|
|
45
60
|
i.log(`✗ Delivery blocked — ${gate.inRun.length} decision(s) recorded during the run (${IN_RUN_RULE}, hard):`);
|
|
@@ -48,6 +63,7 @@ export async function runDeliveryDecisionStep(i: DeliveryDecisionStepInput): Pro
|
|
|
48
63
|
i.log(` ${acceptCommand}`);
|
|
49
64
|
return null;
|
|
50
65
|
}
|
|
66
|
+
if (blocked) return null; // nothing to accept on top of a delivery the audit already stops
|
|
51
67
|
try {
|
|
52
68
|
await requireTypedConfirmation({
|
|
53
69
|
what: `deliver with ${gate.inRun.length} decision(s) recorded during the run (${IN_RUN_RULE})`,
|
|
@@ -11,10 +11,20 @@
|
|
|
11
11
|
*
|
|
12
12
|
* A MIGRATED waiver (DOS-13.5) was reviewed when its legacy record was committed — the only record the
|
|
13
13
|
* import trusts — so it is judged by THAT commit (migrated-waiver-review.ts), never by its own decision
|
|
14
|
-
* file: in-run when the record was committed after the run started, or
|
|
15
|
-
*
|
|
16
|
-
* project's
|
|
17
|
-
*
|
|
14
|
+
* file: in-run when the record was committed after the run started, or — run start or not (#925) —
|
|
15
|
+
* when it could not have been the import's source: not strictly before the decision file, the unit's
|
|
16
|
+
* import marker or the project's adoption of 3.3 appeared. Holding it to its decision file's commit would make
|
|
17
|
+
* every legacy project's waivers in-run the day they are imported (phase 09). An entry HEAD's legacy
|
|
18
|
+
* file does not vouch for — absent, uncommitted, different, introduced beyond the commits the walk
|
|
19
|
+
* reads (#924) or at a shallow clone's boundary — is judged as any other; in the last two cases an
|
|
20
|
+
* in-run finding says why.
|
|
21
|
+
*
|
|
22
|
+
* A manual triage classification that claims a human typed it (`triage-classification` with
|
|
23
|
+
* `humanConfirmed`, #928) is a claim the entry makes about itself — a script can write one. What backs
|
|
24
|
+
* it is review, so it is in-run while git HEAD does not hold it (and, with no git, always: nothing
|
|
25
|
+
* shows it was reviewed). Judged by HEAD membership only: it is always recorded after the run it
|
|
26
|
+
* judges, so its commit time says nothing. Only a test-defect / environment entry that is not
|
|
27
|
+
* superseded counts — the only kind the reader puts in force on that claim.
|
|
18
28
|
*/
|
|
19
29
|
import * as fs from 'fs';
|
|
20
30
|
import * as path from 'path';
|
|
@@ -22,7 +32,8 @@ import { DECISION_FILE_PATH } from '../../decisions/decision-file';
|
|
|
22
32
|
import { DecisionVisibility } from '../../decisions/decision-schema';
|
|
23
33
|
import { gitOutput } from '../../decisions/git-head-trust';
|
|
24
34
|
import { readDecisions } from '../../decisions/read-decisions';
|
|
25
|
-
import {
|
|
35
|
+
import { HUMAN_ONLY_TRIAGE, ManualTriageClass, TRIAGE_DECISION_KIND } from '../../graph/manual-triage-decisions';
|
|
36
|
+
import { migratedWaiverReview } from './migrated-waiver-review';
|
|
26
37
|
|
|
27
38
|
export const IN_RUN_KINDS = ['gate-override', 'waiver', 'contract-approval'] as const;
|
|
28
39
|
export const IN_RUN_RULE = 'decision.in-run';
|
|
@@ -55,23 +66,36 @@ export function runStartOf(resultsPath: string | null): string | null {
|
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
const isInRunKind = (kind: string): boolean => (IN_RUN_KINDS as readonly string[]).includes(kind);
|
|
69
|
+
/**
|
|
70
|
+
* A triage classification claiming a human typed it (#928): reviewed only once git HEAD holds it.
|
|
71
|
+
* Only what the reader (manual-triage-decisions.ts) can put in force: a superseded entry is never read,
|
|
72
|
+
* and a product classification needs no human — its claim backs nothing, so it blocks nothing.
|
|
73
|
+
*/
|
|
74
|
+
const claimsHumanTriage = (e: { kind: string; payload?: Record<string, unknown>; superseded_by?: string }): boolean =>
|
|
75
|
+
e.kind === TRIAGE_DECISION_KIND && !e.superseded_by && e.payload?.humanConfirmed === true
|
|
76
|
+
&& HUMAN_ONLY_TRIAGE.has(e.payload?.class as ManualTriageClass);
|
|
58
77
|
|
|
59
78
|
/** In-run decisions among `scopes`, judged against one run start (null: HEAD membership only) and the committed set. */
|
|
60
79
|
export function inRunDecisions(projectRoot: string, scopes: string[], runStart: string | null, committed: Set<string> | null): InRunDecision[] {
|
|
61
80
|
const out: InRunDecision[] = [];
|
|
62
81
|
const after = (t: string): boolean => !!runStart && Date.parse(t) > Date.parse(runStart);
|
|
82
|
+
const migratedReview = committed ? migratedWaiverReview(projectRoot) : null;
|
|
63
83
|
for (const scope of scopes) {
|
|
64
84
|
for (const e of readDecisions(projectRoot, scope).entries) {
|
|
65
|
-
if (!isInRunKind(e.kind)) continue;
|
|
66
85
|
const rel = path.relative(projectRoot, e.path).split(path.sep).join('/');
|
|
86
|
+
if (claimsHumanTriage(e) && !committed?.has(rel)) {
|
|
87
|
+
out.push({ id: e.id, kind: e.kind, path: rel, visibility: e.visibility, why: [committed
|
|
88
|
+
? 'a classification by hand that claims a human typed it, not in git HEAD (never committed through review)'
|
|
89
|
+
: 'a classification by hand that claims a human typed it, and no git to show it was committed through review'] });
|
|
90
|
+
}
|
|
91
|
+
if (!isInRunKind(e.kind)) continue;
|
|
92
|
+
// A migrated waiver re-confirmed on a terminal is replaced: the new decision is judged on its own.
|
|
93
|
+
if (e.superseded_by && e.source?.where === 'migrated') continue;
|
|
67
94
|
const why: string[] = [];
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
? `migrated: its legacy record was committed at ${legacyAt}, after the run started ${runStart}`
|
|
73
|
-
: `migrated: its legacy record was committed at ${legacyAt}, after this decision first appeared`);
|
|
74
|
-
}
|
|
95
|
+
const migrated = migratedReview?.reviewOf(scope, e) ?? null;
|
|
96
|
+
if (migrated?.at) {
|
|
97
|
+
if (after(migrated.at)) why.push(`migrated: its legacy record was committed at ${migrated.at}, after the run started ${runStart}`);
|
|
98
|
+
if (migrated.notImported) why.push(migrated.notImported);
|
|
75
99
|
} else if (committed && !committed.has(rel)) {
|
|
76
100
|
why.push('not in git HEAD (never committed through review)');
|
|
77
101
|
} else if (committed && runStart) {
|
|
@@ -82,6 +106,7 @@ export function inRunDecisions(projectRoot: string, scopes: string[], runStart:
|
|
|
82
106
|
} else if (!committed && after(e.valid_from)) {
|
|
83
107
|
why.push(`recorded ${e.valid_from}, after the run started ${runStart}`);
|
|
84
108
|
}
|
|
109
|
+
if (why.length && migrated?.unvouched) why.push(migrated.unvouched);
|
|
85
110
|
if (why.length) out.push({ id: e.id, kind: e.kind, path: rel, visibility: e.visibility, why });
|
|
86
111
|
}
|
|
87
112
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Could a migrated waiver's legacy record have been the source of the import that wrote it? (#925)
|
|
3
|
+
*
|
|
4
|
+
* The one-time import (legacy-waiver-migration.ts) reads a record git HEAD ALREADY holds, and only
|
|
5
|
+
* before 3.3 takes over. So a genuine record was committed strictly before each of:
|
|
6
|
+
* 1. the decision file's first appearance — its first commit, else its creation on disk;
|
|
7
|
+
* 2. the unit's import marker `.sungen/migrations/<slug>.waivers-imported.json` — the commit that
|
|
8
|
+
* added the one HEAD holds (an agent can delete or recreate the file, not that commit), else its
|
|
9
|
+
* creation on disk; once it exists the import never reads that unit's legacy file again;
|
|
10
|
+
* 3. the project's adoption of 3.3 — the first commit that added any 3.3-only file: an import marker
|
|
11
|
+
* or other `.sungen/migrations/` file, a decision file (`qa/**\/decisions/*.yaml`) or a contract lock
|
|
12
|
+
* (`qa/**\/contracts.lock.yaml`). Only the 3.3 CLI writes them, and only committed ones count: a
|
|
13
|
+
* legacy waiver is one written before the project's first reviewed 3.3 change. Mixed versions pay
|
|
14
|
+
* for it: a waiver a pre-3.3 CLI writes after that commit must be re-confirmed on a terminal.
|
|
15
|
+
* A record failing any bound claims a migration that never happened: the decision is in-run.
|
|
16
|
+
*
|
|
17
|
+
* What stays open (the limit of any time-based check, as for ordinary decisions): a record forged and
|
|
18
|
+
* committed before the project's first 3.3 commit — indistinguishable from a real legacy waiver, as on
|
|
19
|
+
* a project adopting 3.3 for the very first time — and commit times forged with GIT_COMMITTER_DATE.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { committedText, gitOutput } from '../../decisions/git-head-trust';
|
|
24
|
+
import { formatCommand } from '../../decisions/human-confirmation';
|
|
25
|
+
import { importMarkerPath } from '../../decisions/legacy-waiver-migration';
|
|
26
|
+
import type { ReadDecision } from '../../decisions/read-decisions';
|
|
27
|
+
|
|
28
|
+
/** Files only the 3.3 CLI writes, as pathspecs relative to the project. */
|
|
29
|
+
const ADOPTION_PATHSPECS = ['.sungen/migrations', ':(glob)qa/**/decisions/*.yaml', ':(glob)qa/**/contracts.lock.yaml'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What a human does about a migrated waiver its legacy record cannot vouch for: record it anew on a
|
|
33
|
+
* terminal (a `cli` decision that supersedes it — the superseded one is no longer judged), or accept it
|
|
34
|
+
* for one delivery.
|
|
35
|
+
*/
|
|
36
|
+
export const migratedWaiverRemedy = (scope: string, ob: string): string =>
|
|
37
|
+
`re-confirm it on a terminal: ${formatCommand(['sungen', 'journey', '--screen', scope.split('/').pop() ?? scope, '--waive', ob, '--reason', '…'])}, `
|
|
38
|
+
+ 'then commit the new decision; or accept it once with `sungen delivery --accept-in-run`';
|
|
39
|
+
|
|
40
|
+
const validMs = (iso: string | undefined): number | null => (iso && !Number.isNaN(Date.parse(iso)) ? Date.parse(iso) : null);
|
|
41
|
+
const createdMs = (abs: string): number | null => {
|
|
42
|
+
try { const s = fs.statSync(abs); return s.birthtimeMs || s.mtimeMs; } catch { return null; }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** One run's bounds: each decision's first appearance, each unit's marker and the project's adoption, read once. */
|
|
46
|
+
export function migratedWaiverImportBounds(projectRoot: string) {
|
|
47
|
+
const relOf = (abs: string): string => path.relative(projectRoot, abs).split(path.sep).join('/');
|
|
48
|
+
const firstSeen = new Map<string, number>();
|
|
49
|
+
const markers = new Map<string, number | null>();
|
|
50
|
+
let adoption: number | null | undefined;
|
|
51
|
+
|
|
52
|
+
/** When a decision file first appeared: its first commit, else its creation (or last change) on disk. */
|
|
53
|
+
const decisionMs = (e: ReadDecision): number => {
|
|
54
|
+
if (!firstSeen.has(e.path)) {
|
|
55
|
+
const added = gitOutput(projectRoot, ['log', '--diff-filter=A', '--format=%cI', '--', relOf(e.path)])?.trim().split('\n').filter(Boolean).pop();
|
|
56
|
+
firstSeen.set(e.path, validMs(added) ?? createdMs(e.path) ?? Number.NEGATIVE_INFINITY);
|
|
57
|
+
}
|
|
58
|
+
return firstSeen.get(e.path)!;
|
|
59
|
+
};
|
|
60
|
+
const markerMs = (slug: string): number | null => {
|
|
61
|
+
if (!markers.has(slug)) {
|
|
62
|
+
const abs = importMarkerPath(projectRoot, slug);
|
|
63
|
+
const added = committedText(projectRoot, abs) !== null
|
|
64
|
+
? validMs(gitOutput(projectRoot, ['log', '-1', '--diff-filter=A', '--format=%cI', '--', relOf(abs)])?.trim()) : null;
|
|
65
|
+
markers.set(slug, added ?? createdMs(abs));
|
|
66
|
+
}
|
|
67
|
+
return markers.get(slug)!;
|
|
68
|
+
};
|
|
69
|
+
const adoptionMs = (): number | null => {
|
|
70
|
+
if (adoption === undefined) {
|
|
71
|
+
const added = (gitOutput(projectRoot, ['log', '--diff-filter=A', '--format=%cI', '--', ...ADOPTION_PATHSPECS]) ?? '')
|
|
72
|
+
.split('\n').map((l) => validMs(l.trim())).filter((t): t is number => t !== null);
|
|
73
|
+
adoption = added.length ? Math.min(...added) : null;
|
|
74
|
+
}
|
|
75
|
+
return adoption;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The first bound a record committed at `legacy` (ms) crosses, or undefined. */
|
|
79
|
+
const boundCrossed = (slug: string, e: ReadDecision, legacy: number): string | undefined => {
|
|
80
|
+
const decision = decisionMs(e);
|
|
81
|
+
if (legacy > decision) return 'after this decision first appeared';
|
|
82
|
+
if (legacy === decision) return 'when this decision first appeared, not before it — an import only reads a record already committed';
|
|
83
|
+
const marker = markerMs(slug);
|
|
84
|
+
if (marker !== null && legacy >= marker) {
|
|
85
|
+
return `after this unit's waivers were imported (${relOf(importMarkerPath(projectRoot, slug))}, ${new Date(marker).toISOString()}) `
|
|
86
|
+
+ '— from then on the import never reads the legacy file';
|
|
87
|
+
}
|
|
88
|
+
const adopted = adoptionMs();
|
|
89
|
+
if (adopted !== null && legacy >= adopted) {
|
|
90
|
+
return `after this project adopted sungen 3.3 (${new Date(adopted).toISOString()}: the first commit adding a .sungen/migrations/ file, `
|
|
91
|
+
+ 'a decision or a contract lock) — a legacy waiver is one written before that';
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
/** Why a record committed at `at` (state file of `slug`) cannot have been the source of `e`'s import; undefined when it can. */
|
|
98
|
+
notImported(slug: string, e: ReadDecision, at: string): string | undefined {
|
|
99
|
+
const why = boundCrossed(slug, e, Date.parse(at));
|
|
100
|
+
return why && `migrated: its legacy record was committed at ${at}, ${why}; ${migratedWaiverRemedy(e.scope, String(e.payload?.obligation))}`;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|