@gcunharodrigues/wrxn 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ ---
2
+ name: harvest
3
+ description: Curate the knowledge tiers — run a health-check, then through operator-confirmed proposals MERGE near-duplicate pages into one evidence-grounded survivor and FORWARD-LINK or FLAG superseded/orphaned pages. The destructive sibling to dream (additive) and sync (drift). Use when someone says "harvest", "curate the wiki", "merge duplicate pages", "clean up stale knowledge", or at handoff when there is curation debt.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # harvest — knowledge curation
8
+
9
+ harvest is the third maintenance loop, sibling to **dream** (consolidation, additive-only) and **sync**
10
+ (drift). It does what dream deliberately doesn't: **merge, forward-link, and flag** the four knowledge
11
+ tiers (`concepts` / `decisions` / `gotchas` / `_rules`), which silt up over time — near-duplicates pile
12
+ beside each other, superseded pages linger next to their replacements, and pages whose source code was
13
+ deleted go orphaned. Left alone, Recall surfaces a thicket where one clean page belongs.
14
+
15
+ You **propose**; the deterministic adapter **gates and writes**. Every destructive act is shown to the
16
+ operator and **nothing is merged, annotated, or deleted without confirmation** — and a rejected proposal
17
+ is re-checked at the write boundary, so it can never mutate a page. The only sanctioned hard-delete is a
18
+ **merge**: an absorbed page is deleted *only* once its content provably lives in the survivor.
19
+
20
+ ## Indirection contract (MUST)
21
+
22
+ > Drive the adapter. NEVER edit, annotate, or delete a wiki page directly, and NEVER re-implement the gate.
23
+
24
+ - Health-check, merge (`stage` / `commit`), and decay (`decay propose` / `decay confirm`) all go through
25
+ **`.wrxn/harvest.cjs`** — the report, the safety gate (secret-scan, integrity hash, path-confinement),
26
+ the audit trail, and the writer.
27
+ - To DRAFT a survivor you may **read** the absorbed pages — they are plain `.md` at the paths the report
28
+ gives — and confirm recall via **`.wrxn/wiki.cjs query`**. Reading never mutates; the indirection
29
+ contract forbids only direct writes/deletes and re-implementing the gate.
30
+ - The skill is the **semantic** filter (you don't draft a junk merge); the adapter is the **mechanical**
31
+ backstop (it rejects what slips through). A proposal the gate rejects is never written.
32
+
33
+ ## The loop
34
+
35
+ 1. **Health-check** — `node .wrxn/harvest.cjs check` writes a durable report; nothing is touched.
36
+ 2. **Read the report** — one record per finding: `near_dup` clusters, `decay_candidate`s
37
+ (orphaned / superseded), and `malformed` pages.
38
+ 3. **Per finding, draft a proposal** — a merge survivor for a near-dup cluster; a `stale:` /
39
+ `superseded_by:` annotation for a decay candidate.
40
+ 4. **Stage** — `stage` (merge) or `decay propose` (annotate): record by-reference, the live page untouched.
41
+ 5. **Present + confirm** — show the operator the survivor (and the pages it deletes) or the annotation, and
42
+ wait. Nothing is written on staging alone.
43
+ 6. **Commit** — `commit` (merge) or `decay confirm` (annotate) the operator-approved subset only.
44
+
45
+ If the report is clean, **say so and stop** — a tree with no debt is a successful no-op. Do not manufacture
46
+ findings.
47
+
48
+ ## 1 — Health-check (`check`)
49
+
50
+ Run from inside the install (the adapter walks up to `wrxn.install.json` — no `--root` needed):
51
+
52
+ ```bash
53
+ node .wrxn/harvest.cjs check
54
+ ```
55
+
56
+ It scans the 4 tiers and writes a fresh, never-mutated `.wrxn/harvest/<ts>.jsonl`, then prints
57
+ `{ report, summary: { nearDupStatus, findings: { near_dup, decay_candidate, malformed } } }`. Read the
58
+ report file it names. Each record carries enough to seed a proposal:
59
+
60
+ - **`near_dup`** — a cluster of pages over the measured semantic-similarity threshold (`members[]` with
61
+ `slug` / `path` / `tier`, the strongest-edge `score`). The merge candidates.
62
+ - **`decay_candidate`** — `subtype: "orphaned"` (its `derived_from:` source file is gone, carries
63
+ `missing_source`). The decay candidates. Pages already annotated `stale:`/`superseded_by:` are treated
64
+ as resolved and excluded. A supersession is raised via `decay propose` with a replacement target
65
+ (operator/skill judgment), not auto-detected by `check`.
66
+ - **`malformed`** — bad frontmatter (the wiki-lint signal). Report-only here (see §4).
67
+
68
+ If `nearDupStatus` is `"unavailable"`, the recon serve door was cold — near-dup detection was **skipped**
69
+ (the local decay + malformed scans still ran). Tell the operator "near-dup unavailable — start
70
+ `recon-wrxn serve` and re-run `check`"; never read silence as "no duplicates".
71
+
72
+ ## 2 — Merge (a `near_dup` cluster → one survivor)
73
+
74
+ A merge folds N near-duplicate pages into **one net-new survivor** and deletes the absorbed originals.
75
+ The survivor is a **fresh** page (a new kebab slug, a path that does not yet exist) — list **every**
76
+ near-dup member in `absorbed`, including any whose name you'd like to reuse (the adapter writes the
77
+ survivor *before* it deletes, so the survivor path can't already exist). The survivor's provenance lands
78
+ as `merged_from: [<absorbed slugs>]` on the surviving page.
79
+
80
+ ### The merge reflection rubric (MUST — run it before you stage)
81
+
82
+ You are **merging, not authoring**. Read every absorbed page, then self-check the drafted survivor against
83
+ each one. Stage only if all four hold:
84
+
85
+ 1. **No dropped facts.** Every distinct fact in *each* absorbed page appears in the survivor. A merge that
86
+ loses knowledge is worse than no merge.
87
+ 2. **No invented facts.** Every survivor line **traces to a source page** — do not add knowledge, dates,
88
+ versions, paths, or claims that were not in an absorbed page. If it isn't in a source, it doesn't belong
89
+ in the survivor.
90
+ 3. **Union of evidence preserved.** Every evidence quote, citation, and provenance marker from each
91
+ absorbed page is carried into the survivor — the merge is loss-free on evidence, not just on prose.
92
+ 4. **No secrets.** Redact any credential that surfaced (the gate also rejects `contains_secret`, but you
93
+ are the first filter).
94
+
95
+ If any check fails, fix the survivor and re-run the rubric. If the cluster members are **not** actually
96
+ duplicates (your judgment overrides the threshold), **abstain** — stage nothing for that cluster.
97
+
98
+ ### Stage the merge (PROPOSE)
99
+
100
+ Write the proposal to a throwaway temp file and stage it by-reference (the live pages are untouched):
101
+
102
+ ```bash
103
+ node .wrxn/harvest.cjs stage /tmp/harvest-merge.json
104
+ ```
105
+
106
+ ```jsonc
107
+ {
108
+ "survivor": ".wrxn/wiki/concepts/auth-flow.md", // NEW path under a knowledge tier, .md, kebab slug
109
+ "description": "one-line page description", // becomes the survivor's frontmatter description
110
+ "body": "# Auth flow\n\n…the synthesised union…", // the rubric-checked survivor markdown
111
+ "absorbed": [".wrxn/wiki/concepts/login-flow.md", ".wrxn/wiki/concepts/auth-overview.md"]
112
+ }
113
+ ```
114
+
115
+ `stage` path-confines the survivor + every absorbed target to a knowledge tier, rejects a body over the
116
+ cap, secret-scans, then records the proposal + an integrity hash under `.wrxn/harvest/staged.jsonl`. It
117
+ prints `{ staged, survivor, absorbed, stagedFile }` and **writes/deletes nothing**.
118
+
119
+ ### Present, then confirm
120
+
121
+ Show the operator the **diff**: the absorbed pages (read them at the report's paths) and the one survivor
122
+ that replaces them, naming exactly which pages will be **deleted**. Wait for confirmation. If the operator
123
+ approves none, you are done — commit nothing.
124
+
125
+ ### Commit the merge (CONFIRM, by reference)
126
+
127
+ Build a JSON array of the approved **survivor paths** (not a rebuilt proposal) and commit:
128
+
129
+ ```bash
130
+ node .wrxn/harvest.cjs commit /tmp/harvest-approved.json # [".wrxn/wiki/concepts/auth-flow.md"] (or {"approved":[…]})
131
+ ```
132
+
133
+ `commit` looks up each approved survivor's staged merge, **re-runs the gate** (secret-scan → integrity →
134
+ path-confine survivor + every absorbed → survivor is new), then **writes the survivor first** (knowledge
135
+ preserved) and **only then deletes each absorbed** (survivor-before-delete). It prints `{ merged, skipped }`.
136
+ An **empty** approval is the decline — nothing changes.
137
+
138
+ ## 3 — Decay / supersession (a `decay_candidate` → annotate, never delete)
139
+
140
+ Decay is **non-destructive**: it stamps a single forward-link key into a page's frontmatter (the body is
141
+ byte-for-byte preserved) so Recall and the operator know its status. Provenance survives — decay never
142
+ deletes (deletion is merge's job alone). Two kinds:
143
+
144
+ - **`stale: <missing-source-path>`** — an orphaned page whose `derived_from:` source file is gone.
145
+ Mechanical: `decay propose` **auto-derives** these from the same scan `check` ran — no draft needed.
146
+ - **`superseded_by: <path>`** — a page replaced by another. A **judgment** (auto-scan can't invent the
147
+ replacement) — you draft it into a proposal file.
148
+
149
+ ### Propose the decay (STAGE)
150
+
151
+ Auto-scan only (stages a `stale:` annotation for every orphaned page):
152
+
153
+ ```bash
154
+ node .wrxn/harvest.cjs decay propose
155
+ ```
156
+
157
+ Or pass a draft file to add `superseded_by:` judgments (it overrides the auto stale for the same page):
158
+
159
+ ```bash
160
+ node .wrxn/harvest.cjs decay propose /tmp/harvest-decay.json
161
+ ```
162
+
163
+ ```jsonc
164
+ // one object, an array, or { "proposals": [ … ] }; key ∈ {stale, superseded_by}; value is a path
165
+ { "page": ".wrxn/wiki/gotchas/old-auth-bug.md", "key": "superseded_by",
166
+ "value": ".wrxn/wiki/gotchas/new-auth-bug.md", "reason": "replaced by the post-refactor write-up" }
167
+ ```
168
+
169
+ `decay propose` gates each candidate — page confined + present, key allowlisted, value sanitised +
170
+ secret-scanned, the page **not reinforced** (a page surfaced in Recall within the last 30 days is live
171
+ knowledge and is skipped), and **not already annotated** (idempotent) — and records survivors to
172
+ `.wrxn/harvest/decay-staged.jsonl`. It prints `{ staged, skipped, stagedFile }`. A `skipped` entry names
173
+ its reason (`reinforced`, `already_annotated`, `unsafe_page`, …).
174
+
175
+ ### Present, then confirm
176
+
177
+ Show the operator each staged annotation — the page, the key, the value, and the reason. Wait.
178
+
179
+ ### Confirm the decay (COMMIT, by reference)
180
+
181
+ ```bash
182
+ node .wrxn/harvest.cjs decay confirm /tmp/harvest-decay-approved.json # ["…/old-auth-bug.md"] (or {"approved":[…]})
183
+ ```
184
+
185
+ `decay confirm` re-gates each approved page and stamps the one frontmatter key in place (body preserved,
186
+ never deleted). It prints `{ annotated, skipped }`. An empty approval is the decline.
187
+
188
+ ## 4 — Malformed pages (report-only)
189
+
190
+ `check` also reports pages with bad frontmatter. harvest does **not** auto-fix them — surface the list to
191
+ the operator to repair by hand (the wiki-lint Stop hook flags the same signal). Fixing frontmatter is
192
+ authoring, outside harvest's gated destructive scope.
193
+
194
+ ## Boundaries
195
+
196
+ - **Curation only, on the 4 knowledge tiers.** Never the retired `sessions` tier, never `_slots`
197
+ (the focus slot is dream's), never code.
198
+ - **Confirm-gated + re-checked.** Every merge and decay is staged, presented, and confirmed by reference;
199
+ the gate re-validates at the write boundary, so a rejected/tampered proposal can never mutate a page.
200
+ - **Merge is the only hard-delete.** Decay forward-links / flags; it never deletes. No free-form delete
201
+ path exists — only a staged cluster's absorbed members are ever removed.
202
+ - **Restraint.** A clean health-check is a success: say so, stage nothing, commit nothing.
203
+ - **Never autonomous.** harvest is deliberate and operator-confirmed — never a background sweep.
204
+
205
+ ## Source
206
+
207
+ WRXN Kernel issue harvest-06, integrating harvest-02 (`check`), harvest-03 (merge `stage` / `commit`),
208
+ and harvest-04 (`decay propose` / `confirm`). Adapter: `.wrxn/harvest.cjs`. Skill + adapter shape and the
209
+ propose→confirm-by-reference spine adapted from `dream` (`SKILL.md`, `.wrxn/dream.cjs`) and the `sync`
210
+ report→propose→confirm walkthrough. ADR 0005; PRD `harvest-prd`.
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: sync
3
+ description: Report which derived docs have drifted from the source code they describe — query recon's computable drift set over the warm serve door and list each stale page, the source symbol that moved, and the watermark it was last reconciled at. Use when someone says "sync", "check for stale docs", "what docs have drifted", or wants to know if the prose is still reconciled with the code before a handoff or release.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # sync — drift report
8
+
9
+ `sync` tells you which **derived prose** has fallen out of step with the **source code** it documents.
10
+ A page that declares `derived_from: path#symbol` carries a `synced_to:` watermark — the source version
11
+ it was last reconciled against. recon-wrxn computes, purely from its index, the set of docs whose source
12
+ symbol has since changed (an AST fingerprint, so reformatting and comment edits do **not** trip drift).
13
+ This skill **queries that set and reports it**. It is the third maintenance loop alongside `dream`
14
+ (memory) — `sync` keeps derived prose reconciled with source.
15
+
16
+ > The **report** is read-only — it never edits a doc or advances a watermark. For **prose** drift you can
17
+ > then go one step further: `sync` can DRAFT a reconciling edit and, on your explicit confirm, write it in
18
+ > place and advance the watermark (the `propose → confirm` loop below). Auto-regen of *mechanical* derived
19
+ > files is still a separate, later step.
20
+
21
+ ## Indirection contract (MUST)
22
+
23
+ > Drive the adapter. NEVER hand-compute drift, re-read a doc's frontmatter, or write any file.
24
+
25
+ - The drift query goes through **`.wrxn/sync.cjs report`** (the install-local door client + report gate).
26
+ - The adapter consumes the `synced_to` watermark **from the recon_drift door response** — recon parsed
27
+ it out of the doc's frontmatter. Do not open wiki files to re-derive it; `sync` is strictly read-only.
28
+
29
+ ## The loop
30
+
31
+ 1. **Run the report** from inside the install (the adapter walks up to `wrxn.install.json` — no `--root`
32
+ needed):
33
+
34
+ ```bash
35
+ node .wrxn/sync.cjs report
36
+ ```
37
+
38
+ 2. **Read the JSON** it prints — `{ status, stale[], unwatermarked[] }`:
39
+
40
+ - **`status: "synced"`** — the stale set is empty. **Say so briefly ("all synced") and stop.** Do not
41
+ manufacture findings. A clean tree is a successful no-op.
42
+ - **`status: "drift"`** — present each `stale[]` entry to the operator: the **doc** page, the **symbol**
43
+ that moved, and **`synced_to` → `current`** (the watermark vs the source's current fingerprint). If
44
+ `unwatermarked[]` is non-empty, note those separately — docs that declare `derived_from` but were
45
+ never watermarked (so drift can't yet be computed for them).
46
+ - **`status: "unavailable"`** — recon's serve door is not warm (no `recon-wrxn serve` running, or it was
47
+ unreachable). Report "drift unavailable — start `recon-wrxn serve` and retry." Never treat this as
48
+ "all synced": unknown is not clean.
49
+
50
+ 3. **Decide per stale doc.** Hand the stale list to the operator. For a **prose** page, you may reconcile
51
+ it with the `propose → confirm` loop below. Regenerating a *mechanical* derived file is still out of
52
+ scope here.
53
+
54
+ ## Propose → confirm (prose re-stamp, sync-06)
55
+
56
+ For a stale PROSE doc, reconcile it WITHOUT ever auto-rewriting words: you draft the edit, the operator
57
+ confirms, then the watermark advances. The watermark means **"verified fresh"**, never "stamped without
58
+ checking". Same split as `dream`: **you (the skill) draft the prose; `.wrxn/sync.cjs` gates and writes.**
59
+
60
+ 1. **Draft + propose (stage).** From a `stale[]` entry, write the reconciling markdown body and stage it
61
+ by-reference — secret-scanned, recorded under `.wrxn/sync/staged.jsonl`, the live doc untouched:
62
+
63
+ ```bash
64
+ node .wrxn/sync.cjs propose proposal.json
65
+ ```
66
+
67
+ `proposal.json` carries the drift record's own fields (do NOT re-derive them) plus your drafted body:
68
+
69
+ ```json
70
+ { "doc": ".wrxn/wiki/concepts/auth-flow.md", "symbol": "src/auth.ts#login",
71
+ "synced_to": "<old watermark from the report>", "current": "<current fingerprint from the report>",
72
+ "body": "# Auth flow\n\n…the reconciled prose…" }
73
+ ```
74
+
75
+ 2. **Present it to the operator and wait.** Show the drafted edit. Nothing is written and the watermark is
76
+ NOT advanced until the operator confirms — staging alone never re-stamps.
77
+
78
+ 3. **Confirm (commit) or decline.** On approval, confirm BY REFERENCE (the doc path). The adapter re-reads
79
+ the staged edit, re-runs the secret-scan + an integrity check (a tampered or altered proposal cannot
80
+ write), edits the doc in place, and advances `synced_to:` to `current`:
81
+
82
+ ```bash
83
+ node .wrxn/sync.cjs confirm approved.json
84
+ ```
85
+
86
+ where `approved.json` is the operator-approved doc list — `[".wrxn/wiki/concepts/auth-flow.md"]` or
87
+ `{ "approved": [".wrxn/wiki/concepts/auth-flow.md"] }`. **Decline** = confirm an empty approval
88
+ (`{ "approved": [] }`) — the file AND the watermark stay exactly as they were.
89
+
90
+ ## Boundaries
91
+
92
+ - **Report is read-only.** Prose `propose → confirm` is the ONLY write path, and only on explicit operator
93
+ confirm. Regen of mechanical derived files is a later sync slice.
94
+ - **Never auto-rewrite words.** The reconciling edit is staged and presented; the in-place write + watermark
95
+ advance happen only on confirm, re-validated at the write boundary.
96
+ - **Declared provenance only.** Only docs carrying a `derived_from:` anchor participate; an undocumented
97
+ file is never "drifted". This is opt-in by provenance, by design.
98
+ - **Fail-soft, never alarmist.** If recon is unreachable the answer is "unavailable", not "stale" and not
99
+ "synced". The adapter never throws; neither should your report.
100
+
101
+ ## Source
102
+
103
+ WRXN Kernel issues sync-04 (report) + sync-06 (prose propose → confirm → re-stamp). Adapter: `.wrxn/sync.cjs`.
104
+ Drift signal: recon-wrxn `recon_drift` (sync-03), watermark storage (sync-01) + AST fingerprint (sync-02).
105
+ Door discovery mirrors `recall-surface.cjs`; skill+adapter shape (stage → commit-by-reference, secret-scan)
106
+ mirrors `dream`. PRD `sync-prd`; ADR 0004.
@@ -146,6 +146,43 @@ function normalizeTitle(t) {
146
146
  return String(t == null ? '' : t).toLowerCase().replace(/\s+/g, ' ').trim();
147
147
  }
148
148
 
149
+ // ── importance stamp — the decay-weight PRODUCER (harvest-10) ──────────────────
150
+ // dream already scores each page (`confidence`, gate floor 0.75) but never PERSISTED it, so recon
151
+ // D1/D3's `recency × importance` recall weight collapsed to `recency × tier-prior`. On commit we persist
152
+ // that EXISTING score as a single `importance:` frontmatter scalar — we do NOT recompute or invent a new
153
+ // model. Clamped to [0,1] and rendered through Number() so it is one bare scalar that cannot inject a
154
+ // newline/colon (AC4, same discipline as sync.cjs's `synced_to:` watermark).
155
+ function clamp01(score) {
156
+ const n = Number(score);
157
+ if (Number.isNaN(n)) return 0; // a non-numeric/garbage score floors to 0 — never a raw string in the page
158
+ return Math.max(0, Math.min(1, n));
159
+ }
160
+
161
+ // PURE in-place stamp (mirrors sync.cjs restampDoc): update `importance:` when present (no duplicate key),
162
+ // else append one frontmatter line; the body after the closing fence is preserved byte-for-byte (no churn).
163
+ // A page with no frontmatter fence is returned unchanged (defensive — wiki pages always carry one).
164
+ function stampImportance(content, score) {
165
+ const text = String(content);
166
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
167
+ if (!m) return text;
168
+ const lines = m[1].split(/\r?\n/);
169
+ const value = clamp01(score);
170
+ let found = false;
171
+ for (let i = 0; i < lines.length; i++) {
172
+ if (/^importance:\s*/.test(lines[i])) { lines[i] = `importance: ${value}`; found = true; break; }
173
+ }
174
+ if (!found) lines.push(`importance: ${value}`);
175
+ // splice ONLY the frontmatter fence back; the body after the closing --- is byte-for-byte preserved.
176
+ return text.slice(0, m.index) + ['---', lines.join('\n'), '---'].join('\n') + text.slice(m.index + m[0].length);
177
+ }
178
+
179
+ // Stamp the just-written wiki page in place (read → stampImportance → write). The page is reached by its
180
+ // tier+slug under .wrxn/wiki/ — the same page wiki.cjs just wrote — so the stamp never re-routes the write.
181
+ function stampPageImportance(root, tier, slug, score) {
182
+ const file = path.join(root, '.wrxn', 'wiki', tier, `${slug}.md`);
183
+ fs.writeFileSync(file, stampImportance(fs.readFileSync(file, 'utf8'), score));
184
+ }
185
+
149
186
  // ── anti-superstition negative filters ────────────────────────────────────────
150
187
  // A mechanical backstop to the dream skill's prompt (the skill is the primary semantic filter; this is
151
188
  // "reinforced where mechanical"). Each pattern catches a class of transient/false "memory" that, if
@@ -407,6 +444,7 @@ function runCommit() {
407
444
  }
408
445
  try {
409
446
  const r = wikiWritePage(root, p.tier, p.slug, p.title, p.body);
447
+ stampPageImportance(root, p.tier, p.slug, p.confidence); // persist dream's score as importance: (harvest-10)
410
448
  written.push({ slug: p.slug, tier: p.tier, file: r.written });
411
449
  } catch (err) {
412
450
  // wiki.cjs write-page does process.exit(2) on an existing page — catch the non-zero exit so a
@@ -465,4 +503,8 @@ function main() {
465
503
  }
466
504
  }
467
505
 
468
- main();
506
+ if (require.main === module) {
507
+ main();
508
+ }
509
+
510
+ module.exports = { stampImportance };