rvlib-shipkit 0.30.0 → 0.31.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.31.0
4
+
5
+ - **Every repo browses its shared wiki from its own hub.** A repo whose CLAUDE.md include chain points at a wiki living in another repo (`@~/dev/<other-repo>/wiki/index.md`, read from `CLAUDE.md`, `CLAUDE.local.md` or `CLAUDE.private.md`) mounts it at `/graph/base`, named after the repo that owns it. That wiki binds every agent working here, so the hub lets a human read it where the work happens. An index inside the repo is skipped, since it is already its own mount, and the card carries no lint verdict: the owning repo lints its own pages.
6
+ - **The graphs section stops advertising a graph the server does not serve.** A repo without a `wiki/` dir, which every fresh `shipkit start` is, still got a `wiki graph` card, and opening it embedded the hub's own 404 body ("not found — the hub page is at /") inside the card. Routes, `/hub.json` tools and cards now read ONE `GraphMount[]` that `resolveHubDeps` resolves (`HubDeps.wikiGraph` and `HubDeps.agentGraph` are gone, `HubDeps.graphs` replaces both), instead of the collection re-deriving the wiki graph from the repo root. A graph that is not mounted has no card, and the test asserts every href the section advertises answers 200.
7
+
3
8
  ## 0.30.0
4
9
 
5
10
  - **The studio asset page is tools on one shared canvas, not tabs on a drawer.** Every metadata family is a toggleable TOOL: `oval`, `legs`, `size` (the geometry editor split into its parts), each repo `customPanel`, `asset json`, `roll meta`. Several can be on at once, the URL records the set (`#<assetId>?tools=a,b`, shareable, restored on load), and the last set per kind is remembered. Kit-declared TABS (`meta.tabs: [{ id, tools }]`) are one-click presets, validated at kit load (an undeclared panel OR an unknown tool id refuses, loud). Panel tools float as draggable cards ON the canvas (a drag clamps inside the stage), `customPanel({ anchor: 'bl' })` starts a card bottom-left, so switching a tab never reflows the stage, and the drawer, its tab row and the bottom json body are deleted. Toggling a custom editor on does fold the variants bar, the one deliberate reflow. Custom panels can draw on the art in place: `api.stageOverlay()` returns an element covering the displayed roll (the tint-mask painter paints THERE, controls stay in its floating card), `api.dropOverlays()` cleans up on unmount, and the host calls it. Variants (rolls) move to a thin resizable right bar (drag handle, width remembered, slim-rail collapse, auto-folds when a custom editor turns on). The prompt keeps its fold. The stage remeasures through a ResizeObserver, so overlays track the image through any layout change, not only window resizes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,34 +1,32 @@
1
- // graphs collection — the mounted md graphs as CHECKED section entries:
2
- // each graph instance carries its lint state (orphans + dangling refs =
3
- // yellow with counts; clean = quiet). Custom wiki-graph providers (repos
4
- // with their own loader + their own lint) get a link-only instance — their
5
- // repo lint owns correctness, never double-judged here.
6
- import { existsSync } from 'fs'
7
- import { join } from 'path'
1
+ // graphs collection — the MOUNTED md graphs as CHECKED section entries. The
2
+ // mounts arrive as data from the hub's own route resolution (serve/run-serve),
3
+ // never re-derived here: a card linking at an unmounted route renders the 404
4
+ // body inside its iframe. Each instance carries its lint state (orphans +
5
+ // dangling refs = yellow with counts; clean = quiet). A graph whose lint
6
+ // belongs to another repo is link-only, never double-judged here.
8
7
  import { lintMd } from '../wiki/lint-md.ts'
8
+ import type { GraphMount } from '../wiki/graph-mount.ts'
9
9
  import type { Collection, Instance } from './collection-types.ts'
10
10
 
11
- export function graphsCollection(root: string, o: { customWiki: boolean; agentIndex?: string }): Collection {
11
+ export function graphsCollection(mounts: GraphMount[]): Collection {
12
12
  return {
13
13
  id: 'graphs',
14
14
  title: 'graphs',
15
15
  media: 'data',
16
- list: async () => {
17
- const out: Instance[] = []
18
- const checked = (id: string, title: string, href: string, dir: string, index?: string): Instance => {
19
- const r = lintMd({ root: dir, index })
16
+ list: async () =>
17
+ mounts.map((m): Instance => {
18
+ const id = `graph:${m.id}`
19
+ const href = `/graph/${m.id}`
20
+ const label = m.note != null ? `${m.label} (${m.note})` : m.label
21
+ if (m.lint == null) return { id, text: label, href }
22
+ const r = lintMd({ root: m.lint.root, index: m.lint.index })
20
23
  const issues = r.orphans.length + r.dangling.length
21
24
  return {
22
25
  id,
23
- text: issues === 0 ? title : `${title} — ${r.orphans.length} orphan(s), ${r.dangling.length} dangling ref(s)`,
26
+ text: issues === 0 ? label : `${label} — ${r.orphans.length} orphan(s), ${r.dangling.length} dangling ref(s)`,
24
27
  severity: issues === 0 ? undefined : 'yellow',
25
28
  href,
26
29
  }
27
- }
28
- if (o.customWiki) out.push({ id: 'graph:wiki', text: 'wiki graph (repo-owned lint)', href: '/graph/wiki' })
29
- else out.push(checked('graph:wiki', 'wiki graph', '/graph/wiki', root))
30
- if (existsSync(join(root, 'agent'))) out.push(checked('graph:agent', 'agent graph', '/graph/agent', join(root, 'agent'), o.agentIndex))
31
- return out
32
- },
30
+ }),
33
31
  }
34
32
  }
@@ -1,12 +1,14 @@
1
1
  // shipkit serve — resolve the repo's toolbox and run the hub. What mounts
2
2
  // comes from convention + repo.config's `serve` block: the wiki graph is
3
3
  // always on (custom provider via serve.wikiGraph, else the generic md
4
- // scan), agent/ graph when the dir exists, the studio when .rv-assets/
4
+ // scan), the base-context wiki when a CLAUDE.md include names one, agent/
5
+ // graph when the dir exists, the studio when .rv-assets/
5
6
  // kit.ts loads. Port: --port > serve.port > a deterministic house-range
6
7
  // default (4800-4979, hashed from the repo name — stable across launches;
7
8
  // pin serve.port when two repos collide).
8
9
  import { existsSync, readFileSync } from 'fs'
9
- import { join } from 'path'
10
+ import { homedir } from 'os'
11
+ import { basename, dirname, join, resolve, sep } from 'path'
10
12
  import { fileURLToPath } from 'url'
11
13
  import { loadKit, type ResolvedKit } from '../assets/kit-config.ts'
12
14
  import { studioRoutes, type StudioRoutes } from '../assets/server/studio-server.ts'
@@ -15,6 +17,7 @@ import { graphsCollection } from '../collections/graphs.ts'
15
17
  import { pingTick } from '../collections/pings.ts'
16
18
  import { buildCollections } from '../collections/providers.ts'
17
19
  import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
20
+ import type { GraphMount } from '../wiki/graph-mount.ts'
18
21
  import { scanMdGraph } from '../wiki/scan-md.ts'
19
22
  import { serveHub, type HubDeps } from './serve-hub.ts'
20
23
 
@@ -26,19 +29,54 @@ export function defaultHubPort(name: string): number {
26
29
  return 4800 + (h % 180)
27
30
  }
28
31
 
32
+ // the BASE-CONTEXT wiki: the shared wiki this repo's CLAUDE.md include chain
33
+ // points at (`@~/dev/<other-repo>/wiki/index.md`). It lives in ANOTHER repo and
34
+ // binds every agent working here, so the hub makes it browsable. Never linted
35
+ // from here. The local-include forms (`@./agent/index.md`,
36
+ // `@./CLAUDE.local.md`) and any index inside this repo are skipped: those are
37
+ // already their own mounts.
38
+ export function baseWikiRoot(root: string): string | null {
39
+ const abs = resolve(root)
40
+ for (const f of ['CLAUDE.md', 'CLAUDE.local.md', 'CLAUDE.private.md']) {
41
+ const p = join(abs, f)
42
+ if (!existsSync(p)) continue
43
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
44
+ const m = line.trim().match(/^@(\S+\.md)$/)
45
+ if (m == null) continue
46
+ const raw = m[1]!
47
+ const target = resolve(abs, raw.startsWith('~/') ? join(homedir(), raw.slice(2)) : raw)
48
+ const dir = dirname(target)
49
+ if (!existsSync(target)) continue
50
+ if (dir === abs || dir.startsWith(abs + sep)) continue
51
+ return dir
52
+ }
53
+ }
54
+ return null
55
+ }
56
+
57
+ // `~/dev/<repo>/wiki` reads as "<repo> wiki" — a dir literally named wiki says
58
+ // nothing on its own, so the owning repo names the card
59
+ const wikiLabel = (dir: string): string => (basename(dir) === 'wiki' ? `${basename(dirname(dir))} wiki` : basename(dir))
60
+
29
61
  export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o?: { port?: number }): Promise<HubDeps> {
30
- // the default wiki graph exists only when the repo HAS a wiki dir: a
31
- // game repo's hub advertising a wiki route was startup noise
32
- // 2026-08-05). A config-declared wikiGraph always mounts.
33
- const wikiGraph = c.serve?.wikiGraph != null
34
- ? { title: c.serve.wikiGraph.title ?? `${c.name} wiki`, load: c.serve.wikiGraph.load }
35
- : existsSync(join(root, 'wiki'))
36
- ? { title: `${c.name} wiki`, load: () => scanMdGraph({ root }) }
37
- : null
62
+ // ONE list: the routes, the /hub.json tools and the graphs section all
63
+ // read it, so nothing can advertise a graph the server does not serve.
64
+ // The repo's OWN wiki mounts when it has one (a game repo's hub
65
+ // advertising an empty wiki route was startup noise), a config-declared
66
+ // provider always mounts, and the base-context wiki (the shared one named
67
+ // by the CLAUDE.md include chain) mounts wherever it is reachable: an
68
+ // agent working here obeys that wiki, so a reader can open it.
69
+ const graphs: GraphMount[] = []
70
+ if (c.serve?.wikiGraph != null)
71
+ graphs.push({ id: 'wiki', label: 'wiki graph', title: c.serve.wikiGraph.title ?? `${c.name} wiki`, load: c.serve.wikiGraph.load, lint: null, note: 'repo-owned lint' })
72
+ else if (existsSync(join(root, 'wiki')))
73
+ graphs.push({ id: 'wiki', label: 'wiki graph', title: `${c.name} wiki`, load: () => scanMdGraph({ root }), lint: { root } })
74
+ const baseWiki = baseWikiRoot(root)
75
+ if (baseWiki != null)
76
+ graphs.push({ id: 'base', label: wikiLabel(baseWiki), title: wikiLabel(baseWiki), load: () => scanMdGraph({ root: baseWiki }), lint: null })
38
77
  const agentRoot = join(root, 'agent')
39
- const agentGraph = existsSync(agentRoot)
40
- ? { title: `${c.name} agent/`, load: () => scanMdGraph({ root: agentRoot, index: c.serve?.agentIndex }) }
41
- : null
78
+ if (existsSync(agentRoot))
79
+ graphs.push({ id: 'agent', label: 'agent graph', title: `${c.name} agent/`, load: () => scanMdGraph({ root: agentRoot, index: c.serve?.agentIndex }), lint: { root: agentRoot, index: c.serve?.agentIndex } })
42
80
  // ONE page for every repo: the studio ALWAYS
43
81
  // mounts — kit-less repos get an empty media section, never a second
44
82
  // page. A broken kit degrades to the same empty kit, loudly.
@@ -60,15 +98,14 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
60
98
  const cli = fileURLToPath(new URL('../cli.ts', import.meta.url))
61
99
  const allCollections = () => [
62
100
  ...buildCollections(root, c, kit, { bootVersion: BOOT_VERSION, libVersion }),
63
- graphsCollection(root, { customWiki: c.serve?.wikiGraph != null, agentIndex: c.serve?.agentIndex }),
101
+ graphsCollection(graphs),
64
102
  ]
65
103
  const port = o?.port ?? c.serve?.port ?? defaultHubPort(c.name)
66
104
  return {
67
105
  root,
68
106
  name: c.name,
69
107
  port,
70
- wikiGraph,
71
- agentGraph,
108
+ graphs,
72
109
  studio,
73
110
  checkCmd: [process.execPath, cli, 'check'],
74
111
  needs: () => projectNeeds(allCollections()),
@@ -6,23 +6,21 @@
6
6
  import { existsSync } from 'fs'
7
7
  import { join } from 'path'
8
8
  import { graphHtml } from 'rvlib-shipkit/src/wiki/graph-page.ts'
9
- import type { MdGraph } from 'rvlib-shipkit/src/wiki/md-graph.ts'
10
9
  import type { StudioRoutes } from '../assets/server/studio-server.ts'
11
10
  import { appendAskRemark, appendAssetNote, appendPostFeedback, askDone, askProof, markPosted, pickPostVariant } from '../collections/actions.ts'
12
11
  import type { Collection, Instance, NeedItem } from '../collections/collection-types.ts'
12
+ import type { GraphMount } from '../wiki/graph-mount.ts'
13
13
  import hubIndex from './hub-app/index.html'
14
14
 
15
- export type HubGraph = { title: string; load: () => MdGraph }
16
15
  export type HubTool = { path: string; label: string; desc: string }
17
16
  export type HubDeps = {
18
17
  root: string
19
18
  name: string
20
19
  /** 0 = random (tests); real launches pass repo.config/default */
21
20
  port: number
22
- /** null = this repo has no wiki to graph — the route stays unmounted */
23
- wikiGraph: HubGraph | null
24
- /** null = no agent/ dir */
25
- agentGraph: HubGraph | null
21
+ /** every md graph this hub serves, in card order: /graph/<id>. run-serve
22
+ * resolves the list, the routes AND the graphs section read it */
23
+ graphs: GraphMount[]
26
24
  /** the media API (mounted under /media/*); null only if the route
27
25
  * table failed to build — the page shows the WARN, never a second page */
28
26
  studio: StudioRoutes | null
@@ -43,11 +41,11 @@ export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; stop
43
41
 
44
42
  // the toolbox as data — stdout ok: lines and /hub.json render THIS list,
45
43
  // so they can never disagree
46
- export function hubTools(d: Pick<HubDeps, 'wikiGraph' | 'agentGraph'>): HubTool[] {
47
- const tools: HubTool[] = [{ path: '/', label: 'hub', desc: 'THE page — media · asks · creds · posts · health · graphs' }]
48
- if (d.wikiGraph != null) tools.push({ path: '/graph/wiki', label: 'wiki graph', desc: d.wikiGraph.title })
49
- if (d.agentGraph != null) tools.push({ path: '/graph/agent', label: 'agent graph', desc: d.agentGraph.title })
50
- return tools
44
+ export function hubTools(d: Pick<HubDeps, 'graphs'>): HubTool[] {
45
+ return [
46
+ { path: '/', label: 'hub', desc: 'THE page — media · asks · creds · posts · health · graphs' },
47
+ ...d.graphs.map((g) => ({ path: `/graph/${g.id}`, label: g.label, desc: g.title })),
48
+ ]
51
49
  }
52
50
 
53
51
  // drill-down pages (graph canvases) get one fixed home link back to THE page
@@ -125,10 +123,10 @@ export function serveHub(d: HubDeps): Hub {
125
123
  return new Response(proc.stdout, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
126
124
  }
127
125
  const page = (s: string): Response => new Response(withHome(s), { headers: { 'content-type': 'text/html; charset=utf-8' } })
128
- if (d.wikiGraph != null && path === '/graph/wiki') return page(graphHtml(d.wikiGraph.load(), { title: d.wikiGraph.title }))
129
- if (d.wikiGraph != null && path === '/graph/wiki/graph.json') return Response.json(d.wikiGraph.load())
130
- if (d.agentGraph != null && path === '/graph/agent') return page(graphHtml(d.agentGraph.load(), { title: d.agentGraph.title }))
131
- if (d.agentGraph != null && path === '/graph/agent/graph.json') return Response.json(d.agentGraph.load())
126
+ for (const g of d.graphs) {
127
+ if (path === `/graph/${g.id}`) return page(graphHtml(g.load(), { title: g.title }))
128
+ if (path === `/graph/${g.id}/graph.json`) return Response.json(g.load())
129
+ }
132
130
  // the media API namespace — the studio route table, ONE prefix strip
133
131
  if (d.studio != null && path.startsWith('/media/')) return d.studio.handle(req, path.slice('/media'.length))
134
132
  // stale bookmarks (/assets/, /status, …) land here — name the way home
package/src/shipkit.ts CHANGED
@@ -5,8 +5,9 @@ export type { RepoConfig, ServeConfig, FloorResolution, FloorSource } from './re
5
5
  export { contractsConfigFrom, activeContracts, applyContractsResult } from './contracts/apply-contracts.ts'
6
6
  // the hub (`shipkit serve`): one server, one port, every tool a click away
7
7
  export { serveHub, hubTools } from './serve/serve-hub.ts'
8
- export type { Hub, HubDeps, HubGraph, HubTool } from './serve/serve-hub.ts'
9
- export { runServe, resolveHubDeps, defaultHubPort } from './serve/run-serve.ts'
8
+ export type { Hub, HubDeps, HubTool } from './serve/serve-hub.ts'
9
+ export type { GraphMount } from './wiki/graph-mount.ts'
10
+ export { runServe, resolveHubDeps, defaultHubPort, baseWikiRoot } from './serve/run-serve.ts'
10
11
  export { projectNeeds, redCount } from './collections/collection-types.ts'
11
12
  export type { Collection, Instance, NeedItem, NeedSeverity, CurationVerb } from './collections/collection-types.ts'
12
13
  export { buildCollections } from './collections/providers.ts'
@@ -0,0 +1,21 @@
1
+ // one MOUNTED md graph, as the hub serves it and as the graphs section shows
2
+ // it. ONE list per repo (serve/run-serve builds it): the route, the card and
3
+ // the lint verdict all read the same entries, so a card can never link at a
4
+ // route that is not served.
5
+ import type { MdGraph } from 'rvlib-shipkit/src/wiki/md-graph.ts'
6
+
7
+ export type GraphMount = {
8
+ /** route + card id: /graph/<id>, instance graph:<id> */
9
+ id: string
10
+ /** card text and tool label, e.g. `wiki graph`, `<repo> wiki` */
11
+ label: string
12
+ /** graph page title */
13
+ title: string
14
+ load: () => MdGraph
15
+ /** null = someone else owns this graph's lint (a repo-owned provider, or a
16
+ * wiki living in another repo): the card links, it never judges */
17
+ lint: { root: string; index?: string } | null
18
+ /** parenthesized on the CARD only, never in the terminal banner: why this
19
+ * graph carries no verdict here */
20
+ note?: string
21
+ }