devrites 6.0.1 → 6.0.3
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 +16 -0
- package/README.md +15 -64
- package/SECURITY.md +7 -11
- package/bin/devrites.mjs +0 -8
- package/docs/adr/0034-workflow-artifact-byte-corrections-get-code-review.md +60 -0
- package/docs/adr/README.md +2 -1
- package/docs/architecture.md +2 -5
- package/docs/cli.md +4 -11
- package/docs/command-map.md +1 -2
- package/docs/engine/commands.md +6 -6
- package/docs/release.md +5 -6
- package/engine/help.go +12 -7
- package/engine/help_test.go +1 -0
- package/engine/internal/gate/readiness_binding.go +19 -0
- package/engine/internal/lib/cli_observe.go +16 -0
- package/engine/internal/lib/context.go +191 -19
- package/engine/internal/lib/context_next_test.go +186 -0
- package/engine/internal/lib/dispatch.go +146 -11
- package/engine/internal/lib/drift.go +10 -2
- package/engine/internal/lib/drift_test.go +29 -0
- package/engine/internal/lib/slicecheck.go +19 -0
- package/engine/internal/lib/slicecheck_test.go +41 -0
- package/engine/internal/lib/windows.go +109 -13
- package/engine/internal/lib/windows_test.go +85 -0
- package/engine/internal/release/release.go +3 -13
- package/engine/internal/release/release_test.go +6 -197
- package/engine/main.go +2 -2
- package/install.sh +1 -116
- package/pack/.claude/agents/devrites-slice-wright.md +3 -1
- package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/.claude/skills/rite-build/SKILL.md +1 -1
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/.claude/skills/rite-define/SKILL.md +2 -1
- package/pack/.claude/skills/rite-plan/SKILL.md +2 -1
- package/pack/.claude/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/.claude/skills/rite-vet/SKILL.md +9 -1
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/claude/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/claude/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/claude/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/codex/agents/devrites-slice-wright.toml +3 -1
- package/pack/generated/codex/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/codex/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/codex/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/codex/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/devin/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/devin/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/devin/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/devin/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/devin/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/devin/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/devin/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/omp/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/omp/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/omp/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/omp/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/omp/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/omp/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/omp/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/pi/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/pi/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/pi/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/pi/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/pi/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/pi/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +6 -2
- package/package.json +1 -1
- package/scripts/install-lib.sh +0 -16
|
@@ -14,6 +14,8 @@ import (
|
|
|
14
14
|
"strings"
|
|
15
15
|
|
|
16
16
|
"github.com/devrites/devrites/internal/devritespaths"
|
|
17
|
+
"github.com/devrites/devrites/internal/markdowntext"
|
|
18
|
+
"github.com/devrites/devrites/internal/state"
|
|
17
19
|
)
|
|
18
20
|
|
|
19
21
|
// loadsManifest is the machine-readable read-set a skill declares in an HTML
|
|
@@ -24,29 +26,35 @@ import (
|
|
|
24
26
|
// role key is present its list replaces "workspace" entirely (an empty list
|
|
25
27
|
// means the role needs no workspace artifacts). "agents" maps a --role name to
|
|
26
28
|
// an agents-root-relative path when it deviates from the devrites-<role>
|
|
27
|
-
// convention.
|
|
29
|
+
// convention. "rootOnly" lists always/trigger paths that only the controlling
|
|
30
|
+
// root executes; a --role bundle names them instead of inlining them.
|
|
28
31
|
type loadsManifest struct {
|
|
29
32
|
Always []string `json:"always"`
|
|
30
33
|
Triggers map[string][]string `json:"triggers"`
|
|
31
34
|
Workspace []string `json:"workspace"`
|
|
32
35
|
WorkspaceByRole map[string][]string `json:"workspaceByRole"`
|
|
33
36
|
Agents map[string]string `json:"agents"`
|
|
37
|
+
RootOnly []string `json:"rootOnly"`
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
const loadsMarker = "<!-- loads:"
|
|
37
41
|
|
|
38
|
-
const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
|
|
42
|
+
const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--slice <SLICE-###>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
|
|
39
43
|
|
|
40
44
|
Emits one deduplicated context bundle for a phase or standalone skill
|
|
41
45
|
(+ optional dispatch role) into .devrites/work/<slug>/ctx/ so an agent
|
|
42
46
|
reads a single file. With --skill, slug may be omitted; then --out is
|
|
43
|
-
required and no workspace section is included.
|
|
47
|
+
required and no workspace section is included. --slice scopes the
|
|
48
|
+
workspace section to one slice: tasks.md contributes that slice's block,
|
|
49
|
+
and an artifact over its line/byte budget becomes a by-reference index
|
|
50
|
+
(path, SHA-256, headings, and line hits for every ID the block cites).
|
|
51
|
+
Repeat --slice for a recheck that spans several slices.
|
|
44
52
|
`
|
|
45
53
|
|
|
46
54
|
// RunContext emits one deduplicated context bundle for a phase (+ optional
|
|
47
55
|
// dispatch role) so an agent reads a single file instead of selecting files.
|
|
48
56
|
//
|
|
49
|
-
// context <slug> --phase <p> [--role <r>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
|
|
57
|
+
// context <slug> --phase <p> [--role <r>] [--slice <SLICE-###>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
|
|
50
58
|
func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
51
59
|
opts, code := parseContextArgs(args, stderr)
|
|
52
60
|
if code != 0 {
|
|
@@ -82,6 +90,27 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
82
90
|
return 2
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
var sliceBlock string
|
|
94
|
+
var sliceIDs []citedID
|
|
95
|
+
if opts.slice != "" {
|
|
96
|
+
raw, err := os.ReadFile(filepath.Join(featureDir, "tasks.md")) // #nosec G304 -- featureDir is the validated feature workspace
|
|
97
|
+
if err != nil {
|
|
98
|
+
fmt.Fprintf(stderr, "context: %v\n", err)
|
|
99
|
+
return 2
|
|
100
|
+
}
|
|
101
|
+
var blocks []string
|
|
102
|
+
for _, id := range opts.slices {
|
|
103
|
+
block, ok := state.ExtractTaskSlice(raw, id)
|
|
104
|
+
if !ok {
|
|
105
|
+
fmt.Fprintf(stdout, "context: BLOCKED: %s not found in tasks.md\n", id)
|
|
106
|
+
return 3
|
|
107
|
+
}
|
|
108
|
+
blocks = append(blocks, block)
|
|
109
|
+
}
|
|
110
|
+
sliceBlock = strings.Join(blocks, "\n")
|
|
111
|
+
sliceIDs = citedIDs(sliceBlock)
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
var files []string
|
|
86
115
|
var missingRequired []string
|
|
87
116
|
var autoTriggers []string
|
|
@@ -124,12 +153,31 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
124
153
|
|
|
125
154
|
seen := map[string]bool{}
|
|
126
155
|
loaded := 0
|
|
156
|
+
// The role contract leads the bundle so the agent reads its rules before
|
|
157
|
+
// reference text.
|
|
158
|
+
if opts.role != "" {
|
|
159
|
+
rel := roleAgentPath(manifest, opts.role)
|
|
160
|
+
if writeContextEntry(&bundle, filepath.Join(skillsRoot, "..", "agents", filepath.FromSlash(rel)),
|
|
161
|
+
"agents/"+rel, seen, true) {
|
|
162
|
+
loaded++
|
|
163
|
+
} else {
|
|
164
|
+
// The agent contract is a pack file, not a workspace artifact: a
|
|
165
|
+
// miss means a typo'd role or a pack bug, and a dispatch without
|
|
166
|
+
// its contract must fail loudly.
|
|
167
|
+
missingRequired = append(missingRequired, "agents/"+rel)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
var rootOnly []string
|
|
127
171
|
for _, rel := range files {
|
|
128
172
|
abs := filepath.Join(skillsRoot, filepath.FromSlash(rel))
|
|
129
173
|
if seen[abs] {
|
|
130
174
|
continue
|
|
131
175
|
}
|
|
132
176
|
seen[abs] = true
|
|
177
|
+
if opts.role != "" && slices.Contains(manifest.RootOnly, rel) {
|
|
178
|
+
rootOnly = append(rootOnly, rel)
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
133
181
|
data, err := os.ReadFile(abs) // #nosec G304 -- abs is resolved from the validated skills root
|
|
134
182
|
fmt.Fprintf(&bundle, "===== %s =====\n", rel)
|
|
135
183
|
if err != nil {
|
|
@@ -144,34 +192,37 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
144
192
|
bundle.WriteByte('\n')
|
|
145
193
|
loaded++
|
|
146
194
|
}
|
|
195
|
+
if len(rootOnly) > 0 {
|
|
196
|
+
fmt.Fprintf(&bundle, "===== root-only (not inlined for role %s) =====\n%s\n\n", opts.role, strings.Join(rootOnly, "\n"))
|
|
197
|
+
}
|
|
147
198
|
|
|
199
|
+
whole, byReference := 0, 0
|
|
148
200
|
workspaceList, workspaceScoped := workspaceReadSet(manifest, opts.role)
|
|
149
201
|
if hasManifest && featureDir != "" {
|
|
150
202
|
if workspaceScoped && len(workspaceList) == 0 {
|
|
151
203
|
bundle.WriteString("===== workspace =====\nskipped: role-scoped read-set declares no workspace artifacts\n\n")
|
|
152
204
|
}
|
|
153
205
|
for _, rel := range workspaceList {
|
|
154
|
-
|
|
155
|
-
|
|
206
|
+
abs := filepath.Join(featureDir, filepath.FromSlash(rel))
|
|
207
|
+
if opts.slice == "" {
|
|
208
|
+
if writeContextEntry(&bundle, abs, "workspace/"+rel, seen, false) {
|
|
209
|
+
loaded++
|
|
210
|
+
}
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
ok, byRef := writeSliceEntry(&bundle, filepath.Dir(root), abs, "workspace/"+rel, opts.slice, sliceBlock, sliceIDs, seen)
|
|
214
|
+
if ok {
|
|
156
215
|
loaded++
|
|
157
216
|
}
|
|
217
|
+
if byRef {
|
|
218
|
+
byReference++
|
|
219
|
+
} else if ok {
|
|
220
|
+
whole++
|
|
221
|
+
}
|
|
158
222
|
}
|
|
159
223
|
} else if hasManifest && len(workspaceList) > 0 {
|
|
160
224
|
bundle.WriteString("===== workspace =====\nskipped: no feature workspace (slug omitted)\n\n")
|
|
161
225
|
}
|
|
162
|
-
if opts.role != "" {
|
|
163
|
-
rel := roleAgentPath(manifest, opts.role)
|
|
164
|
-
if writeContextEntry(&bundle, filepath.Join(skillsRoot, "..", "agents", filepath.FromSlash(rel)),
|
|
165
|
-
"agents/"+rel, seen, true) {
|
|
166
|
-
loaded++
|
|
167
|
-
} else {
|
|
168
|
-
// The agent contract is a pack file, not a workspace artifact: a
|
|
169
|
-
// miss means a typo'd role or a pack bug, and a dispatch without
|
|
170
|
-
// its contract must fail loudly.
|
|
171
|
-
missingRequired = append(missingRequired, "agents/"+rel)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
226
|
out := opts.out
|
|
176
227
|
if out == "" {
|
|
177
228
|
out = filepath.Join(featureDir, "ctx", bundleName(opts))
|
|
@@ -216,6 +267,9 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
216
267
|
fmt.Fprintf(stdout, ", MISSING: %s", strings.Join(missingRequired, ","))
|
|
217
268
|
}
|
|
218
269
|
fmt.Fprintln(stdout, ")")
|
|
270
|
+
if opts.slice != "" {
|
|
271
|
+
fmt.Fprintf(stdout, "scope: slice %s (%d whole, %d by-reference)\n", opts.slice, whole, byReference)
|
|
272
|
+
}
|
|
219
273
|
if workspaceScoped {
|
|
220
274
|
fmt.Fprintf(stdout, "workspace: role-scoped for %s (%d of %d declared artifacts)\n",
|
|
221
275
|
opts.role, len(workspaceList), len(manifest.Workspace))
|
|
@@ -280,6 +334,110 @@ func expandIncludes(abs string, data []byte) []byte {
|
|
|
280
334
|
})
|
|
281
335
|
}
|
|
282
336
|
|
|
337
|
+
// writeSliceEntry writes one workspace artifact of a --slice bundle: tasks.md
|
|
338
|
+
// as the slice block plus a by-reference index, any other canonical artifact
|
|
339
|
+
// over its line/byte budget as a by-reference index, everything else whole.
|
|
340
|
+
func writeSliceEntry(bundle *strings.Builder, project, abs, label, slice, block string, ids []citedID, seen map[string]bool) (loaded, byRef bool) {
|
|
341
|
+
if seen[abs] {
|
|
342
|
+
return false, false
|
|
343
|
+
}
|
|
344
|
+
data, err := os.ReadFile(abs) // #nosec G304 -- abs is resolved from the validated feature workspace
|
|
345
|
+
name := filepath.Base(abs)
|
|
346
|
+
if err == nil && name == "tasks.md" {
|
|
347
|
+
seen[abs] = true
|
|
348
|
+
fmt.Fprintf(bundle, "===== %s (%s block) =====\n%s\n", label, slice, block)
|
|
349
|
+
} else if status, ok := state.ArtifactBudget(name, data); err != nil || !ok || !status.Over {
|
|
350
|
+
return writeContextEntry(bundle, abs, label, seen, false), false
|
|
351
|
+
} else {
|
|
352
|
+
seen[abs] = true
|
|
353
|
+
}
|
|
354
|
+
writeAnchorIndex(bundle, project, abs, label, data, ids)
|
|
355
|
+
return true, true
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
var (
|
|
359
|
+
citedIDRE = regexp.MustCompile(`\b[A-Z]{1,6}-\d{2,}(?:-[A-Z][A-Z0-9]*)*\b`)
|
|
360
|
+
anchorHeadingRE = regexp.MustCompile(`^#{1,3}\s`)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
// maxIDHits bounds the line anchors listed per ID; the live file holds the rest.
|
|
364
|
+
const maxIDHits = 20
|
|
365
|
+
|
|
366
|
+
// citedID is one ID the slice block cites with its word-bounded matcher.
|
|
367
|
+
type citedID struct {
|
|
368
|
+
id string
|
|
369
|
+
re *regexp.Regexp
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// citedIDs returns each distinct ID the slice block cites (AC-###, DEC-###,
|
|
373
|
+
// T-###, PF-##, SLICE-###, ...).
|
|
374
|
+
func citedIDs(block string) []citedID {
|
|
375
|
+
var out []citedID
|
|
376
|
+
seen := map[string]bool{}
|
|
377
|
+
for _, id := range citedIDRE.FindAllString(block, -1) {
|
|
378
|
+
if !seen[id] {
|
|
379
|
+
seen[id] = true
|
|
380
|
+
out = append(out, citedID{id, regexp.MustCompile(`\b` + regexp.QuoteMeta(id) + `\b`)})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// writeAnchorIndex emits a by-reference entry: identity, heading anchors, and
|
|
387
|
+
// line hits for each cited ID, so the agent reads owning sections from the
|
|
388
|
+
// live file instead of the whole artifact.
|
|
389
|
+
func writeAnchorIndex(bundle *strings.Builder, project, abs, label string, data []byte, ids []citedID) {
|
|
390
|
+
rel, err := filepath.Rel(project, abs)
|
|
391
|
+
if err != nil {
|
|
392
|
+
rel = abs
|
|
393
|
+
}
|
|
394
|
+
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
|
|
395
|
+
structural := lines
|
|
396
|
+
if masked, err := markdowntext.Structural(data); err == nil {
|
|
397
|
+
structural = strings.Split(strings.ReplaceAll(string(masked), "\r\n", "\n"), "\n")
|
|
398
|
+
}
|
|
399
|
+
fmt.Fprintf(bundle, "===== %s (by reference) =====\n", label)
|
|
400
|
+
fmt.Fprintf(bundle, "path: %s sha256: %x bytes: %d lines: %d\n", filepath.ToSlash(rel), sha256.Sum256(data), len(data), len(lines))
|
|
401
|
+
bundle.WriteString("Not inlined. Read owning sections from the live file by line anchor; nothing is omitted there.\n")
|
|
402
|
+
bundle.WriteString("headings:\n")
|
|
403
|
+
for i, line := range structural {
|
|
404
|
+
if anchorHeadingRE.MatchString(line) {
|
|
405
|
+
fmt.Fprintf(bundle, "- L%d %s\n", i+1, clipRunes(lines[i], 120))
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if len(ids) > 0 {
|
|
409
|
+
bundle.WriteString("id hits (IDs cited by the slice block):\n")
|
|
410
|
+
}
|
|
411
|
+
for _, cited := range ids {
|
|
412
|
+
var at []string
|
|
413
|
+
total := 0
|
|
414
|
+
for i, line := range lines {
|
|
415
|
+
if cited.re.MatchString(line) {
|
|
416
|
+
total++
|
|
417
|
+
if len(at) < maxIDHits {
|
|
418
|
+
at = append(at, fmt.Sprintf("L%d", i+1))
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
hits := "none"
|
|
423
|
+
if total > 0 {
|
|
424
|
+
hits = strings.Join(at, " ")
|
|
425
|
+
if total > len(at) {
|
|
426
|
+
hits += fmt.Sprintf(" (+%d more)", total-len(at))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
fmt.Fprintf(bundle, "- %s: %s\n", cited.id, hits)
|
|
430
|
+
}
|
|
431
|
+
bundle.WriteByte('\n')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
func clipRunes(s string, n int) string {
|
|
435
|
+
if r := []rune(s); len(r) > n {
|
|
436
|
+
return string(r[:n]) + "…"
|
|
437
|
+
}
|
|
438
|
+
return s
|
|
439
|
+
}
|
|
440
|
+
|
|
283
441
|
func writeContextEntry(bundle *strings.Builder, abs, label string, seen map[string]bool, expand bool) bool {
|
|
284
442
|
if seen[abs] {
|
|
285
443
|
return false
|
|
@@ -308,6 +466,8 @@ type contextOpts struct {
|
|
|
308
466
|
phase string
|
|
309
467
|
skill string
|
|
310
468
|
role string
|
|
469
|
+
slice string // display label: slices joined with "+"
|
|
470
|
+
slices []string
|
|
311
471
|
triggers []string
|
|
312
472
|
skillsRoot string
|
|
313
473
|
out string
|
|
@@ -325,6 +485,8 @@ func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
|
|
|
325
485
|
opts.skill = value
|
|
326
486
|
case "--role":
|
|
327
487
|
opts.role = value
|
|
488
|
+
case "--slice":
|
|
489
|
+
opts.slices = append(opts.slices, value)
|
|
328
490
|
case "--trigger", "--triggers":
|
|
329
491
|
for _, name := range strings.Split(value, ",") {
|
|
330
492
|
if name = strings.TrimSpace(name); name != "" {
|
|
@@ -353,6 +515,13 @@ func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
|
|
|
353
515
|
fmt.Fprintf(stderr, "context: --skill must be a directory name, not a path\n")
|
|
354
516
|
return opts, 2
|
|
355
517
|
}
|
|
518
|
+
for _, id := range opts.slices {
|
|
519
|
+
if !state.ValidSliceID(id) || len(positional) == 0 {
|
|
520
|
+
fmt.Fprintf(stderr, "context: --slice needs a slug and a SLICE-### id, got %q\n", id)
|
|
521
|
+
return opts, 2
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
opts.slice = strings.Join(opts.slices, "+")
|
|
356
525
|
if len(positional) == 1 {
|
|
357
526
|
opts.slug = positional[0]
|
|
358
527
|
} else if opts.out == "" {
|
|
@@ -429,6 +598,9 @@ func bundleName(opts contextOpts) string {
|
|
|
429
598
|
if opts.role != "" {
|
|
430
599
|
name += "-" + opts.role
|
|
431
600
|
}
|
|
601
|
+
if opts.slice != "" {
|
|
602
|
+
name += "." + opts.slice
|
|
603
|
+
}
|
|
432
604
|
return name + ".bundle.md"
|
|
433
605
|
}
|
|
434
606
|
|
|
@@ -672,3 +672,189 @@ func TestContextWorkspaceIncludeMarkerStaysLiteral(t *testing.T) {
|
|
|
672
672
|
t.Fatalf("workspace include marker not kept literal:\n%s", text)
|
|
673
673
|
}
|
|
674
674
|
}
|
|
675
|
+
|
|
676
|
+
func TestContextSliceScopesWorkspaceAndRole(t *testing.T) {
|
|
677
|
+
root, featureDir := newWorkspace(t, "build")
|
|
678
|
+
skills := newSkillsRoot(t)
|
|
679
|
+
writeFile(t, filepath.Join(skills, "rite-build", "reference", "root-procedure.md"), "# root procedure body\n")
|
|
680
|
+
writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\",\"rite-build/reference/root-procedure.md\"],\"rootOnly\":[\"rite-build/reference/root-procedure.md\"],\"workspace\":[\"tasks.md\",\"decisions.md\",\"spec.md\"]} -->\n# build\n")
|
|
681
|
+
writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: first body\n\n## SLICE-002 Second\nGoal: second body\nSatisfies: AC-001\nDecisions: DEC-010\ndepends_on: [SLICE-001]\n")
|
|
682
|
+
var decisions strings.Builder
|
|
683
|
+
decisions.WriteString("# Decisions\n")
|
|
684
|
+
for i := 0; i < 250; i++ {
|
|
685
|
+
decisions.WriteString("filler decision row\n")
|
|
686
|
+
}
|
|
687
|
+
decisions.WriteString("## DEC-010 Use the store\nDEC-0100 is a different id\n")
|
|
688
|
+
writeFile(t, filepath.Join(featureDir, "decisions.md"), decisions.String())
|
|
689
|
+
writeFile(t, filepath.Join(featureDir, "spec.md"), "# spec\nAC-001 small spec body\n")
|
|
690
|
+
|
|
691
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
692
|
+
args := []string{"feat", "--phase", "build", "--role", "slice-wright", "--slice", "SLICE-002", "--skills-root", skills}
|
|
693
|
+
if code := RunContext(root, args, stdout, stderr); code != 0 {
|
|
694
|
+
t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
|
|
695
|
+
}
|
|
696
|
+
if !strings.Contains(stdout.String(), "scope: slice SLICE-002 (1 whole, 2 by-reference)") {
|
|
697
|
+
t.Fatalf("scope line missing:\n%s", stdout)
|
|
698
|
+
}
|
|
699
|
+
raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build-slice-wright.SLICE-002.bundle.md"))
|
|
700
|
+
if err != nil {
|
|
701
|
+
t.Fatalf("bundle: %v", err)
|
|
702
|
+
}
|
|
703
|
+
text := string(raw)
|
|
704
|
+
for _, want := range []string{
|
|
705
|
+
"Goal: second body",
|
|
706
|
+
"workspace/decisions.md (by reference)",
|
|
707
|
+
"/work/feat/decisions.md sha256: ",
|
|
708
|
+
"- L252 ## DEC-010 Use the store",
|
|
709
|
+
"- DEC-010: L252\n",
|
|
710
|
+
"- SLICE-001: L3 L10\n",
|
|
711
|
+
"- AC-001: L8\n",
|
|
712
|
+
"small spec body",
|
|
713
|
+
"===== root-only (not inlined for role slice-wright) =====\nrite-build/reference/root-procedure.md",
|
|
714
|
+
} {
|
|
715
|
+
if !strings.Contains(text, want) {
|
|
716
|
+
t.Fatalf("bundle missing %q:\n%s", want, text)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for _, absent := range []string{"Goal: first body", "filler decision row", "root procedure body"} {
|
|
720
|
+
if strings.Contains(text, absent) {
|
|
721
|
+
t.Fatalf("bundle inlined %q:\n%s", absent, text)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if strings.Index(text, "# wright") > strings.Index(text, "# core") {
|
|
725
|
+
t.Fatalf("role contract must lead the bundle:\n%s", text)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Without --role the root still gets its procedure inline.
|
|
729
|
+
stdout.Reset()
|
|
730
|
+
if code := RunContext(root, []string{"feat", "--phase", "build", "--skills-root", skills}, stdout, stderr); code != 0 {
|
|
731
|
+
t.Fatalf("phase bundle code=%d\n%s", code, stderr)
|
|
732
|
+
}
|
|
733
|
+
phase, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.bundle.md"))
|
|
734
|
+
if err != nil || !strings.Contains(string(phase), "root procedure body") || !strings.Contains(string(phase), "Goal: first body") {
|
|
735
|
+
t.Fatalf("phase bundle must stay whole: err=%v\n%s", err, phase)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
func TestContextSliceRejectsBadOrUnknownID(t *testing.T) {
|
|
740
|
+
root, featureDir := newWorkspace(t, "build")
|
|
741
|
+
skills := newSkillsRoot(t)
|
|
742
|
+
writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: x\n")
|
|
743
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
744
|
+
if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "slice-1", "--skills-root", skills}, stdout, stderr); code != 2 {
|
|
745
|
+
t.Fatalf("malformed id: code=%d want 2\n%s", code, stderr)
|
|
746
|
+
}
|
|
747
|
+
if code := RunContext(root, []string{"--skill", "rite-build", "--slice", "SLICE-001", "--out", "x.md", "--skills-root", skills}, stdout, stderr); code != 2 {
|
|
748
|
+
t.Fatalf("slice without slug: code=%d want 2\n%s", code, stderr)
|
|
749
|
+
}
|
|
750
|
+
stdout.Reset()
|
|
751
|
+
if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "SLICE-009", "--skills-root", skills}, stdout, stderr); code != 3 || !strings.Contains(stdout.String(), "BLOCKED: SLICE-009 not found") {
|
|
752
|
+
t.Fatalf("unknown slice: code=%d\n%s", code, stdout)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
func TestDispatchWrightRoundCap(t *testing.T) {
|
|
757
|
+
root, featureDir := newWorkspace(t, "build")
|
|
758
|
+
run := func(args ...string) (int, string) {
|
|
759
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
760
|
+
code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
|
|
761
|
+
return code, stdout.String() + stderr.String()
|
|
762
|
+
}
|
|
763
|
+
open := func(wave string, extra ...string) (int, string) {
|
|
764
|
+
return run(append([]string{"open", "--phase", "build", "--wave", wave, "--role", "slice-wright", "--slice", "SLICE-001"}, extra...)...)
|
|
765
|
+
}
|
|
766
|
+
if code, out := run("open", "--phase", "build", "--wave", "w0", "--role", "slice-wright"); code != 2 || !strings.Contains(out, "needs --slice") {
|
|
767
|
+
t.Fatalf("missing --slice: code=%d\n%s", code, out)
|
|
768
|
+
}
|
|
769
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v1", "--role", "plan-reviewer"); code != 0 {
|
|
770
|
+
t.Fatalf("non-build wave must not need --slice: code=%d\n%s", code, out)
|
|
771
|
+
}
|
|
772
|
+
for i := 1; i <= wrightRoundsPerReview; i++ {
|
|
773
|
+
code, out := open("r" + string(rune('0'+i)))
|
|
774
|
+
if code != 0 || !strings.Contains(out, "SLICE-001 wright round "+string(rune('0'+i))) {
|
|
775
|
+
t.Fatalf("round %d: code=%d\n%s", i, code, out)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if code, out := run("abandon", "--wave", "r5", "--reason", "orphaned"); !strings.Contains(out, "wave r5 abandoned") {
|
|
779
|
+
t.Fatalf("abandon: code=%d\n%s", code, out)
|
|
780
|
+
}
|
|
781
|
+
if code, out := open("r5b"); code != 0 || !strings.Contains(out, "wright round 5") {
|
|
782
|
+
t.Fatalf("abandoned wave must not count: code=%d\n%s", code, out)
|
|
783
|
+
}
|
|
784
|
+
if code, out := open("r6"); code != 3 || !strings.Contains(out, "BLOCKED: SLICE-001 reached 5 wright rounds") {
|
|
785
|
+
t.Fatalf("round 6 must block: code=%d\n%s", code, out)
|
|
786
|
+
}
|
|
787
|
+
writeFile(t, filepath.Join(featureDir, "drift.md"), "# Drift\n\n## D-010 — SLICE-002 unrelated\n\n## D-011 — SLICE-001 reslice review: no smaller AC-preserving split\n")
|
|
788
|
+
if code, out := open("r6", "--reslice-review", "D-099"); code != 3 {
|
|
789
|
+
t.Fatalf("unknown drift id: code=%d\n%s", code, out)
|
|
790
|
+
}
|
|
791
|
+
if code, out := open("r6", "--reslice-review", "D-010"); code != 3 || !strings.Contains(out, "naming SLICE-001") {
|
|
792
|
+
t.Fatalf("drift heading for another slice: code=%d\n%s", code, out)
|
|
793
|
+
}
|
|
794
|
+
if code, out := open("r6", "--reslice-review", "D-011"); code != 0 || !strings.Contains(out, "wright round 6") {
|
|
795
|
+
t.Fatalf("valid review must admit: code=%d\n%s", code, out)
|
|
796
|
+
}
|
|
797
|
+
for i := 7; i <= 2*wrightRoundsPerReview; i++ {
|
|
798
|
+
if code, out := open("r" + string(rune('a'+i))); code != 0 {
|
|
799
|
+
t.Fatalf("round %d inside the reviewed window: code=%d\n%s", i, code, out)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if code, out := open("r11", "--reslice-review", "D-011"); code != 3 || !strings.Contains(out, "already admitted") {
|
|
803
|
+
t.Fatalf("reused review: code=%d\n%s", code, out)
|
|
804
|
+
}
|
|
805
|
+
if code, out := run("open", "--phase", "build", "--wave", "s2", "--role", "slice-wright", "--slice", "SLICE-002", "--reslice-review", "D-010"); code != 2 {
|
|
806
|
+
t.Fatalf("review before the cap must be refused: code=%d\n%s", code, out)
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
func TestDispatchVetRoundsCountSeparately(t *testing.T) {
|
|
811
|
+
root, featureDir := newWorkspace(t, "build")
|
|
812
|
+
run := func(args ...string) (int, string) {
|
|
813
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
814
|
+
code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
|
|
815
|
+
return code, stdout.String() + stderr.String()
|
|
816
|
+
}
|
|
817
|
+
for i := 0; i < 7; i++ {
|
|
818
|
+
if code, out := run("open", "--phase", "vet", "--wave", "full"+string(rune('a'+i)), "--role", "plan-reviewer"); code != 0 {
|
|
819
|
+
t.Fatalf("full Vet without --slice stays uncapped: code=%d\n%s", code, out)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if code, out := run("open", "--phase", "build", "--wave", "w1", "--role", "slice-wright", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "wright round 1") {
|
|
823
|
+
t.Fatalf("wright round must not see vet waves: code=%d\n%s", code, out)
|
|
824
|
+
}
|
|
825
|
+
for i := 1; i <= wrightRoundsPerReview; i++ {
|
|
826
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v"+string(rune('0'+i)), "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "vet round "+string(rune('0'+i))) {
|
|
827
|
+
t.Fatalf("vet round %d: code=%d\n%s", i, code, out)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 3 || !strings.Contains(out, "reached 5 vet rounds") {
|
|
831
|
+
t.Fatalf("vet round 6 must block: code=%d\n%s", code, out)
|
|
832
|
+
}
|
|
833
|
+
writeFile(t, filepath.Join(featureDir, "drift.md"), "## D-001 — SLICE-001 reslice review: proof seam already minimal\n")
|
|
834
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001", "--reslice-review", "D-001"); code != 0 {
|
|
835
|
+
t.Fatalf("reviewed vet round 6: code=%d\n%s", code, out)
|
|
836
|
+
}
|
|
837
|
+
if code, out := run("open", "--phase", "plan", "--wave", "p1", "--role", "plan-drafter", "--slice", "SLICE-001"); code != 2 {
|
|
838
|
+
t.Fatalf("--slice on a plan wave: code=%d\n%s", code, out)
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
func TestContextRepeatedSliceUnion(t *testing.T) {
|
|
843
|
+
root, featureDir := newWorkspace(t, "build")
|
|
844
|
+
skills := newSkillsRoot(t)
|
|
845
|
+
writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\"],\"workspace\":[\"tasks.md\"]} -->\n# build\n")
|
|
846
|
+
writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 A\nGoal: alpha body\n\n## SLICE-002 B\nGoal: beta body\n\n## SLICE-003 C\nGoal: gamma body\n")
|
|
847
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
848
|
+
args := []string{"feat", "--phase", "build", "--slice", "SLICE-001", "--slice", "SLICE-003", "--skills-root", skills}
|
|
849
|
+
if code := RunContext(root, args, stdout, stderr); code != 0 {
|
|
850
|
+
t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
|
|
851
|
+
}
|
|
852
|
+
raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.SLICE-001+SLICE-003.bundle.md"))
|
|
853
|
+
if err != nil {
|
|
854
|
+
t.Fatal(err)
|
|
855
|
+
}
|
|
856
|
+
text := string(raw)
|
|
857
|
+
if !strings.Contains(text, "alpha body") || !strings.Contains(text, "gamma body") || strings.Contains(text, "beta body") {
|
|
858
|
+
t.Fatalf("union of slice blocks wrong:\n%s", text)
|
|
859
|
+
}
|
|
860
|
+
}
|