Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .mex/ROUTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ edges:
condition: when refreshing the release README, badges, community links, or architecture illustrations
- target: patterns/hub-first-run-onboarding.md
condition: when adding or changing the Hub first-run tour or its checkout-local completion state
last_updated: 2026-09-28
last_updated: 2026-10-03
---

# Session Bootstrap
Expand Down Expand Up @@ -385,10 +385,12 @@ Then read this file fully before doing anything else in this session.
appear only when a stable status observation makes the requested operation
safe; migration-required or unstable Wiki observations never fabricate a
repair action.
- The Wiki CLI's `serviceOptions` carries no code graph, so `wiki validate`
cannot resolve a grounding and `wiki migrate`'s body-hash backfill never runs.
Both degrade silently rather than failing; the validate notice now reports
that the pass had no graph instead of asserting the checkout has none.
- The Wiki CLI's graph-aware commands (validate, rebuild-index, migrate, apply,
regenerate-views) reach the graph only through the repository graph port's
fresh snapshot (#232). A stale or changing graph is not used: the command
runs without it and reports `CODE_GRAPH_UNAVAILABLE`, so `mex graph` must be
current for Wiki grounding verdicts. Synthesis (`wiki build/prepare/propose`)
still opens `graph.db` directly, without that freshness proof.

## Routing Table

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ All notable changes to this project will be documented in this file.
- `STALE_FILE` no longer counts every repository commit. Its commit signal now counts only commits made since the file last changed that touched a path the file references: its path claims, and the files of the code it grounds (committed `file`, else the node's file in the graph, plus inline `mex://` anchors). A scaffold file that references no paths is judged by its age and `last_updated` alone. Paths that are scaffold knowledge, URLs, and anything that would match the whole repository are not counted. `SETUP.md`, `SYNC.md`, `patterns/README.md` and `patterns/INDEX.md` are text mex ships, kept current by `mex pattern add` and the index-sync check, so they are no longer reported. On mex's own scaffold, `STALE_FILE` findings fell from 16 to 1. The previous counts ran from 50 to 289 commits, and the same files now count 0 to 6 commits to their referenced paths. The day and commit thresholds are unchanged (#237).

### Fixed
- The Wiki commands now use the code graph. `mex wiki validate` always reported `codeGraphAvailable: false` and resolved no grounding, `mex wiki rebuild-index` stored no grounding health, so `--health changed` matched nothing, `mex wiki migrate` never backfilled a `bodyHash`, and `mex wiki apply` could not ground. All of this happened silently, even in a checkout with a fresh `graph.db`. `validate`, `rebuild-index`, `migrate`, `apply` and `regenerate-views` now reach the graph the way the Project Hub does: through one read-only snapshot per command, proved fresh again before anything derived from it is reported. `rebuild-index` publishes its health only after that proof. A graph that is stale, incompatible, corrupt or changing during the pass is not used; the command does its work without it and adds the info notice `CODE_GRAPH_UNAVAILABLE`, which says why and names `mex graph`. A checkout with no graph stays as quiet as before. Three resolution faults that the missing graph had hidden are fixed with it:
- Migration's backfill now writes the body hash the grounding was made against: the baseline `mex ground` cached when its fingerprint is the committed one, otherwise the current body only while the fingerprint still matches, otherwise none. It no longer adopts code that may have drifted.
- A grounding whose node is reached through a re-minted id (#240), or rebound after a move, is fresh when its body is byte-identical.
- A grounding with no `bodyHash` is judged by its code tokens, not by the ids of its callers and callees.

On a populated Hono scaffold, `wiki validate` and the index went from resolving nothing to the same verdicts as `mex check`: 7 groundings fresh, and both groundings of an edited method changed. Without the last two fixes, 2 entities there and 3 on a second scaffold built before #240 read changed with their code unchanged (#232).
- `mex wiki query` now answers natural-language questions. It ANDed every word of the query, stop words included, so "Why does Hono avoid runtime dependencies?" required `why`, `does` and `avoid` and found nothing, although an entity titled *Zero runtime dependencies, Web Standards only* exists. Stop words and question words are now dropped before the every-term search, which otherwise works as before. The rest of the page is filled with entities matching some of the terms, ranked by how much of the query they cover (each term's inverse document frequency, weighted by title, summary or body) and found through a conservative stem, so `dependencies` meets `dependency`. Such an entity needs at least two distinctive terms, and a term found in most of the wiki, usually the project's own name, does not count as one. Within a field, hits are now ordered by relevance (FTS5 `bm25`) after lifecycle and health rather than by title, so the entity most about the words comes first; title still breaks ties. Each hit carries `match` (`all_terms` or `some_terms`) and, for a broader hit, `matchedTerms`; the result carries `terms`, `ignoredTerms` and `unmatchedTerms`. A broader answer adds the info notice `WIKI_QUERY_PARTIAL_MATCH`, and an empty one adds `WIKI_QUERY_NO_MATCH`, which names the terms nothing mentions and suggests keywords, `ROUTER.md` or `mex graph scope` instead of leaving a bare empty list; neither changes the exit code. Hub search uses the same plan and order, with every all-terms hit ahead of the broader ones. On the two evaluation scaffolds, with their unindexed knowledge adopted (#227), natural-language hit@5 went from 0.00 to 0.92 (Hono) and 0.77 (mex), keyword hit@5 to 1.00 on both, keyword hit@1 from 0.50 to 0.75 and from 0.69 to 0.85, exact titles stayed at 1.00, and all six off-topic evaluation queries still return nothing. Of 24 further off-topic questions, 6 return broader hits, each labelled with the terms it matched and the terms nothing does (#235).
- A refresh that must extract every TypeScript and JavaScript file in full no longer mis-indexes files outside every `tsconfig`. When such a change reached the program built for them (for example a new plain script, whose declarations the other files can see), the signal to extract in full was taken for a crashed program, and every one of those files was indexed by the fallback parser with different node ids and edges until the next rebuild. On FastAPI, a jump over 80 commits that added one documentation script left 48 nodes and 78 edges different from a clean build; it now matches exactly (#209).
- `mex check` no longer reports two false positives seen on clean scaffolds. A brace-expanded script name such as `bun run test:{node,workerd}` is expanded and each script it names is checked against `package.json`; a name that still holds braces after expansion is a placeholder and is skipped, as is one naming more than 64 scripts. A command written in code inside a dependency entry, such as `` **`bun test` / `deno test`** ``, is no longer claimed as a dependency, since a package name never contains whitespace (#231).

### Compatibility

- `CODE_GRAPH_UNAVAILABLE` is a new Wiki diagnostic code, at info severity. `wiki validate` can now report `GROUNDING_STALE`, `GROUNDING_MISSING` and `GROUNDING_UNRESOLVED` on scaffolds where it reported none, all at warning severity, so its exit status is unchanged. The index and graph schemas are unchanged; run `mex wiki rebuild-index` once to record health in an existing index (#232).
- Earlier MEX versions cannot read `mh2:` fingerprints. On a Hono scaffold re-encoded to `mh2:`, 0.8.3 reported 10 groundings it could not reconcile as `GROUNDING_GONE`, and its Wiki rejected 11 groundings as malformed and 5 entities as unparseable, breaking 3 relations; capture also treats such a grounding as changed and asks for a review. Upgrade everyone who shares a scaffold before anyone commits new groundings. Graph and Wiki index schemas are unchanged (#233).
- A root `type` key is optional and additive. Earlier versions ignore it and leave the file outside the Wiki as before; once migrated it is an ordinary `mex.type`. Adoption may now remove a root `type` key, alongside `grounds_to` and `topics`. `KNOWLEDGE_NOT_ADOPTED` and `KNOWLEDGE_UNTYPED` are new `IssueCode` values and new Wiki diagnostic codes. Graph and Wiki index schemas are unchanged (#227).
- `codeHash` is an optional, additive `grounds_to` field; graph and Wiki index schemas are unchanged. Earlier versions read scaffolds that carry it, ignore it, and report every changed body as `GROUNDING_DRIFT` as before. `GROUNDING_COMMENT_DRIFT` is a new `IssueCode` value (#236).
Expand Down
25 changes: 20 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,21 @@ function wikiIo(): import("./wiki/cli/commands.js").CommandIo {
};
}

/**
* `wikiIo()` plus the code graph, for the commands that resolve groundings
* (#232): validate, rebuild-index, migrate, apply and regenerate-views.
*
* The graph is the repository graph port, the same one the Project Hub hands
* the Wiki: it opens nothing until a command asks for a snapshot, refuses one
* that is missing, stale or incompatible, and never maintains the index. The
* read-only commands keep `wikiIo()` and pay for none of it.
*/
async function groundedWikiIo(): Promise<import("./wiki/cli/commands.js").CommandIo> {
const base = wikiIo();
const { createRepositoryGraphPort } = await import("./graph/application-adapter.js");
return { ...base, groundingBridge: createRepositoryGraphPort(base.projectRoot ?? resolve(base.scaffoldRoot, "..")) };
}

/** The §15.1 filters, added to whichever commands they apply to. */
function withReadFilters(command: Command): Command {
return command
Expand Down Expand Up @@ -671,7 +686,7 @@ wikiCommand
.option("--json", "emit one enveloped JSON object")
.action(async (options) => {
const { runValidate } = await import("./wiki/cli/commands.js");
runValidate(wikiIo(), options);
await runValidate(await groundedWikiIo(), options);
});

withReadFilters(wikiCommand.command("graph").description("A bounded slice of the relation graph")).action(
Expand All @@ -687,7 +702,7 @@ wikiCommand
.option("--json", "emit one enveloped JSON object")
.action(async (options) => {
const { runRebuildIndex } = await import("./wiki/cli/commands.js");
runRebuildIndex(wikiIo(), options);
await runRebuildIndex(await groundedWikiIo(), options);
});

wikiCommand
Expand All @@ -697,7 +712,7 @@ wikiCommand
.option("--json", "emit one enveloped JSON object")
.action(async (options) => {
const { runRegenerateViews } = await import("./wiki/cli/commands.js");
runRegenerateViews(wikiIo(), options);
await runRegenerateViews(await groundedWikiIo(), options);
});

wikiCommand
Expand All @@ -707,7 +722,7 @@ wikiCommand
.option("--json", "emit one enveloped JSON object")
.action(async (options) => {
const { runMigrate } = await import("./wiki/cli/commands.js");
runMigrate(wikiIo(), options);
await runMigrate(await groundedWikiIo(), options);
});

wikiCommand
Expand All @@ -718,7 +733,7 @@ wikiCommand
.option("--json", "emit one enveloped JSON object")
.action(async (file: string, options) => {
const { runApply } = await import("./wiki/cli/commands.js");
runApply(wikiIo(), file, options);
await runApply(await groundedWikiIo(), file, options);
});

/**
Expand Down
17 changes: 17 additions & 0 deletions src/graph/__tests__/fingerprint-encoding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createFingerprint,
deserializeFingerprint,
sameFingerprint,
sameFingerprintCode,
serializeFingerprint,
} from "../fingerprint.js";
import type { Fingerprint } from "../reconcile.js";
Expand Down Expand Up @@ -97,6 +98,22 @@ describe("compact fingerprint encoding (#233)", () => {
}
});

it("compares code alone, setting neighbours aside, when asked whether the code changed", () => {
// Renumbered neighbours (#240) and a new caller elsewhere are not this node's code changing.
const renumbered = { ...realistic, neighbors: realistic.neighbors.map((id) => `${id.split(":")[0]}:${"0".repeat(32)}`) };
const calledMore = { ...realistic, neighbors: [...realistic.neighbors, nodeId("function", "new-caller")] };
for (const other of [renumbered, calledMore]) {
expect(sameFingerprintCode(legacy(realistic), serializeFingerprint(other))).toBe(true);
expect(sameFingerprint(legacy(realistic), serializeFingerprint(other))).toBe(false);
}
const minhash = [...realistic.minhash];
minhash[0] = (minhash[0]! + 1) % 0x100000000;
for (const other of [{ ...realistic, minhash }, { ...realistic, tokenCount: realistic.tokenCount + 1 }]) {
expect(sameFingerprintCode(serializeFingerprint(realistic), serializeFingerprint(other))).toBe(false);
}
expect(sameFingerprintCode("mh2:64:not-a-fingerprint", serializeFingerprint(realistic))).toBe(false);
});

it("compares strings that do not decode only by identity", () => {
expect(sameFingerprint("not-a-fingerprint", "not-a-fingerprint")).toBe(true);
expect(sameFingerprint("not-a-fingerprint", "also-not")).toBe(false);
Expand Down
20 changes: 20 additions & 0 deletions src/graph/fingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,26 @@ export function sameFingerprint(left: string, right: string): boolean {
return left === right || canonicalFingerprint(left) === canonicalFingerprint(right);
}

/**
* Whether two serialized fingerprints describe the same code: the same MinHash
* signature over the same number of tokens, in either encoding.
*
* The neighbour list is left out on purpose. It names the node's callers and
* callees, so it changes when code *elsewhere* calls this node or when node
* ids are re-minted (#240) — neither of which is this node's code changing.
* Use it to answer "is this still the code?" without a body hash; identity
* questions, which neighbours help settle, keep {@link sameFingerprint}.
*/
export function sameFingerprintCode(left: string, right: string): boolean {
if (sameFingerprint(left, right)) return true;
const a = deserializeFingerprint(left);
const b = deserializeFingerprint(right);
if (a === null || b === null) return false;
return a.tokenCount === b.tokenCount
&& a.minhash.length === b.minhash.length
&& a.minhash.every((value, index) => value === b.minhash[index]);
}

/** Kind indexes 0–254 name a table entry; 0xff introduces a verbatim id. */
const VERBATIM_NEIGHBOR = 0xff;
const NEIGHBOR_ID = /^([a-z][a-z0-9_]*):([0-9a-f]{32})$/;
Expand Down
6 changes: 6 additions & 0 deletions src/wiki/__tests__/diagnostic-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { planOperation } from "../operations/plan.js";
import { applyOperation } from "../operations/apply.js";
import { readAuditLog } from "../operations/audit.js";
import type { GroundingResolver } from "../index/write.js";
import { codeGraphUnavailableDiagnostic } from "../grounding/availability.js";

/** Run `body` against a fresh scratch directory, cleaning up afterwards. */
function inScratch<T>(body: (directory: string) => T): T {
Expand Down Expand Up @@ -676,6 +677,11 @@ See [the code](mex://function:0123456789abcdef).
return validateScaffold({ scaffoldRoot: directory }).diagnostics;
}),

CODE_GRAPH_UNAVAILABLE: () =>
// A graph that exists but could not be trusted for the pass (#232). A
// missing graph is ordinary and has no notice, so it is not the case here.
[codeGraphUnavailableDiagnostic("stale")!],

KNOWLEDGE_UNTYPED: () =>
// A domain context file population wrote without a `type`.
inScratch((directory) => {
Expand Down
Loading
Loading