opencode-extended-sidebar 0.2.90
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/.oesignore +17 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/oes.json +11 -0
- package/package.json +100 -0
- package/scripts/install-git-hooks.mjs +29 -0
- package/src/pware.oc.core/constants/index.ts +18 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
- package/src/pware.oc.core/git/index.ts +7 -0
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
- package/src/pware.oc.core/index.ts +18 -0
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
- package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
- package/src/pware.oc.omo/constants/index.ts +15 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
- package/src/pware.oc.omo/index.ts +7 -0
- package/src/pware.oc.omo/resolver/index.ts +19 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
- package/src/pware.oc.opencode/constants/index.ts +10 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
- package/src/pware.oc.opencode/index.ts +8 -0
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
- package/src/pware.oc.opencode/resolver/index.ts +170 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
- package/src/pware.oc.perf/index.ts +14 -0
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
- package/src/pware.oc.runtime/index.ts +13 -0
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
- package/src/pware.oc.runtime/resolver/index.ts +224 -0
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
- package/src/pware.oc.ui/index.ts +12 -0
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
- package/src/pware.oc.ui.tsx +98 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.omo.resolver.doc
|
|
3
|
+
*
|
|
4
|
+
* OMO document index: the plan, drafts, notepads and evidence under `.omo/`.
|
|
5
|
+
* Names, ages and sizes only — a document is read on click, by preview.ts.
|
|
6
|
+
* Scanned lazily behind a short TTL, never on the poll path. Hosts the single
|
|
7
|
+
* per-root omo scan cache shared with the approval classifier (`approval.ts`):
|
|
8
|
+
* each root keeps one record whose per-kind stat rows are filled on first
|
|
9
|
+
* request, so `.omo/` is walked once per TTL no matter how many consumers ask.
|
|
10
|
+
* One listing per kind (`listOmoFiles`); the per-kind `*File` resolvers wrap it
|
|
11
|
+
* with a `list` method so each document kind can be listed and
|
|
12
|
+
* session-filtered on its own.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs"
|
|
15
|
+
import path from "node:path"
|
|
16
|
+
import { createStampCache } from "../../pware.oc.core/pware.oc.core.cache.js"
|
|
17
|
+
import { profile } from "../../pware.oc.core/pware.oc.core.debug.js"
|
|
18
|
+
import { canonicalizePath } from "../../pware.oc.core/pware.oc.core.paths.js"
|
|
19
|
+
import { canPreviewPath } from "../../pware.oc.core/pware.oc.core.preview.js"
|
|
20
|
+
import type { SqlDb } from "../../pware.oc.core/pware.oc.core.sqlite.js"
|
|
21
|
+
import {
|
|
22
|
+
DOC_KIND_DRAFT,
|
|
23
|
+
DOC_KIND_NOTEPAD,
|
|
24
|
+
DOC_KIND_PLAN,
|
|
25
|
+
DOC_KIND_PROOF,
|
|
26
|
+
type DocKind,
|
|
27
|
+
} from "../constants/pware.oc.omo.constants.docKind.js"
|
|
28
|
+
import { PLAN_STATUS_DONE, WORK_STATE_COMPLETED, type WorkState } from "../constants/pware.oc.omo.constants.planStatus.js"
|
|
29
|
+
import { findOmoWatchDirs, planWorkStateByPlanName } from "./pware.oc.omo.resolver.boulder.js"
|
|
30
|
+
import { approvalName, parsePlanStatus } from "./pware.oc.omo.resolver.plan.js"
|
|
31
|
+
import { omoFileIndex, omoWriterSession } from "./pware.oc.omo.resolver.planFile.js"
|
|
32
|
+
|
|
33
|
+
export type { DocKind }
|
|
34
|
+
|
|
35
|
+
export type DocView = {
|
|
36
|
+
kind: DocKind
|
|
37
|
+
/** Row label: basename, or `change/file` under evidence. */
|
|
38
|
+
name: string
|
|
39
|
+
/** Project-relative path. The panel must never show a root. */
|
|
40
|
+
rel: string
|
|
41
|
+
updatedAt: number | null
|
|
42
|
+
sizeBytes: number
|
|
43
|
+
previewable: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The `.omo/` (and `.sisyphus/`) subdirectory + walk depth for each kind. */
|
|
47
|
+
const DOC_KIND_DIRS: Record<DocKind, { dir: string; depth: number }> = {
|
|
48
|
+
[DOC_KIND_PLAN]: { dir: "plans", depth: 1 },
|
|
49
|
+
[DOC_KIND_DRAFT]: { dir: "drafts", depth: 1 },
|
|
50
|
+
[DOC_KIND_NOTEPAD]: { dir: "notepads", depth: 1 },
|
|
51
|
+
// Evidence is one folder per change, files inside.
|
|
52
|
+
[DOC_KIND_PROOF]: { dir: "evidence", depth: 2 },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const DOC_KIND_LABEL: Record<DocKind, string> = {
|
|
56
|
+
[DOC_KIND_PLAN]: "plan",
|
|
57
|
+
[DOC_KIND_DRAFT]: "drafts",
|
|
58
|
+
[DOC_KIND_NOTEPAD]: "notepads",
|
|
59
|
+
[DOC_KIND_PROOF]: "proof",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const MAX_PER_KIND = 20
|
|
63
|
+
const MAX_WALK = 400
|
|
64
|
+
const TTL_MS = 2_000
|
|
65
|
+
|
|
66
|
+
/** Options for `listOmoFiles`: session/status filters, ordering and a row cap. */
|
|
67
|
+
export type ListOmoFilesOptions = {
|
|
68
|
+
/** Only files last written by this session. Needs `db` to resolve writers. */
|
|
69
|
+
sessionId?: string | null
|
|
70
|
+
/** Readonly DB handle for the session filter. Ignored when `sessionId` is null. */
|
|
71
|
+
db?: SqlDb | null
|
|
72
|
+
/** Plan/draft frontmatter status, reconciled against boulder. Null = all. */
|
|
73
|
+
status?: string | null
|
|
74
|
+
/** Most files of this kind to return. Defaults to 20. */
|
|
75
|
+
limit?: number
|
|
76
|
+
/** Ordering. Defaults to newest-first (`updatedAt` desc, then name). */
|
|
77
|
+
sort?: "newest" | "name"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function statOf(abs: string): { updatedAt: number | null; sizeBytes: number } | null {
|
|
81
|
+
try {
|
|
82
|
+
const st = fs.statSync(abs)
|
|
83
|
+
if (!st.isFile()) return null
|
|
84
|
+
return { updatedAt: Math.round(st.mtimeMs), sizeBytes: st.size }
|
|
85
|
+
} catch {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Files under `base`, at most `depth` levels deep. Symlinks are not followed. */
|
|
91
|
+
function walk(base: string, depth: number, budget: { left: number }): string[] {
|
|
92
|
+
if (depth <= 0 || budget.left <= 0) return []
|
|
93
|
+
let entries: fs.Dirent[]
|
|
94
|
+
try {
|
|
95
|
+
entries = fs.readdirSync(base, { withFileTypes: true })
|
|
96
|
+
} catch {
|
|
97
|
+
return []
|
|
98
|
+
}
|
|
99
|
+
const out: string[] = []
|
|
100
|
+
for (const e of entries) {
|
|
101
|
+
if (budget.left <= 0) break
|
|
102
|
+
if (e.name.startsWith(".")) continue
|
|
103
|
+
const abs = path.join(base, e.name)
|
|
104
|
+
if (e.isDirectory()) {
|
|
105
|
+
out.push(...walk(abs, depth - 1, budget))
|
|
106
|
+
} else if (e.isFile()) {
|
|
107
|
+
budget.left -= 1
|
|
108
|
+
out.push(abs)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function newestFirst(a: DocView, b: DocView): number {
|
|
115
|
+
const d = (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
|
|
116
|
+
return d !== 0 ? d : a.name.localeCompare(b.name)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Every file of one kind under the project's `.omo/` and `.sisyphus/` dirs. */
|
|
120
|
+
function scanKind(kind: DocKind, projectRoot: string): DocView[] {
|
|
121
|
+
const root = canonicalizePath(projectRoot)
|
|
122
|
+
const src = DOC_KIND_DIRS[kind]
|
|
123
|
+
const seen = new Set<string>()
|
|
124
|
+
const out: DocView[] = []
|
|
125
|
+
for (const omoDir of findOmoWatchDirs(root)) {
|
|
126
|
+
const base = path.join(omoDir, src.dir)
|
|
127
|
+
for (const abs of walk(base, src.depth, { left: MAX_WALK })) {
|
|
128
|
+
const name = path.relative(base, abs).replace(/\\/g, "/")
|
|
129
|
+
const rel = path.relative(root, abs).replace(/\\/g, "/")
|
|
130
|
+
if (!rel || rel.startsWith("..") || seen.has(rel)) continue
|
|
131
|
+
const st = statOf(abs)
|
|
132
|
+
if (!st) continue
|
|
133
|
+
seen.add(rel)
|
|
134
|
+
out.push({
|
|
135
|
+
kind,
|
|
136
|
+
name,
|
|
137
|
+
rel,
|
|
138
|
+
updatedAt: st.updatedAt,
|
|
139
|
+
sizeBytes: st.sizeBytes,
|
|
140
|
+
previewable: canPreviewPath(abs),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
out.sort(newestFirst)
|
|
145
|
+
return out
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One cached per-root omo scan: stat-only rows per doc kind, filled on request. */
|
|
149
|
+
export type OmoDocScan = {
|
|
150
|
+
rows: Partial<Record<DocKind, DocView[]>>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const omoScanCache = createStampCache<OmoDocScan>({ ttlMs: TTL_MS })
|
|
154
|
+
|
|
155
|
+
/** The shared per-root omo scan record, or null without a project root. */
|
|
156
|
+
export function omoScanRecord(projectRoot: string | null | undefined): OmoDocScan | null {
|
|
157
|
+
if (!projectRoot) return null
|
|
158
|
+
const root = canonicalizePath(projectRoot)
|
|
159
|
+
return omoScanCache.get(root, () => ({ rows: {} }))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stat-only rows of one doc kind from the shared per-root omo scan — the single
|
|
164
|
+
* stamp cache behind both `listOmoFiles` and the approval classifier
|
|
165
|
+
* (`approval.ts`). Each kind is scanned lazily on first request and cached with
|
|
166
|
+
* the root record until the TTL expires. Draft rows are the one place where a
|
|
167
|
+
* draft superseded by its plan is dropped: a draft whose slug already exists as
|
|
168
|
+
* a plan file is hidden everywhere (Session-tab draft list, My-work action
|
|
169
|
+
* groups, the Draft docs archive), because the plan is the live artifact.
|
|
170
|
+
*/
|
|
171
|
+
export function omoKindRows(kind: DocKind, projectRoot: string | null | undefined): DocView[] {
|
|
172
|
+
if (!projectRoot) return []
|
|
173
|
+
const scan = omoScanRecord(projectRoot)
|
|
174
|
+
if (!scan) return []
|
|
175
|
+
const cached = scan.rows[kind]
|
|
176
|
+
if (cached) return cached
|
|
177
|
+
const rows = profile("omo.docs", () => {
|
|
178
|
+
try {
|
|
179
|
+
return scanKind(kind, projectRoot)
|
|
180
|
+
} catch {
|
|
181
|
+
return []
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
scan.rows[kind] = kind === DOC_KIND_DRAFT ? dropSupersededDrafts(rows, projectRoot) : rows
|
|
185
|
+
return scan.rows[kind]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The slugs that already have a plan file. A draft sharing one of these is
|
|
190
|
+
* superseded — the plan is the live artifact, so the draft must not surface in
|
|
191
|
+
* any list (Session drafts, My-work action groups or the Draft docs archive).
|
|
192
|
+
*/
|
|
193
|
+
function plannedSlugs(projectRoot: string): ReadonlySet<string> {
|
|
194
|
+
const slugs = new Set<string>()
|
|
195
|
+
for (const row of omoKindRows(DOC_KIND_PLAN, projectRoot)) {
|
|
196
|
+
if (!row.rel.toLowerCase().endsWith(".md")) continue
|
|
197
|
+
const slug = approvalName(row.rel)
|
|
198
|
+
if (slug) slugs.add(slug)
|
|
199
|
+
}
|
|
200
|
+
return slugs
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Drafts whose basename already exists as a plan (same slug) are superseded. */
|
|
204
|
+
function dropSupersededDrafts(rows: readonly DocView[], projectRoot: string): DocView[] {
|
|
205
|
+
const planned = plannedSlugs(projectRoot)
|
|
206
|
+
if (planned.size === 0) return rows as DocView[]
|
|
207
|
+
return rows.filter((d) => !planned.has(approvalName(d.rel)))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Drop the shared omo scan cache so the next read hits the filesystem. */
|
|
211
|
+
export function resetDocsCache(): void {
|
|
212
|
+
omoScanCache.reset()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The effective plan/draft status: the frontmatter `status:`, overridden to
|
|
217
|
+
* `done` when boulder shows the plan's work completed — the LLM may not have
|
|
218
|
+
* updated the file. Falls back to the frontmatter alone when boulder is
|
|
219
|
+
* absent. Null when the file has no status or cannot be read.
|
|
220
|
+
*/
|
|
221
|
+
function planStatusOf(
|
|
222
|
+
projectRoot: string,
|
|
223
|
+
rel: string,
|
|
224
|
+
workStates: Map<string, WorkState>,
|
|
225
|
+
): string | null {
|
|
226
|
+
let text = ""
|
|
227
|
+
try {
|
|
228
|
+
text = fs.readFileSync(path.join(projectRoot, rel), "utf8")
|
|
229
|
+
} catch {
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
const status = parsePlanStatus(text)
|
|
233
|
+
if (workStates.get(approvalName(rel)) === WORK_STATE_COMPLETED) return PLAN_STATUS_DONE
|
|
234
|
+
if (!status) return null
|
|
235
|
+
return status
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Files of one document kind, newest-first. `sessionId` filters to the files
|
|
240
|
+
* that session last wrote (resolved against the OpenCode DB via `db`); `status`
|
|
241
|
+
* filters plan/draft files to their effective status; `limit` and `sort` cap
|
|
242
|
+
* and reorder the result. Soft-fails to `[]` on a missing root or a
|
|
243
|
+
* filesystem error.
|
|
244
|
+
*/
|
|
245
|
+
export function listOmoFiles(
|
|
246
|
+
kind: DocKind,
|
|
247
|
+
projectRoot: string | null | undefined,
|
|
248
|
+
opts: ListOmoFilesOptions = {},
|
|
249
|
+
): DocView[] {
|
|
250
|
+
if (!projectRoot) return []
|
|
251
|
+
const root = canonicalizePath(projectRoot)
|
|
252
|
+
const all = omoKindRows(kind, root)
|
|
253
|
+
let docs = all
|
|
254
|
+
const sessionId = opts.sessionId
|
|
255
|
+
const db = opts.db
|
|
256
|
+
if (sessionId && db && docs.length > 0) {
|
|
257
|
+
// One index build per listing — the writer lookup per file is a map read.
|
|
258
|
+
const writers = omoFileIndex(db, null, null, kind)
|
|
259
|
+
docs = docs.filter((d) => omoWriterSession(writers, d.rel) === sessionId)
|
|
260
|
+
}
|
|
261
|
+
if (opts.status && (kind === DOC_KIND_PLAN || kind === DOC_KIND_DRAFT)) {
|
|
262
|
+
const want = opts.status.toLowerCase()
|
|
263
|
+
const workStates = planWorkStateByPlanName(root)
|
|
264
|
+
docs = docs.filter((d) => (planStatusOf(root, d.rel, workStates) ?? "").toLowerCase() === want)
|
|
265
|
+
}
|
|
266
|
+
if (opts.sort === "name") docs = [...docs].sort((a, b) => a.name.localeCompare(b.name))
|
|
267
|
+
const limit = opts.limit ?? MAX_PER_KIND
|
|
268
|
+
return docs.slice(0, limit)
|
|
269
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.omo.resolver.draftFile
|
|
3
|
+
*
|
|
4
|
+
* Draft-file → writer-session index. Thin re-export of the shared omo file
|
|
5
|
+
* engine (`planFile.ts`), pinned to the `.omo/drafts/` document kind.
|
|
6
|
+
*/
|
|
7
|
+
import { DOC_KIND_DRAFT } from "../constants/pware.oc.omo.constants.docKind.js"
|
|
8
|
+
import {
|
|
9
|
+
makeOmoFileList,
|
|
10
|
+
makeOmoFileResolver,
|
|
11
|
+
} from "./pware.oc.omo.resolver.planFile.js"
|
|
12
|
+
|
|
13
|
+
/** Bidirectional draft-file index for one project (see `omoFileIndex`). */
|
|
14
|
+
export const { sessionIndex: draftSessionIndex, sessionForFile: sessionForDraftFile } =
|
|
15
|
+
makeOmoFileResolver("draft")
|
|
16
|
+
|
|
17
|
+
/** Draft files under `.omo/drafts/`, optionally filtered by writer session. */
|
|
18
|
+
export const DraftFile = makeOmoFileList(DOC_KIND_DRAFT)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.omo.resolver.notepadsFile
|
|
3
|
+
*
|
|
4
|
+
* Notepad-file → writer-session index. Thin re-export of the shared omo file
|
|
5
|
+
* engine (`planFile.ts`), pinned to the `.omo/notepads/` document kind.
|
|
6
|
+
*/
|
|
7
|
+
import { DOC_KIND_NOTEPAD } from "../constants/pware.oc.omo.constants.docKind.js"
|
|
8
|
+
import {
|
|
9
|
+
makeOmoFileList,
|
|
10
|
+
makeOmoFileResolver,
|
|
11
|
+
} from "./pware.oc.omo.resolver.planFile.js"
|
|
12
|
+
|
|
13
|
+
/** Bidirectional notepad-file index for one project (see `omoFileIndex`). */
|
|
14
|
+
export const { sessionIndex: notepadsSessionIndex, sessionForFile: sessionForNotepadFile } =
|
|
15
|
+
makeOmoFileResolver("notepad")
|
|
16
|
+
|
|
17
|
+
/** Notepad files under `.omo/notepads/`, optionally filtered by writer session. */
|
|
18
|
+
export const NotepadFile = makeOmoFileList(DOC_KIND_NOTEPAD)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.omo.resolver.plan
|
|
3
|
+
*
|
|
4
|
+
* Plan.md parsing: status / slug / pending-action from YAML frontmatter or the
|
|
5
|
+
* `## State` section, plus the approval row label. Pure text — no fs, no scan.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path"
|
|
8
|
+
import type { WorkState } from "../constants/pware.oc.omo.constants.planStatus.js"
|
|
9
|
+
|
|
10
|
+
export type ApprovalItem = {
|
|
11
|
+
/** Project-relative path. The panel must never show a root. */
|
|
12
|
+
rel: string
|
|
13
|
+
name: string
|
|
14
|
+
/** Frontmatter `status:`; null when the draft has none (a bare working note). */
|
|
15
|
+
status: string | null
|
|
16
|
+
/** What happens after approval — e.g. `write .omo/plans/<slug>.md`. */
|
|
17
|
+
pendingAction: string | null
|
|
18
|
+
updatedAt: number | null
|
|
19
|
+
/** ulw-plan review lifecycle from the draft frontmatter; null = no review requested. */
|
|
20
|
+
review: ReviewState | null
|
|
21
|
+
/** Boulder work completion state, for the My-work reconciliation. */
|
|
22
|
+
workState: WorkState
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One ulw-plan review lane (momus / independent). */
|
|
26
|
+
export type ReviewLane = {
|
|
27
|
+
/** Lane lifecycle: pending | launching | in_flight | approved | changes_requested | inconclusive. */
|
|
28
|
+
status: string | null
|
|
29
|
+
/** Lane verdict, when the lane reported one (pass / fail / approved / blocked / …). */
|
|
30
|
+
result: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The ulw-plan review state persisted in the draft/plan frontmatter: one live
|
|
35
|
+
* round keyed by `review_round_id` + `plan_sha256` (any plan change bumps the
|
|
36
|
+
* hash and starts a fresh round), with a `momus` and an `independent` lane.
|
|
37
|
+
*/
|
|
38
|
+
export type ReviewState = {
|
|
39
|
+
required: boolean
|
|
40
|
+
/** `review_round_id` — the live round; a changed `plan_sha256` forces a new one. */
|
|
41
|
+
roundId: string | null
|
|
42
|
+
/** `round_status` — `active` while a round is live. */
|
|
43
|
+
roundStatus: string | null
|
|
44
|
+
/** `plan_sha256` — the reviewed plan's hash, null until a round is initialized. */
|
|
45
|
+
planSha256: string | null
|
|
46
|
+
lanes: { momus: ReviewLane; independent: ReviewLane }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Row label for an approval item — basename without the `.md` extension. */
|
|
50
|
+
export function approvalName(rel: string): string {
|
|
51
|
+
return path.basename(rel).replace(/\.md$/i, "")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function frontmatter(text: string): string | null {
|
|
55
|
+
const m = text.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/)
|
|
56
|
+
return m ? (m[1] ?? "") : null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stateSection(text: string): string | null {
|
|
60
|
+
const m = text.match(/##\s+State\s*\r?\n([\s\S]*?)(?=\r?\n##\s|$)/)
|
|
61
|
+
return m ? (m[1] ?? "") : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parsePlanStatus(text: string): string | null {
|
|
65
|
+
const fm = frontmatter(text)
|
|
66
|
+
if (fm) {
|
|
67
|
+
const m = fm.match(/^status:\s*["']?([^"'\r\n]+)["']?/m)
|
|
68
|
+
if (m?.[1]) return m[1].trim()
|
|
69
|
+
}
|
|
70
|
+
const state = stateSection(text)
|
|
71
|
+
if (state) {
|
|
72
|
+
const m = state.match(/status:\s*[`'"]?\s*([^\s`'"\r\n]+)/m)
|
|
73
|
+
if (m?.[1]) return m[1].trim()
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parsePlanSlug(text: string): string | null {
|
|
79
|
+
const fm = frontmatter(text)
|
|
80
|
+
if (fm) {
|
|
81
|
+
const m = fm.match(/^slug:\s*["']?([^"'\r\n]+)["']?/m)
|
|
82
|
+
if (m?.[1]) return m[1].trim()
|
|
83
|
+
}
|
|
84
|
+
const state = stateSection(text)
|
|
85
|
+
if (state) {
|
|
86
|
+
const m = state.match(/slug:\s*[`'"]?\s*([^\s`'"\r\n]+)/m)
|
|
87
|
+
if (m?.[1]) return m[1].trim()
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parsePlanPendingAction(text: string): string | null {
|
|
93
|
+
const fm = frontmatter(text)
|
|
94
|
+
if (fm) {
|
|
95
|
+
const m = fm.match(/^pending-action:\s*["']?([^"'\r\n]+)["']?/m)
|
|
96
|
+
if (m?.[1]) return m[1].trim()
|
|
97
|
+
}
|
|
98
|
+
const slug = parsePlanSlug(text)
|
|
99
|
+
return slug ? `.omo/plans/${slug}.md` : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scalar(fm: string, key: string): string | null {
|
|
103
|
+
const v = fm.match(new RegExp(`^${key}:\\s*(\\S+)`, "m"))?.[1]
|
|
104
|
+
if (!v || v === "null") return null
|
|
105
|
+
return v.trim()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stripQuotes(v: string): string {
|
|
109
|
+
return v.replace(/^["']|["']$/g, "")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* One review lane (`momus:` / `independent:`) from the frontmatter lines:
|
|
114
|
+
* the lane's own `status` and `result`, stopping at the next top-level key.
|
|
115
|
+
* Line-based so the last frontmatter line needs no trailing newline.
|
|
116
|
+
*/
|
|
117
|
+
function laneState(fm: string, lane: "momus" | "independent"): ReviewLane | null {
|
|
118
|
+
const lines = fm.split(/\r?\n/)
|
|
119
|
+
const idx = lines.findIndex((l) => l.trim() === `${lane}:`)
|
|
120
|
+
if (idx < 0) return null
|
|
121
|
+
const laneIndent = lines[idx]!.match(/^\s*/)?.[0].length ?? 0
|
|
122
|
+
const out: ReviewLane = { status: null, result: null }
|
|
123
|
+
for (const line of lines.slice(idx + 1)) {
|
|
124
|
+
if ((line.match(/^\s*/)?.[0].length ?? 0) <= laneIndent) break
|
|
125
|
+
const m = line.match(/^\s*(\w[\w-]*):\s*(\S+)?/)
|
|
126
|
+
if (!m) continue
|
|
127
|
+
const val = m[2] && m[2] !== "null" ? stripQuotes(m[2]) : null
|
|
128
|
+
if (m[1] === "status") out.status = val
|
|
129
|
+
else if (m[1] === "result") out.result = val
|
|
130
|
+
}
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* ulw-plan review lifecycle from the frontmatter `review:` block and its
|
|
136
|
+
* siblings (`review_required`, `review_round_id`, `round_status`,
|
|
137
|
+
* `plan_sha256`). Null when the draft carries no review fields at all.
|
|
138
|
+
*/
|
|
139
|
+
export function parseReviewBlock(text: string): ReviewState | null {
|
|
140
|
+
const fm = frontmatter(text)
|
|
141
|
+
if (!fm) return null
|
|
142
|
+
const required = /^review_required:\s*true/m.test(fm)
|
|
143
|
+
const hasBlock = /^review:\s*$/m.test(fm)
|
|
144
|
+
if (!required && !hasBlock) return null
|
|
145
|
+
return {
|
|
146
|
+
required,
|
|
147
|
+
roundId: scalar(fm, "review_round_id"),
|
|
148
|
+
roundStatus: scalar(fm, "round_status"),
|
|
149
|
+
planSha256: scalar(fm, "plan_sha256"),
|
|
150
|
+
lanes: {
|
|
151
|
+
momus: laneState(fm, "momus") ?? { status: null, result: null },
|
|
152
|
+
independent: laneState(fm, "independent") ?? { status: null, result: null },
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|