devrites 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/engine/commands.md +65 -6
- package/engine/go.mod +1 -1
- package/engine/internal/gate/gate.go +154 -46
- package/engine/internal/gate/gate_test.go +290 -2
- package/engine/internal/gate/readiness_binding.go +66 -100
- package/engine/internal/gate/readiness_binding_test.go +126 -3
- package/engine/internal/lib/resolve.go +6 -6
- package/engine/internal/state/cmd/workflowmanifest/main.go +32 -3
- package/engine/internal/state/feature.go +1 -100
- package/engine/internal/state/observation.go +584 -0
- package/engine/internal/state/observation_open_other.go +9 -0
- package/engine/internal/state/observation_open_unix.go +12 -0
- package/engine/internal/state/observation_test.go +960 -0
- package/engine/internal/state/observation_unix_test.go +68 -0
- package/engine/internal/state/schema.go +60 -160
- package/engine/internal/state/state_test.go +489 -101
- package/engine/internal/state/status.go +67 -48
- package/engine/tests/adr_0004_required_by_phase_test.go +16 -13
- package/engine/tests/adr_0011_define_plan_test.go +14 -12
- package/engine/tests/gate_test.go +403 -28
- package/engine/tests/workspace_observation_migration_test.go +1065 -0
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/.claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/.claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/.claude/skills/rite-build/SKILL.md +51 -79
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/.claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/.claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/.claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/.claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/.claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/claude/skills/rite-build/SKILL.md +51 -79
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/generated/claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/codex/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/codex/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/codex/skills/rite-build/SKILL.md +51 -79
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/generated/codex/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/codex/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/codex/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/codex/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/codex/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [4.2.0](https://github.com/ViktorsBaikers/DevRites/compare/v4.1.0...v4.2.0) (2026-08-23)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
* **rite:** ship workflow artifact, reslice, and observation ([#36](https://github.com/ViktorsBaikers/DevRites/issues/36)) ([456a00e](https://github.com/ViktorsBaikers/DevRites/commit/456a00e8280acdab9525a762a267be7084b26a9f))
|
|
10
|
+
|
|
5
11
|
## [4.1.0](https://github.com/ViktorsBaikers/DevRites/compare/v4.0.12...v4.1.0) (2026-08-12)
|
|
6
12
|
|
|
7
13
|
### Added
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ project-conventional push, tag, or PR action, and archive the workspace.
|
|
|
28
28
|
Unattended runs may create local WIP checkpoint commits along the way, but they
|
|
29
29
|
remain local unless Ship's disclosed plan includes an approved remote action.
|
|
30
30
|
|
|
31
|
-
**Status:** [`v4.
|
|
31
|
+
**Status:** [`v4.2.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v4.2.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
32
32
|
|
|
33
33
|
This is the latest published release; `main` may contain unreleased work.
|
|
34
34
|
|
package/docs/engine/commands.md
CHANGED
|
@@ -12,9 +12,9 @@ workflow strategy.
|
|
|
12
12
|
| `update [flags]` | Refresh an existing managed installation. |
|
|
13
13
|
| `uninstall [flags]` | Remove managed artifacts while preserving runtime workspace state. |
|
|
14
14
|
| `check candidate <slug>` | Validate the strict manifest and compute the content-bound project-candidate identity. |
|
|
15
|
-
| `check readiness <slug>` | Check
|
|
15
|
+
| `check readiness <slug>` | Check target-Phase files, open human gates from Clarify onward, and the current stable Build-input binding when applicable. |
|
|
16
16
|
| `check readiness --emit-binding <slug>` | Render the exact stable Build-input binding for Vet to record after review. |
|
|
17
|
-
| `check seal <slug>` | Check
|
|
17
|
+
| `check seal <slug>` | Check files required by target Phase `seal`, open human gates, the stable Build-input binding, and exact candidate bindings. |
|
|
18
18
|
| `state resolve <qid> "<answer>"` | Resolve an open question and update `questions.md` plus `state.md` atomically. |
|
|
19
19
|
| `state close <slug>` | Archive a shipped workspace and clear matching `ACTIVE`. |
|
|
20
20
|
| `secret-scan [--staged] [--stdin] [slug]` | Scan exact staged blobs, stdin, or touched regular files for credential material. |
|
|
@@ -29,10 +29,12 @@ not to the engine command namespace.
|
|
|
29
29
|
## Check boundary
|
|
30
30
|
|
|
31
31
|
The candidate gate validates and hashes path/state/type/mode/content identity;
|
|
32
|
-
it does not infer scope from Git. The readiness gate checks
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
it does not infer scope from Git. The readiness gate checks target-Phase
|
|
33
|
+
structure and applies open-question blocking only when that target is Clarify
|
|
34
|
+
or later, plus the exact stable Build-input binding after Vet. The seal gate
|
|
35
|
+
always targets Phase `seal`, repeats that binding, and checks exact candidate
|
|
36
|
+
bindings in evidence, optional browser evidence, review, and seal. None judges
|
|
37
|
+
the meaning of `CLEAR`/`READY` prose,
|
|
36
38
|
parses reviewer narratives, infers acceptance coverage, counts assertions,
|
|
37
39
|
interprets capability deltas, or decides whether a technical plan is sound.
|
|
38
40
|
|
|
@@ -104,6 +106,63 @@ ambient Git state. Ordinary readiness and Seal require that exact standalone
|
|
|
104
106
|
line in `eng-review.md`; stale input returns
|
|
105
107
|
`reason: DRV-GATE-READINESS-STALE` and routes through `/rite-vet`.
|
|
106
108
|
|
|
109
|
+
### Workspace observation diagnostics
|
|
110
|
+
|
|
111
|
+
Lifecycle checks acquire the fixed workspace Markdown inventory once. Each
|
|
112
|
+
artifact is classified as `absent`, `empty`, `malformed`, `unsafe`,
|
|
113
|
+
`unreadable`, or `present`. Retained content is limited to 1 MiB per file and
|
|
114
|
+
8 MiB aggregate. Diagnostic lines use this exact shape:
|
|
115
|
+
`artifact: <logical-path>: <state> (<code>)`.
|
|
116
|
+
|
|
117
|
+
The closed diagnostic codes and recoveries are:
|
|
118
|
+
|
|
119
|
+
| Code | Exact Gate recovery | Exact standalone readiness-binding payload |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `malformed_markdown` | `next: repair <logical-path>: replace invalid Markdown with valid Markdown; required artifacts need substantive content` | `readiness input <logical-path> is malformed (malformed_markdown); replace invalid Markdown with valid Markdown` |
|
|
122
|
+
| `parent_symlink` | `next: repair <logical-path>: replace the symlinked parent with a real directory` | `readiness input <logical-path> is unsafe (parent_symlink); replace the symlinked parent with a real directory` |
|
|
123
|
+
| `final_symlink` | `next: repair <logical-path>: replace the symlink with a regular file` | `readiness input <logical-path> is unsafe (final_symlink); replace the symlink with a regular file` |
|
|
124
|
+
| `non_regular` | `next: repair <logical-path>: replace the non-regular entry with a regular file` | `readiness input <logical-path> is unsafe (non_regular); replace the non-regular entry with a regular file` |
|
|
125
|
+
| `file_too_large` | `next: repair <logical-path>: reduce the file to at most 1 MiB` | `readiness input <logical-path> is unsafe (file_too_large); reduce the file to at most 1 MiB` |
|
|
126
|
+
| `permission_denied` | `next: repair <logical-path>: grant read permission` | `readiness input <logical-path> is unreadable (permission_denied); grant read permission` |
|
|
127
|
+
| `read_failure` | `next: repair <logical-path>: restore a readable regular file` | `readiness input <logical-path> is unreadable (read_failure); restore a readable regular file` |
|
|
128
|
+
|
|
129
|
+
The Gate recovery column remains exact for target-policy-required artifacts. For
|
|
130
|
+
a selected optional readiness input, the same code-specific repair appends
|
|
131
|
+
`; optional readiness input may instead be removed` and does not call the input
|
|
132
|
+
required.
|
|
133
|
+
|
|
134
|
+
These seven codes are the closed Workspace Observation classification and
|
|
135
|
+
recovery mapping outcomes. A selected public consumer emits only a code
|
|
136
|
+
reachable for its consumed fixed logical path. Invalid workspace ancestry is
|
|
137
|
+
`workspace_invalid`, not an artifact `parent_symlink` diagnostic.
|
|
138
|
+
|
|
139
|
+
Status emits diagnostics without recovery or `next:` lines, after section rows
|
|
140
|
+
and before `result`. Gate emits diagnostics after `reason` and before recovery,
|
|
141
|
+
`invariant`, and `retry` lines. Generic add-content recovery applies only to
|
|
142
|
+
absent or empty target-required artifacts. Standalone readiness-binding
|
|
143
|
+
failures use the existing `readiness-binding: BLOCKED:` prefix and the logical
|
|
144
|
+
readiness-input state/code plus recovery; they never disclose physical paths or
|
|
145
|
+
content.
|
|
146
|
+
|
|
147
|
+
Whole observation failures are `workspace_invalid`, `aggregate_too_large`, and
|
|
148
|
+
`concurrent_change`. Their disclosure-safe payloads are exact:
|
|
149
|
+
|
|
150
|
+
- `workspace observation: workspace_invalid: workspace is unavailable; verify the selected logical workspace and canonical workspace override, then retry`
|
|
151
|
+
- `workspace observation: aggregate_too_large: retained content exceeds the 8 MiB aggregate limit; reduce retained Markdown below 8 MiB, then retry`
|
|
152
|
+
- `workspace observation: concurrent_change: workspace changed during acquisition; retry`
|
|
153
|
+
|
|
154
|
+
An absent or empty `state.md` appends `add real content to state.md and retry` to
|
|
155
|
+
the existing logical error. A malformed, unsafe, or unreadable `state.md`
|
|
156
|
+
appends `repair state.md and retry`. A ledger without a phase appends `record
|
|
157
|
+
phase in state.md and retry`; an unknown phase appends `record a known phase in
|
|
158
|
+
state.md and retry`.
|
|
159
|
+
|
|
160
|
+
Whole observation failures use stderr, exit `2`, and no lifecycle result or
|
|
161
|
+
reason on stdout. Standalone readiness-binding failures use one stderr line,
|
|
162
|
+
exit `3`, and empty stdout. Per-artifact lifecycle blocks keep existing reason
|
|
163
|
+
IDs and stdout exit `3`; successful checks keep stdout exit `0`. Seal evidence
|
|
164
|
+
freshness still runs separately after a successful Seal gate.
|
|
165
|
+
|
|
107
166
|
- `0`: passed or completed.
|
|
108
167
|
- `2`: common invalid request or unreadable-state result.
|
|
109
168
|
- `3`: common deterministic lifecycle or safety block.
|
package/engine/go.mod
CHANGED
|
@@ -6,11 +6,8 @@ package gate
|
|
|
6
6
|
|
|
7
7
|
import (
|
|
8
8
|
"fmt"
|
|
9
|
-
"os"
|
|
10
|
-
"path/filepath"
|
|
11
9
|
"strings"
|
|
12
10
|
|
|
13
|
-
"github.com/devrites/devrites/internal/devritespaths"
|
|
14
11
|
"github.com/devrites/devrites/internal/reason"
|
|
15
12
|
"github.com/devrites/devrites/internal/state"
|
|
16
13
|
)
|
|
@@ -27,50 +24,82 @@ const (
|
|
|
27
24
|
Seal Kind = "seal"
|
|
28
25
|
)
|
|
29
26
|
|
|
30
|
-
// Result is a gate outcome. Blocked is true iff a
|
|
31
|
-
//
|
|
27
|
+
// Result is a gate outcome. Blocked is true iff a deterministic requirement
|
|
28
|
+
// failed; missing files and diagnostics retain their canonical policy order.
|
|
32
29
|
type Result struct {
|
|
33
|
-
Kind
|
|
34
|
-
Slug
|
|
35
|
-
Phase
|
|
36
|
-
Target
|
|
37
|
-
Missing
|
|
38
|
-
MissingFiles
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
Kind Kind
|
|
31
|
+
Slug string
|
|
32
|
+
Phase state.Phase
|
|
33
|
+
Target state.Phase
|
|
34
|
+
Missing []state.Section
|
|
35
|
+
MissingFiles []string
|
|
36
|
+
Diagnostics []state.ArtifactDiagnostic
|
|
37
|
+
AddContentFiles []string
|
|
38
|
+
StateProblems []string
|
|
39
|
+
Blocked bool
|
|
40
|
+
ReasonID reason.ID
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
// Check runs a gate against feature <slug> under root.
|
|
45
|
-
//
|
|
46
|
-
// return errors; missing required content returns a blocked Result.
|
|
43
|
+
// Check runs a gate against feature <slug> under root. One workspace observation
|
|
44
|
+
// supplies every lifecycle, question, and readiness fact used by the result.
|
|
47
45
|
func Check(kind Kind, root, slug string) (*Result, error) {
|
|
48
|
-
|
|
46
|
+
observation, err := state.ObserveWorkspace(root, slug)
|
|
49
47
|
if err != nil {
|
|
50
48
|
return nil, fmt.Errorf("gate %s: %w", kind, err)
|
|
51
49
|
}
|
|
52
|
-
|
|
50
|
+
result, err := checkObservation(kind, observation)
|
|
51
|
+
if err != nil {
|
|
52
|
+
return nil, fmt.Errorf("gate %s: %w", kind, err)
|
|
53
|
+
}
|
|
54
|
+
return result, nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func checkObservation(kind Kind, observation *state.WorkspaceObservation) (*Result, error) {
|
|
58
|
+
phase, err := observation.DeclaredPhase()
|
|
59
|
+
if err != nil {
|
|
60
|
+
return nil, err
|
|
61
|
+
}
|
|
62
|
+
target := phase
|
|
53
63
|
if kind == Seal {
|
|
54
64
|
target = state.PhaseSeal
|
|
55
65
|
}
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
policy, ok := state.PolicyFor(target)
|
|
67
|
+
if !ok {
|
|
68
|
+
return nil, fmt.Errorf("unknown target phase %q", target)
|
|
69
|
+
}
|
|
70
|
+
target = policy.Target
|
|
71
|
+
|
|
72
|
+
missing := missingSections(observation, policy.RequiredSections)
|
|
73
|
+
missingArtifacts, diagnostics := observation.Missing(policy.RequiredArtifacts)
|
|
74
|
+
missingFiles := make([]string, len(missingArtifacts))
|
|
75
|
+
var addContentFiles []string
|
|
76
|
+
for i, artifact := range missingArtifacts {
|
|
77
|
+
missingFiles[i] = string(artifact)
|
|
78
|
+
fact, ok := observation.Fact(artifact)
|
|
79
|
+
if ok && (fact.State() == state.ArtifactAbsent || fact.State() == state.ArtifactEmpty) {
|
|
80
|
+
addContentFiles = append(addContentFiles, string(artifact))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
58
83
|
blocked := len(missingFiles) > 0
|
|
59
84
|
readinessStale := false
|
|
60
85
|
var stateProblems []string
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if
|
|
64
|
-
problem
|
|
86
|
+
|
|
87
|
+
if policy.BlocksOpenQuestions {
|
|
88
|
+
if gates, awaitingHuman := retainedHumanGates(observation); len(gates) > 0 {
|
|
89
|
+
problem := fmt.Sprintf("open %s human question(s) remain in questions.md", strings.Join(gates, "/"))
|
|
90
|
+
if !awaitingHuman {
|
|
91
|
+
problem += " but state.md is not awaiting_human"
|
|
92
|
+
}
|
|
93
|
+
stateProblems = append(stateProblems, problem)
|
|
94
|
+
blocked = true
|
|
65
95
|
}
|
|
66
|
-
stateProblems = append(stateProblems, problem)
|
|
67
|
-
blocked = true
|
|
68
96
|
}
|
|
69
|
-
if len(missingFiles) == 0 && phaseRequiresReadinessBinding(
|
|
70
|
-
expected, bindingErr := verifyReadinessBinding(
|
|
97
|
+
if len(missingFiles) == 0 && phaseRequiresReadinessBinding(policy) {
|
|
98
|
+
expected, bindingErr := verifyReadinessBinding(observation)
|
|
71
99
|
if bindingErr != nil {
|
|
72
100
|
readinessStale = true
|
|
73
101
|
blocked = true
|
|
102
|
+
diagnostics = append(diagnostics, readinessDiagnostics(observation)...)
|
|
74
103
|
if expected == "" {
|
|
75
104
|
stateProblems = append(stateProblems, bindingErr.Error()+"; repair the input and rerun /rite-vet")
|
|
76
105
|
} else {
|
|
@@ -78,15 +107,18 @@ func Check(kind Kind, root, slug string) (*Result, error) {
|
|
|
78
107
|
}
|
|
79
108
|
}
|
|
80
109
|
}
|
|
110
|
+
|
|
81
111
|
result := &Result{
|
|
82
|
-
Kind:
|
|
83
|
-
Slug:
|
|
84
|
-
Phase:
|
|
85
|
-
Target:
|
|
86
|
-
Missing:
|
|
87
|
-
MissingFiles:
|
|
88
|
-
|
|
89
|
-
|
|
112
|
+
Kind: kind,
|
|
113
|
+
Slug: observation.Slug(),
|
|
114
|
+
Phase: phase,
|
|
115
|
+
Target: target,
|
|
116
|
+
Missing: missing,
|
|
117
|
+
MissingFiles: missingFiles,
|
|
118
|
+
Diagnostics: diagnostics,
|
|
119
|
+
AddContentFiles: addContentFiles,
|
|
120
|
+
StateProblems: stateProblems,
|
|
121
|
+
Blocked: blocked,
|
|
90
122
|
}
|
|
91
123
|
result.ReasonID = ResultReasonID(kind, blocked)
|
|
92
124
|
if readinessStale {
|
|
@@ -95,6 +127,24 @@ func Check(kind Kind, root, slug string) (*Result, error) {
|
|
|
95
127
|
return result, nil
|
|
96
128
|
}
|
|
97
129
|
|
|
130
|
+
func missingSections(observation *state.WorkspaceObservation, required []state.Section) []state.Section {
|
|
131
|
+
var missing []state.Section
|
|
132
|
+
for _, section := range required {
|
|
133
|
+
artifact := state.ArtifactPath(string(section) + ".md")
|
|
134
|
+
switch section {
|
|
135
|
+
case state.SectionProof:
|
|
136
|
+
artifact = state.EvidenceFile
|
|
137
|
+
case state.SectionStatus:
|
|
138
|
+
artifact = state.LedgerFile
|
|
139
|
+
}
|
|
140
|
+
fact, ok := observation.Fact(artifact)
|
|
141
|
+
if !ok || fact.State() != state.ArtifactPresent {
|
|
142
|
+
missing = append(missing, section)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return missing
|
|
146
|
+
}
|
|
147
|
+
|
|
98
148
|
// ResultReasonID returns the typed outcome owned by the lifecycle gate.
|
|
99
149
|
func ResultReasonID(kind Kind, blocked bool) reason.ID {
|
|
100
150
|
switch kind {
|
|
@@ -114,7 +164,7 @@ func ResultReasonID(kind Kind, blocked bool) reason.ID {
|
|
|
114
164
|
}
|
|
115
165
|
|
|
116
166
|
// Render returns stable, greppable output with a trailing newline. A blocked
|
|
117
|
-
// result names
|
|
167
|
+
// result names missing files, selected diagnostics, recovery, and the retry.
|
|
118
168
|
func (r *Result) Render() string {
|
|
119
169
|
var b strings.Builder
|
|
120
170
|
fmt.Fprintf(&b, "gate: %s\n", r.Kind)
|
|
@@ -135,8 +185,17 @@ func (r *Result) Render() string {
|
|
|
135
185
|
fmt.Fprintf(&b, "result: blocked (missing to leave %q: %s)\n", r.Phase, strings.Join(missing, ", "))
|
|
136
186
|
}
|
|
137
187
|
fmt.Fprintf(&b, "reason: %s\n", r.ReasonID)
|
|
138
|
-
|
|
139
|
-
fmt.Fprintf(&b, "
|
|
188
|
+
for _, diagnostic := range r.Diagnostics {
|
|
189
|
+
fmt.Fprintf(&b, "artifact: %s: %s (%s)\n", diagnostic.Path, diagnostic.State, diagnostic.Code)
|
|
190
|
+
}
|
|
191
|
+
if len(r.AddContentFiles) > 0 {
|
|
192
|
+
fmt.Fprintf(&b, "next: add real content to %s\n", strings.Join(r.AddContentFiles, ", "))
|
|
193
|
+
}
|
|
194
|
+
for _, diagnostic := range r.Diagnostics {
|
|
195
|
+
if recovery := diagnosticRecovery(diagnostic, targetRequiresArtifact(r.Target, diagnostic.Path)); recovery != "" {
|
|
196
|
+
b.WriteString(recovery)
|
|
197
|
+
b.WriteByte('\n')
|
|
198
|
+
}
|
|
140
199
|
}
|
|
141
200
|
for _, problem := range r.StateProblems {
|
|
142
201
|
fmt.Fprintf(&b, "invariant: %s\n", problem)
|
|
@@ -145,19 +204,68 @@ func (r *Result) Render() string {
|
|
|
145
204
|
return b.String()
|
|
146
205
|
}
|
|
147
206
|
|
|
207
|
+
func diagnosticRecovery(diagnostic state.ArtifactDiagnostic, required bool) string {
|
|
208
|
+
prefix := fmt.Sprintf("next: repair %s: ", diagnostic.Path)
|
|
209
|
+
repair := diagnosticRepair(diagnostic.Code)
|
|
210
|
+
if repair == "" {
|
|
211
|
+
return ""
|
|
212
|
+
}
|
|
213
|
+
if required && diagnostic.Code == state.DiagnosticMalformedMarkdown {
|
|
214
|
+
repair += "; required artifacts need substantive content"
|
|
215
|
+
}
|
|
216
|
+
if !required {
|
|
217
|
+
repair += "; optional readiness input may instead be removed"
|
|
218
|
+
}
|
|
219
|
+
return prefix + repair
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func targetRequiresArtifact(target state.Phase, path state.ArtifactPath) bool {
|
|
223
|
+
policy, ok := state.PolicyFor(target)
|
|
224
|
+
if !ok {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
for _, required := range policy.RequiredArtifacts {
|
|
228
|
+
if required == path {
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func diagnosticRepair(code state.DiagnosticCode) string {
|
|
236
|
+
switch code {
|
|
237
|
+
case state.DiagnosticMalformedMarkdown:
|
|
238
|
+
return "replace invalid Markdown with valid Markdown"
|
|
239
|
+
case state.DiagnosticParentSymlink:
|
|
240
|
+
return "replace the symlinked parent with a real directory"
|
|
241
|
+
case state.DiagnosticFinalSymlink:
|
|
242
|
+
return "replace the symlink with a regular file"
|
|
243
|
+
case state.DiagnosticNonRegular:
|
|
244
|
+
return "replace the non-regular entry with a regular file"
|
|
245
|
+
case state.DiagnosticFileTooLarge:
|
|
246
|
+
return "reduce the file to at most 1 MiB"
|
|
247
|
+
case state.DiagnosticPermissionDenied:
|
|
248
|
+
return "grant read permission"
|
|
249
|
+
case state.DiagnosticReadFailure:
|
|
250
|
+
return "restore a readable regular file"
|
|
251
|
+
default:
|
|
252
|
+
return ""
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
148
256
|
const gateSpaceChars = " \t\n\v\f\r"
|
|
149
257
|
|
|
150
|
-
func
|
|
151
|
-
|
|
152
|
-
if
|
|
258
|
+
func retainedHumanGates(observation *state.WorkspaceObservation) ([]string, bool) {
|
|
259
|
+
questions, ok := observation.Fact("questions.md")
|
|
260
|
+
if !ok || (questions.State() != state.ArtifactPresent && questions.State() != state.ArtifactEmpty) {
|
|
153
261
|
return nil, false
|
|
154
262
|
}
|
|
155
|
-
gates := openBlockingQuestionGates(
|
|
263
|
+
gates := openBlockingQuestionGates(questions.Bytes())
|
|
156
264
|
if len(gates) == 0 {
|
|
157
265
|
return nil, false
|
|
158
266
|
}
|
|
159
|
-
|
|
160
|
-
return gates,
|
|
267
|
+
ledger, ok := observation.Fact(state.LedgerFile)
|
|
268
|
+
return gates, ok && ledger.State() == state.ArtifactPresent && stateAwaitingHuman(ledger.Bytes())
|
|
161
269
|
}
|
|
162
270
|
|
|
163
271
|
func openBlockingQuestionGates(data []byte) []string {
|