@cyber-dash-tech/revela 0.16.4 → 0.17.1
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 +7 -5
- package/README.zh-CN.md +7 -5
- package/lib/commands/brief.ts +9 -0
- package/lib/commands/help.ts +5 -2
- package/lib/commands/init.ts +42 -27
- package/lib/commands/narrative.ts +39 -6
- package/lib/commands/research.ts +36 -20
- package/lib/commands/review.ts +35 -28
- package/lib/ctx.ts +1 -1
- package/lib/decks-state.ts +38 -4
- package/lib/edit/prompt.ts +1 -1
- package/lib/hook-notifications.ts +53 -0
- package/lib/media/download.ts +23 -3
- package/lib/media/save.ts +1 -0
- package/lib/media/types.ts +1 -0
- package/lib/narrative-state/display.ts +74 -4
- package/lib/narrative-state/map-html.ts +242 -107
- package/lib/narrative-state/render-plan.ts +238 -35
- package/lib/narrative-state/research-binding-eval.ts +260 -0
- package/lib/narrative-state/research-gaps.ts +2 -88
- package/lib/narrative-vault/authoring-contract.ts +127 -0
- package/lib/narrative-vault/authoring-guard.ts +122 -0
- package/lib/narrative-vault/auto-compile.ts +134 -0
- package/lib/narrative-vault/bootstrap.ts +63 -0
- package/lib/narrative-vault/cache.ts +14 -0
- package/lib/narrative-vault/compile-mirror.ts +45 -0
- package/lib/narrative-vault/compile.ts +350 -0
- package/lib/narrative-vault/constants.ts +6 -0
- package/lib/narrative-vault/diagnostic-report.ts +117 -0
- package/lib/narrative-vault/export.ts +71 -0
- package/lib/narrative-vault/frontmatter.ts +41 -0
- package/lib/narrative-vault/hook-targets.ts +40 -0
- package/lib/narrative-vault/index.ts +18 -0
- package/lib/narrative-vault/inventory.ts +392 -0
- package/lib/narrative-vault/markdown-qa.ts +237 -0
- package/lib/narrative-vault/markdown.ts +34 -0
- package/lib/narrative-vault/migration.ts +52 -0
- package/lib/narrative-vault/mutate.ts +361 -0
- package/lib/narrative-vault/paths.ts +19 -0
- package/lib/narrative-vault/read.ts +52 -0
- package/lib/narrative-vault/relations.ts +32 -0
- package/lib/narrative-vault/source-loader.ts +19 -0
- package/lib/narrative-vault/timestamp.ts +32 -0
- package/lib/narrative-vault/types.ts +44 -0
- package/lib/qa/checks.ts +206 -5
- package/lib/qa/measure.ts +63 -1
- package/lib/refine/server.ts +157 -20
- package/lib/source-materials.ts +98 -0
- package/lib/tool-result.ts +34 -0
- package/package.json +2 -2
- package/plugin.ts +60 -22
- package/skill/NARRATIVE_SKILL.md +25 -10
- package/skill/SKILL.md +6 -1
- package/tools/decks.ts +363 -67
- package/tools/narrative-view.ts +16 -0
- package/tools/research-save.ts +3 -0
- package/tools/workspace-scan.ts +1 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { VaultDiagnostic, VaultDiagnosticSeverity } from "./types"
|
|
2
|
+
|
|
3
|
+
export interface VaultDiagnosticDisplay {
|
|
4
|
+
code: string
|
|
5
|
+
severity: VaultDiagnosticSeverity
|
|
6
|
+
file?: string
|
|
7
|
+
nodeId?: string
|
|
8
|
+
message: string
|
|
9
|
+
suggestedFix: string
|
|
10
|
+
suggestedAction?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface VaultDiagnosticReport {
|
|
14
|
+
ok: boolean
|
|
15
|
+
errorCount: number
|
|
16
|
+
warningCount: number
|
|
17
|
+
blockers: VaultDiagnosticDisplay[]
|
|
18
|
+
warnings: VaultDiagnosticDisplay[]
|
|
19
|
+
nextActions: string[]
|
|
20
|
+
summary: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatVaultDiagnosticReport(diagnostics: VaultDiagnostic[]): VaultDiagnosticReport {
|
|
24
|
+
const displays = diagnostics.map(formatDiagnostic)
|
|
25
|
+
const blockers = displays.filter((diagnostic) => diagnostic.severity === "error")
|
|
26
|
+
const warnings = displays.filter((diagnostic) => diagnostic.severity === "warning")
|
|
27
|
+
const nextActions = unique(displays.map((diagnostic) => diagnostic.suggestedAction).filter(Boolean) as string[])
|
|
28
|
+
const summary = blockers.length === 0 && warnings.length === 0
|
|
29
|
+
? "Narrative vault diagnostics: clean."
|
|
30
|
+
: `Narrative vault diagnostics: ${blockers.length} blocker(s), ${warnings.length} warning(s).`
|
|
31
|
+
return {
|
|
32
|
+
ok: blockers.length === 0,
|
|
33
|
+
errorCount: blockers.length,
|
|
34
|
+
warningCount: warnings.length,
|
|
35
|
+
blockers,
|
|
36
|
+
warnings,
|
|
37
|
+
nextActions,
|
|
38
|
+
summary,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatVaultDiagnosticMarkdown(report: VaultDiagnosticReport): string {
|
|
43
|
+
if (report.errorCount === 0 && report.warningCount === 0) return ""
|
|
44
|
+
const lines = ["**Narrative vault diagnostics**", "", report.summary]
|
|
45
|
+
if (report.blockers.length > 0) {
|
|
46
|
+
lines.push("", "Blockers:")
|
|
47
|
+
for (const diagnostic of report.blockers) lines.push(`- ${formatDisplayLine(diagnostic)}`)
|
|
48
|
+
}
|
|
49
|
+
if (report.warnings.length > 0) {
|
|
50
|
+
lines.push("", "Warnings:")
|
|
51
|
+
for (const diagnostic of report.warnings) lines.push(`- ${formatDisplayLine(diagnostic)}`)
|
|
52
|
+
}
|
|
53
|
+
if (report.nextActions.length > 0) {
|
|
54
|
+
lines.push("", "Next actions:")
|
|
55
|
+
for (const action of report.nextActions) lines.push(`- ${action}`)
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatDiagnostic(diagnostic: VaultDiagnostic): VaultDiagnosticDisplay {
|
|
61
|
+
const suggestion = suggestionForCode(diagnostic.code)
|
|
62
|
+
return {
|
|
63
|
+
code: diagnostic.code,
|
|
64
|
+
severity: diagnostic.severity,
|
|
65
|
+
file: diagnostic.file,
|
|
66
|
+
nodeId: diagnostic.nodeId,
|
|
67
|
+
message: diagnostic.message,
|
|
68
|
+
suggestedFix: suggestion.fix,
|
|
69
|
+
suggestedAction: suggestion.action,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function suggestionForCode(code: string): { fix: string; action?: string } {
|
|
74
|
+
switch (code) {
|
|
75
|
+
case "empty_vault":
|
|
76
|
+
return { fix: "Export the existing JSON narrative with exportNarrativeVault, or add the required Markdown narrative nodes.", action: "Run exportNarrativeVault if DECKS.json has a narrative; otherwise add core vault Markdown nodes." }
|
|
77
|
+
case "duplicate_id":
|
|
78
|
+
return { fix: "Rename one Markdown node id so each canonical narrative id is unique.", action: "Edit the duplicate Markdown node id and rerun compileNarrativeVault." }
|
|
79
|
+
case "missing_type":
|
|
80
|
+
return { fix: "Add the required frontmatter field type with a valid narrative vault node type.", action: "Edit the reported Markdown file and rerun compileNarrativeVault." }
|
|
81
|
+
case "missing_id":
|
|
82
|
+
return { fix: "Add the required frontmatter field id and keep it stable after creation.", action: "Edit the reported Markdown file and rerun compileNarrativeVault." }
|
|
83
|
+
case "unknown_node_type":
|
|
84
|
+
return { fix: "Use one of the supported node types: index, audience, decision, thesis, claim, evidence, objection, risk, or research-gap.", action: "Correct the type frontmatter and rerun compileNarrativeVault." }
|
|
85
|
+
case "broken_link":
|
|
86
|
+
return { fix: "Create the linked target node or correct the wikilink id in the Relations section.", action: "Repair the wikilink and rerun compileNarrativeVault." }
|
|
87
|
+
case "illegal_relation_target":
|
|
88
|
+
return { fix: "Change the relation type or target node so the typed relation connects allowed node types.", action: "Repair the relation and rerun compileNarrativeVault." }
|
|
89
|
+
case "compile_failed":
|
|
90
|
+
return { fix: "Fix structural vault diagnostics first; the narrative could not be normalized.", action: "Resolve blocker diagnostics and rerun compileNarrativeVault." }
|
|
91
|
+
case "evidence_claim_missing":
|
|
92
|
+
return { fix: "Correct the evidence claimId or create the target claim before treating the evidence as support.", action: "Use upsertVaultClaim for the missing claim or update the evidence claimId, then rerun compileNarrativeVault." }
|
|
93
|
+
case "stale_approval_hash":
|
|
94
|
+
return { fix: "The latest approval no longer matches the current narrative hash.", action: "Run /revela story and ask for approval or an explicit render override after review." }
|
|
95
|
+
case "evidence_trace_incomplete":
|
|
96
|
+
return { fix: "Add explicit source, quote/snippet, support scope, unsupported scope, caveat, and strength to the evidence node.", action: "Use upsertVaultEvidence with complete source trace fields." }
|
|
97
|
+
case "orphan_central_claim":
|
|
98
|
+
return { fix: "Add or repair claim relations, or confirm the central claim should stand alone.", action: "Review the claim in /revela story; edit Relations only when the relation is explicit." }
|
|
99
|
+
case "claim_missing_evidence":
|
|
100
|
+
return { fix: "Bind explicit evidence or keep the missing support visible as a research gap.", action: "Run /revela research or use upsertVaultEvidence when source trace is explicit." }
|
|
101
|
+
case "research_gap_unresolved":
|
|
102
|
+
return { fix: "Resolve, close, or keep researching the gap; do not treat it as evidence until bound.", action: "Run /revela research or updateVaultResearchGap with the current lifecycle status." }
|
|
103
|
+
case "gap_evidence_missing":
|
|
104
|
+
return { fix: "Correct evidenceBindingIds or create the referenced evidence node.", action: "Use upsertVaultEvidence for the missing evidence or updateVaultResearchGap to remove the bad reference." }
|
|
105
|
+
default:
|
|
106
|
+
return { fix: "Inspect the reported Markdown file or node and rerun compileNarrativeVault after the smallest safe fix.", action: "Rerun compileNarrativeVault after fixing the reported vault node." }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatDisplayLine(diagnostic: VaultDiagnosticDisplay): string {
|
|
111
|
+
const location = [diagnostic.file, diagnostic.nodeId].filter(Boolean).join(" / ")
|
|
112
|
+
return `\`${diagnostic.code}\`${location ? ` (${location})` : ""}: ${diagnostic.message} Fix: ${diagnostic.suggestedFix}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function unique(values: string[]): string[] {
|
|
116
|
+
return [...new Set(values)]
|
|
117
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "fs"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import { narrativeVaultPath } from "./paths"
|
|
4
|
+
import type { NarrativeStateV1 } from "../narrative-state/types"
|
|
5
|
+
|
|
6
|
+
export interface ExportNarrativeVaultResult {
|
|
7
|
+
files: string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function exportNarrativeStateToVault(workspaceRoot: string, narrative: NarrativeStateV1): ExportNarrativeVaultResult {
|
|
11
|
+
const root = narrativeVaultPath(workspaceRoot)
|
|
12
|
+
const files: string[] = []
|
|
13
|
+
mkdirSync(root, { recursive: true })
|
|
14
|
+
for (const dir of ["claims", "evidence", "objections", "risks", "research-gaps"]) mkdirSync(join(root, dir), { recursive: true })
|
|
15
|
+
|
|
16
|
+
write(root, files, "index.md", frontmatter({ type: "index", id: narrative.id, status: narrative.status }) + "\n")
|
|
17
|
+
write(root, files, "audience.md", frontmatter({ type: "audience", ...narrative.audience }) + "\n")
|
|
18
|
+
write(root, files, "decision.md", frontmatter({ type: "decision", ...narrative.decision }) + "\n")
|
|
19
|
+
if (narrative.thesis) write(root, files, "thesis.md", frontmatter({ type: "thesis", ...narrative.thesis }) + `\n${narrative.thesis.statement}\n`)
|
|
20
|
+
|
|
21
|
+
const relationsByClaim = new Map<string, string[]>()
|
|
22
|
+
for (const relation of narrative.claimRelations ?? []) {
|
|
23
|
+
const lines = relationsByClaim.get(relation.fromClaimId) ?? []
|
|
24
|
+
lines.push(`- ${relation.relation}: [[${relation.toClaimId}]]${relation.rationale ? ` - ${relation.rationale}` : ""}`)
|
|
25
|
+
relationsByClaim.set(relation.fromClaimId, lines)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const claim of narrative.claims) {
|
|
29
|
+
const relationLines = relationsByClaim.get(claim.id) ?? []
|
|
30
|
+
const caveats = claim.caveats?.length ? `\n## Caveats\n\n${claim.caveats.map((item) => `- ${item}`).join("\n")}\n` : ""
|
|
31
|
+
const relations = relationLines.length ? `\n## Relations\n\n${relationLines.join("\n")}\n` : ""
|
|
32
|
+
write(root, files, join("claims", `${safeFileName(claim.id)}.md`), frontmatter({ type: "claim", id: claim.id, kind: claim.kind, importance: claim.importance, evidenceRequired: claim.evidenceRequired, supportedScope: claim.supportedScope, unsupportedScope: claim.unsupportedScope }) + `\n${claim.text}\n${caveats}${relations}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const binding of narrative.evidenceBindings) write(root, files, join("evidence", `${safeFileName(binding.id)}.md`), frontmatter({ type: "evidence", ...binding }) + `\n${binding.quote ?? ""}\n`)
|
|
36
|
+
for (const objection of narrative.objections) write(root, files, join("objections", `${safeFileName(objection.id)}.md`), frontmatter({ type: "objection", ...objection }) + `\n${objection.text}\n`)
|
|
37
|
+
for (const risk of narrative.risks) write(root, files, join("risks", `${safeFileName(risk.id)}.md`), frontmatter({ type: "risk", ...risk }) + `\n${risk.text}\n`)
|
|
38
|
+
for (const gap of narrative.researchGaps ?? []) write(root, files, join("research-gaps", `${safeFileName(gap.id)}.md`), frontmatter({ type: "research-gap", ...gap }) + `\n${gap.question}\n`)
|
|
39
|
+
return { files }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function write(root: string, files: string[], relativePath: string, content: string): void {
|
|
43
|
+
const filePath = join(root, relativePath)
|
|
44
|
+
writeFileSync(filePath, content.endsWith("\n") ? content : `${content}\n`, "utf-8")
|
|
45
|
+
files.push(relativePath.split(/[/\\]+/).join("/"))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function frontmatter(values: Record<string, unknown>): string {
|
|
49
|
+
const lines = ["---"]
|
|
50
|
+
for (const [key, value] of Object.entries(values)) {
|
|
51
|
+
if (value === undefined || value === "" || (Array.isArray(value) && value.length === 0)) continue
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
lines.push(`${key}:`)
|
|
54
|
+
for (const item of value) lines.push(` - ${quote(String(item))}`)
|
|
55
|
+
} else if (typeof value === "boolean") {
|
|
56
|
+
lines.push(`${key}: ${value ? "true" : "false"}`)
|
|
57
|
+
} else {
|
|
58
|
+
lines.push(`${key}: ${quote(String(value))}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lines.push("---", "")
|
|
62
|
+
return lines.join("\n")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function quote(value: string): string {
|
|
66
|
+
return JSON.stringify(value)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeFileName(id: string): string {
|
|
70
|
+
return id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "node"
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ParsedVaultFrontmatter {
|
|
2
|
+
frontmatter: Record<string, string | string[] | boolean>
|
|
3
|
+
body: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function parseVaultFrontmatter(markdown: string): ParsedVaultFrontmatter {
|
|
7
|
+
const normalized = markdown.replace(/\r\n/g, "\n")
|
|
8
|
+
if (!normalized.startsWith("---\n")) return { frontmatter: {}, body: normalized.trim() }
|
|
9
|
+
const end = normalized.indexOf("\n---", 4)
|
|
10
|
+
if (end === -1) return { frontmatter: {}, body: normalized.trim() }
|
|
11
|
+
const raw = normalized.slice(4, end).trim()
|
|
12
|
+
const body = normalized.slice(end + 4).replace(/^\n/, "").trim()
|
|
13
|
+
const frontmatter: Record<string, string | string[] | boolean> = {}
|
|
14
|
+
const lines = raw.split("\n")
|
|
15
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
16
|
+
const line = lines[index]
|
|
17
|
+
const match = /^(\w[\w-]*):\s*(.*)$/.exec(line)
|
|
18
|
+
if (!match) continue
|
|
19
|
+
const key = match[1]
|
|
20
|
+
const value = match[2].trim()
|
|
21
|
+
if (value === "true" || value === "false") {
|
|
22
|
+
frontmatter[key] = value === "true"
|
|
23
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
24
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((item) => unquote(item.trim())).filter(Boolean)
|
|
25
|
+
} else if (!value) {
|
|
26
|
+
const items: string[] = []
|
|
27
|
+
while (lines[index + 1]?.trim().startsWith("- ")) {
|
|
28
|
+
index += 1
|
|
29
|
+
items.push(unquote(lines[index].trim().slice(2).trim()))
|
|
30
|
+
}
|
|
31
|
+
frontmatter[key] = items.length > 0 ? items : ""
|
|
32
|
+
} else {
|
|
33
|
+
frontmatter[key] = unquote(value)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { frontmatter, body }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unquote(value: string): string {
|
|
40
|
+
return value.replace(/^['"]|['"]$/g, "").trim()
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { relative, resolve } from "path"
|
|
2
|
+
|
|
3
|
+
const VAULT_MARKDOWN_RE = /^revela-narrative\/(.+)\.md$/
|
|
4
|
+
|
|
5
|
+
export function isNarrativeVaultMarkdownPath(filePath: string, workspaceRoot: string): boolean {
|
|
6
|
+
const workspaceRelative = toWorkspaceRelativePath(filePath, workspaceRoot)
|
|
7
|
+
if (!workspaceRelative) return false
|
|
8
|
+
const match = VAULT_MARKDOWN_RE.exec(workspaceRelative)
|
|
9
|
+
return Boolean(match?.[1])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeNarrativeVaultMarkdownPath(filePath: string, workspaceRoot: string): string | undefined {
|
|
13
|
+
const workspaceRelative = toWorkspaceRelativePath(filePath, workspaceRoot)
|
|
14
|
+
if (!workspaceRelative) return undefined
|
|
15
|
+
return VAULT_MARKDOWN_RE.test(workspaceRelative) ? workspaceRelative : undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function extractNarrativeVaultMarkdownTargetsFromPatch(patchText: string, workspaceRoot: string): string[] {
|
|
19
|
+
const targets = new Set<string>()
|
|
20
|
+
|
|
21
|
+
for (const line of patchText.replace(/\r\n/g, "\n").split("\n")) {
|
|
22
|
+
const match = /^\*\*\*\s+(?:Add File|Update File|Delete File|Move to):\s*(.+?)\s*$/.exec(line)
|
|
23
|
+
if (!match) continue
|
|
24
|
+
const target = normalizeNarrativeVaultMarkdownPath(match[1].trim(), workspaceRoot)
|
|
25
|
+
if (target) targets.add(target)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [...targets]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toWorkspaceRelativePath(filePath: string, workspaceRoot: string): string | undefined {
|
|
32
|
+
const normalized = filePath.replace(/\\/g, "/").trim()
|
|
33
|
+
if (!normalized) return undefined
|
|
34
|
+
|
|
35
|
+
const root = resolve(workspaceRoot)
|
|
36
|
+
const absolute = resolve(root, normalized)
|
|
37
|
+
const rel = relative(root, absolute).replace(/\\/g, "/")
|
|
38
|
+
if (!rel || rel === "." || rel.startsWith("../") || rel === "..") return undefined
|
|
39
|
+
return rel
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./cache"
|
|
2
|
+
export * from "./authoring-contract"
|
|
3
|
+
export * from "./bootstrap"
|
|
4
|
+
export * from "./compile"
|
|
5
|
+
export * from "./diagnostic-report"
|
|
6
|
+
export * from "./export"
|
|
7
|
+
export * from "./frontmatter"
|
|
8
|
+
export * from "./inventory"
|
|
9
|
+
export * from "./markdown"
|
|
10
|
+
export * from "./markdown-qa"
|
|
11
|
+
export * from "./migration"
|
|
12
|
+
export * from "./mutate"
|
|
13
|
+
export * from "./paths"
|
|
14
|
+
export * from "./read"
|
|
15
|
+
export * from "./relations"
|
|
16
|
+
export * from "./source-loader"
|
|
17
|
+
export * from "./timestamp"
|
|
18
|
+
export * from "./types"
|