@davesheffer/hunch 1.2.0 → 1.2.2

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,97 @@
1
+ /**
2
+ * Doc adoption — the wiki TAKES OVER a stale doc. When the specs ledger grades
3
+ * a doc "stale" (do-not-trust), `hunch wiki` copies it into `<wiki>/docs/` as a
4
+ * WIKI-MANAGED page and heals the COPY against the graph:
5
+ *
6
+ * - every pin to a superseded decision is RE-PINNED to the current one,
7
+ * - a "graph correction" callout is injected right after each healed pin,
8
+ * quoting the current decision (and what it rejected) so the stale prose
9
+ * below it can be read safely,
10
+ * - non-pin issues (proposed-but-shipped, dead code refs) surface in a
11
+ * banner at the top.
12
+ *
13
+ * The ORIGINAL file is never touched — "Hunch never rewrites prose" applies to
14
+ * the user's files; the adopted copy is a generated artifact (hunch:wiki
15
+ * header), so healing it is regeneration, not prose-rewriting. From adoption
16
+ * on, the ledger and component pages route readers to the wiki copy; the copy
17
+ * is freshness-hashed over its INPUTS (source content + the current decision
18
+ * per pinned topic + issues), so a source edit or a graph move re-heals it,
19
+ * and a source doc that becomes grounded again (human healed the original) or
20
+ * disappears retires the copy automatically.
21
+ *
22
+ * Deterministic by construction: same source + same graph → byte-identical
23
+ * copy. No LLM anywhere in this path.
24
+ */
25
+ import { createHash } from "node:crypto";
26
+ import { parseDocAnchors } from "../core/docanchors.js";
27
+ import { currentForTopic, rejectedForTopic } from "../core/topics.js";
28
+ /** Page slug for an adopted doc: full rel path, kebab-cased ("docs/api-v2.md" →
29
+ * "docs-api-v2"). Kebab-casing can collide across DIFFERENT rels ("docs/api-v2.md"
30
+ * vs "docs-api/v2.md"), so a `taken` set disambiguates with a short content hash
31
+ * of the rel — deterministic, and wikiStatus is the single caller that assigns
32
+ * slugs (renderers receive the resulting paths; they never re-derive). */
33
+ export function adoptedSlug(rel, taken) {
34
+ let slug = rel.replace(/\.md$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "doc";
35
+ if (taken.has(slug))
36
+ slug = `${slug}-${createHash("sha256").update(rel).digest("hex").slice(0, 6)}`;
37
+ taken.add(slug);
38
+ return slug;
39
+ }
40
+ const clip = (s, n) => (s.length > n ? s.slice(0, n - 1).trimEnd() + "…" : s);
41
+ /** The freshness hash of an adopted copy: source content + what the graph
42
+ * currently says for each pinned topic (everything the renderer quotes,
43
+ * including rejected alternatives) + the grade's issues. Any of these moving
44
+ * re-heals the copy; the copy's own bytes are never an input. */
45
+ export function adoptionHash(content, decisions, doc) {
46
+ const topical = doc.topics.map((t) => {
47
+ const cur = currentForTopic(decisions, t);
48
+ return {
49
+ topic: t,
50
+ current: cur ? { id: cur.id, decision: cur.decision, title: cur.title } : null,
51
+ rejected: rejectedForTopic(decisions, t),
52
+ };
53
+ });
54
+ return createHash("sha256")
55
+ .update(JSON.stringify({ content, topical, issues: doc.issues }))
56
+ .digest("hex").slice(0, 16);
57
+ }
58
+ /** Render the wiki-managed copy of a stale doc, healed against the graph. */
59
+ export function renderAdoptedDoc(doc, content, decisions) {
60
+ const byId = new Map(decisions.map((d) => [d.id, d]));
61
+ const lines = content.split("\n");
62
+ const out = [];
63
+ out.push(`<!-- hunch:wiki doc:${doc.rel} — ADOPTED wiki-managed copy of \`${doc.rel}\`, healed against the graph by \`hunch wiki\`. The original graded stale and is preserved untouched; treat THIS page as the readable version. Do not edit by hand — edit the graph (or heal the original), then \`hunch wiki --heal\`. -->`);
64
+ out.push(`> ⚠ **Wiki-managed copy.** The original \`${doc.rel}\` no longer matches the decision graph and was adopted here. Pins below are healed to CURRENT decisions; corrections are inline. Heal the original to release it back (the copy retires automatically).`);
65
+ // Every grading issue is listed — a dangling pin with NO current successor gets
66
+ // no inline correction below, so the banner is its only visible explanation.
67
+ for (const i of doc.issues)
68
+ out.push(`> - ${i}`);
69
+ out.push("");
70
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
71
+ for (const raw of lines) {
72
+ // Heal EVERY stale pin on the line, and only inside its own marker — a bare
73
+ // decision id in the surrounding prose must never be rewritten.
74
+ let line = raw;
75
+ const corrections = [];
76
+ for (const a of parseDocAnchors(raw)) {
77
+ if (!a.pin)
78
+ continue;
79
+ const pinned = byId.get(a.pin);
80
+ const current = currentForTopic(decisions, a.topic);
81
+ const pinIsStale = (!pinned || pinned.status === "superseded" || !!pinned.superseded_by) && current && current.id !== a.pin;
82
+ if (!pinIsStale || !current)
83
+ continue;
84
+ line = line.replace(new RegExp(`(hunch:topic\\s+${escapeRe(a.topic)}\\s+)${escapeRe(a.pin)}(?![A-Za-z0-9])`), `$1${current.id}`);
85
+ corrections.push(`> **🧭 Graph correction** — this section was written against ${pinned ? `superseded \`${a.pin}\`` : `\`${a.pin}\`, which no longer exists`}. Current decision for \`${a.topic}\` is \`${current.id}\` — **${current.title}**: ${clip(current.decision, 400)}`);
86
+ const rejected = rejectedForTopic(decisions, a.topic);
87
+ if (rejected.length)
88
+ corrections.push(`> Rejected along the way: ${rejected.slice(0, 3).map((r) => clip(r, 120)).join("; ")}`);
89
+ }
90
+ out.push(line);
91
+ if (corrections.length)
92
+ out.push(...corrections, "");
93
+ }
94
+ out.push("", "---", "", `_Adopted from \`${doc.rel}\` — a derived, wiki-managed copy. Regenerate: \`hunch wiki --heal\`._`, "");
95
+ return out.join("\n");
96
+ }
97
+ //# sourceMappingURL=adopt.js.map