rvlib-shipkit 0.10.0 → 0.13.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/README.md +17 -3
  3. package/package.json +5 -2
  4. package/src/asks/asks-queue.ts +42 -0
  5. package/src/assets/kit-config.ts +2 -3
  6. package/src/assets/roll-workspace.ts +2 -1
  7. package/src/assets/run-assets.ts +0 -11
  8. package/src/assets/server/studio-server.ts +7 -24
  9. package/src/cli/cmd_Assets.ts +3 -14
  10. package/src/cli/cmd_Check.ts +22 -4
  11. package/src/cli/cmd_Needs.ts +47 -0
  12. package/src/cli/cmd_Reflect.ts +116 -0
  13. package/src/cli/cmd_Rules.ts +32 -0
  14. package/src/cli/cmd_Serve.ts +1 -1
  15. package/src/cli.ts +7 -2
  16. package/src/collections/actions.ts +85 -0
  17. package/src/collections/collection-types.ts +65 -0
  18. package/src/collections/graphs.ts +34 -0
  19. package/src/collections/pings.ts +72 -0
  20. package/src/collections/posts.ts +125 -0
  21. package/src/collections/providers.ts +275 -0
  22. package/src/contracts/apply-contracts.ts +47 -0
  23. package/src/contracts/run-contracts.ts +8 -3
  24. package/src/core/canonical-scripts.ts +46 -0
  25. package/src/core/check-suite.ts +6 -3
  26. package/src/core/locked-write.ts +39 -0
  27. package/src/core/motherhome.ts +87 -0
  28. package/src/core/run-check.ts +28 -15
  29. package/src/core/run-ci.ts +4 -0
  30. package/src/core/write-status.ts +7 -26
  31. package/src/init-repo.ts +24 -11
  32. package/src/reflect/prompt-corpus.ts +305 -0
  33. package/src/repo-config.ts +26 -38
  34. package/src/rules/SK001-package-json-shape.ts +156 -0
  35. package/src/rules/SK002-contracts-fulfillness.ts +19 -0
  36. package/src/rules/SK003-claude-md-shape.ts +64 -0
  37. package/src/rules/SK004-gitignore-policy.ts +105 -0
  38. package/src/rules/SK005-rvlib-versions.ts +99 -0
  39. package/src/rules/SK006-npm-publish-safety.ts +42 -0
  40. package/src/rules/SK007-github-visibility.ts +63 -0
  41. package/src/rules/rule-registry.ts +32 -0
  42. package/src/rules/rule.ts +62 -0
  43. package/src/rules/run-fix.ts +57 -0
  44. package/src/serve/hub-app/col-card.tsx +192 -0
  45. package/src/serve/hub-app/cols-svc.ts +136 -0
  46. package/src/serve/hub-app/geometry-view.tsx +155 -0
  47. package/src/serve/hub-app/health-svc.ts +49 -0
  48. package/src/serve/hub-app/hub-app.tsx +240 -127
  49. package/src/serve/hub-app/hub-st.ts +169 -0
  50. package/src/serve/hub-app/hub-types.ts +72 -0
  51. package/src/serve/hub-app/hub.css +286 -35
  52. package/src/serve/hub-app/index.html +1 -1
  53. package/src/serve/hub-app/md.ts +24 -0
  54. package/src/serve/hub-app/media-svc.ts +429 -0
  55. package/src/serve/hub-app/media-view.tsx +299 -0
  56. package/src/serve/run-serve.ts +46 -91
  57. package/src/serve/serve-hub.ts +99 -49
  58. package/src/shipkit.ts +13 -6
  59. package/src/assets/server/studio-page.ts +0 -687
  60. package/src/core/package-json-check.ts +0 -88
  61. package/src/serve/hub-pages.ts +0 -45
@@ -1,18 +1,16 @@
1
- // shipkit serve — ONE hub server per repo, ONE port (his call 2026-07-22:
2
- // "a single shipkit task in vscode with multiple urls I can click"). The
3
- // landing page's links are the whole toolbox: /graph/wiki · /graph/agent ·
4
- // /assets/ (the studio, mounted IN-PROCESS) · /status (STATUS.md + run
5
- // check). Route logic only — resolving WHAT to mount from repo.config is
6
- // run-serve.ts's job.
1
+ // shipkit serve — the repo's ONE Bun.serve, serving the ONE page (his
2
+ // rules 2026-07-23: one server per repo, one surface, mobx+react). `/` is
3
+ // the hub app (Bun HTML import — media studio + asks + creds + posts +
4
+ // health + graphs as sections); the studio route table mounts as the media
5
+ // API under /media/*; graph canvases stay as drill-down pages. Route logic
6
+ // only — resolving WHAT to mount from repo.config is run-serve.ts's job.
7
7
  import { existsSync } from 'fs'
8
8
  import { join } from 'path'
9
9
  import { graphHtml } from 'rvlib-shipkit/src/wiki/graph-page.ts'
10
10
  import type { MdGraph } from 'rvlib-shipkit/src/wiki/md-graph.ts'
11
11
  import type { StudioRoutes } from '../assets/server/studio-server.ts'
12
- import { withRail, type AttentionSection } from './hub-pages.ts'
13
- // the hub UI is a Bun-native React app (HTML import, no vite, no build) —
14
- // build/webapp doctrine; legacy pages (graphs, studio SPA) keep the
15
- // server-injected rail until they migrate
12
+ import { appendAskRemark, appendAssetNote, appendPostFeedback, askDone, askProof, markPosted, pickPostVariant } from '../collections/actions.ts'
13
+ import type { Collection, Instance, NeedItem } from '../collections/collection-types.ts'
16
14
  import hubIndex from './hub-app/index.html'
17
15
 
18
16
  export type HubGraph = { title: string; load: () => MdGraph }
@@ -25,74 +23,126 @@ export type HubDeps = {
25
23
  wikiGraph: HubGraph
26
24
  /** null = no agent/ dir */
27
25
  agentGraph: HubGraph | null
28
- /** null = no .rv-assets/kit.ts (or it failed to load — caller prints the WARN) */
26
+ /** the media API (mounted under /media/*); null only if the route
27
+ * table failed to build — the page shows the WARN, never a second page */
29
28
  studio: StudioRoutes | null
30
- /** argv shelling the check run streamed by /status/run (test seam) */
29
+ /** argv shelling the check run streamed by /api/check/run (test seam) */
31
30
  checkCmd: string[]
32
- /** the / dashboard's data — everything red, gathered per request
33
- * (checks · asks · assets · contracts · posts; run-serve builds it) */
34
- attention: () => Promise<AttentionSection[]>
31
+ /** every human-action item, red/yellow, gathered per request from the
32
+ * collection providers (run-serve builds it) */
33
+ needs: () => Promise<NeedItem[]>
34
+ /** BROWSE-ALL: the collections with every instance incl. healthy —
35
+ * the page shows everything that exists (his trust rule) */
36
+ collections: () => Collection[]
37
+ /** asks dir (repo-root-relative) for the act-in-place ask verbs */
38
+ asksDir: string
39
+ /** RED-only ping tick, called on an interval while serving (null = off) */
40
+ pingTick?: ((needs: NeedItem[]) => void) | null
35
41
  }
36
42
  export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; stop: () => void }
37
43
 
38
- // the toolbox as data — the landing page, /hub.json, and the stdout ok:
39
- // lines all render THIS list, so they can never disagree
40
- export function hubTools(d: Pick<HubDeps, 'wikiGraph' | 'agentGraph' | 'studio'>): HubTool[] {
41
- const tools: HubTool[] = [{ path: '/graph/wiki', label: 'wiki graph', desc: d.wikiGraph.title }]
44
+ // the toolbox as data — stdout ok: lines and /hub.json render THIS list,
45
+ // 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
+ tools.push({ path: '/graph/wiki', label: 'wiki graph', desc: d.wikiGraph.title })
42
49
  if (d.agentGraph != null) tools.push({ path: '/graph/agent', label: 'agent graph', desc: d.agentGraph.title })
43
- if (d.studio != null) tools.push({ path: '/assets/', label: 'assets studio', desc: 'roll, curate, pick, ship' })
44
- tools.push({ path: '/status', label: 'status', desc: 'STATUS.md + run check' })
45
50
  return tools
46
51
  }
47
52
 
53
+ // drill-down pages (graph canvases) get one fixed home link back to THE page
54
+ const HOME_LINK = `<style>.skhome{position:fixed;left:10px;top:10px;z-index:9999;font-size:17px;text-decoration:none;
55
+ background:#17140f;border:1px solid #332b22;border-radius:9px;padding:5px 9px}</style><a class=skhome href="/" title="hub — THE page">⌂</a>`
56
+ const withHome = (html: string): string => html.replace(/<body([^>]*)>/, `<body$1>${HOME_LINK}`)
57
+
48
58
  export function serveHub(d: HubDeps): Hub {
49
59
  const tools = hubTools(d)
50
- // every HTML page carries the rail — one keystroke between surfaces;
51
- // the red dot self-fetches client-side (no collector on passive navs)
52
- const shell = (s: string, active: string): Response =>
53
- new Response(withRail(s, { tools, active }), { headers: { 'content-type': 'text/html; charset=utf-8' } })
54
60
  const server: ReturnType<typeof Bun.serve> = Bun.serve({
55
61
  hostname: '127.0.0.1',
56
62
  port: d.port,
57
- // / (attention dashboard) and /status are the React app — one HTML
58
- // bundle, view picked by pathname client-side
59
- routes: { '/': hubIndex, '/status': hubIndex },
63
+ // `/` IS the page — the react app; there is no other surface
64
+ routes: { '/': hubIndex },
60
65
  async fetch(req): Promise<Response> {
61
66
  const path = new URL(req.url).pathname
62
67
  // identity probe: lets a second launch tell "same repo, another
63
68
  // window" from "a DIFFERENT repo hashed onto my port"
64
69
  if (path === '/hub.json') return Response.json({ hub: 'shipkit', name: d.name, tools })
65
- if (path === '/api/attention.json') {
66
- const sections = await d.attention().catch((e: Error) => [{ title: 'attention collector broke', tone: 'red' as const, lines: [{ text: e.message }] }])
67
- return Response.json({ sections })
70
+ if (path === '/api/needs.json') {
71
+ const needs = await d.needs().catch((e: Error) => [{ collection: 'hub', id: 'hub:broken', text: `needs projection broke: ${e.message}`, severity: 'red' as const }])
72
+ return Response.json({ needs })
73
+ }
74
+ if (path === '/api/collections.json') {
75
+ // browse-all: every instance of every collection, healthy
76
+ // included — failure-isolated per collection
77
+ const out: { id: string; title: string; media: string; instances: Instance[] }[] = []
78
+ for (const col of d.collections()) {
79
+ try {
80
+ out.push({ id: col.id, title: col.title, media: col.media, instances: await col.list() })
81
+ } catch (e) {
82
+ out.push({ id: col.id, title: col.title, media: col.media, instances: [{ id: `${col.id}:broken`, text: `collector broke: ${(e as Error).message}`, severity: 'red' }] })
83
+ }
84
+ }
85
+ return Response.json({ collections: out })
86
+ }
87
+ if (path.startsWith('/api/act/') && req.method === 'POST') {
88
+ // act-in-place verbs — every mutation hits a git-tracked truth
89
+ // file; errors surface as 400 text the card shows verbatim
90
+ try {
91
+ const b = (await req.json()) as Record<string, string>
92
+ if (path === '/api/act/ask-done') askDone(d.root, d.asksDir, b.file ?? '')
93
+ else if (path === '/api/act/ask-proof') askProof(d.root, d.asksDir, b.file ?? '', b.url ?? '')
94
+ else if (path === '/api/act/post-pick') pickPostVariant(d.root, b.channel ?? '', b.file ?? '')
95
+ else if (path === '/api/act/post-posted') {
96
+ const rating = b.rating != null ? Number(b.rating) : undefined
97
+ if (rating != null && (!Number.isFinite(rating) || rating < 1 || rating > 5)) return new Response('rating must be 1-5', { status: 400 })
98
+ markPosted(d.root, b.channel ?? '', b.file ?? '', b.url ?? '', rating)
99
+ }
100
+ else if (path === '/api/act/post-feedback') appendPostFeedback(d.root, b.channel ?? '', b.file ?? '', b.text ?? '')
101
+ else if (path === '/api/act/remark') {
102
+ // ONE remark verb, routed by collection — his words
103
+ // land in the item's own truth file
104
+ const text = (b.text ?? '').trim()
105
+ if (text === '') return new Response('empty remark', { status: 400 })
106
+ if (b.kind === 'ask') appendAskRemark(d.root, d.asksDir, b.file ?? '', text)
107
+ else if (b.kind === 'media') appendAssetNote(d.root, b.id ?? '', text)
108
+ else if (b.kind === 'post') appendPostFeedback(d.root, b.channel ?? '', b.file ?? '', text)
109
+ else return new Response('unknown remark kind', { status: 400 })
110
+ }
111
+ else return new Response('unknown act', { status: 404 })
112
+ return Response.json({ ok: true })
113
+ } catch (e) {
114
+ return new Response((e as Error).message, { status: 400 })
115
+ }
68
116
  }
69
117
  if (path === '/api/status.json') {
70
118
  const p = join(d.root, 'STATUS.md')
71
119
  return Response.json({ statusMd: existsSync(p) ? await Bun.file(p).text() : null })
72
120
  }
73
- if (path === '/graph/wiki') return shell(graphHtml(d.wikiGraph.load(), { title: d.wikiGraph.title }), path)
74
- if (path === '/graph/wiki/graph.json') return Response.json(d.wikiGraph.load())
75
- if (d.agentGraph != null && path === '/graph/agent') return shell(graphHtml(d.agentGraph.load(), { title: d.agentGraph.title }), path)
76
- if (d.agentGraph != null && path === '/graph/agent/graph.json') return Response.json(d.agentGraph.load())
77
- if (path === '/status/run' && req.method === 'POST') {
121
+ if (path === '/api/check/run' && req.method === 'POST') {
78
122
  // stream wire lines as the check emits them; sh merges stderr
79
123
  // so FAIL lines reach the page, not just the terminal
80
124
  const proc = Bun.spawn(['sh', '-c', 'exec "$0" "$@" 2>&1', ...d.checkCmd], { cwd: d.root, stdout: 'pipe', stderr: 'ignore' })
81
125
  return new Response(proc.stdout, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
82
126
  }
83
- if (d.studio != null) {
84
- // the studio page fetches RELATIVE urls — it must live under
85
- // the slash-terminated prefix for them to resolve
86
- if (path === '/assets') return new Response(null, { status: 307, headers: { location: '/assets/' } })
87
- if (path === '/assets/' && req.method === 'GET') {
88
- // the SPA document gets the rail too; API routes untouched
89
- const res = await d.studio.handle(req, '/')
90
- return shell(await res.text(), '/assets/')
91
- }
92
- if (path.startsWith('/assets/')) return d.studio.handle(req, path.slice('/assets'.length))
93
- }
94
- return new Response('not found', { status: 404 })
127
+ const page = (s: string): Response => new Response(withHome(s), { headers: { 'content-type': 'text/html; charset=utf-8' } })
128
+ if (path === '/graph/wiki') return page(graphHtml(d.wikiGraph.load(), { title: d.wikiGraph.title }))
129
+ if (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())
132
+ // the media API namespace — the studio route table, ONE prefix strip
133
+ if (d.studio != null && path.startsWith('/media/')) return d.studio.handle(req, path.slice('/media'.length))
134
+ // stale bookmarks (/assets/, /status, …) land here — name the way home
135
+ return new Response('not found — the hub page is at /', { status: 404 })
95
136
  },
96
137
  })
97
- return { server, tools, stop: () => server.stop(true) }
138
+ // RED-only pings ride the serving process: one tick now (state file
139
+ // guarantees a restart never re-fires), then every minute
140
+ let pingTimer: ReturnType<typeof setInterval> | null = null
141
+ if (d.pingTick != null) {
142
+ // tick failures are LOUD (console = the serve terminal), never swallowed
143
+ const tick = (): void => void d.needs().then((n) => d.pingTick?.(n)).catch((e: Error) => console.error(`WARN ping tick broke: ${e.message}`))
144
+ tick()
145
+ pingTimer = setInterval(tick, 60_000)
146
+ }
147
+ return { server, tools, stop: () => { if (pingTimer != null) clearInterval(pingTimer); server.stop(true) } }
98
148
  }
package/src/shipkit.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  // rvlib-shipkit — barrel. One import per repo: defineRepo in repo.config.ts;
2
2
  // everything else is the machinery behind the shipkit CLI.
3
- export { defineRepo, loadRepoConfig, resolveFloor, allTasks, contractsTask, contractsConfigFrom, activeContracts } from './repo-config.ts'
3
+ export { defineRepo, loadRepoConfig, resolveFloor, allTasks, contractsTask } from './repo-config.ts'
4
4
  export type { RepoConfig, ServeConfig, FloorResolution, FloorSource } from './repo-config.ts'
5
+ export { contractsConfigFrom, activeContracts, applyContractsResult } from './contracts/apply-contracts.ts'
5
6
  // the hub (`shipkit serve`): one server, one port, every tool a click away
6
7
  export { serveHub, hubTools } from './serve/serve-hub.ts'
7
8
  export type { Hub, HubDeps, HubGraph, HubTool } from './serve/serve-hub.ts'
8
9
  export { runServe, resolveHubDeps, defaultHubPort } from './serve/run-serve.ts'
9
- export { withRail, attentionRedCount } from './serve/hub-pages.ts'
10
- export type { AttentionSection, AttentionLine } from './serve/hub-pages.ts'
10
+ export { projectNeeds, redCount } from './collections/collection-types.ts'
11
+ export type { Collection, Instance, NeedItem, NeedSeverity, CurationVerb } from './collections/collection-types.ts'
12
+ export { buildCollections } from './collections/providers.ts'
13
+ export { socialPostsCollection, listPosts } from './collections/posts.ts'
11
14
  export { discoverChecks } from './core/discover-checks.ts'
12
15
  export type { DiscoveredCheck } from './core/discover-checks.ts'
13
16
  export { runTasks, selectTasks } from './core/task-runner.ts'
@@ -24,6 +27,11 @@ export { writeStatus } from './core/write-status.ts'
24
27
  export type { StatusInput, StatusOutcome } from './core/write-status.ts'
25
28
  export { formatLine, parseDuration, fmtAge, parseWireOutput } from './core/wire.ts'
26
29
  export type { Line } from './core/wire.ts'
30
+ // the SK rule registry — stable-id invariants `shipkit check` enforces;
31
+ // consumers (e.g. a wiki lint verifying mentioned ids exist) read RULES
32
+ export { RULES, ruleById } from './rules/rule-registry.ts'
33
+ export { tagRuleLines } from './rules/rule.ts'
34
+ export type { Rule, RuleEnv, FixResult } from './rules/rule.ts'
27
35
  export { evaluateContracts, pngDims } from './contracts/contracts-engine.ts'
28
36
  export type { ContractsConfig, ContractsResult, ContractOutcome } from './contracts/contracts-engine.ts'
29
37
  export { defineContract, validateManifest, file, png, glob, md, dir, custom, script, fileContains, dirExists } from './contracts/contract-types.ts'
@@ -38,9 +46,8 @@ export { defineKit, defineKind, comfy, canvas, manual, gemini, mediaOf, MEDIA_EX
38
46
  export type { KitConfig, ResolvedKit, Kind, KindMeta, Media, AssetSpec, GeneratorSpec, ComfyGen, CanvasGen, ManualGen, GeminiGen, PanelSpec, GeometryPanelSpec, GridPanelSpec, CustomPanelSpec, FoundAsset, ShipContext, ShipFn } from './assets/kit-config.ts'
39
47
  export { identityKinds, renderIdentity, renderIdentityFromMark, NO_PICK_FIX, FAVICON_SIZE, OG_W, OG_H, LOGO_SIZE } from './assets/kits/identity-kit.ts'
40
48
  export type { Brand } from './assets/kits/identity-kit.ts'
41
- export { serveStudio, studioRoutes } from './assets/server/studio-server.ts'
42
- export type { Studio, StudioDeps, StudioRoutes } from './assets/server/studio-server.ts'
43
- export { page as studioPage } from './assets/server/studio-page.ts'
49
+ export { studioRoutes } from './assets/server/studio-server.ts'
50
+ export type { StudioDeps, StudioRoutes } from './assets/server/studio-server.ts'
44
51
  export { StudioQueue } from './assets/server/studio-queue.ts'
45
52
  export type { Job, QueueState } from './assets/server/studio-queue.ts'
46
53
  export { cleanRect, liveRect, defaultLegs, migrateCircle } from './assets/panels/geometry-rects.ts'