@cyber-dash-tech/revela 0.15.0 → 0.15.2
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 +6 -7
- package/README.zh-CN.md +6 -7
- package/designs/starter/DESIGN.md +168 -171
- package/designs/starter/preview.html +2 -2
- package/designs/summit/DESIGN.md +283 -129
- package/lib/commands/edit.ts +2 -21
- package/lib/commands/help.ts +1 -2
- package/lib/commands/narrative.ts +26 -0
- package/lib/commands/review.ts +49 -12
- package/lib/decks-state.ts +122 -3
- package/lib/design/designs.ts +1 -2
- package/lib/edit/prompt.ts +18 -5
- package/lib/edit/resolve-deck.ts +1 -1
- package/lib/inspect/prompt.ts +6 -1
- package/lib/media/download.ts +36 -11
- package/lib/media/save.ts +24 -0
- package/lib/media/search.ts +385 -0
- package/lib/media/types.ts +12 -0
- package/lib/narrative-state/render-plan.ts +10 -1
- package/lib/qa/artifact.ts +77 -0
- package/lib/qa/checks.ts +101 -10
- package/lib/qa/index.ts +81 -8
- package/lib/qa/measure.ts +85 -0
- package/lib/refine/open.ts +21 -1
- package/lib/refine/server.ts +884 -71
- package/lib/workspace-state/types.ts +1 -0
- package/package.json +1 -1
- package/plugin.ts +36 -130
- package/skill/NARRATIVE_SKILL.md +1 -1
- package/skill/SKILL.md +5 -10
- package/tools/decks.ts +29 -3
- package/tools/media-save.ts +6 -0
- package/tools/narrative-view.ts +1 -1
- package/tools/qa.ts +17 -11
package/lib/commands/edit.ts
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
import { openRefineDeck } from "../refine/open"
|
|
2
|
-
|
|
3
1
|
export async function handleEdit(
|
|
4
2
|
options: { client: any; sessionID: string; workspaceRoot: string; openBrowser?: boolean },
|
|
5
3
|
send: (text: string) => Promise<void>,
|
|
6
4
|
): Promise<void> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
client: options.client,
|
|
10
|
-
sessionID: options.sessionID,
|
|
11
|
-
workspaceRoot: options.workspaceRoot,
|
|
12
|
-
mode: "edit",
|
|
13
|
-
openBrowser: options.openBrowser,
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
await send(
|
|
17
|
-
`\`/revela edit\` is deprecated. Opened \`/revela refine\` in Edit mode for the active HTML deck.\n` +
|
|
18
|
-
`File: \`${result.deck.file}\`\n` +
|
|
19
|
-
`${result.stateNote}\n` +
|
|
20
|
-
`URL: ${result.url}\n\n` +
|
|
21
|
-
`Use \`/revela refine\` directly going forward. Use Ctrl/Cmd-click in the browser to reference elements, then use the Edit tab to send targeted change comments.`
|
|
22
|
-
)
|
|
23
|
-
} catch (e: any) {
|
|
24
|
-
await send(`**Edit failed:** ${e.message || String(e)}`)
|
|
25
|
-
}
|
|
5
|
+
void options
|
|
6
|
+
await send("`/revela edit` has been removed. Use `/revela refine` for the unified reading, inspection, and editing workspace.")
|
|
26
7
|
}
|
package/lib/commands/help.ts
CHANGED
|
@@ -28,7 +28,7 @@ export async function handleHelp(
|
|
|
28
28
|
`\`/revela disable\` — disable ambient Revela mode\n` +
|
|
29
29
|
`\`/revela init\` — initialize or refresh workspace DECKS.json\n` +
|
|
30
30
|
`\`/revela research\` — research, bind evidence, and reduce story gaps\n` +
|
|
31
|
-
`\`/revela story\`
|
|
31
|
+
`\`/revela story [-l language]\` — open the read-only story workspace UI\n` +
|
|
32
32
|
`\`/revela review\` — legacy readiness report for story state\n` +
|
|
33
33
|
`\`/revela narrative\` — compatibility alias for /revela story\n` +
|
|
34
34
|
`\`/revela make deck\` — make a deck from approved story state\n` +
|
|
@@ -37,7 +37,6 @@ export async function handleHelp(
|
|
|
37
37
|
`\`/revela deck\` — compatibility alias for /revela make deck\n` +
|
|
38
38
|
`\`/revela brief [file.md]\` — compatibility alias for /revela make brief\n` +
|
|
39
39
|
`\`/revela refine\` — open unified reading, inspection, and editing workspace\n` +
|
|
40
|
-
`\`/revela edit\` — deprecated compatibility shim to /revela refine Edit\n` +
|
|
41
40
|
`\`/revela inspect\` — deprecated compatibility shim to /revela refine Inspect\n` +
|
|
42
41
|
`\`/revela remember <text>\` — save an explicit preference to DECKS.json\n` +
|
|
43
42
|
`\`/revela design\` — list installed designs\n` +
|
|
@@ -12,7 +12,33 @@ export interface NarrativeArgs {
|
|
|
12
12
|
raw: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface StoryArgs {
|
|
16
|
+
language: NarrativeViewLanguage
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
export type ParseNarrativeArgsResult = { ok: true; args: NarrativeArgs } | { ok: false; error: string }
|
|
20
|
+
export type ParseStoryArgsResult = { ok: true; args: StoryArgs } | { ok: false; error: string }
|
|
21
|
+
|
|
22
|
+
export function parseStoryArgs(param: string): ParseStoryArgsResult {
|
|
23
|
+
const tokens = param.trim().split(/\s+/).filter(Boolean)
|
|
24
|
+
let language: NarrativeViewLanguage = "en"
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
27
|
+
const token = tokens[i]
|
|
28
|
+
if (token === "--language" || token === "-l") {
|
|
29
|
+
const value = tokens[++i]
|
|
30
|
+
if (!value) return { ok: false, error: "Usage: `/revela story [--language <language> | -l <language>]`" }
|
|
31
|
+
language = normalizeLanguageRequest(value)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
if (token.startsWith("--language=")) {
|
|
35
|
+
return { ok: false, error: "Usage: `/revela story --language <language>` or `/revela story -l <language>`. Do not use `--language=<language>`." }
|
|
36
|
+
}
|
|
37
|
+
return { ok: false, error: "Usage: `/revela story [--language <language> | -l <language>]`. Use `/revela review` for a readiness report." }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { ok: true, args: { language } }
|
|
41
|
+
}
|
|
16
42
|
|
|
17
43
|
export function parseNarrativeArgs(param: string): ParseNarrativeArgsResult {
|
|
18
44
|
const tokens = param.trim().split(/\s+/).filter(Boolean)
|
package/lib/commands/review.ts
CHANGED
|
@@ -73,12 +73,13 @@ export function buildDeckPrompt({
|
|
|
73
73
|
? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
|
|
74
74
|
: `${DECKS_STATE_FILE} does not exist yet. Do not invent a deck; initialize narrative state first with /revela init.`
|
|
75
75
|
|
|
76
|
-
return `Begin Revela deck
|
|
76
|
+
return `Begin Revela deck plan handoff.
|
|
77
77
|
|
|
78
78
|
Goal:
|
|
79
|
-
- Treat this as the explicit transition from approved narrative state to deck
|
|
79
|
+
- Treat this as the explicit transition from approved narrative state to user-confirmed deck planning.
|
|
80
80
|
- Use the deck-render prompt mode for design, layout, component, HTML, QA, and deck artifact rules.
|
|
81
|
-
-
|
|
81
|
+
- Default behavior is two-stage: first show the compiled deck plan with low-fidelity layout sketches, then stop for user confirmation before any artifact review or HTML writing.
|
|
82
|
+
- Do not write or overwrite \`decks/*.html\` until the narrative handoff, explicit user deck-plan confirmation, and deck/artifact gate are all satisfied.
|
|
82
83
|
- Do not treat legacy \`writeReadiness.status\`, old review snapshots, or existing HTML decks as narrative approval.
|
|
83
84
|
- Do not bypass the deck HTML contract, review snapshot freshness, source-trace expectations, or export preflight protections.
|
|
84
85
|
|
|
@@ -92,25 +93,61 @@ Workflow:
|
|
|
92
93
|
3. If narrative readiness is \`approved\`, continue. If it is \`ready_for_approval\`, ask the user for explicit approval before continuing. If it is blocked, stale, or needs research, stop and report the smallest next action. Do not call \`approveNarrative\` unless the user explicitly approves or requests a render override.
|
|
93
94
|
4. After approval or explicit render override exists, call \`revela-decks\` action \`compileDeckPlan\`. This projects canonical narrative claims and evidence bindings into compatibility \`slides[]\` and \`slides[].evidence[]\`; it must not write HTML.
|
|
94
95
|
5. If \`compileDeckPlan\` returns \`skipped\`, stop and report the reason. Do not invent slide specs manually to bypass approval.
|
|
95
|
-
6.
|
|
96
|
-
7.
|
|
97
|
-
8.
|
|
98
|
-
9.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
6. Present the compiled deck plan to the user and include a low-fidelity layout sketch for every slide. The sketch is ASCII/text structure only; do not generate visual images or HTML mockups.
|
|
97
|
+
7. Stop after presenting the plan. Ask the user to confirm or request changes. Do not call \`revela-decks review\`, do not fetch design context, and do not write HTML in the same turn unless the user had already explicitly confirmed the current plan before this command.
|
|
98
|
+
8. Only after explicit user confirmation of the current slide plan, call \`revela-decks\` action \`confirmDeckPlan\` with \`approvalBy=user\` and a compact \`approvalNote\`.
|
|
99
|
+
9. After confirmation is recorded, ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
|
|
100
|
+
10. Update only deck/artifact metadata through \`revela-decks upsertDeck\` / \`upsertSlides\` when required by confirmed design/layout choices. Do not change canonical narrative claims unless the user asks to revise the narrative.
|
|
101
|
+
11. Call \`revela-decks\` action \`review\` as the artifact gate. It computes \`writeReadiness\` and review snapshots for deck HTML writing. If it reports \`slide_plan_unconfirmed\`, stop and ask for explicit deck-plan confirmation.
|
|
102
|
+
12. Write \`decks/*.html\` only if the deck/artifact gate is ready and all deck HTML contract requirements can be satisfied. After each HTML write, the system automatically runs artifact QA before opening Refine.
|
|
103
|
+
13. If post-write artifact QA reports hard errors, fix them and let QA run again. Refine opens only after hard errors pass. Density warnings about thin claim/evidence substance should be reported and improved when useful, but they do not block Refine.
|
|
104
|
+
|
|
105
|
+
Deck plan report format:
|
|
106
|
+
- Start with \`Deck plan: awaiting confirmation\` when a plan was compiled and has not yet been confirmed.
|
|
102
107
|
- Include narrative readiness status and narrative hash when available.
|
|
103
108
|
- Include whether \`compileDeckPlan\` compiled or skipped.
|
|
109
|
+
- For every slide, include: slide index, title, purpose, narrative role, low-fidelity layout sketch, layout, components, primary/supporting claim ids, evidence binding ids or source summary, visual intent, and caveats/unsupported scope.
|
|
110
|
+
- Use this sketch style or similarly simple ASCII boxes:
|
|
111
|
+
|
|
112
|
+
\`\`\`text
|
|
113
|
+
Slide N: <title>
|
|
114
|
+
|
|
115
|
+
Purpose:
|
|
116
|
+
<one sentence>
|
|
117
|
+
|
|
118
|
+
Layout sketch:
|
|
119
|
+
┌──────────────────────────────────────────────┐
|
|
120
|
+
│ Headline │
|
|
121
|
+
├──────────────────────┬───────────────────────┤
|
|
122
|
+
│ Main chart/media │ Evidence boxes │
|
|
123
|
+
│ │ Source/caveat note │
|
|
124
|
+
└──────────────────────┴───────────────────────┘
|
|
125
|
+
|
|
126
|
+
Layout:
|
|
127
|
+
Components:
|
|
128
|
+
Primary claim:
|
|
129
|
+
Supporting claims:
|
|
130
|
+
Evidence bindings:
|
|
131
|
+
Caveats / unsupported scope:
|
|
132
|
+
\`\`\`
|
|
133
|
+
- End by asking the user to confirm the deck plan or request changes.
|
|
134
|
+
|
|
135
|
+
Report format before any HTML write after confirmation:
|
|
136
|
+
- Start with \`Deck handoff: <status>\`.
|
|
137
|
+
- Include which user-confirmed plan, approved narrative hash, and deck review snapshot authorized the artifact work.
|
|
104
138
|
- If deck/artifact review is blocked, list blockers separately from narrative blockers.
|
|
105
|
-
-
|
|
139
|
+
- After writing HTML, read the appended \`Artifact QA\` report from the tool output. If it failed, fix hard errors before considering the deck ready for Refine.
|
|
106
140
|
|
|
107
141
|
Rules:
|
|
108
142
|
- \`compileDeckPlan\` is the canonical narrative-to-deck planning path. Do not manually invent slide specs to avoid it.
|
|
109
143
|
- Deck slide specs are render-target projections. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, risks, and approval.
|
|
110
144
|
- Applying evidence candidates, rewriting canonical claims, or approving narratives requires explicit user instruction.
|
|
145
|
+
- If the user requests slide order, layout, component, or visual-intent changes that do not alter meaning, update only the deck projection through \`upsertSlides\` and present the revised plan for confirmation.
|
|
146
|
+
- If the user requests claim, evidence, caveat, decision, or recommendation meaning changes, update canonical narrative first and rerun narrative review/approval or explicit render override before compiling a new deck plan.
|
|
111
147
|
- Do not store secrets, credentials, tokens, or sensitive personal information.
|
|
148
|
+
- Artifact QA requires each slide to render exactly 1920x1080px, not merely any 16:9 ratio. It also checks component compliance, text overflow/clipping, page scrollbars, and whether normal QA-enabled content slides have enough claim/evidence/source substance.
|
|
112
149
|
|
|
113
|
-
Start now by reading ${DECKS_STATE_FILE}, reviewing narrative readiness,
|
|
150
|
+
Start now by reading ${DECKS_STATE_FILE}, reviewing narrative readiness, compiling the deck plan only if approval or explicit render override is current, then showing the deck plan with low-fidelity layout sketches and stopping for user confirmation.`
|
|
114
151
|
}
|
|
115
152
|
|
|
116
153
|
export function buildDeckReviewPrompt({
|
package/lib/decks-state.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./workspace-state/review-snapshots"
|
|
19
19
|
import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type WorkspaceAction } from "./workspace-state/types"
|
|
20
20
|
import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
|
|
21
|
+
import { computeNarrativeHash } from "./narrative-state/hash"
|
|
21
22
|
import type { NarrativeStateV1 } from "./narrative-state/types"
|
|
22
23
|
|
|
23
24
|
export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
|
|
@@ -91,6 +92,7 @@ export interface DeckSpec {
|
|
|
91
92
|
researchPlan: ResearchAxis[]
|
|
92
93
|
slides: SlideSpec[]
|
|
93
94
|
assets: DeckAsset[]
|
|
95
|
+
planReview?: DeckPlanReview
|
|
94
96
|
writeReadiness: {
|
|
95
97
|
status: WriteReadinessStatus
|
|
96
98
|
blockers: string[]
|
|
@@ -98,6 +100,15 @@ export interface DeckSpec {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
export interface DeckPlanReview {
|
|
104
|
+
status: "pending" | "confirmed"
|
|
105
|
+
narrativeHash: string
|
|
106
|
+
planHash: string
|
|
107
|
+
confirmedAt?: string
|
|
108
|
+
confirmedBy?: "user"
|
|
109
|
+
summary?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
export interface NarrativeBrief {
|
|
102
113
|
audienceBeliefBefore?: string
|
|
103
114
|
audienceBeliefAfter?: string
|
|
@@ -202,6 +213,7 @@ export type ReadinessSeverity = "blocker" | "warning"
|
|
|
202
213
|
export type ReadinessIssueType =
|
|
203
214
|
| "missing_required_input"
|
|
204
215
|
| "missing_slide_spec"
|
|
216
|
+
| "slide_plan_unconfirmed"
|
|
205
217
|
| "research_not_ready"
|
|
206
218
|
| "missing_evidence"
|
|
207
219
|
| "weak_evidence"
|
|
@@ -278,6 +290,22 @@ const SOURCE_TRACE_ACTION = "Add slide evidence with source plus source trace su
|
|
|
278
290
|
|
|
279
291
|
export interface ReviewDeckStateOptions {
|
|
280
292
|
workspaceRoot?: string
|
|
293
|
+
narrativeHash?: string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface ConfirmDeckPlanOptions {
|
|
297
|
+
approvedBy?: "user"
|
|
298
|
+
note?: string
|
|
299
|
+
now?: string
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface ConfirmDeckPlanResult {
|
|
303
|
+
confirmed: boolean
|
|
304
|
+
skipped: boolean
|
|
305
|
+
reason?: string
|
|
306
|
+
slug?: string
|
|
307
|
+
narrativeHash?: string
|
|
308
|
+
planHash?: string
|
|
281
309
|
}
|
|
282
310
|
|
|
283
311
|
export function decksStatePath(workspaceRoot: string): string {
|
|
@@ -357,10 +385,74 @@ export function createDeckSpec(input: Partial<DeckSpec> & { slug: string }): Dec
|
|
|
357
385
|
researchPlan: input.researchPlan ?? [],
|
|
358
386
|
slides: normalizeSlides(input.slides ?? []),
|
|
359
387
|
assets: input.assets ?? [],
|
|
388
|
+
planReview: normalizeDeckPlanReview(input.planReview),
|
|
360
389
|
writeReadiness: input.writeReadiness ?? { status: "blocked", blockers: [] },
|
|
361
390
|
}
|
|
362
391
|
}
|
|
363
392
|
|
|
393
|
+
export function deckPlanHash(slides: SlideSpec[]): string {
|
|
394
|
+
return createHash("sha1")
|
|
395
|
+
.update(JSON.stringify(normalizeSlides(slides).map((slide) => ({
|
|
396
|
+
index: slide.index,
|
|
397
|
+
title: slide.title,
|
|
398
|
+
purpose: slide.purpose,
|
|
399
|
+
narrativeRole: slide.narrativeRole,
|
|
400
|
+
layout: slide.layout,
|
|
401
|
+
components: slide.components,
|
|
402
|
+
claimIds: slide.claimIds ?? [],
|
|
403
|
+
claimRefs: slide.claimRefs ?? [],
|
|
404
|
+
evidenceBindingIds: slide.evidenceBindingIds ?? [],
|
|
405
|
+
content: slide.content,
|
|
406
|
+
evidence: slide.evidence,
|
|
407
|
+
visuals: slide.visuals ?? [],
|
|
408
|
+
}))))
|
|
409
|
+
.digest("hex")
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function currentDeckPlanReviewStatus(deck: DeckSpec, narrativeHash?: string): { current: boolean; stale: boolean; reason?: string; planHash: string } {
|
|
413
|
+
const planHash = deckPlanHash(deck.slides)
|
|
414
|
+
const review = deck.planReview
|
|
415
|
+
if (!review) return { current: false, stale: false, reason: "deck plan has not been shown and confirmed", planHash }
|
|
416
|
+
if (review.status !== "confirmed") return { current: false, stale: false, reason: "deck plan is pending user confirmation", planHash }
|
|
417
|
+
if (narrativeHash && review.narrativeHash !== narrativeHash) return { current: false, stale: true, reason: "deck plan confirmation is stale because the narrative hash changed", planHash }
|
|
418
|
+
if (review.planHash !== planHash) return { current: false, stale: true, reason: "deck plan confirmation is stale because the slide plan changed", planHash }
|
|
419
|
+
return { current: true, stale: false, planHash }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function confirmDeckPlan(state: DecksState, options: ConfirmDeckPlanOptions = {}): { state: DecksState; result: ConfirmDeckPlanResult } {
|
|
423
|
+
const normalized = normalizeDecksStateWithNarrative(state)
|
|
424
|
+
const key = currentDeckKey(normalized)
|
|
425
|
+
const deck = key ? normalized.decks[key] : undefined
|
|
426
|
+
if (!deck) {
|
|
427
|
+
return { state: normalized, result: { confirmed: false, skipped: true, reason: `No active deck exists in ${DECKS_STATE_FILE}.` } }
|
|
428
|
+
}
|
|
429
|
+
if (deck.slides.length === 0) {
|
|
430
|
+
return { state: normalized, result: { confirmed: false, skipped: true, slug: deck.slug, reason: "Cannot confirm a deck plan with no slides." } }
|
|
431
|
+
}
|
|
432
|
+
const narrative = normalizeNarrativeState(normalized)
|
|
433
|
+
const narrativeHash = computeNarrativeHash(narrative)
|
|
434
|
+
const planHash = deckPlanHash(deck.slides)
|
|
435
|
+
const pending = deck.planReview
|
|
436
|
+
if (pending && pending.status === "pending" && (pending.narrativeHash !== narrativeHash || pending.planHash !== planHash)) {
|
|
437
|
+
return { state: normalized, result: { confirmed: false, skipped: true, slug: deck.slug, narrativeHash, planHash, reason: "Cannot confirm because the pending deck plan is stale. Re-run compileDeckPlan first." } }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
deck.planReview = {
|
|
441
|
+
status: "confirmed",
|
|
442
|
+
narrativeHash,
|
|
443
|
+
planHash,
|
|
444
|
+
confirmedAt: options.now ?? new Date().toISOString(),
|
|
445
|
+
confirmedBy: options.approvedBy ?? "user",
|
|
446
|
+
summary: cleanOptionalText(options.note),
|
|
447
|
+
}
|
|
448
|
+
deck.requiredInputs = { ...deck.requiredInputs, slidePlanConfirmed: true }
|
|
449
|
+
deck.writeReadiness = { status: "blocked", blockers: [] }
|
|
450
|
+
normalized.decks[deck.slug] = deck
|
|
451
|
+
normalized.activeDeck = deck.slug
|
|
452
|
+
normalized.narrative = narrative
|
|
453
|
+
return { state: normalized, result: { confirmed: true, skipped: false, slug: deck.slug, narrativeHash, planHash } }
|
|
454
|
+
}
|
|
455
|
+
|
|
364
456
|
export function readDecksState(workspaceRoot: string): DecksState {
|
|
365
457
|
return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
366
458
|
}
|
|
@@ -487,7 +579,10 @@ export function reviewDeckState(state: DecksState, slug?: string, options: Revie
|
|
|
487
579
|
}
|
|
488
580
|
}
|
|
489
581
|
|
|
490
|
-
const issues = computeDeckReadinessIssues(deck, normalized.workspace,
|
|
582
|
+
const issues = computeDeckReadinessIssues(deck, normalized.workspace, {
|
|
583
|
+
...options,
|
|
584
|
+
narrativeHash: options.narrativeHash ?? computeNarrativeHash(normalizeNarrativeState(normalized)),
|
|
585
|
+
})
|
|
491
586
|
const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
|
|
492
587
|
const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
|
|
493
588
|
const evidenceCandidates = issues.flatMap((issue) => issue.evidenceCandidates ?? [])
|
|
@@ -544,7 +639,9 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
544
639
|
}
|
|
545
640
|
}
|
|
546
641
|
|
|
547
|
-
const issues = computeDeckReadinessIssues(deck, normalized.workspace
|
|
642
|
+
const issues = computeDeckReadinessIssues(deck, normalized.workspace, {
|
|
643
|
+
narrativeHash: computeNarrativeHash(normalizeNarrativeState(normalized)),
|
|
644
|
+
})
|
|
548
645
|
const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
|
|
549
646
|
const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
|
|
550
647
|
if (normalizeDeckPath(deck.outputPath) !== targetPath) {
|
|
@@ -651,7 +748,7 @@ export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 140
|
|
|
651
748
|
}
|
|
652
749
|
let text = JSON.stringify(compact, null, 2)
|
|
653
750
|
if (text.length > maxChars) text = text.slice(0, maxChars).trimEnd() + "\n[DECKS.json state truncated for prompt size.]"
|
|
654
|
-
return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, evidence, render targets, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- ${DECKS_STATE_FILE} deck slides use 1-based \`slides[].index\` values. Render every HTML \`<section class="slide">\` with a matching 1-based \`data-slide-index\` attribute, and do not use 0-based \`data-index\` as slide identity.\n- The active HTML deck is represented as a \`renderTarget\` of type \`html_deck\`; PDF/PPTX exports should be recorded as derived render targets, not as separate deck specs.\n- \`writeReadiness\` is a compatibility projection
|
|
751
|
+
return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, evidence, render targets, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- ${DECKS_STATE_FILE} deck slides use 1-based \`slides[].index\` values. Render every HTML \`<section class="slide">\` with a matching 1-based \`data-slide-index\` attribute, and do not use 0-based \`data-index\` as slide identity.\n- The active HTML deck is represented as a \`renderTarget\` of type \`html_deck\`; PDF/PPTX exports should be recorded as derived render targets, not as separate deck specs.\n- \`writeReadiness\` is a compatibility projection for the /revela make deck generation workflow, not a hard blocker for targeted artifact-level HTML fixes.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- For /revela make deck generated HTML, use the current deck's outputPath and satisfy the deck HTML contract. For targeted artifact-level edits, patch the requested deck HTML directly without treating \`writeReadiness\` or \`planReview\` as a precondition.`
|
|
655
752
|
}
|
|
656
753
|
|
|
657
754
|
function compactWorkspaceForPrompt(workspace: DecksState["workspace"]): DecksState["workspace"] {
|
|
@@ -765,6 +862,18 @@ function normalizeDecksStateWithNarrative(input: DecksState): DecksState {
|
|
|
765
862
|
return state
|
|
766
863
|
}
|
|
767
864
|
|
|
865
|
+
function normalizeDeckPlanReview(input: DeckPlanReview | undefined): DeckPlanReview | undefined {
|
|
866
|
+
if (!input || !input.narrativeHash || !input.planHash) return undefined
|
|
867
|
+
return {
|
|
868
|
+
status: input.status === "confirmed" ? "confirmed" : "pending",
|
|
869
|
+
narrativeHash: input.narrativeHash,
|
|
870
|
+
planHash: input.planHash,
|
|
871
|
+
confirmedAt: cleanOptionalText(input.confirmedAt),
|
|
872
|
+
confirmedBy: input.confirmedBy === "user" ? "user" : undefined,
|
|
873
|
+
summary: cleanOptionalText(input.summary),
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
768
877
|
function currentDeckKey(state: DecksState): string | undefined {
|
|
769
878
|
if (state.activeDeck && state.decks[state.activeDeck]) return state.activeDeck
|
|
770
879
|
const keys = Object.keys(state.decks)
|
|
@@ -800,6 +909,16 @@ function computeDeckReadinessIssues(deck: DeckSpec, workspace: DecksState["works
|
|
|
800
909
|
}
|
|
801
910
|
|
|
802
911
|
if (deck.slides.length === 0) issues.push(blockerIssue("missing_slide_spec", "slides are missing", "Add the confirmed slide plan through revela-decks upsertSlides."))
|
|
912
|
+
if (deck.slides.length > 0) {
|
|
913
|
+
const planReview = currentDeckPlanReviewStatus(deck, options.narrativeHash)
|
|
914
|
+
if (!planReview.current) {
|
|
915
|
+
issues.push(blockerIssue(
|
|
916
|
+
"slide_plan_unconfirmed",
|
|
917
|
+
planReview.stale ? `Deck slide plan confirmation is stale: ${planReview.reason}` : `Deck slide plan is not confirmed: ${planReview.reason}`,
|
|
918
|
+
"Show the compiled deck plan with low-fidelity layout sketches to the user, then call revela-decks confirmDeckPlan only after explicit user confirmation.",
|
|
919
|
+
))
|
|
920
|
+
}
|
|
921
|
+
}
|
|
803
922
|
for (const slide of deck.slides) {
|
|
804
923
|
const slideRef = { slideIndex: slide.index, slideTitle: slide.title }
|
|
805
924
|
if (!slide.title.trim()) issues.push(blockerIssue("missing_slide_spec", `Slide ${slide.index} title is missing`, "Add a slide title to the slide spec.", slideRef))
|
package/lib/design/designs.ts
CHANGED
|
@@ -642,7 +642,6 @@ const UNIVERSAL_CLASSES = new Set([
|
|
|
642
642
|
"slide-canvas",
|
|
643
643
|
"visible",
|
|
644
644
|
"reveal",
|
|
645
|
-
"editable",
|
|
646
645
|
"page",
|
|
647
646
|
"bg",
|
|
648
647
|
"fg",
|
|
@@ -655,7 +654,7 @@ const UNIVERSAL_CLASSES = new Set([
|
|
|
655
654
|
* CSS class prefixes that are always exempt from compliance checks.
|
|
656
655
|
* Third-party libraries (icons, charts) generate classes with these prefixes.
|
|
657
656
|
*/
|
|
658
|
-
export const DEFAULT_PREFIX_EXEMPTIONS: string[] = ["lucide-", "echarts-"
|
|
657
|
+
export const DEFAULT_PREFIX_EXEMPTIONS: string[] = ["lucide-", "echarts-"]
|
|
659
658
|
|
|
660
659
|
export interface DesignClassVocabulary {
|
|
661
660
|
/** Complete set of allowed CSS class names. */
|
package/lib/edit/prompt.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface EditCommentPayload extends EditSelectedElementPayload {
|
|
|
24
24
|
comment: string
|
|
25
25
|
elements?: EditSelectedElementPayload[]
|
|
26
26
|
comments?: EditCommentDraftPayload[]
|
|
27
|
+
asset?: Record<string, unknown>
|
|
28
|
+
drop?: Record<string, unknown>
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export function buildEditPrompt(payload: EditCommentPayload): string {
|
|
@@ -54,6 +56,8 @@ export function buildEditPrompt(payload: EditCommentPayload): string {
|
|
|
54
56
|
deck: payload.deck,
|
|
55
57
|
file: payload.file,
|
|
56
58
|
comments,
|
|
59
|
+
asset: payload.asset,
|
|
60
|
+
drop: payload.drop,
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
return `The user left a visual edit comment on a Revela slide deck.
|
|
@@ -72,14 +76,23 @@ Instructions:
|
|
|
72
76
|
- If there are multiple comments, apply them as one coherent edit pass and avoid changes from one comment overwriting another.
|
|
73
77
|
- Each comment may reference one or more selected elements. Treat the elements in a single comment as a group.
|
|
74
78
|
- Preserve the narrative boundary: if the requested edit changes audience framing, belief shift, decision/action, thesis, recommendation, claim wording, evidence scope, caveat, risk, objection, or decision ask, do not patch the HTML directly. Explain that the canonical narrative must be updated first through ${"`revela-decks`"} action ${"`upsertNarrative`"}, then reviewed/approved or explicitly overridden before updating the deck projection.
|
|
75
|
-
- Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, or deck HTML contract fixes may remain an artifact-level edit.
|
|
79
|
+
- Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, runtime JavaScript fixes, or deck HTML contract fixes may remain an artifact-level edit.
|
|
76
80
|
- If the request mixes content meaning and visual polish, treat it as narrative-impacting unless the user clarifies otherwise.
|
|
77
81
|
- Preserve the existing deck structure, active design language, typography, spacing system, animations, and slide count unless the comment explicitly asks otherwise.
|
|
82
|
+
- If an asset/drop payload is present, this is an asset placement request. Use only the saved local asset path from the asset payload in deck HTML. Prefer asset.deckPath when present because it is relative to the target HTML file; otherwise use asset.path.
|
|
83
|
+
- Do not write remote imageUrl, thumbnailUrl, source page URLs, or ${"`/__revela_asset`"} proxy URLs into deck HTML.
|
|
84
|
+
- Logo assets should remain small, clear, and brand-like; do not use logos as decorative backgrounds.
|
|
85
|
+
- Photography can be cropped or masked when appropriate, but must not cover text, charts, tables, evidence, or important claims.
|
|
86
|
+
- Screenshots, diagrams, charts, tables, and evidence images must remain readable and should not be converted into decorative hero imagery.
|
|
87
|
+
- For asset targetMode ${"`replace`"}, prefer replacing the targeted image or visual element. For ${"`insert-into`"}, place the asset inside the targeted card, media box, or semantic container while preserving that element's layout role. For ${"`add`"}, place the asset near the drop coordinates within the existing layout or semantic box. Do not invent a new visual system when the existing deck grammar can express the placement.
|
|
88
|
+
- If an asset payload is present without drop coordinates, use the user's comment and selected element context to choose placement; if placement remains ambiguous, ask one concise clarification question instead of guessing.
|
|
89
|
+
- Preserve source/license/attribution facts if you surface them in visible notes; do not invent missing licensing or attribution.
|
|
78
90
|
- Do not rewrite unrelated slides or broad sections of the deck.
|
|
79
91
|
- Locate each target primarily with slideIndex, slideTitle, selected text, nearbyText, and outerHTMLExcerpt. Use selector/domPath as hints; they may be approximate.
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
92
|
+
- For targeted artifact-level edits, patch ${"`decks/*.html`"} directly. Do not call ${"`revela-decks`"} action ${"`review`"} as a precondition, and do not let ${"`writeReadiness`"}, ${"`planReview`"}, or ${"`slide_plan_unconfirmed`"} block the patch.
|
|
93
|
+
- Do not patch or write ${"`DECKS.json`"} directly. If state must change, use the ${"`revela-decks`"} tool.
|
|
94
|
+
- Apply the edit to ${payload.file} with the smallest targeted HTML patch that satisfies the comment.
|
|
95
|
+
- Artifact QA runs automatically after deck writes/patches/edits. It checks deck HTML contract, design component compliance, exact 1920x1080 slide geometry, scrollbars, element overflow, text clipping, and claim/evidence content-density warnings.
|
|
96
|
+
- If the tool result reports hard QA errors, fix them with the smallest targeted patch and let the post-write QA run again. Refine opens automatically only after hard errors pass; warnings such as thin claim/evidence substance do not block opening.
|
|
84
97
|
- If the comment is ambiguous, ask one concise clarification question instead of guessing.`
|
|
85
98
|
}
|
package/lib/edit/resolve-deck.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface EditableDeck {
|
|
|
12
12
|
|
|
13
13
|
export function resolveEditableDeck(workspaceRoot: string, input = ""): EditableDeck {
|
|
14
14
|
if (input.trim()) {
|
|
15
|
-
throw new Error("/revela
|
|
15
|
+
throw new Error("/revela refine does not accept a target. It opens the active HTML deck or the only HTML deck in decks/.")
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
if (hasDecksState(workspaceRoot)) {
|
package/lib/inspect/prompt.ts
CHANGED
|
@@ -5,21 +5,25 @@ export function buildInspectionPrompt(input: {
|
|
|
5
5
|
file: string
|
|
6
6
|
projection: InspectionPromptProjection
|
|
7
7
|
language?: string
|
|
8
|
+
comment?: string
|
|
8
9
|
}): string {
|
|
9
10
|
const language = normalizeInspectLanguage(input.language)
|
|
11
|
+
const comment = typeof input.comment === "string" && input.comment.trim() ? input.comment.trim() : ""
|
|
10
12
|
return `A user selected slide content in Revela Evidence Inspector. The selection may contain one referenced element, a whole slide, or multiple referenced elements selected with Cmd/Ctrl-click.
|
|
11
13
|
|
|
12
14
|
Target file: ${input.file}
|
|
13
15
|
Inspection request id: ${input.requestId}
|
|
14
16
|
Display language: ${language}
|
|
17
|
+
User inspect comment: ${comment || "(none; explain purpose and source only)"}
|
|
15
18
|
|
|
16
|
-
Use the structured projection below to produce the final inspector cards. This is LLM judgment with grounded boundaries
|
|
19
|
+
Use the structured projection below to produce the final inspector cards. This is LLM judgment with grounded boundaries. The user's inspect comment is the complete request about the selected reference; do not parse it into a separate question field. The user primarily wants to understand the selected component: what purpose it serves and what source support exists. Use narrative reading and exploratory reading only as internal grounding unless needed to answer the user's comment. Do not edit files. Do not mutate DECKS.json. Do not invent claim ids, evidence binding ids, sources, quotes, URLs, page references, caveats, objections, risks, artifact coverage, or evidence not present in the projection.
|
|
17
20
|
|
|
18
21
|
Language boundary: the selected display language affects only human-readable card copy. Preserve all claim ids, canonical claim ids, evidence binding ids, source paths, findings files, URLs, numbers, quoted/source facts, caveats, artifact ids, and coverage statuses exactly as grounded in the projection. If the display language is Auto, use projection.deck.language when available; otherwise follow the user's/browser context or default to English.
|
|
19
22
|
|
|
20
23
|
Return the result only by calling the \`revela-inspection-result\` tool with this request id. Do not answer in chat.
|
|
21
24
|
|
|
22
25
|
Required card model:
|
|
26
|
+
- User inspect comment: if present, answer it through the Purpose and Source cards first. If it asks about trust, provenance, evidence, factuality, or where a number came from, prioritize Source. If it asks why something is on the slide or what it is doing, prioritize Purpose.
|
|
23
27
|
- Narrative Reading: when the projection includes a matched claim, preserve its claim id, canonical claim id, evidence binding ids, supported scope, unsupported scope, caveats, related objections, related risks, and artifact coverage. Artifact coverage must come only from projection.cards.artifacts; do not invent where a claim appears or whether an artifact is stale/current/partial/missing. If canonical narrative linkage is missing, say so and fall back to the matched slide claim; do not invent canonical ids.
|
|
24
28
|
- Candidate boundary: when projection.match.claim is absent but projection.match.candidateClaims is present, explain the selected child element only within those candidate claim boundaries. You may describe that the child element functions as a detail, prerequisite, source note, risk cue, or evidence cue inside the slide, but you must not select one candidate claim id by semantic guess. If projection.match.confidence is none or candidateClaims is empty, explain the mapping gap instead of inventing a plausible claim.
|
|
25
29
|
- Exploratory Reading: provide bounded, non-official reading cues for objection prep, audience reframing, appendix leads, and meeting prep only from the projection. Mark official as false. Keep missing evidence, caveats, unsupported scope, and stale artifacts visible. Do not make exploratory text sound like approved artifact content, and do not turn this into chat or a fix plan.
|
|
@@ -29,6 +33,7 @@ Required card model:
|
|
|
29
33
|
Boundaries:
|
|
30
34
|
- Do not hunt for problems. If it works, say it works.
|
|
31
35
|
- Do not recommend edits or fixes; this inspector view only explains narrative context, bounded exploratory reading context, purpose, and source credibility.
|
|
36
|
+
- Keep Purpose and Source concise and directly useful. Avoid long narrative-reading exposition unless the selected content cannot be explained without it.
|
|
32
37
|
- Do not turn every caveat into a problem.
|
|
33
38
|
- If confidence is low, use unclear or unknown instead of pretending certainty.
|
|
34
39
|
|
package/lib/media/download.ts
CHANGED
|
@@ -3,11 +3,15 @@ import { extname } from "path"
|
|
|
3
3
|
const MIME_TO_EXT: Record<string, string> = {
|
|
4
4
|
"image/png": ".png",
|
|
5
5
|
"image/jpeg": ".jpg",
|
|
6
|
+
"image/svg+xml": ".svg",
|
|
6
7
|
"image/webp": ".webp",
|
|
7
8
|
"image/gif": ".gif",
|
|
9
|
+
"image/x-icon": ".ico",
|
|
10
|
+
"image/vnd.microsoft.icon": ".ico",
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"])
|
|
13
|
+
const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif", ".ico"])
|
|
14
|
+
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 10_000
|
|
11
15
|
|
|
12
16
|
function normalizeExtension(ext: string): string {
|
|
13
17
|
const value = ext.toLowerCase()
|
|
@@ -30,6 +34,7 @@ export function inferImageExtension(contentType: string | null, sourceName = "")
|
|
|
30
34
|
|
|
31
35
|
export async function downloadImageFromUrl(
|
|
32
36
|
url: string,
|
|
37
|
+
options: { timeoutMs?: number } = {},
|
|
33
38
|
): Promise<{ buffer: Buffer; contentType: string | null; extension: string }> {
|
|
34
39
|
let parsed: URL
|
|
35
40
|
try {
|
|
@@ -42,16 +47,36 @@ export async function downloadImageFromUrl(
|
|
|
42
47
|
throw new Error("INVALID_URL")
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
50
|
+
const controller = new AbortController()
|
|
51
|
+
let timedOut = false
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
timedOut = true
|
|
54
|
+
controller.abort()
|
|
55
|
+
}, Math.max(1, options.timeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS))
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
let response: Response
|
|
58
|
+
let buffer: Buffer
|
|
59
|
+
try {
|
|
60
|
+
response = await fetch(parsed, {
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
|
63
|
+
"User-Agent":
|
|
64
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
|
|
65
|
+
"(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
|
66
|
+
},
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`DOWNLOAD_FAILED:${response.status}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
buffer = Buffer.from(await response.arrayBuffer())
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (timedOut) throw new Error("DOWNLOAD_TIMEOUT")
|
|
77
|
+
throw error
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timer)
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
const contentType = response.headers.get("content-type")
|
|
@@ -61,7 +86,7 @@ export async function downloadImageFromUrl(
|
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
return {
|
|
64
|
-
buffer
|
|
89
|
+
buffer,
|
|
65
90
|
contentType,
|
|
66
91
|
extension,
|
|
67
92
|
}
|
package/lib/media/save.ts
CHANGED
|
@@ -69,6 +69,12 @@ function buildFailureRecord(
|
|
|
69
69
|
alt: input.alt,
|
|
70
70
|
notes: input.notes,
|
|
71
71
|
failureReason,
|
|
72
|
+
provider: input.provider,
|
|
73
|
+
sourcePageUrl: input.sourcePageUrl,
|
|
74
|
+
license: input.license,
|
|
75
|
+
attribution: input.attribution,
|
|
76
|
+
width: input.width,
|
|
77
|
+
height: input.height,
|
|
72
78
|
savedAt: nowIso(),
|
|
73
79
|
}
|
|
74
80
|
}
|
|
@@ -112,6 +118,12 @@ function saveFailureResult(
|
|
|
112
118
|
sourcePath: sourcePath ?? existing.sourcePath,
|
|
113
119
|
alt: input.alt ?? existing.alt,
|
|
114
120
|
notes: input.notes ?? existing.notes,
|
|
121
|
+
provider: input.provider ?? existing.provider,
|
|
122
|
+
sourcePageUrl: input.sourcePageUrl ?? existing.sourcePageUrl,
|
|
123
|
+
license: input.license ?? existing.license,
|
|
124
|
+
attribution: input.attribution ?? existing.attribution,
|
|
125
|
+
width: input.width ?? existing.width,
|
|
126
|
+
height: input.height ?? existing.height,
|
|
115
127
|
failureReason,
|
|
116
128
|
}
|
|
117
129
|
: buildFailureRecord(input, topic, status, failureReason, sourcePath)
|
|
@@ -196,6 +208,12 @@ export async function saveMediaAsset(input: MediaSaveInput, workspaceDir: string
|
|
|
196
208
|
intendedSection: input.intendedSection,
|
|
197
209
|
alt: input.alt,
|
|
198
210
|
notes: input.notes,
|
|
211
|
+
provider: input.provider,
|
|
212
|
+
sourcePageUrl: input.sourcePageUrl,
|
|
213
|
+
license: input.license,
|
|
214
|
+
attribution: input.attribution,
|
|
215
|
+
width: input.width,
|
|
216
|
+
height: input.height,
|
|
199
217
|
savedAt: nowIso(),
|
|
200
218
|
}
|
|
201
219
|
const { manifest: nextManifest } = upsertAsset(manifest, record)
|
|
@@ -248,6 +266,12 @@ export async function saveMediaAsset(input: MediaSaveInput, workspaceDir: string
|
|
|
248
266
|
intendedSection: input.intendedSection,
|
|
249
267
|
alt: input.alt,
|
|
250
268
|
notes: input.notes,
|
|
269
|
+
provider: input.provider,
|
|
270
|
+
sourcePageUrl: input.sourcePageUrl,
|
|
271
|
+
license: input.license,
|
|
272
|
+
attribution: input.attribution,
|
|
273
|
+
width: input.width,
|
|
274
|
+
height: input.height,
|
|
251
275
|
savedAt: nowIso(),
|
|
252
276
|
}
|
|
253
277
|
const { manifest: nextManifest } = upsertAsset(manifest, record)
|