@copperbox/why 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -0
- package/dist/anchor.d.ts +112 -0
- package/dist/anchor.js +697 -0
- package/dist/anchors.d.ts +101 -0
- package/dist/anchors.js +223 -0
- package/dist/audit.d.ts +120 -0
- package/dist/audit.js +483 -0
- package/dist/blame.d.ts +91 -0
- package/dist/blame.js +294 -0
- package/dist/bundle.d.ts +78 -0
- package/dist/bundle.js +188 -0
- package/dist/capture.d.ts +76 -0
- package/dist/capture.js +462 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +641 -0
- package/dist/dig-state.d.ts +46 -0
- package/dist/dig-state.js +146 -0
- package/dist/dig.d.ts +125 -0
- package/dist/dig.js +380 -0
- package/dist/discover.d.ts +11 -0
- package/dist/discover.js +47 -0
- package/dist/doctor.d.ts +135 -0
- package/dist/doctor.js +263 -0
- package/dist/evidence.d.ts +73 -0
- package/dist/evidence.js +425 -0
- package/dist/export.d.ts +67 -0
- package/dist/export.js +106 -0
- package/dist/find-symbol.d.ts +19 -0
- package/dist/find-symbol.js +267 -0
- package/dist/git.d.ts +17 -0
- package/dist/git.js +51 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +100 -0
- package/dist/lint.d.ts +40 -0
- package/dist/lint.js +161 -0
- package/dist/serve-assets.d.ts +10 -0
- package/dist/serve-assets.js +55 -0
- package/dist/serve.d.ts +53 -0
- package/dist/serve.js +226 -0
- package/dist/trace-range.d.ts +22 -0
- package/dist/trace-range.js +216 -0
- package/package.json +44 -0
- package/ui/app.js +323 -0
- package/ui/graph.js +164 -0
- package/ui/highlight.js +202 -0
- package/ui/story-panel.d.ts +9 -0
- package/ui/story-panel.js +125 -0
- package/ui/style.css +229 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# why
|
|
2
|
+
|
|
3
|
+
**Decision archaeology for codebases.** `git blame` tells you *who* and *when*. The code tells you *how*. `why` recovers and maintains the *why* — the decisions, constraints, failed attempts, and incidents that gave the code its shape — and keeps that history anchored to the living code as it moves.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ why blame src/lock.rs:47
|
|
7
|
+
|
|
8
|
+
src/lock.rs:41-58 · acquire_shared()
|
|
9
|
+
|
|
10
|
+
● Queue-based locking decision · 2024-03-14 · corroborated
|
|
11
|
+
Chosen after striped RwLocks deadlocked under production load.
|
|
12
|
+
because of ▸ 2024-03 lock stall (incident)
|
|
13
|
+
instead of ▸ Striped RwLock (attempt — failed)
|
|
14
|
+
evidence ▸ PR #212, commit a3f9c2e
|
|
15
|
+
|
|
16
|
+
⚠ Acme 45s gateway timeout constraint · EXPIRED 2025-06-30
|
|
17
|
+
The 47s request deadline exists for a customer contract that has ended.
|
|
18
|
+
→ downstream decision "47s request deadline" may now be scar tissue.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The problem
|
|
22
|
+
|
|
23
|
+
Every mature codebase is full of load-bearing weirdness: the timeout nobody dares change, the lock nobody dares refactor, the dependency pinned to an old version for reasons lost to time. The rationale existed once — in a Slack thread, a PR comment, a departed engineer's head — and evaporated. Teams pay for this constantly:
|
|
24
|
+
|
|
25
|
+
- **Re-litigating settled decisions.** "Why don't we just use X?" — because X was tried, failed, and nobody wrote down how.
|
|
26
|
+
- **Fear-driven ossification.** Code that *could* be simplified isn't, because nobody knows whether the weirdness is still load-bearing.
|
|
27
|
+
- **Expired constraints living forever.** Most odd code is scar tissue from a constraint (a contract, a platform bug, a perf budget) that stopped being true years ago. Nothing ever tells you the wound healed.
|
|
28
|
+
|
|
29
|
+
## What `why` is
|
|
30
|
+
|
|
31
|
+
Three layers, deliberately separable:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
35
|
+
│ 1. ARCHAEOLOGY why dig │
|
|
36
|
+
│ Agent-driven ingestion: git history, PRs, issues, │
|
|
37
|
+
│ postmortems → reconstructed decision concepts, each │
|
|
38
|
+
│ with evidence citations and an honest confidence label. │
|
|
39
|
+
├──────────────────────────────────────────────────────────────┤
|
|
40
|
+
│ 2. THE BUNDLE .why/ — an OKF markdown knowledge base │
|
|
41
|
+
│ One markdown file per decision/constraint/attempt/ │
|
|
42
|
+
│ incident/question; links form the causal graph. Humans │
|
|
43
|
+
│ browse it in any editor or Obsidian; agents query it │
|
|
44
|
+
│ through okf-mcp. No database. Diffs in code review. │
|
|
45
|
+
├──────────────────────────────────────────────────────────────┤
|
|
46
|
+
│ 3. LIVENESS why anchor · why audit · why blame │
|
|
47
|
+
│ Re-anchoring keeps concepts pointing at code as it moves. │
|
|
48
|
+
│ Auditing re-verifies constraints and flags the expired │
|
|
49
|
+
│ ones — and the decisions downstream of them. │
|
|
50
|
+
└──────────────────────────────────────────────────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The bundle is the center of gravity, and it is **not a new format**: it's [OKF v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) markdown served by [okf-mcp](../okf-mcp). `why` adds a schema on top (concept types, a `why:` frontmatter extension, link-section conventions) plus the tooling OKF deliberately doesn't provide: code anchoring, constraint auditing, and the archaeology pipeline. See [DESIGN.md](DESIGN.md) for the full schema.
|
|
54
|
+
|
|
55
|
+
## The knowledge model, in one table
|
|
56
|
+
|
|
57
|
+
| Type | What it records | The question it answers |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `decision` | A choice: problem, alternatives, rationale, outcome | "Why is it this way?" |
|
|
60
|
+
| `constraint` | An external force (contract, platform limit, budget, policy) — with a way to check whether it's *still true* | "What forced this — and does it still?" |
|
|
61
|
+
| `attempt` | Something tried that failed or was abandoned, and how | "Why don't we just…?" |
|
|
62
|
+
| `incident` | A production event that forced change | "What did we learn the hard way?" |
|
|
63
|
+
| `question` | A why the archaeology could not recover | "What don't we know?" |
|
|
64
|
+
|
|
65
|
+
Links between concepts are ordinary markdown links; the section a link sits in (`# Because of`, `# Instead of`, `# Superseded by`) gives the edge its meaning. Evidence is spec-standard `# Citations` — commit SHAs, PR and issue URLs — so every claim is checkable.
|
|
66
|
+
|
|
67
|
+
Two properties are non-negotiable and shape everything:
|
|
68
|
+
|
|
69
|
+
1. **Honesty about confidence.** Reconstructed history is partly inference. Every concept carries `confidence: recorded | corroborated | inferred | speculative`, and rationale below `corroborated` is always rendered with hedging. A wrong "why" stated confidently is worse than no "why". Unrecoverable rationale becomes a `question` concept, not a guess.
|
|
70
|
+
2. **Anchors are live or dead, never silently stale.** Every code-touching concept anchors to `path + symbol + line range + as-of commit`. `why anchor` re-resolves anchors across renames and refactors; an anchor it cannot re-resolve is marked `lost` and surfaces in `why doctor` — it never silently points at the wrong code. Where it can confirm the code but not the provenance, it says exactly that (`unverified as_of`) rather than guessing in either direction — see [Squash merges and `as_of`](#squash-merges-and-as_of).
|
|
71
|
+
|
|
72
|
+
## Why OKF as the substrate
|
|
73
|
+
|
|
74
|
+
- **Decisions are naturally documents** — structured frontmatter plus narrative body, linked into a graph.
|
|
75
|
+
- **Dual audience built in.** Agents write and query through MCP; humans browse the same files in Obsidian or review them in PRs. No custom viewer, no database.
|
|
76
|
+
- **MCP-native consumption.** Any coding agent (Claude Code included) mounts the bundle and asks "what's the story behind this file" during normal work — `why` needs no client integration of its own.
|
|
77
|
+
- **Permissive by design.** Reconstructed history is messy and partial; a format where imperfect documents still serve fits reality.
|
|
78
|
+
- **`git`-visible.** The decision record itself has history, review, and blame. `why` can be run on its own bundle.
|
|
79
|
+
|
|
80
|
+
## What's honestly hard
|
|
81
|
+
|
|
82
|
+
Named here so we never pretend otherwise (expanded in [DESIGN.md §Open problems](DESIGN.md#open-problems)):
|
|
83
|
+
|
|
84
|
+
- **Anchor drift** is the hard engineering problem. Line ranges rot instantly; symbol + blame-based re-anchoring across renames, moves, and rewrites is the make-or-break component.
|
|
85
|
+
- **Hallucinated rationale** is the hard trust problem. The confidence ladder and evidence-citation requirements exist because a decision archive people can't trust is worse than none.
|
|
86
|
+
- **Cold start** is the hard adoption problem. Nobody hand-writes ADRs retroactively; `why dig` must produce a genuinely useful first bundle from history alone, unattended, or the tool never gets a chance.
|
|
87
|
+
|
|
88
|
+
## Squash merges and `as_of`
|
|
89
|
+
|
|
90
|
+
If your project squash-merges (or rebases) pull requests — most do — this section is worth two minutes, because it explains a report you will eventually see.
|
|
91
|
+
|
|
92
|
+
Every anchor records an `as_of`: the commit at which `why` last verified that this concept was about this code. It is provenance, and it is also how re-anchoring works — `why anchor` traces your lines *forward from `as_of`* to find where they live now.
|
|
93
|
+
|
|
94
|
+
A squash merge throws that commit away. Ten commits on `add-rate-limiter` become one brand-new commit on `main`, and the branch commits are never part of `main`'s history. So an anchor stamped while you were on the branch names a commit that, after the merge, exists only in the clone that made it:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
main A ─────────────── S S = the squash. Your branch's commits
|
|
98
|
+
\ / are not in main's history at all.
|
|
99
|
+
branch B ─── C ───┘ C = what `as_of: c0ffee` pointed to
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Anchor `as_of: c0ffee` still resolves on *your* laptop, because your clone kept the branch. In CI — a fresh clone of `main` — that commit does not exist. A tool that read history through it would answer differently on each machine, which is worse than a tool that fails: it would be a health report you cannot reproduce.
|
|
103
|
+
|
|
104
|
+
**What `why` does about it.**
|
|
105
|
+
|
|
106
|
+
- **`why anchor` avoids creating the problem.** When you are on a branch that git can tell is not the integration branch, it stamps the *merge-base* — the newest commit certain to survive the squash — instead of your branch HEAD, but only when it can verify the identical span is really there. When it can't verify that (usually because your branch is what changed those lines), it stamps HEAD honestly rather than assert a span it didn't check.
|
|
107
|
+
- **Resolution never reads through an orphan.** An `as_of` that isn't an ancestor of HEAD is not used for rename-following or blame-tracing, even when your local clone could technically answer. This is what keeps CI and your laptop agreeing.
|
|
108
|
+
- **An unreadable `as_of` is not a dead anchor.** If the path is still there but the recorded *lines* can't be re-traced — no symbol to re-find, no readable `as_of` to trace from — `why anchor` reports `unverified as_of`, changes nothing, and `why doctor` lists it. The code is fine; only the provenance is unreadable. Calling that `lost` would be a false alarm, and false alarms are how a health report gets ignored.
|
|
109
|
+
- **An orphaned `as_of` is repaired, not kept as a scar.** When the claim re-verifies at HEAD without reading `as_of` at all — the file is present for a whole-file anchor, or the symbol is re-found at its recorded span — `why anchor` re-stamps the orphan, and only to a commit that survives: HEAD on the integration branch, or the merge-base on a feature branch when the identical span verifiably holds there. When neither is available it declines and leaves the orphan reported, rather than write a branch HEAD the next squash would discard.
|
|
110
|
+
|
|
111
|
+
**What you'll see.** `why doctor` may report a few `stale as_of` findings right after a squash-merged PR — yellow, not red, and they don't fail CI: the anchor isn't asserting anything false, it just can't show its work. The post-merge `why-anchor` job clears them on the next run from `main`, re-stamping each orphan whose claim still verifies at HEAD to the commit that landed.
|
|
112
|
+
|
|
113
|
+
One case stays yellow: a bare `path + lines` anchor whose `as_of` was squashed away. With no symbol to re-find and no history to trace from, nothing can verify what those line numbers name anymore, so `why anchor` reports `unverified as_of` and refuses to guess — re-stamping would assert a span nothing verified. That finding is accurate and needs a human (or a re-dig) to confirm the lines; it is one more reason DESIGN.md prefers `symbol` over bare line ranges.
|
|
114
|
+
|
|
115
|
+
An `as_of` that is a clean *ancestor* of HEAD is never re-stamped, orphan repair included: an old ancestor `as_of` means the anchor has *survived unchanged* since then — provenance, not drift ([decision](.why/decisions/as-of-is-provenance.md)).
|
|
116
|
+
|
|
117
|
+
Nothing here requires configuration. `why` reads your integration branch from `refs/remotes/origin/HEAD` (git records it at clone time; `git remote set-head origin -a` refreshes it). Where there is no such record — a single-branch CI clone, a repo with no remote — `why` stamps HEAD, which is what it did before and remains truthful.
|
|
118
|
+
|
|
119
|
+
## Self-hosted
|
|
120
|
+
|
|
121
|
+
`why` runs on its own repository: [`.why/`](.why/index.md) is this repo's live
|
|
122
|
+
decision archive — the project's bootstrap decisions captured as `decision`
|
|
123
|
+
concepts (confidence `recorded`, citations to the actual commits and PRs) —
|
|
124
|
+
and the living demo of the schema on a real codebase. It stays true
|
|
125
|
+
mechanically: every PR runs `why lint` + `why anchor --check --allow-drift`,
|
|
126
|
+
every push to `main` re-anchors from the commit that landed, a weekly job runs
|
|
127
|
+
`why audit`, and merged PRs get drafted into `.why/.drafts/` by `why capture` —
|
|
128
|
+
the exact workflows documented in [docs/ci.md](docs/ci.md), active under
|
|
129
|
+
[`.github/workflows/`](.github/workflows). Browse it like any bundle:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx -y @copperbox/okf-mcp --bundle why=.why inspect
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Getting started
|
|
136
|
+
|
|
137
|
+
All ten subcommands ship, the four CI recipes run on this repo's own `.why/`
|
|
138
|
+
bundle, and both viewers (`why serve` and the VS Code extension) render live.
|
|
139
|
+
**[HOWTO.md](HOWTO.md) is the adoption guide** — scaffold a bundle, cold-start
|
|
140
|
+
a dig, wire up the PR gate / re-anchor / weekly audit / post-merge capture jobs, and the
|
|
141
|
+
team habits that make it pay off.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npx -y @copperbox/why init --capture-snippet # scaffold .why/ + teach CLAUDE.md
|
|
145
|
+
npx -y @copperbox/why blame <path>[:line] # read the story of a span
|
|
146
|
+
npx -y @copperbox/why doctor # archive health at a glance
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The ten subcommands — `dig · anchor · audit · blame · capture · lint · doctor ·
|
|
150
|
+
export · init · serve` — are specified in [DESIGN.md §8](DESIGN.md#8-implementation-shape),
|
|
151
|
+
and each operational recipe has a page under [`docs/`](docs).
|
|
152
|
+
|
|
153
|
+
## Project map
|
|
154
|
+
|
|
155
|
+
| File | What it is |
|
|
156
|
+
|---|---|
|
|
157
|
+
| [HOWTO.md](HOWTO.md) | Adoption guide — get a team running `why` on any repo |
|
|
158
|
+
| [DESIGN.md](DESIGN.md) | Full schema and architecture spec — the source of truth |
|
|
159
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Development setup, the invariants, and how to send a PR |
|
|
160
|
+
| [CLAUDE.md](CLAUDE.md) | Orientation for agent sessions in this repo |
|
|
161
|
+
| [examples/harbor/](examples/harbor/) | A hand-authored example bundle for a fictional service — the schema, fully realized in six concepts |
|
|
162
|
+
| [.why/](.why/index.md) | This repo's own decision archive — the self-hosted bundle, kept honest by the CI recipes in [docs/ci.md](docs/ci.md) |
|
|
163
|
+
|
|
164
|
+
The example bundle is browsable today:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
cd ../okf-mcp && npm run dev -- --bundle harbor=../why/examples/harbor inspect
|
|
168
|
+
```
|
package/dist/anchor.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type LineRange } from "./blame.js";
|
|
2
|
+
import { type Anchor, type WhyBundle } from "./bundle.js";
|
|
3
|
+
/** An anchor run that must stop cleanly (exit 1), not crash. */
|
|
4
|
+
export declare class AnchorError extends Error {
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A read-only view of the repository at HEAD, with per-run caches. Anchor
|
|
8
|
+
* paths are repo-root-relative, so everything resolves from the toplevel.
|
|
9
|
+
* Exported for `why doctor`, whose as_of-freshness check is the same view.
|
|
10
|
+
*/
|
|
11
|
+
export declare class GitView {
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly headFull: string;
|
|
14
|
+
readonly headShort: string;
|
|
15
|
+
private readonly headContent;
|
|
16
|
+
private readonly renameMaps;
|
|
17
|
+
private readonly commitShas;
|
|
18
|
+
private readonly ancestry;
|
|
19
|
+
private readonly revContent;
|
|
20
|
+
private survivingBaseCache?;
|
|
21
|
+
private constructor();
|
|
22
|
+
static open(startDir: string): Promise<GitView>;
|
|
23
|
+
/** File content at HEAD, or undefined when the path does not exist there. */
|
|
24
|
+
fileAtHead(path: string): Promise<string | undefined>;
|
|
25
|
+
/** Full sha of a commit-ish, or undefined when it does not resolve. */
|
|
26
|
+
commitSha(rev: string): Promise<string | undefined>;
|
|
27
|
+
/** Whether `sha` is an ancestor of (or equal to) HEAD. */
|
|
28
|
+
isAncestor(sha: string): Promise<boolean>;
|
|
29
|
+
private contains;
|
|
30
|
+
/**
|
|
31
|
+
* An `as_of` usable as a history origin, or undefined. Usable means it both
|
|
32
|
+
* resolves *and* is an ancestor of HEAD. Ancestry is not pedantry: git will
|
|
33
|
+
* happily diff or blame between two commits that share no history, so an
|
|
34
|
+
* `as_of` a squash merge discarded still answers rename questions in a clone
|
|
35
|
+
* that kept the branch it lived on, and answers nothing in a clone that did
|
|
36
|
+
* not. Ancestry is the only property of an `as_of` that every clone of the
|
|
37
|
+
* same history agrees on, so it is the gate for reading history through one.
|
|
38
|
+
*/
|
|
39
|
+
historyOrigin(asOf: string | undefined): Promise<string | undefined>;
|
|
40
|
+
/** File content at an arbitrary commit, or undefined when absent there. */
|
|
41
|
+
fileAt(rev: string, path: string): Promise<string | undefined>;
|
|
42
|
+
shortSha(rev: string): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* The newest commit certain to outlive this branch: the merge-base with the
|
|
45
|
+
* integration branch git records as the remote's default
|
|
46
|
+
* (`refs/remotes/origin/HEAD`). Undefined when there is no such record —
|
|
47
|
+
* nothing to be certain about — or when HEAD is already contained in it, in
|
|
48
|
+
* which case HEAD survives and is the honest stamp. See `stampFor`.
|
|
49
|
+
*/
|
|
50
|
+
survivingBase(): Promise<string | undefined>;
|
|
51
|
+
private computeSurvivingBase;
|
|
52
|
+
/**
|
|
53
|
+
* Where git's rename/copy detection says `path` went between `asOf` and
|
|
54
|
+
* HEAD — the "git history connects them" gate for following a symbol into
|
|
55
|
+
* a different file (DESIGN.md §4 step 1).
|
|
56
|
+
*/
|
|
57
|
+
renamedTo(asOf: string, path: string): Promise<string | undefined>;
|
|
58
|
+
/**
|
|
59
|
+
* Blame-trace (DESIGN.md §4 step 2): follow the anchored lines forward from
|
|
60
|
+
* `asOf` with reverse blame. Lines annotated with HEAD's sha still exist at
|
|
61
|
+
* HEAD — their positions (and file, since blame follows whole-file renames)
|
|
62
|
+
* are the new claim. No surviving lines → the trace honestly fails.
|
|
63
|
+
*/
|
|
64
|
+
traceLines(path: string, range: LineRange, asOf: string): Promise<{
|
|
65
|
+
path: string;
|
|
66
|
+
lines: LineRange;
|
|
67
|
+
} | undefined>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The 1-based line span of `symbol`'s definition in `content`, or undefined
|
|
71
|
+
* when the symbol is absent or ambiguous. Exported for issue #10's torture
|
|
72
|
+
* harness and for the doctor's diagnostics.
|
|
73
|
+
*/
|
|
74
|
+
export declare function findSymbolSpan(content: string, symbol: string): LineRange | undefined;
|
|
75
|
+
export type AnchorOutcome = "current" | "resolved" | "moved" | "repaired" | "unverified" | "lost";
|
|
76
|
+
export interface AnchorResult {
|
|
77
|
+
conceptId: string;
|
|
78
|
+
/** Position within the concept's why.anchors list. */
|
|
79
|
+
index: number;
|
|
80
|
+
before: Anchor;
|
|
81
|
+
/** The anchor as it should read now; equals `before` when nothing changed. */
|
|
82
|
+
after: Anchor;
|
|
83
|
+
outcome: AnchorOutcome;
|
|
84
|
+
/** Whether the bundle file needs (or, under --check, would need) a write. */
|
|
85
|
+
changed: boolean;
|
|
86
|
+
/** A lost anchor that resolved back to live — DESIGN.md's recovery case. */
|
|
87
|
+
recovered: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface AnchorReport {
|
|
90
|
+
/** Short sha the run resolved against. */
|
|
91
|
+
head: string;
|
|
92
|
+
results: AnchorResult[];
|
|
93
|
+
/** Concepts left untouched because their why.anchors have schema problems. */
|
|
94
|
+
skipped: string[];
|
|
95
|
+
}
|
|
96
|
+
export interface ResolveOptions {
|
|
97
|
+
/** Scope the run to one concept, by id or bundle-relative path. */
|
|
98
|
+
concept?: string;
|
|
99
|
+
}
|
|
100
|
+
/** Re-evaluate every anchor claim in the bundle against HEAD. Pure read. */
|
|
101
|
+
export declare function resolveAnchors(bundle: WhyBundle, options?: ResolveOptions): Promise<AnchorReport>;
|
|
102
|
+
/**
|
|
103
|
+
* Persist the changed anchors through okf-mcp's updateConcept: a frontmatter
|
|
104
|
+
* patch of the `why` map with `keepTimestamp`, so the body and every other
|
|
105
|
+
* key survive byte-for-byte. Returns the concept ids written.
|
|
106
|
+
*/
|
|
107
|
+
export declare function writeAnchorUpdates(bundle: WhyBundle, report: AnchorReport): Promise<string[]>;
|
|
108
|
+
export declare function renderAnchorReport(report: AnchorReport, mode: {
|
|
109
|
+
check: boolean;
|
|
110
|
+
written: string[];
|
|
111
|
+
allowDrift?: boolean;
|
|
112
|
+
}): string[];
|