@cyber-dash-tech/revela 0.17.18 → 0.17.21
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 +25 -3
- package/README.zh-CN.md +25 -3
- package/designs/monet/DESIGN.md +58 -38
- package/lib/commands/designs-new.ts +3 -0
- package/lib/design/designs.ts +139 -2
- package/lib/domain/domains.ts +221 -1
- package/lib/qa/checks.ts +3 -6
- package/lib/runtime/index.ts +112 -1
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/hooks/revela_guard.ts +19 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +37 -6
- package/plugins/revela/mcp/revela-server.ts +86 -0
- package/plugins/revela/skills/revela-design/SKILL.md +5 -3
- package/plugins/revela/skills/revela-domain/SKILL.md +13 -1
- package/plugins/revela/skills/revela-upgrade/SKILL.md +33 -0
package/lib/domain/domains.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
statSync,
|
|
23
23
|
writeFileSync,
|
|
24
24
|
} from "fs"
|
|
25
|
-
import { join, resolve, basename } from "path"
|
|
25
|
+
import { dirname, join, resolve, basename } from "path"
|
|
26
26
|
import { tmpdir } from "os"
|
|
27
27
|
import { parseFrontmatter } from "../frontmatter"
|
|
28
28
|
import {
|
|
@@ -49,6 +49,44 @@ export interface DomainInfo {
|
|
|
49
49
|
skillText: string
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface CreateDomainPackageArgs {
|
|
53
|
+
name: string
|
|
54
|
+
domainMd: string
|
|
55
|
+
overwrite?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CreateDomainDraftArgs extends CreateDomainPackageArgs {
|
|
59
|
+
workspaceRoot: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CreateDomainPackageResult {
|
|
63
|
+
ok: true
|
|
64
|
+
name: string
|
|
65
|
+
path: string
|
|
66
|
+
files: string[]
|
|
67
|
+
overwritten: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface InstallDomainDraftArgs {
|
|
71
|
+
workspaceRoot: string
|
|
72
|
+
name: string
|
|
73
|
+
overwrite?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface InstallDomainDraftResult extends CreateDomainPackageResult {
|
|
77
|
+
sourcePath: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ValidateDomainPackageResult {
|
|
81
|
+
ok: boolean
|
|
82
|
+
name: string
|
|
83
|
+
path: string
|
|
84
|
+
hasIndustryMd: boolean
|
|
85
|
+
hasRequiredFrontmatter: boolean
|
|
86
|
+
hasBody: boolean
|
|
87
|
+
errors: string[]
|
|
88
|
+
}
|
|
89
|
+
|
|
52
90
|
// ---------------------------------------------------------------------------
|
|
53
91
|
// Seed
|
|
54
92
|
// ---------------------------------------------------------------------------
|
|
@@ -148,6 +186,188 @@ export function getDomainSkillMd(name?: string): string {
|
|
|
148
186
|
return info.skillText
|
|
149
187
|
}
|
|
150
188
|
|
|
189
|
+
/** Normalize and validate a domain package name. */
|
|
190
|
+
export function normalizeDomainName(name: string): string {
|
|
191
|
+
const normalized = name.trim().toLowerCase()
|
|
192
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(normalized)) {
|
|
193
|
+
throw new Error("Domain name must be kebab-case using lowercase letters, numbers, and hyphens")
|
|
194
|
+
}
|
|
195
|
+
return normalized
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Create a local domain package in ~/.config/revela/domains/<name>/. */
|
|
199
|
+
export function createDomainPackage(args: CreateDomainPackageArgs): CreateDomainPackageResult {
|
|
200
|
+
const name = normalizeDomainName(args.name)
|
|
201
|
+
const domainMd = args.domainMd?.trim()
|
|
202
|
+
|
|
203
|
+
if (!domainMd) throw new Error("domainMd is required")
|
|
204
|
+
|
|
205
|
+
const target = join(DOMAINS_DIR, name)
|
|
206
|
+
const existed = existsSync(target)
|
|
207
|
+
if (existed && !args.overwrite) {
|
|
208
|
+
throw new Error(`Domain '${name}' already exists. Pass overwrite=true to replace it.`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
mkdirSync(DOMAINS_DIR, { recursive: true })
|
|
212
|
+
if (existed) {
|
|
213
|
+
rmSync(target, { recursive: true, force: true })
|
|
214
|
+
}
|
|
215
|
+
mkdirSync(target, { recursive: true })
|
|
216
|
+
writeFileSync(join(target, DOMAIN_FILE), `${domainMd}\n`, "utf-8")
|
|
217
|
+
|
|
218
|
+
const validation = validateDomainPackage(name)
|
|
219
|
+
if (!validation.ok) {
|
|
220
|
+
throw new Error(`Created domain package is invalid: ${validation.errors.join("; ")}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
name,
|
|
226
|
+
path: target,
|
|
227
|
+
files: [DOMAIN_FILE],
|
|
228
|
+
overwritten: existed,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Create a project-local domain draft under .revela/drafts/domains/<name>/. */
|
|
233
|
+
export function createDomainDraftPackage(args: CreateDomainDraftArgs): CreateDomainPackageResult {
|
|
234
|
+
const name = normalizeDomainName(args.name)
|
|
235
|
+
const domainMd = args.domainMd?.trim()
|
|
236
|
+
|
|
237
|
+
if (!domainMd) throw new Error("domainMd is required")
|
|
238
|
+
|
|
239
|
+
const target = domainDraftDir(args.workspaceRoot, name)
|
|
240
|
+
const existed = existsSync(target)
|
|
241
|
+
if (existed && !args.overwrite) {
|
|
242
|
+
throw new Error(`Domain draft '${name}' already exists. Pass overwrite=true to replace it.`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
246
|
+
if (existed) {
|
|
247
|
+
rmSync(target, { recursive: true, force: true })
|
|
248
|
+
}
|
|
249
|
+
mkdirSync(target, { recursive: true })
|
|
250
|
+
writeFileSync(join(target, DOMAIN_FILE), `${domainMd}\n`, "utf-8")
|
|
251
|
+
|
|
252
|
+
const validation = validateDomainDraftPackage(args.workspaceRoot, name)
|
|
253
|
+
if (!validation.ok) {
|
|
254
|
+
throw new Error(`Created domain draft is invalid: ${validation.errors.join("; ")}`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
name,
|
|
260
|
+
path: target,
|
|
261
|
+
files: [DOMAIN_FILE],
|
|
262
|
+
overwritten: existed,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Validate a project-local domain draft. */
|
|
267
|
+
export function validateDomainDraftPackage(workspaceRoot: string, nameInput: string): ValidateDomainPackageResult {
|
|
268
|
+
let name = nameInput
|
|
269
|
+
try {
|
|
270
|
+
name = normalizeDomainName(nameInput)
|
|
271
|
+
} catch {
|
|
272
|
+
// validateDomainPackageAt records the invalid-name error.
|
|
273
|
+
}
|
|
274
|
+
return validateDomainPackageAt(nameInput, domainDraftDir(workspaceRoot, name))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Install a validated project-local domain draft into the user-level domain registry. */
|
|
278
|
+
export function installDomainDraftPackage(args: InstallDomainDraftArgs): InstallDomainDraftResult {
|
|
279
|
+
const name = normalizeDomainName(args.name)
|
|
280
|
+
const sourcePath = domainDraftDir(args.workspaceRoot, name)
|
|
281
|
+
const validation = validateDomainDraftPackage(args.workspaceRoot, name)
|
|
282
|
+
if (!validation.ok) {
|
|
283
|
+
throw new Error(`Domain draft is invalid: ${validation.errors.join("; ")}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const target = join(DOMAINS_DIR, name)
|
|
287
|
+
const existed = existsSync(target)
|
|
288
|
+
if (existed && !args.overwrite) {
|
|
289
|
+
throw new Error(`Domain '${name}' already exists. Pass overwrite=true to replace it.`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
mkdirSync(DOMAINS_DIR, { recursive: true })
|
|
294
|
+
if (existed) {
|
|
295
|
+
rmSync(target, { recursive: true, force: true })
|
|
296
|
+
}
|
|
297
|
+
cpSync(sourcePath, target, { recursive: true })
|
|
298
|
+
} catch (e) {
|
|
299
|
+
throw new Error(`Installing domain draft requires write access to Revela user config at ${DOMAINS_DIR}: ${e instanceof Error ? e.message : String(e)}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
name,
|
|
305
|
+
path: target,
|
|
306
|
+
sourcePath,
|
|
307
|
+
files: [DOMAIN_FILE],
|
|
308
|
+
overwritten: existed,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Validate a local domain package for the minimum Revela domain contract. */
|
|
313
|
+
export function validateDomainPackage(nameInput: string): ValidateDomainPackageResult {
|
|
314
|
+
let name = nameInput
|
|
315
|
+
try {
|
|
316
|
+
name = normalizeDomainName(nameInput)
|
|
317
|
+
} catch {
|
|
318
|
+
// validateDomainPackageAt records the invalid-name error.
|
|
319
|
+
}
|
|
320
|
+
return validateDomainPackageAt(nameInput, join(DOMAINS_DIR, name))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function validateDomainPackageAt(nameInput: string, dir: string): ValidateDomainPackageResult {
|
|
324
|
+
let name = nameInput
|
|
325
|
+
const errors: string[] = []
|
|
326
|
+
try {
|
|
327
|
+
name = normalizeDomainName(nameInput)
|
|
328
|
+
} catch (e) {
|
|
329
|
+
errors.push(e instanceof Error ? e.message : String(e))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const mdPath = join(dir, DOMAIN_FILE)
|
|
333
|
+
const hasIndustryMd = existsSync(mdPath)
|
|
334
|
+
let hasRequiredFrontmatter = false
|
|
335
|
+
let hasBody = false
|
|
336
|
+
|
|
337
|
+
if (!existsSync(dir)) errors.push(`Domain directory does not exist: ${dir}`)
|
|
338
|
+
if (!hasIndustryMd) errors.push(`${DOMAIN_FILE} is missing`)
|
|
339
|
+
|
|
340
|
+
if (hasIndustryMd) {
|
|
341
|
+
try {
|
|
342
|
+
const text = readFileSync(mdPath, "utf-8")
|
|
343
|
+
const { meta, body } = parseFrontmatter(text)
|
|
344
|
+
const required = ["name", "description", "author", "version"]
|
|
345
|
+
const missing = required.filter((field) => !meta[field]?.trim())
|
|
346
|
+
hasRequiredFrontmatter = missing.length === 0
|
|
347
|
+
hasBody = body.trim().length > 0
|
|
348
|
+
if (!hasRequiredFrontmatter) errors.push(`INDUSTRY.md frontmatter is missing required field(s): ${missing.join(", ")}`)
|
|
349
|
+
if (!hasBody) errors.push("INDUSTRY.md body/guidance is empty")
|
|
350
|
+
if (meta.name && meta.name.trim() !== name) errors.push(`INDUSTRY.md frontmatter name '${meta.name.trim()}' does not match package name '${name}'`)
|
|
351
|
+
} catch (e) {
|
|
352
|
+
errors.push(`INDUSTRY.md could not be parsed: ${e instanceof Error ? e.message : String(e)}`)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
ok: errors.length === 0,
|
|
358
|
+
name,
|
|
359
|
+
path: dir,
|
|
360
|
+
hasIndustryMd,
|
|
361
|
+
hasRequiredFrontmatter,
|
|
362
|
+
hasBody,
|
|
363
|
+
errors,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function domainDraftDir(workspaceRoot: string, name: string): string {
|
|
368
|
+
return resolve(workspaceRoot, ".revela", "drafts", "domains", name)
|
|
369
|
+
}
|
|
370
|
+
|
|
151
371
|
/** Remove an installed domain. Throws if not found or is the protected default. */
|
|
152
372
|
export function removeDomain(name: string): void {
|
|
153
373
|
if (name === DEFAULT_DOMAIN) {
|
package/lib/qa/checks.ts
CHANGED
|
@@ -209,19 +209,16 @@ function checkCanvas(metrics: SlideMetrics): LayoutIssue[] {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
const canvasBad = Math.abs(metrics.canvasRect.width - CANVAS_W) > tol || Math.abs(metrics.canvasRect.height - CANVAS_H) > tol
|
|
212
|
-
const slideBad = Math.abs(metrics.slideRect.width - CANVAS_W) > tol || Math.abs(metrics.slideRect.height - CANVAS_H) > tol
|
|
213
212
|
|
|
214
|
-
if (canvasBad
|
|
213
|
+
if (canvasBad) {
|
|
215
214
|
issues.push({
|
|
216
215
|
type: "canvas",
|
|
217
216
|
sub: "size_mismatch",
|
|
218
217
|
severity: "error",
|
|
219
|
-
detail:
|
|
218
|
+
detail: `.slide-canvas must render exactly ${CANVAS_W}x${CANVAS_H}px. Measured canvas ${Math.round(metrics.canvasRect.width)}x${Math.round(metrics.canvasRect.height)}px.`,
|
|
220
219
|
data: {
|
|
221
220
|
expectedWidth: CANVAS_W,
|
|
222
221
|
expectedHeight: CANVAS_H,
|
|
223
|
-
slideWidth: Math.round(metrics.slideRect.width),
|
|
224
|
-
slideHeight: Math.round(metrics.slideRect.height),
|
|
225
222
|
canvasWidth: Math.round(metrics.canvasRect.width),
|
|
226
223
|
canvasHeight: Math.round(metrics.canvasRect.height),
|
|
227
224
|
},
|
|
@@ -911,7 +908,7 @@ export function formatReport(report: QAReport): string {
|
|
|
911
908
|
`### Action Required`,
|
|
912
909
|
``,
|
|
913
910
|
`Please fix the above hard-error issues in the HTML file. For each issue type:`,
|
|
914
|
-
`- **canvas**: ensure each slide and .slide-canvas
|
|
911
|
+
`- **canvas**: ensure each canonical .slide has exactly one direct .slide-canvas and that .slide-canvas renders exactly 1920x1080px, not merely any 16:9 size.`,
|
|
915
912
|
`- **scrollbar**: remove horizontal document/body scrolling and slide-internal scrolling. Multi-slide decks may use normal vertical document scroll for navigation.`,
|
|
916
913
|
`- **navigation**: keep every .slide in normal document flow; do not stack slides with fixed/absolute positioning or rely on aria-hidden/visibility toggles for pagination. Use scrollIntoView-based navigation.`,
|
|
917
914
|
`- **overflow**: reduce font size, padding, or content amount for the affected element.`,
|
package/lib/runtime/index.ts
CHANGED
|
@@ -4,18 +4,24 @@ import { dirname, resolve } from "path"
|
|
|
4
4
|
import {
|
|
5
5
|
activeDesign,
|
|
6
6
|
activateDesign,
|
|
7
|
+
createDesignDraftPackage,
|
|
7
8
|
createDesignPackage,
|
|
8
9
|
getDesignSection,
|
|
9
10
|
getDesignSkillMd,
|
|
11
|
+
installDesignDraftPackage,
|
|
10
12
|
listDesigns,
|
|
11
13
|
seedBuiltinDesigns,
|
|
14
|
+
validateDesignDraftPackage,
|
|
12
15
|
validateDesignPackage,
|
|
13
16
|
} from "../design/designs"
|
|
14
17
|
import { createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
|
|
15
|
-
import { activeDomain, activateDomain, getDomainSkillMd, listDomains, seedBuiltinDomains } from "../domain/domains"
|
|
18
|
+
import { activeDomain, activateDomain, createDomainDraftPackage, createDomainPackage, getDomainSkillMd, installDomainDraftPackage, listDomains, seedBuiltinDomains, validateDomainDraftPackage, validateDomainPackage } from "../domain/domains"
|
|
16
19
|
import { computeNarrativeHash } from "../narrative-state/hash"
|
|
17
20
|
import { compileNarrativeVault } from "../narrative-vault/compile"
|
|
21
|
+
import { autoCompileNarrativeVault } from "../narrative-vault/auto-compile"
|
|
22
|
+
import { extractNarrativeVaultMarkdownTargetsFromPatch } from "../narrative-vault/hook-targets"
|
|
18
23
|
import { runNarrativeMarkdownQa, type MarkdownQaOptions } from "../narrative-vault/markdown-qa"
|
|
24
|
+
import { formatArtifactQaUserNotice, formatMarkdownQaUserNotice } from "../hook-notifications"
|
|
19
25
|
import { readDeckPlanArtifact } from "../narrative-state/deck-plan-artifact"
|
|
20
26
|
import { extractDesignClasses } from "../design/designs"
|
|
21
27
|
import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/rendered-artifacts"
|
|
@@ -32,6 +38,10 @@ export interface RuntimeFileInput extends RuntimeWorkspaceInput {
|
|
|
32
38
|
file: string
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
export interface RuntimeNarrativeAutoCompileInput extends RuntimeWorkspaceInput {
|
|
42
|
+
touched?: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
export interface RuntimeDeckFoundationInput extends RuntimeWorkspaceInput {
|
|
36
46
|
outputPath: string
|
|
37
47
|
title: string
|
|
@@ -55,12 +65,28 @@ export interface RuntimeDesignCreateInput {
|
|
|
55
65
|
overwrite?: boolean
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
export interface RuntimeDesignDraftCreateInput extends RuntimeDesignCreateInput, RuntimeWorkspaceInput {}
|
|
69
|
+
|
|
70
|
+
export interface RuntimeDomainCreateInput {
|
|
71
|
+
name: string
|
|
72
|
+
domainMd: string
|
|
73
|
+
overwrite?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RuntimeDomainDraftCreateInput extends RuntimeDomainCreateInput, RuntimeWorkspaceInput {}
|
|
77
|
+
|
|
78
|
+
export interface RuntimeDraftInstallInput extends RuntimeWorkspaceInput {
|
|
79
|
+
name: string
|
|
80
|
+
overwrite?: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
export interface RuntimeNameInput {
|
|
59
84
|
name: string
|
|
60
85
|
}
|
|
61
86
|
|
|
62
87
|
export function doctor(input: RuntimeWorkspaceInput = {}) {
|
|
63
88
|
const workspaceRoot = root(input.workspaceRoot)
|
|
89
|
+
const domain = activeDomainDoctorInfo()
|
|
64
90
|
return {
|
|
65
91
|
ok: true,
|
|
66
92
|
version: pkg.version,
|
|
@@ -69,6 +95,8 @@ export function doctor(input: RuntimeWorkspaceInput = {}) {
|
|
|
69
95
|
hasDeckPlan: existsSync(resolve(workspaceRoot, "deck-plan")),
|
|
70
96
|
hasDecksJson: existsSync(resolve(workspaceRoot, "DECKS.json")),
|
|
71
97
|
activeDesign: safe(activeDesign),
|
|
98
|
+
activeDomain: domain.name,
|
|
99
|
+
activeDomainDescription: domain.description,
|
|
72
100
|
}
|
|
73
101
|
}
|
|
74
102
|
|
|
@@ -86,6 +114,18 @@ export function markdownQa(input: RuntimeWorkspaceInput & MarkdownQaOptions = {}
|
|
|
86
114
|
})
|
|
87
115
|
}
|
|
88
116
|
|
|
117
|
+
export function autoCompileNarrative(input: RuntimeNarrativeAutoCompileInput = {}) {
|
|
118
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
119
|
+
return autoCompileNarrativeVault(workspaceRoot, input.touched ?? [])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function extractNarrativeVaultMarkdownPatchTargets(input: RuntimeWorkspaceInput & { patch: string }) {
|
|
123
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
124
|
+
return extractNarrativeVaultMarkdownTargetsFromPatch(input.patch, workspaceRoot)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { formatArtifactQaUserNotice, formatMarkdownQaUserNotice }
|
|
128
|
+
|
|
89
129
|
export function readDeckPlan(input: RuntimeWorkspaceInput = {}) {
|
|
90
130
|
const workspaceRoot = root(input.workspaceRoot)
|
|
91
131
|
const compiled = compileNarrativeVault(workspaceRoot)
|
|
@@ -220,6 +260,30 @@ export function designValidate(input: RuntimeNameInput) {
|
|
|
220
260
|
return validateDesignPackage(requiredName(input, "design"))
|
|
221
261
|
}
|
|
222
262
|
|
|
263
|
+
export function designDraftCreate(input: RuntimeDesignDraftCreateInput) {
|
|
264
|
+
return createDesignDraftPackage({
|
|
265
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
266
|
+
name: requiredString(input?.name, "design name"),
|
|
267
|
+
base: input.base,
|
|
268
|
+
designMd: requiredString(input?.designMd, "designMd"),
|
|
269
|
+
previewHtml: requiredString(input?.previewHtml, "previewHtml"),
|
|
270
|
+
overwrite: input.overwrite ?? false,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function designDraftValidate(input: RuntimeWorkspaceInput & RuntimeNameInput) {
|
|
275
|
+
return validateDesignDraftPackage(root(input.workspaceRoot), requiredName(input, "design draft"))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function designDraftInstall(input: RuntimeDraftInstallInput) {
|
|
279
|
+
seedBuiltinDesigns()
|
|
280
|
+
return installDesignDraftPackage({
|
|
281
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
282
|
+
name: requiredName(input, "design draft"),
|
|
283
|
+
overwrite: input.overwrite ?? false,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
223
287
|
export interface DesignRulesReadinessResult {
|
|
224
288
|
ok: boolean
|
|
225
289
|
activeDesign: string
|
|
@@ -322,6 +386,42 @@ export function domainActivate(input: RuntimeNameInput) {
|
|
|
322
386
|
}
|
|
323
387
|
}
|
|
324
388
|
|
|
389
|
+
export function domainCreate(input: RuntimeDomainCreateInput) {
|
|
390
|
+
seedBuiltinDomains()
|
|
391
|
+
return createDomainPackage({
|
|
392
|
+
name: requiredString(input?.name, "domain name"),
|
|
393
|
+
domainMd: requiredString(input?.domainMd, "domainMd"),
|
|
394
|
+
overwrite: input.overwrite ?? false,
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function domainValidate(input: RuntimeNameInput) {
|
|
399
|
+
seedBuiltinDomains()
|
|
400
|
+
return validateDomainPackage(requiredName(input, "domain"))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function domainDraftCreate(input: RuntimeDomainDraftCreateInput) {
|
|
404
|
+
return createDomainDraftPackage({
|
|
405
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
406
|
+
name: requiredString(input?.name, "domain name"),
|
|
407
|
+
domainMd: requiredString(input?.domainMd, "domainMd"),
|
|
408
|
+
overwrite: input.overwrite ?? false,
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function domainDraftValidate(input: RuntimeWorkspaceInput & RuntimeNameInput) {
|
|
413
|
+
return validateDomainDraftPackage(root(input.workspaceRoot), requiredName(input, "domain draft"))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function domainDraftInstall(input: RuntimeDraftInstallInput) {
|
|
417
|
+
seedBuiltinDomains()
|
|
418
|
+
return installDomainDraftPackage({
|
|
419
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
420
|
+
name: requiredName(input, "domain draft"),
|
|
421
|
+
overwrite: input.overwrite ?? false,
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
325
425
|
function root(workspaceRoot: string | undefined): string {
|
|
326
426
|
return resolve(workspaceRoot || process.cwd())
|
|
327
427
|
}
|
|
@@ -334,6 +434,17 @@ function safe<T>(fn: () => T): T | undefined {
|
|
|
334
434
|
}
|
|
335
435
|
}
|
|
336
436
|
|
|
437
|
+
function activeDomainDoctorInfo(): { name: string; description: string } {
|
|
438
|
+
try {
|
|
439
|
+
seedBuiltinDomains()
|
|
440
|
+
const name = activeDomain()
|
|
441
|
+
const description = listDomains().find((domain) => domain.name === name)?.description ?? ""
|
|
442
|
+
return { name, description }
|
|
443
|
+
} catch {
|
|
444
|
+
return { name: safe(activeDomain) ?? "", description: "" }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
337
448
|
function requiredName(input: RuntimeNameInput, label: string): string {
|
|
338
449
|
const name = input?.name?.trim()
|
|
339
450
|
if (!name) throw new Error(`${label} name is required`)
|
package/package.json
CHANGED
package/plugins/revela/.mcp.json
CHANGED
|
@@ -17,6 +17,15 @@ export async function runPreWriteChecks(input: string): Promise<HookResult> {
|
|
|
17
17
|
messages.push(`Revela controls ${controlledStateFile}. Use Revela MCP/runtime tools or file-native narrative files instead of direct ${controlledStateFile} patches.`)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const cacheTargets = extractNarrativeCachePatchTargets(input)
|
|
21
|
+
if (cacheTargets.length > 0) {
|
|
22
|
+
messages.push([
|
|
23
|
+
"Revela narrative cache patches are blocked.",
|
|
24
|
+
`Controlled cache target(s): ${cacheTargets.map((target) => `\`${target}\``).join(", ")}`,
|
|
25
|
+
"Edit `revela-narrative/**/*.md` instead; compile/cache files under `.opencode/revela/narrative-cache/` are regenerated.",
|
|
26
|
+
].join("\n"))
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
const deckTargets = extractDeckHtmlPatchTargets(input)
|
|
21
30
|
if (deckTargets.length > 0) {
|
|
22
31
|
const pluginRoot = resolve(process.env.PLUGIN_ROOT || dirname(dirname(fileURLToPath(import.meta.url))))
|
|
@@ -55,6 +64,16 @@ export function extractDeckHtmlPatchTargets(input: string): string[] {
|
|
|
55
64
|
return [...targets].sort((a, b) => a.localeCompare(b))
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
export function extractNarrativeCachePatchTargets(input: string): string[] {
|
|
68
|
+
const targets = new Set<string>()
|
|
69
|
+
for (const patch of patchPayloads(input)) {
|
|
70
|
+
const pattern = /(?:^\*\*\* Update File: |^\*\*\* Add File: |^\*\*\* Delete File: |^\*\*\* Move to: )([^\r\n]*\.opencode\/revela\/narrative-cache\/[^\r\n]+)\s*$/gm
|
|
71
|
+
let match: RegExpExecArray | null
|
|
72
|
+
while ((match = pattern.exec(patch))) targets.add(match[1].trim())
|
|
73
|
+
}
|
|
74
|
+
return [...targets].sort((a, b) => a.localeCompare(b))
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
function patchPayloads(input: string): string[] {
|
|
59
78
|
try {
|
|
60
79
|
const parsed = JSON.parse(input)
|
|
@@ -24,6 +24,20 @@ export function extractDeckHtmlTargets(input: string): string[] {
|
|
|
24
24
|
return [...targets].sort((a, b) => a.localeCompare(b))
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function patchPayloadsFromInput(input: string): string[] {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(input)
|
|
30
|
+
return [
|
|
31
|
+
parsed.patch,
|
|
32
|
+
parsed.args?.patch,
|
|
33
|
+
parsed.tool_input?.patch,
|
|
34
|
+
parsed.toolInput?.patch,
|
|
35
|
+
].filter((item): item is string => typeof item === "string")
|
|
36
|
+
} catch {
|
|
37
|
+
return [input]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
export function workspaceRootFromInput(input: string): string {
|
|
28
42
|
try {
|
|
29
43
|
const parsed = JSON.parse(input)
|
|
@@ -47,18 +61,16 @@ export function workspaceRootFromInput(input: string): string {
|
|
|
47
61
|
|
|
48
62
|
export async function runPostWriteChecks(input: string): Promise<HookResult> {
|
|
49
63
|
const messages: string[] = []
|
|
50
|
-
if (/revela-narrative\/.*\.md/.test(input)) {
|
|
51
|
-
messages.push("Revela narrative Markdown changed. Run `revela_markdown_qa` and `revela_compile_narrative` before treating the graph as usable.")
|
|
52
|
-
}
|
|
53
|
-
|
|
54
64
|
const deckTargets = extractDeckHtmlTargets(input)
|
|
55
|
-
|
|
65
|
+
const hasPossibleNarrativeMarkdown = /revela-narrative\/.*\.md/.test(input)
|
|
66
|
+
if (deckTargets.length === 0 && !hasPossibleNarrativeMarkdown) return { ok: true, messages }
|
|
56
67
|
|
|
57
68
|
const pluginRoot = resolve(process.env.PLUGIN_ROOT || dirname(dirname(fileURLToPath(import.meta.url))))
|
|
58
69
|
const runtime = resolveRevelaRuntime({ pluginRoot })
|
|
59
70
|
if (!runtime.ok || !runtime.runtimePath) {
|
|
71
|
+
const changed = deckTargets.length > 0 ? "deck HTML changed" : "narrative Markdown changed"
|
|
60
72
|
messages.push([
|
|
61
|
-
|
|
73
|
+
`Revela ${changed}, but Codex hook could not locate the Revela runtime to run write-after checks.`,
|
|
62
74
|
...runtime.diagnostics.map((item) => `- ${item}`),
|
|
63
75
|
].join("\n"))
|
|
64
76
|
return { ok: false, messages }
|
|
@@ -67,9 +79,28 @@ export async function runPostWriteChecks(input: string): Promise<HookResult> {
|
|
|
67
79
|
const workspaceRoot = workspaceRootFromInput(input)
|
|
68
80
|
const runtimeModule = await import(pathToFileURL(runtime.runtimePath).href)
|
|
69
81
|
let ok = true
|
|
82
|
+
|
|
83
|
+
if (hasPossibleNarrativeMarkdown) {
|
|
84
|
+
const touched = new Set<string>()
|
|
85
|
+
for (const patch of patchPayloadsFromInput(input)) {
|
|
86
|
+
const targets = runtimeModule.extractNarrativeVaultMarkdownPatchTargets({ workspaceRoot, patch })
|
|
87
|
+
for (const target of targets) touched.add(target)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (touched.size > 0) {
|
|
91
|
+
const result = runtimeModule.autoCompileNarrative({ workspaceRoot, touched: [...touched] })
|
|
92
|
+
messages.push(result.markdown ?? JSON.stringify(result, null, 2))
|
|
93
|
+
const notice = runtimeModule.formatMarkdownQaUserNotice?.(result)
|
|
94
|
+
if (notice) messages.push(notice)
|
|
95
|
+
if (!result.ok) ok = false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
for (const target of deckTargets) {
|
|
71
100
|
const result = await runtimeModule.runDeckQa({ workspaceRoot, file: target })
|
|
72
101
|
messages.push(result.markdown ?? JSON.stringify(result, null, 2))
|
|
102
|
+
const notice = runtimeModule.formatArtifactQaUserNotice?.(result.report)
|
|
103
|
+
if (notice) messages.push(notice)
|
|
73
104
|
if (!result.ok) ok = false
|
|
74
105
|
}
|
|
75
106
|
|