Phase 5: scaffold Crucible (sow-tools) — dispatcher + builder shims, container, PR-first CI, fail-closed (D11, D19).
This commit is contained in:
@@ -1,812 +0,0 @@
|
||||
# Generated Wiki Stale Purge Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add an opt-in `purge` stale policy that permanently removes only manifest-tracked generated NodeBB wiki topics that no longer exist in current generator output.
|
||||
|
||||
**Architecture:** Keep stale ownership and candidate selection inside the existing `deploy-wiki` manifest planner. Extend the deploy result/plan model with purge counts and purge actions, add a thin NodeBB topic purge client call, remove successfully purged entries from the next manifest, and surface the policy through existing toolkit validation and module release/docs paths.
|
||||
|
||||
**Tech Stack:** Go toolkit code and `httptest` deploy tests, YAML-backed toolkit project config validation, POSIX shell module release wrapper, Markdown docs.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
Toolkit files:
|
||||
|
||||
- Modify `internal/project/project.go`: validate `purge` as a supported stale policy and remove the unsupported `unpublish` config assumption.
|
||||
- Modify `internal/project/project_test.go`: prove project config accepts `purge` and rejects unsupported stale policies.
|
||||
- Modify `internal/topdata/wiki_deploy.go`: add purge deploy state, stale purge planning, manifest removal, and NodeBB topic purge requests.
|
||||
- Modify `internal/topdata/wiki_deploy_test.go`: cover dry-run/live purge and missing-topic-id refusal without changing archive/report behavior.
|
||||
- Modify `internal/app/app.go`: expose `purge` in deploy CLI help and print `purged` deploy counts.
|
||||
- Modify `internal/app/app_test.go`: cover CLI output/help parsing for the new count and policy text.
|
||||
- Modify `README.md`: document the destructive generated-manifest-scoped purge mode in the toolkit CLI docs.
|
||||
|
||||
Module files:
|
||||
|
||||
- Modify `../module/scripts/release-all.sh`: allow `SOW_MODULE_WIKI_DEPLOY_STALE_POLICY=purge`.
|
||||
- Modify `../module/README.md`: document release usage and the destructive boundary.
|
||||
- Modify `../module/topdata/README.md`: document `deploy-wiki --stale-policy purge`.
|
||||
- Modify `../module/tests/release-all-cache-cleanup.sh`: prove release automation forwards `purge` into both wiki deploy phases.
|
||||
|
||||
## Task 1: Align Stale Policy Validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/project/project.go`
|
||||
- Modify: `internal/project/project_test.go`
|
||||
|
||||
- [ ] **Step 1: Write the failing project-config test**
|
||||
|
||||
Add a project validation case in `internal/project/project_test.go` that loads stale policy config with `purge` and expects validation to pass:
|
||||
|
||||
```go
|
||||
func TestProjectValidationAcceptsWikiStalePurgePolicy(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
topdata:
|
||||
wiki:
|
||||
stale_pages:
|
||||
default: report
|
||||
live_cleanup: purge
|
||||
`)
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
if err := proj.ValidateLayout(); err != nil {
|
||||
t.Fatalf("validate purge stale policy: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/project -run TestProjectValidationAcceptsWikiStalePurgePolicy -count=1
|
||||
```
|
||||
|
||||
Expected: FAIL because `topdata.wiki.stale_pages.live_cleanup "purge" is not supported`.
|
||||
|
||||
- [ ] **Step 3: Replace unsupported project validation vocabulary**
|
||||
|
||||
In `internal/project/project.go`, update the stale-policy switch from:
|
||||
|
||||
```go
|
||||
switch policy {
|
||||
case "report", "archive", "unpublish":
|
||||
default:
|
||||
failures = append(failures, fmt.Errorf("%s %q is not supported", key, policy))
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```go
|
||||
switch policy {
|
||||
case "report", "archive", "purge":
|
||||
default:
|
||||
failures = append(failures, fmt.Errorf("%s %q is not supported", key, policy))
|
||||
}
|
||||
```
|
||||
|
||||
This removes the config-only `unpublish` value that the deploy command cannot execute.
|
||||
|
||||
- [ ] **Step 4: Add the unsupported-policy regression**
|
||||
|
||||
Add or extend a project validation test so `unpublish` and an arbitrary bad value fail:
|
||||
|
||||
```go
|
||||
func TestProjectValidationRejectsUnsupportedWikiStalePolicies(t *testing.T) {
|
||||
for _, policy := range []string{"unpublish", "remove-everything"} {
|
||||
t.Run(policy, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), fmt.Sprintf(`
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
topdata:
|
||||
wiki:
|
||||
stale_pages:
|
||||
live_cleanup: %s
|
||||
`, policy))
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
err = proj.ValidateLayout()
|
||||
if err == nil || !strings.Contains(err.Error(), policy) {
|
||||
t.Fatalf("expected %q stale policy validation failure, got %v", policy, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add `fmt` to the existing `internal/project/project_test.go` imports for the `Sprintf` YAML fixture.
|
||||
|
||||
- [ ] **Step 5: Run project tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/project -count=1
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit the validation change**
|
||||
|
||||
```bash
|
||||
git add internal/project/project.go internal/project/project_test.go
|
||||
git commit -m "feat: validate wiki stale purge policy"
|
||||
```
|
||||
|
||||
## Task 2: Plan Manifest-Scoped Purge Actions
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/topdata/wiki_deploy.go`
|
||||
- Modify: `internal/topdata/wiki_deploy_test.go`
|
||||
|
||||
- [ ] **Step 1: Write failing deploy tests for dry-run purge planning**
|
||||
|
||||
In `internal/topdata/wiki_deploy_test.go`, add:
|
||||
|
||||
```go
|
||||
func TestDeployWikiDryRunPlansTrackedStalePagePurge(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "pages")
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil {
|
||||
t.Fatalf("create source dir: %v", err)
|
||||
}
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||
Version: "nodebb-v1",
|
||||
Pages: map[string]wikiDeployManifestPage{
|
||||
"skills:retired": {Hash: "old-hash", Title: "Retired Skill", Namespace: "skills", TID: 7, PID: 42, CID: 3},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run purge must not call NodeBB, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
SourceDir: sourceDir,
|
||||
Endpoint: server.URL,
|
||||
Token: "nodebb-token",
|
||||
ManifestPath: manifestPath,
|
||||
DryRun: true,
|
||||
StalePolicy: "purge",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DeployWikiWithOptions stale purge dry run failed: %v", err)
|
||||
}
|
||||
if result.Stale != 1 || result.Purged != 1 {
|
||||
t.Fatalf("expected one planned stale purge, got %#v", result)
|
||||
}
|
||||
if _, ok := loadDeployManifest(manifestPath).Pages["skills:retired"]; !ok {
|
||||
t.Fatalf("dry-run purge must not change the deploy manifest")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the failing missing-topic-id test**
|
||||
|
||||
Add:
|
||||
|
||||
```go
|
||||
func TestDeployWikiPurgeRefusesStaleManifestEntryWithoutTopicID(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "pages")
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil {
|
||||
t.Fatalf("create source dir: %v", err)
|
||||
}
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||
Version: "nodebb-v1",
|
||||
Pages: map[string]wikiDeployManifestPage{
|
||||
"skills:retired": {Hash: "old-hash", Title: "Retired Skill", Namespace: "skills", PID: 42, CID: 3},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("missing-topic-id purge must not call NodeBB, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
SourceDir: sourceDir,
|
||||
Endpoint: server.URL,
|
||||
Token: "nodebb-token",
|
||||
ManifestPath: manifestPath,
|
||||
DryRun: true,
|
||||
StalePolicy: "purge",
|
||||
}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "topic id") {
|
||||
t.Fatalf("expected stale purge to require tracked topic id, got %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the focused tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/topdata -run 'TestDeployWiki.*Purge' -count=1
|
||||
```
|
||||
|
||||
Expected: FAIL because `purge` is not yet accepted and `deployResult` has no `Purged` field.
|
||||
|
||||
- [ ] **Step 4: Add purge plan/result model fields**
|
||||
|
||||
In `internal/topdata/wiki_deploy.go`, extend `deployResult`:
|
||||
|
||||
```go
|
||||
type deployResult struct {
|
||||
LocalPages int
|
||||
Created int
|
||||
Updated int
|
||||
Stale int
|
||||
Archived int
|
||||
Purged int
|
||||
Skipped int
|
||||
Drifted int
|
||||
Renamed int
|
||||
Manifest string
|
||||
}
|
||||
```
|
||||
|
||||
Allow `purge` during deploy option validation:
|
||||
|
||||
```go
|
||||
if opts.StalePolicy != "" &&
|
||||
opts.StalePolicy != "report" &&
|
||||
opts.StalePolicy != "archive" &&
|
||||
opts.StalePolicy != "purge" {
|
||||
return deployResult{}, fmt.Errorf("wiki stale policy %q is not supported", opts.StalePolicy)
|
||||
}
|
||||
```
|
||||
|
||||
Add the purge count to the deploy plan progress line so dry-run logs show the destructive action explicitly:
|
||||
|
||||
```go
|
||||
progress(fmt.Sprintf("NodeBB wiki plan: create %d, update %d, rename %d, skip %d, stale %d, archive %d, purge %d, drift %d",
|
||||
result.Created,
|
||||
result.Updated,
|
||||
result.Renamed,
|
||||
result.Skipped,
|
||||
result.Stale,
|
||||
result.Archived,
|
||||
result.Purged,
|
||||
result.Drifted,
|
||||
))
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add stale purge planning**
|
||||
|
||||
In the stale manifest loop inside `planNodeBBDeploy`, keep the current report/archive branches and add the purge branch:
|
||||
|
||||
```go
|
||||
case "purge":
|
||||
if entry.TID == 0 {
|
||||
return nil, result, next, fmt.Errorf("stale generated wiki page %q cannot be purged without tracked NodeBB topic id", pageID)
|
||||
}
|
||||
result.Purged++
|
||||
delete(next.Pages, pageID)
|
||||
plans = append(plans, wikiDeployPlan{
|
||||
Page: wikiDeployPage{PageID: pageID, Title: entry.Title, Namespace: entry.Namespace},
|
||||
Entry: entry,
|
||||
Action: "purge",
|
||||
})
|
||||
```
|
||||
|
||||
Use a `switch policy` or equivalent structure so `report`, `archive`, and `purge` remain explicit. Do not add namespace search or title matching to find purge targets.
|
||||
|
||||
- [ ] **Step 6: Run the focused purge planning tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/topdata -run 'TestDeployWiki.*Purge' -count=1
|
||||
```
|
||||
|
||||
Expected: the dry-run and missing-topic-id purge tests pass once they compile.
|
||||
|
||||
- [ ] **Step 7: Commit purge planning**
|
||||
|
||||
```bash
|
||||
git add internal/topdata/wiki_deploy.go internal/topdata/wiki_deploy_test.go
|
||||
git commit -m "feat: plan stale generated wiki purges"
|
||||
```
|
||||
|
||||
## Task 3: Execute NodeBB Topic Purges
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/topdata/wiki_deploy.go`
|
||||
- Modify: `internal/topdata/wiki_deploy_test.go`
|
||||
|
||||
- [ ] **Step 1: Write the failing live purge test**
|
||||
|
||||
Add a live stale purge test in `internal/topdata/wiki_deploy_test.go`:
|
||||
|
||||
```go
|
||||
func TestDeployWikiPurgesTrackedStaleGeneratedTopic(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "pages")
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil {
|
||||
t.Fatalf("create source dir: %v", err)
|
||||
}
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||
Version: "nodebb-v1",
|
||||
Pages: map[string]wikiDeployManifestPage{
|
||||
"skills:retired": {Hash: "old-hash", Title: "Retired Skill", Namespace: "skills", TID: 7, PID: 42, CID: 3},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete || r.URL.Path != "/api/v3/topics/7" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
SourceDir: sourceDir,
|
||||
Endpoint: server.URL,
|
||||
Token: "nodebb-token",
|
||||
ManifestPath: manifestPath,
|
||||
StalePolicy: "purge",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DeployWikiWithOptions stale purge failed: %v", err)
|
||||
}
|
||||
if result.Stale != 1 || result.Purged != 1 {
|
||||
t.Fatalf("expected one stale purge, got %#v", result)
|
||||
}
|
||||
if _, ok := loadDeployManifest(manifestPath).Pages["skills:retired"]; ok {
|
||||
t.Fatalf("expected purged stale manifest entry to be removed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This route is the NodeBB core purge route. `DELETE /api/v3/topics/:tid/state` is the separate soft-delete route and must not be used here.
|
||||
|
||||
- [ ] **Step 2: Run the live purge test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/topdata -run 'TestDeployWiki.*Live.*Purge|TestDeployWiki.*Purges.*Stale' -count=1
|
||||
```
|
||||
|
||||
Expected: FAIL because purge plans are not yet executed by the NodeBB client.
|
||||
|
||||
- [ ] **Step 3: Add the NodeBB purge client method**
|
||||
|
||||
Place a thin method near `updatePost` and `renameWikiPage` in `internal/topdata/wiki_deploy.go`:
|
||||
|
||||
```go
|
||||
func (c *nodeBBClient) purgeTopic(tid int) error {
|
||||
if tid == 0 {
|
||||
return fmt.Errorf("NodeBB topic purge requires topic id")
|
||||
}
|
||||
return c.request(http.MethodDelete, fmt.Sprintf("/api/v3/topics/%d", tid), nil, nil)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Execute purge plans**
|
||||
|
||||
In the live plan execution switch, add:
|
||||
|
||||
```go
|
||||
case "purge":
|
||||
if err := client.purgeTopic(plan.Entry.TID); err != nil {
|
||||
return result, fmt.Errorf("deploy wiki page %q: purge NodeBB topic %d: %w", plan.Page.PageID, plan.Entry.TID, err)
|
||||
}
|
||||
```
|
||||
|
||||
Do not acquire wiki edit locks for purge: the NodeBB topic purge endpoint owns its own privilege checks.
|
||||
|
||||
- [ ] **Step 5: Keep current generated pages out of purge coverage**
|
||||
|
||||
Add this test so a current generated page with the same manifest page id is skipped instead of purged:
|
||||
|
||||
```go
|
||||
func TestDeployWikiPurgeDoesNotTargetCurrentGeneratedPages(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "pages")
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil {
|
||||
t.Fatalf("create source dir: %v", err)
|
||||
}
|
||||
generated := "[//]: # (sow-topdata-wiki:managed:start)\n# Athletics\n\nGenerated athletics page\n[//]: # (sow-topdata-wiki:managed:end)\n"
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "skills", "athletics.md"), []byte(generated), 0644); err != nil {
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||
Version: "nodebb-v1",
|
||||
Pages: map[string]wikiDeployManifestPage{
|
||||
"skills:athletics": {
|
||||
Hash: computeManagedHash(generated),
|
||||
Title: "Athletics",
|
||||
Namespace: "skills",
|
||||
TID: 7,
|
||||
PID: 42,
|
||||
CID: 3,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("current generated page must not call NodeBB during matching-hash deploy, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
SourceDir: sourceDir,
|
||||
Endpoint: server.URL,
|
||||
Token: "nodebb-token",
|
||||
ManifestPath: manifestPath,
|
||||
StalePolicy: "purge",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DeployWikiWithOptions current page purge deploy failed: %v", err)
|
||||
}
|
||||
if result.Stale != 0 || result.Purged != 0 || result.Skipped != 1 {
|
||||
t.Fatalf("expected current generated page to skip purge, got %#v", result)
|
||||
}
|
||||
if _, ok := loadDeployManifest(manifestPath).Pages["skills:athletics"]; !ok {
|
||||
t.Fatalf("expected current generated page to remain in deploy manifest")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run deploy tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/topdata -run 'TestDeployWiki' -count=1
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit purge execution**
|
||||
|
||||
```bash
|
||||
git add internal/topdata/wiki_deploy.go internal/topdata/wiki_deploy_test.go
|
||||
git commit -m "feat: purge stale generated wiki topics"
|
||||
```
|
||||
|
||||
## Task 4: Surface Purge In Toolkit CLI And Docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/app/app.go`
|
||||
- Modify: `internal/app/app_test.go`
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Write failing CLI-output coverage**
|
||||
|
||||
In `internal/app/app_test.go`, extend `TestTopdataConsoleDebugProgressAndRelativePaths` so the sample progress and emit call include a purge count:
|
||||
|
||||
```go
|
||||
console.progress("NodeBB wiki plan: create 1, update 2, skip 3, stale 4, archive 5, purge 6, drift 0")
|
||||
console.emitWikiDeployResult(10, 1, 2, 3, 4, 5, 6, 0, "/workspace/project/build/wiki/deploy-manifest.json")
|
||||
```
|
||||
|
||||
Assert:
|
||||
|
||||
```go
|
||||
if !strings.Contains(output, "purged: 6") {
|
||||
t.Fatalf("expected purged deploy count, got %q", output)
|
||||
}
|
||||
```
|
||||
|
||||
Update the debug progress expectation to include `purge 6`.
|
||||
|
||||
- [ ] **Step 2: Run focused app tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/app -run TestTopdataConsoleDebugProgressAndRelativePaths -count=1
|
||||
```
|
||||
|
||||
Expected: FAIL because `emitWikiDeployResult` does not yet take a purge count.
|
||||
|
||||
- [ ] **Step 3: Print purge counts**
|
||||
|
||||
Update the emit signature and output in `internal/app/app.go`:
|
||||
|
||||
```go
|
||||
func (c *topdataConsole) emitWikiDeployResult(localPages, created, updated, skipped, stale, archived, purged, drifted int, manifest string) {
|
||||
spin.linebreak()
|
||||
fmt.Fprintln(c.stdout, "Deploy Wiki ----------")
|
||||
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
|
||||
fmt.Fprintf(c.stdout, "local pages: %d\n", localPages)
|
||||
fmt.Fprintf(c.stdout, "created: %d\n", created)
|
||||
fmt.Fprintf(c.stdout, "updated: %d\n", updated)
|
||||
fmt.Fprintf(c.stdout, "skipped: %d\n", skipped)
|
||||
fmt.Fprintf(c.stdout, "stale: %d\n", stale)
|
||||
fmt.Fprintf(c.stdout, "archived: %d\n", archived)
|
||||
fmt.Fprintf(c.stdout, "purged: %d\n", purged)
|
||||
fmt.Fprintf(c.stdout, "drifted: %d\n", drifted)
|
||||
fmt.Fprintf(c.stdout, "manifest: %s\n", c.relPath(manifest))
|
||||
}
|
||||
```
|
||||
|
||||
Pass `result.Purged` from the deploy command call site.
|
||||
|
||||
- [ ] **Step 4: Update CLI usage text**
|
||||
|
||||
Change deploy-wiki help text in `internal/app/app.go` from:
|
||||
|
||||
```go
|
||||
[--stale-policy <report|archive>]
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```go
|
||||
[--stale-policy <report|archive|purge>]
|
||||
```
|
||||
|
||||
Add this focused help assertion in `internal/app/app_test.go`:
|
||||
|
||||
```go
|
||||
func TestParseDeployWikiHelpListsPurgeStalePolicy(t *testing.T) {
|
||||
_, err := parseDeployWikiArgs("deploy-wiki", []string{"--help"})
|
||||
if err == nil || !strings.Contains(err.Error(), "--stale-policy <report|archive|purge>") {
|
||||
t.Fatalf("expected deploy-wiki help to list purge stale policy, got %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update toolkit README**
|
||||
|
||||
In `README.md`, update the `deploy-wiki` usage and stale-page explanation so it states:
|
||||
|
||||
```markdown
|
||||
`--stale-policy purge` permanently removes only stale generated topics already
|
||||
tracked in the wiki deploy manifest. Dry-run that policy before the live run;
|
||||
it never discovers manual wiki pages from NodeBB for purge.
|
||||
```
|
||||
|
||||
Keep `report` as the documented default and `archive` as the nondestructive cleanup option.
|
||||
|
||||
- [ ] **Step 6: Run app tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/app -count=1
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit toolkit CLI/docs**
|
||||
|
||||
```bash
|
||||
git add internal/app/app.go internal/app/app_test.go README.md
|
||||
git commit -m "docs: expose wiki stale purge policy"
|
||||
```
|
||||
|
||||
## Task 5: Expose Purge Through Module Release Workflow
|
||||
|
||||
**Files:**
|
||||
- Modify: `../module/scripts/release-all.sh`
|
||||
- Modify: `../module/README.md`
|
||||
- Modify: `../module/topdata/README.md`
|
||||
- Modify: `../module/tests/release-all-cache-cleanup.sh`
|
||||
|
||||
- [ ] **Step 1: Write failing release wrapper coverage**
|
||||
|
||||
In `../module/tests/release-all-cache-cleanup.sh`, set the stale policy in the existing release invocation:
|
||||
|
||||
```bash
|
||||
SOW_MODULE_WIKI_DEPLOY_STALE_POLICY="purge" \
|
||||
TAG_NAME="v-test" \
|
||||
"${CHECKOUT}/scripts/release-all.sh" >"${TEST_TMP}/stdout.log" 2>"${TEST_TMP}/stderr.log"
|
||||
```
|
||||
|
||||
Update the wiki log expectations from:
|
||||
|
||||
```bash
|
||||
grep -Fxq "deploy-wiki --dry-run --create" "${TEST_TMP}/wiki.log" \
|
||||
|| fail "wiki dry run was not invoked"
|
||||
grep -Fxq "deploy-wiki --create" "${TEST_TMP}/wiki.log" \
|
||||
|| fail "wiki deploy was not invoked"
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```bash
|
||||
grep -Fxq "deploy-wiki --dry-run --create --stale-policy purge" "${TEST_TMP}/wiki.log" \
|
||||
|| fail "wiki stale purge dry run was not invoked"
|
||||
grep -Fxq "deploy-wiki --create --stale-policy purge" "${TEST_TMP}/wiki.log" \
|
||||
|| fail "wiki stale purge deploy was not invoked"
|
||||
```
|
||||
|
||||
Update the API ordering assertion to match the new dry-run log line:
|
||||
|
||||
```awk
|
||||
$0 == "WIKI deploy-wiki --dry-run --create --stale-policy purge" { wiki_dry_run = NR }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the release wrapper test to verify it fails**
|
||||
|
||||
Run from `../module`:
|
||||
|
||||
```bash
|
||||
./tests/release-all-cache-cleanup.sh
|
||||
```
|
||||
|
||||
Expected: FAIL because `scripts/release-all.sh` rejects `SOW_MODULE_WIKI_DEPLOY_STALE_POLICY=purge`.
|
||||
|
||||
- [ ] **Step 3: Update release wrapper validation**
|
||||
|
||||
In `../module/scripts/release-all.sh`, change:
|
||||
|
||||
```bash
|
||||
if [[ "${SOW_MODULE_WIKI_DEPLOY_STALE_POLICY}" != "report" && "${SOW_MODULE_WIKI_DEPLOY_STALE_POLICY}" != "archive" ]]; then
|
||||
echo "Unsupported SOW_MODULE_WIKI_DEPLOY_STALE_POLICY=${SOW_MODULE_WIKI_DEPLOY_STALE_POLICY}; expected report or archive" >&2
|
||||
return 1
|
||||
fi
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```bash
|
||||
if [[ "${SOW_MODULE_WIKI_DEPLOY_STALE_POLICY}" != "report" && "${SOW_MODULE_WIKI_DEPLOY_STALE_POLICY}" != "archive" && "${SOW_MODULE_WIKI_DEPLOY_STALE_POLICY}" != "purge" ]]; then
|
||||
echo "Unsupported SOW_MODULE_WIKI_DEPLOY_STALE_POLICY=${SOW_MODULE_WIKI_DEPLOY_STALE_POLICY}; expected report, archive, or purge" >&2
|
||||
return 1
|
||||
fi
|
||||
```
|
||||
|
||||
The existing dry-run/live deploy argument plumbing already forwards every non-`report` stale policy through `--stale-policy`.
|
||||
|
||||
- [ ] **Step 4: Run the release wrapper test**
|
||||
|
||||
Run from `../module`:
|
||||
|
||||
```bash
|
||||
./tests/release-all-cache-cleanup.sh
|
||||
```
|
||||
|
||||
Expected: PASS and both release wiki deploy phases include `--stale-policy purge`.
|
||||
|
||||
- [ ] **Step 5: Document release controls**
|
||||
|
||||
In `../module/README.md`, revise the environment variable bullet to name both destructive modes explicitly:
|
||||
|
||||
```markdown
|
||||
- `SOW_MODULE_WIKI_DEPLOY_STALE_POLICY` (defaults to `report`; use `archive`
|
||||
after a controlled dry run to retire stale generated pages in place, or
|
||||
`purge` after a controlled dry run to permanently remove stale generated
|
||||
topics already tracked in the wiki deploy manifest)
|
||||
```
|
||||
|
||||
In the wiki deployment paragraph, replace the archive-only sentence with a sentence that keeps these boundaries:
|
||||
|
||||
- `report` is default
|
||||
- `archive` rewrites stale generated pages in place
|
||||
- `purge` permanently removes only manifest-tracked stale generated topics
|
||||
- manual NodeBB wiki pages are not purge candidates
|
||||
|
||||
- [ ] **Step 6: Document local deploy controls**
|
||||
|
||||
In `../module/topdata/README.md`, replace:
|
||||
|
||||
```markdown
|
||||
reports stale manifest entries by default and can archive them in place with `--stale-policy archive`; it does not delete topics
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```markdown
|
||||
reports stale manifest entries by default, can archive them in place with
|
||||
`--stale-policy archive`, and can permanently purge only manifest-tracked stale
|
||||
generated topics with `--stale-policy purge` after a dry run
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Check module diffs**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git -C ../module diff --check
|
||||
git -C ../module diff -- scripts/release-all.sh tests/release-all-cache-cleanup.sh README.md topdata/README.md
|
||||
```
|
||||
|
||||
Expected: no whitespace failures; diff shows only wrapper policy vocabulary, wrapper coverage, and docs.
|
||||
|
||||
- [ ] **Step 8: Commit module wrapper/docs**
|
||||
|
||||
Run from `../module`:
|
||||
|
||||
```bash
|
||||
git add scripts/release-all.sh tests/release-all-cache-cleanup.sh README.md topdata/README.md
|
||||
git commit -m "docs: allow generated wiki stale purges"
|
||||
```
|
||||
|
||||
## Task 6: Full Verification And Operator Notes
|
||||
|
||||
**Files:**
|
||||
- Verify: toolkit and module working trees
|
||||
|
||||
- [ ] **Step 1: Format Go changes**
|
||||
|
||||
Run from toolkit:
|
||||
|
||||
```bash
|
||||
gofmt -w internal/project/project.go internal/project/project_test.go internal/topdata/wiki_deploy.go internal/topdata/wiki_deploy_test.go internal/app/app.go internal/app/app_test.go
|
||||
```
|
||||
|
||||
Expected: command exits 0.
|
||||
|
||||
- [ ] **Step 2: Run toolkit verification**
|
||||
|
||||
Run from toolkit:
|
||||
|
||||
```bash
|
||||
go test ./internal/project ./internal/topdata ./internal/app
|
||||
go test ./...
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all tests pass and diff check emits no whitespace errors.
|
||||
|
||||
- [ ] **Step 3: Build the toolkit development binary**
|
||||
|
||||
Run from toolkit:
|
||||
|
||||
```bash
|
||||
./build-tool.sh
|
||||
```
|
||||
|
||||
Expected: `tools/sow-toolkit` is rebuilt successfully for module wrapper verification.
|
||||
|
||||
- [ ] **Step 4: Run module verification with the development binary**
|
||||
|
||||
Run from module:
|
||||
|
||||
```bash
|
||||
SOW_TOOLS_DEV_BINARY=../toolkit/tools/sow-toolkit ./validate-topdata.sh
|
||||
SOW_TOOLS_DEV_BINARY=../toolkit/tools/sow-toolkit ./build-wiki.sh --force
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: topdata validation and wiki build pass; module diff check emits no whitespace errors.
|
||||
|
||||
- [ ] **Step 5: Record the cleanup command in the final handoff**
|
||||
|
||||
The final implementation response must state that destructive cleanup is opt-in and show the operator sequence:
|
||||
|
||||
```bash
|
||||
./deploy-wiki.sh --dry-run --stale-policy purge
|
||||
./deploy-wiki.sh --stale-policy purge
|
||||
```
|
||||
|
||||
For tag-triggered module release automation, state the equivalent variable:
|
||||
|
||||
```bash
|
||||
SOW_MODULE_WIKI_DEPLOY_STALE_POLICY=purge
|
||||
```
|
||||
|
||||
The handoff must mention that purge targets only stale generated pages already tracked in the deploy manifest and permanently removes their NodeBB topics/posts.
|
||||
@@ -1,200 +0,0 @@
|
||||
# Class Progression Feat Projections Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make class progression feat grouping configurable from wiki YAML so the module template can render granted feats and level-gated available feats separately.
|
||||
|
||||
**Architecture:** Extend the `class_progression` data provider config with named class-feat projection fields. The toolkit evaluates each configured projection against class feat rows and groups resolved feat links onto progression rows by level, while the module wiki config declares the project-specific `List` rules and the class template owns output wording.
|
||||
|
||||
**Tech Stack:** Go `sow-toolkit`, native topdata wiki YAML, HTML wiki templates, Go tests.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
- `internal/topdata/wiki_data_providers.go`: parse and validate data-provider projection configuration and pass it into class progression row assembly.
|
||||
- `internal/topdata/wiki_tables.go`: share class progression row assembly with configured class feat projections and keep default compatibility behavior.
|
||||
- `internal/topdata/wiki_native_test.go`: regression coverage for provider projection parsing, filtering, grouping, ordering, invalid config, and rendered template fields.
|
||||
- `../module/topdata/wiki/data.yaml`: declare module-owned `GrantedFeats` and `AvailableFeats` projections for class progression.
|
||||
- `../module/topdata/wiki/templates/pages/classes.html`: render bonus feats inline and use projected available feats in the Notes cell.
|
||||
- `../module/topdata/wiki/README.md`: document configurable data-provider projections for template authors.
|
||||
|
||||
### Task 1: Add Configured Class Feat Projection Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/topdata/wiki_native_test.go`
|
||||
|
||||
- [ ] **Step 1: Write failing provider projection tests**
|
||||
|
||||
Add tests that build `wikiDataProviderDefinition` from YAML such as:
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
ClassProgression:
|
||||
source: class_progression
|
||||
levels: 1-4
|
||||
class_feats:
|
||||
fields:
|
||||
GrantedFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List == 3}}"
|
||||
AvailableFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List != 3}}"
|
||||
```
|
||||
|
||||
Assert the parsed provider can render a progression row where `GrantedFeats`
|
||||
contains `List == 3` feat links, `AvailableFeats` contains positive-level
|
||||
non-`3` feat links, matched link order follows fixture source row order, and
|
||||
`BonusFeat` is still present.
|
||||
|
||||
- [ ] **Step 2: Add failing invalid configuration coverage**
|
||||
|
||||
Add a test that loads a provider field with an empty projection name or empty
|
||||
`when` expression and expects an error containing `data.yaml`, the provider
|
||||
name, and the projection field context.
|
||||
|
||||
- [ ] **Step 3: Run focused tests to verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/topdata -run 'TestWiki.*ClassProgression.*Projection|TestWikiData.*Provider.*Projection' -count=1
|
||||
```
|
||||
|
||||
Expected: FAIL because `wikiDataProviderDefinition` does not yet accept
|
||||
`class_feats.fields` and progression rows do not expose `AvailableFeats`.
|
||||
|
||||
### Task 2: Implement Generic Provider Projections
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/topdata/wiki_data_providers.go`
|
||||
- Modify: `internal/topdata/wiki_tables.go`
|
||||
- Test: `internal/topdata/wiki_native_test.go`
|
||||
|
||||
- [ ] **Step 1: Extend provider config types and validation**
|
||||
|
||||
Add typed YAML support for:
|
||||
|
||||
```go
|
||||
type wikiDataProviderClassFeatConfig struct {
|
||||
Fields map[string]wikiDataProviderProjectionField `json:"fields" yaml:"fields"`
|
||||
}
|
||||
|
||||
type wikiDataProviderProjectionField struct {
|
||||
When string `json:"when" yaml:"when"`
|
||||
}
|
||||
```
|
||||
|
||||
Validate each configured projection field name and `when` expression before
|
||||
provider execution.
|
||||
|
||||
- [ ] **Step 2: Carry projections into progression row assembly**
|
||||
|
||||
Change the provider path so `ClassProgression` passes provider-owned class feat
|
||||
projection fields into progression row assembly. Preserve table-based
|
||||
`class_progression` callers by giving them the current default `GrantedFeats`
|
||||
behavior.
|
||||
|
||||
- [ ] **Step 3: Evaluate and group projections**
|
||||
|
||||
In progression row assembly, resolve class feat links once per matching row,
|
||||
evaluate configured `when` expressions against class feat rows, and append
|
||||
matched links to the configured progression field for each positive
|
||||
`GrantedOnLevel` value in source row order.
|
||||
|
||||
- [ ] **Step 4: Run focused tests to verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/topdata -run 'TestWiki.*ClassProgression.*Projection|TestWikiData.*Provider.*Projection' -count=1
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Configure Module Wiki Output
|
||||
|
||||
**Files:**
|
||||
- Modify: `../module/topdata/wiki/data.yaml`
|
||||
- Modify: `../module/topdata/wiki/templates/pages/classes.html`
|
||||
- Modify: `../module/topdata/wiki/README.md`
|
||||
|
||||
- [ ] **Step 1: Declare module projections**
|
||||
|
||||
Update `ClassProgression` in `../module/topdata/wiki/data.yaml` so module YAML
|
||||
owns `GrantedFeats` and `AvailableFeats` selection:
|
||||
|
||||
```yaml
|
||||
ClassProgression:
|
||||
source: class_progression
|
||||
levels: 1-12
|
||||
class_feats:
|
||||
fields:
|
||||
GrantedFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List == 3}}"
|
||||
AvailableFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List != 3}}"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Render projected fields in the class template**
|
||||
|
||||
Keep bonus feats in the Feats cell with a conditional comma. Render
|
||||
`AvailableFeats` in the Notes cell with module-owned “becomes available”
|
||||
wording using supported template loop/filter syntax.
|
||||
|
||||
- [ ] **Step 3: Document the projection surface**
|
||||
|
||||
Add a concise `data.yaml` note to the wiki README explaining that data-provider
|
||||
projection fields can expose configured per-level class feat lists to page
|
||||
templates and that wording stays in templates.
|
||||
|
||||
### Task 4: Verify The Cross-Repo Wiki Path
|
||||
|
||||
**Files:**
|
||||
- Verify: `internal/topdata/...`
|
||||
- Verify: `../module/topdata/wiki/...`
|
||||
|
||||
- [ ] **Step 1: Run toolkit regression tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/topdata -count=1
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Build the local toolkit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
./build-tool.sh
|
||||
```
|
||||
|
||||
Expected: exit `0` and refresh `tools/sow-toolkit` when source changes require
|
||||
it.
|
||||
|
||||
- [ ] **Step 3: Build module wiki with the local toolkit**
|
||||
|
||||
Run from `../module`:
|
||||
|
||||
```bash
|
||||
SOW_TOOLS_DEV_BINARY=../toolkit/tools/sow-toolkit ./build-wiki.sh --force
|
||||
```
|
||||
|
||||
Expected: exit `0` and regenerate class wiki pages under `.cache/wiki/pages/`.
|
||||
|
||||
- [ ] **Step 4: Inspect fighter output**
|
||||
|
||||
Check generated fighter progression output for inline bonus feat text in Feats
|
||||
cells and `becomes available` notes for level-gated selectable feat links.
|
||||
|
||||
- [ ] **Step 5: Run diff checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
in both toolkit and module repos.
|
||||
@@ -1,47 +0,0 @@
|
||||
# Wiki List Columns Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a template-driven topdata wiki option for splitting provider-backed lists into configured columns with a fixed item count and optional forced empty columns.
|
||||
|
||||
**Architecture:** The toolkit wiki renderer gains a generic `columns` data-provider source that wraps another provider and exposes one row per rendered column. Each column row exposes `Items`, `Index`, and count metadata so page templates own markup while YAML owns row selection and chunking behavior. The module class page uses this provider for `ClassSkills`.
|
||||
|
||||
**Tech Stack:** Go `sow-toolkit`, YAML topdata wiki config, local HTML templates, Go tests.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Renderer Provider
|
||||
|
||||
**Files:**
|
||||
- Modify: `toolkit/internal/topdata/wiki_data_providers.go`
|
||||
- Modify: `toolkit/internal/topdata/wiki_template.go`
|
||||
- Test: `toolkit/internal/topdata/wiki_native_test.go`
|
||||
|
||||
- [ ] Write failing tests for a `columns` provider that chunks `ClassSkills` into rows with nested `Items`, including forced empty columns.
|
||||
- [ ] Run `go test ./internal/topdata -run 'TestWikiTemplate.*Column|TestBuildNativeWikiRendersClassSkillColumns'` and confirm the new assertions fail because `columns` is not supported.
|
||||
- [ ] Add `provider`, `columns`, `items_per_column`, and `force_columns` fields to `wikiDataProviderDefinition`.
|
||||
- [ ] Validate that `columns` providers name an existing provider, use positive integer `columns` and `items_per_column`, and do not point at themselves.
|
||||
- [ ] Allow nested `{{#each Items}}` loops by teaching template `#each` to iterate an array field from the current row before falling back to named providers.
|
||||
- [ ] Implement deterministic chunking: each column receives up to `items_per_column`; extra source items continue into additional overflow columns; `force_columns: true` emits empty configured columns when there are fewer items.
|
||||
|
||||
### Task 2: Module Template
|
||||
|
||||
**Files:**
|
||||
- Modify: `module/topdata/wiki/data.yaml`
|
||||
- Modify: `module/topdata/wiki/templates/pages/classes.html`
|
||||
- Modify: `module/topdata/wiki/README.md`
|
||||
|
||||
- [ ] Add `ClassSkillColumns` using `source: columns`, `provider: ClassSkills`, `columns: 3`, `items_per_column: 12`, and `force_columns: true`.
|
||||
- [ ] Replace the flat class skill `<ul>` with a `.wiki-list-columns .wiki-list-columns--3` wrapper and nested `{{#each Items}}` loops.
|
||||
- [ ] Document the generic `columns` provider fields for template authors.
|
||||
|
||||
### Task 3: Verification
|
||||
|
||||
**Files:**
|
||||
- Verify: `toolkit/internal/topdata/...`
|
||||
- Verify: `module/topdata/wiki/...`
|
||||
|
||||
- [ ] Run `go test ./internal/topdata -run 'TestWikiTemplate|TestBuildNativeWiki'`.
|
||||
- [ ] Run `./build-tool.sh` from `toolkit/`.
|
||||
- [ ] Run `SOW_TOOLS_DEV_BINARY=../toolkit/tools/sow-toolkit ./build-wiki.sh --force` from `module/`.
|
||||
- [ ] Inspect generated class HTML for `wiki-list-columns--3`, three configured class-skill lists for shorter classes, and overflow columns for long lists.
|
||||
@@ -1,153 +0,0 @@
|
||||
# Generated Wiki Stale Purge Design
|
||||
|
||||
## Goal
|
||||
|
||||
Add an explicit bulk purge path for generated NodeBB wiki pages that have become
|
||||
stale after generator output changes, such as topdata visibility rules hiding
|
||||
previously generated pages.
|
||||
|
||||
## Context
|
||||
|
||||
The topdata wiki deployer already owns generated page state through
|
||||
`.cache/wiki/.wiki_deploy_manifest.json`. It currently recognizes stale
|
||||
generated pages that remain in the deploy manifest but no longer exist in the
|
||||
current generated wiki output. Existing stale policies can either report those
|
||||
pages or archive them by rewriting their managed content.
|
||||
|
||||
That is not sufficient when the stale generated wiki topics should be removed
|
||||
from NodeBB entirely. The Westgate Wiki plugin already treats a normal wiki topic
|
||||
delete as an irreversible purge, so the deployer should use NodeBB's explicit
|
||||
topic purge API when the operator asks for destructive generated-page cleanup.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Add `purge` as an explicit stale policy for topdata wiki deployment.
|
||||
- Plan purge candidates only from generated page entries already tracked in the
|
||||
wiki deploy manifest.
|
||||
- Purge the exact tracked NodeBB topic for each stale generated page.
|
||||
- Report purge counts in dry-run and live deploy output.
|
||||
- Remove successfully purged page entries from the deploy manifest so later
|
||||
deploys do not repeatedly target removed topics.
|
||||
- Allow release automation to opt into purge through the existing stale-policy
|
||||
environment variable path.
|
||||
- Update toolkit and module documentation for the destructive cleanup mode.
|
||||
|
||||
### Out Of Scope
|
||||
|
||||
- Purging manually-authored NodeBB wiki pages.
|
||||
- Discovering purge candidates by namespace browsing, title, slug, or wiki
|
||||
search.
|
||||
- Adding a soft-delete or restore workflow for stale generated wiki pages.
|
||||
- Changing the Westgate Wiki plugin delete behavior.
|
||||
- Adding a separate stale cleanup command outside `deploy-wiki`.
|
||||
|
||||
## Operator Interface
|
||||
|
||||
The existing `deploy-wiki` stale-policy surface gains a third explicit policy:
|
||||
|
||||
```bash
|
||||
./deploy-wiki.sh --dry-run --stale-policy purge
|
||||
./deploy-wiki.sh --stale-policy purge
|
||||
```
|
||||
|
||||
Release automation may use the same policy through:
|
||||
|
||||
```bash
|
||||
SOW_MODULE_WIKI_DEPLOY_STALE_POLICY=purge
|
||||
```
|
||||
|
||||
`purge` remains opt-in. Existing `report` and `archive` behavior remain
|
||||
unchanged.
|
||||
|
||||
## Purge Eligibility
|
||||
|
||||
A page may be purged only when all of these are true:
|
||||
|
||||
1. The page exists in `.cache/wiki/.wiki_deploy_manifest.json`.
|
||||
2. The page is absent from the current generated wiki page set in the deploy
|
||||
scope.
|
||||
3. The deploy manifest entry has the tracked NodeBB topic id required to call
|
||||
the topic purge API.
|
||||
|
||||
The deployer must not search NodeBB for candidate pages when purging stale
|
||||
generated output. It must not purge pages that are currently generated. It must
|
||||
not purge pages that only exist as manually-authored NodeBB wiki content.
|
||||
|
||||
If a stale manifest entry has no topic id, purge planning fails clearly instead
|
||||
of guessing a remote target.
|
||||
|
||||
## Deploy Flow
|
||||
|
||||
Stale planning stays inside the existing `deploy-wiki` deploy plan:
|
||||
|
||||
- `report` increments the stale count only.
|
||||
- `archive` produces the existing archive post-update action.
|
||||
- `purge` produces a destructive topic-purge action for the manifest entry's
|
||||
tracked topic id.
|
||||
|
||||
Dry runs calculate the same stale and purge counts as live runs but do not call
|
||||
NodeBB and do not mutate the deploy manifest.
|
||||
|
||||
Live purge execution calls NodeBB's topic purge API for each planned stale
|
||||
generated page. NodeBB auth, permission, missing-topic, or transport failures
|
||||
abort the deploy with the failing generated page id and remote topic context.
|
||||
|
||||
## Manifest State
|
||||
|
||||
Archive retains stale entries in the manifest because the archived post remains
|
||||
managed remotely.
|
||||
|
||||
Purge removes each successfully purged page entry from the next deploy manifest.
|
||||
This records that the generated page no longer has a managed remote NodeBB topic
|
||||
and prevents later deploys from attempting to purge an already removed topic.
|
||||
|
||||
If a purge deployment fails partway through, only state written after the
|
||||
existing deploy execution succeeds should reach the manifest. A later rerun may
|
||||
retry the stale manifest entries still present, matching the deployer's current
|
||||
all-or-error manifest persistence behavior.
|
||||
|
||||
## Reporting And Validation
|
||||
|
||||
Toolkit deploy output gains a `purged` count alongside the existing `stale` and
|
||||
`archived` counts. Dry-run summaries must make the destructive plan visible
|
||||
before a live purge run.
|
||||
|
||||
`purge` must be accepted wherever stale policy is validated:
|
||||
|
||||
- project config stale-page settings
|
||||
- `deploy-wiki --stale-policy`
|
||||
- module release wrapper validation for
|
||||
`SOW_MODULE_WIKI_DEPLOY_STALE_POLICY`
|
||||
|
||||
Unsupported stale policy values must keep failing early with clear error text.
|
||||
|
||||
## Testing
|
||||
|
||||
Toolkit tests should cover:
|
||||
|
||||
- stale policy validation accepts `purge`
|
||||
- unsupported stale policy values still fail
|
||||
- dry-run purge reports stale and purged counts without calling NodeBB or
|
||||
changing the manifest
|
||||
- live purge calls the NodeBB topic purge endpoint for a stale tracked generated
|
||||
topic
|
||||
- live purge removes the manifest entry after success
|
||||
- purge refuses a stale manifest entry without a tracked topic id
|
||||
- current generated pages are never purge candidates
|
||||
- archive/report behavior remains unchanged
|
||||
|
||||
Module wrapper/docs coverage should verify:
|
||||
|
||||
- release stale-policy validation accepts `purge`
|
||||
- docs describe purge as destructive, generated-manifest-scoped cleanup
|
||||
|
||||
## Compatibility
|
||||
|
||||
Default behavior remains `report`. Existing archive cleanup remains available and
|
||||
unchanged. The destructive purge path activates only when the operator selects
|
||||
`purge`.
|
||||
|
||||
The purge boundary is intentionally stricter than normal page adoption logic:
|
||||
generated manifest state authorizes the target; NodeBB discovery never does.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Class Progression Feat Availability Design
|
||||
|
||||
## Goal
|
||||
|
||||
Class progression wiki providers should let module-owned wiki configuration
|
||||
distinguish automatically granted class feats from level-gated selectable feats
|
||||
while also exposing bonus feat gains declared by class `bfeat` tables in the
|
||||
progression table.
|
||||
|
||||
## Ownership
|
||||
|
||||
The authored wiki page layout and progression projection policy stay in
|
||||
`sow-module/topdata/wiki/`. The toolkit owns generic progression row data
|
||||
assembly from configured class feat and bonus-feat sources. The change should
|
||||
stay inside those boundaries:
|
||||
|
||||
- module wiki YAML decides which class feat rows project into progression
|
||||
fields;
|
||||
- module page templates decide how progression row fields are rendered;
|
||||
- toolkit progression row assembly groups configured projections by level
|
||||
without hardcoding the module's class feat list semantics or note wording.
|
||||
|
||||
## Behavior
|
||||
|
||||
Class feat rows with positive `GrantedOnLevel` values are not all equivalent.
|
||||
This module uses rows in class feat tables with `List` value `3` as
|
||||
automatically granted feats and positive-level selectable rows as feats that
|
||||
become available at the declared level. That distinction is repository policy
|
||||
and must be declared in wiki YAML rather than hidden in toolkit code.
|
||||
|
||||
The `ClassProgression` data provider should support configured class feat
|
||||
projection fields. This module can declare a shape such as:
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
ClassProgression:
|
||||
source: class_progression
|
||||
levels: 1-12
|
||||
class_feats:
|
||||
fields:
|
||||
GrantedFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List == 3}}"
|
||||
AvailableFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List != 3}}"
|
||||
```
|
||||
|
||||
For each progression row, the toolkit should:
|
||||
|
||||
- continue exposing the per-level bonus feat count from the class `bfeat` table
|
||||
as `BonusFeat`;
|
||||
- expose each configured class feat field as a deterministic list of resolved
|
||||
wiki feat links for class feat rows whose configured expression matches the
|
||||
progression level;
|
||||
- leave non-matching class feat rows out of that configured field.
|
||||
|
||||
The default `class_progression` provider behavior should preserve the current
|
||||
`GrantedFeats` projection when no custom field configuration is declared.
|
||||
|
||||
## Rendering
|
||||
|
||||
The class progression template can render `BonusFeat` inline in its existing
|
||||
Feats cell rather than adding another progression column. It should emit the
|
||||
separator only when `GrantedFeats` already rendered content so bonus-only rows
|
||||
do not start with a comma.
|
||||
|
||||
The existing Notes column should render `AvailableFeats` in the module template
|
||||
with module-owned wording such as `<feat link> becomes available.`. The lower
|
||||
class feat table remains useful for listing class feat membership and list
|
||||
metadata; its level label should not be interpreted as an automatic grant for
|
||||
selectable rows.
|
||||
|
||||
## Data Flow
|
||||
|
||||
The toolkit resolves the class `FeatsTable` while building progression rows.
|
||||
Each configured class feat field evaluates its `when` expression against class
|
||||
feat rows, groups matched rows by positive `GrantedOnLevel`, resolves feat
|
||||
references to wiki link text, and writes the grouped list onto the progression
|
||||
row under the configured field name. Output order follows source table row
|
||||
order so repeated builds are deterministic.
|
||||
|
||||
The class `BonusFeatsTable` lookup continues to populate `BonusFeat` from the
|
||||
row whose id matches the progression level.
|
||||
|
||||
## Errors And Compatibility
|
||||
|
||||
The change should preserve current behavior for missing class feat tables,
|
||||
missing bonus feat tables, unresolved feat references, and classes without
|
||||
custom class feat projections. Invalid projection configuration should fail
|
||||
early with the provider file path and field name. Existing module templates
|
||||
that only read `GrantedFeats`, `BonusFeat`, and `Notes` continue to use the same
|
||||
progression row fields.
|
||||
|
||||
## Tests
|
||||
|
||||
Toolkit coverage should verify:
|
||||
|
||||
- default progression behavior still exposes `GrantedFeats`;
|
||||
- configured class feat fields filter by expression and group by level;
|
||||
- configured output order follows source table row order;
|
||||
- invalid configured class feat fields fail clearly;
|
||||
- bonus feat counts remain available to the progression template.
|
||||
|
||||
Module validation should rebuild the generated wiki output with the local
|
||||
toolkit binary after the toolkit change and inspect a class that has both
|
||||
granted feats and level-gated selectable feats, such as fighter.
|
||||
Reference in New Issue
Block a user