Skip to content

Commit db8291b

Browse files
committed
fix: make demo prompts fully standalone
1 parent 6d8dca8 commit db8291b

6 files changed

Lines changed: 178 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ make tutorial
8282

8383
Everything else, including custom sources, encrypted storage, full theme tokens, and automatic deployment, is optional and documented separately.
8484

85-
Synthetic demo mode includes a **Component lab** with the JSON contract beside each rendered block. Every demo source also includes a **Recreate this page** drawer containing the current base worker and source-specific Markdown prompt, ready for placeholder review and use in a scheduled LLM workflow.
85+
Synthetic demo mode includes a **Component lab** with the JSON contract beside each rendered block. Every demo source also includes a **Recreate this page** drawer containing one standalone scheduled-task prompt. It includes the resolved worker and source, permission boundary, and current executable schemas; the user only replaces the code repository, private data repository, and timezone placeholders before review and use.
8686

8787
The canonical daily bundle contains agenda, inbox attention, work focus, money, news, and the dependency-backed daily overview. Prompt Studio can also create smaller independent bundles, and now generates the matching private-repository setup command.
8888

docs/llm-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ Use [`prompts/base-worker.md`](../prompts/base-worker.md) as the operational con
8686

8787
The safe contract composes complexity in three layers: a page `layout` (`dashboard`, `focus`, or `timeline`), a block `span` (`one`, `two`, or `full`), and up to 16 audited blocks. The producer chooses information shape; it never chooses React components, CSS classes, renderer props, or executable behavior.
8888

89-
In synthetic demo mode, open **Component lab** to inspect a validated JSON block beside its live rendered result. Every synthetic source page also exposes **Recreate this page**, which combines the current base worker and registered domain prompt into copy-ready Markdown. These demonstrations come from the same schemas and renderer used for private snapshots, not a separate mock UI.
89+
In synthetic demo mode, open **Component lab** to inspect a validated JSON block beside its live rendered result. Every synthetic source page also exposes **Recreate this page**, which produces one standalone Markdown document. It resolves the source and worker IDs and embeds the base worker, domain instructions, exact source registration and permission boundary, snapshot schema, UI schema, registered domain schema, LLM contract, and privacy contract. Only the code repository, private data repository, and timezone placeholders remain. These demonstrations come from the same schemas and renderer used for private snapshots, not a separate mock UI.

scripts/build-data-index.mjs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
22
import path from "node:path"
33
import { decryptSnapshotEnvelope, loadSnapshotKey } from "./lib/snapshot-crypto.mjs"
4+
import { assembleStandaloneDemoPrompt } from "./lib/demo-prompt.mjs"
45
import { snapshotFreshness } from "./lib/freshness.mjs"
56
import { resolvedExperience } from "./lib/setup-options.mjs"
67

@@ -68,15 +69,37 @@ const latest = sourceDefinitions.map((definition) => ({
6869
snapshot: bySource[definition.id]?.at(-1) || null,
6970
freshnessState: snapshotFreshness(bySource[definition.id]?.at(-1) || null),
7071
}))
72+
const demoContract = demoMode
73+
? await Promise.all([
74+
readFile(path.join(root, "prompts/base-worker.md"), "utf8"),
75+
readJson("schemas/snapshot.schema.json"),
76+
readJson("schemas/ui-blocks.schema.json"),
77+
readFile(path.join(root, "docs/llm-contract.md"), "utf8"),
78+
readFile(path.join(root, "docs/privacy.md"), "utf8"),
79+
])
80+
: null
7181
const demoPromptsBySource = demoMode
7282
? Object.fromEntries(
7383
await Promise.all(
7484
sourceDefinitions.map(async (definition) => {
75-
const [basePrompt, domainPrompt] = await Promise.all([
76-
readFile(path.join(root, "prompts/base-worker.md"), "utf8"),
85+
const [domainPrompt, domainSchema] = await Promise.all([
7786
readFile(path.join(root, definition.prompt), "utf8"),
87+
readJson(definition.schema_ref),
7888
])
79-
return [definition.id, `${basePrompt.trim()}\n\n---\n\n${domainPrompt.trim()}\n`]
89+
const [basePrompt, snapshotSchema, uiBlocksSchema, llmContract, privacyContract] = demoContract
90+
return [
91+
definition.id,
92+
assembleStandaloneDemoPrompt({
93+
basePrompt,
94+
domainPrompt,
95+
definition,
96+
snapshotSchema,
97+
uiBlocksSchema,
98+
domainSchema,
99+
llmContract,
100+
privacyContract,
101+
}),
102+
]
80103
}),
81104
),
82105
)

scripts/lib/demo-prompt.mjs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
const stripHeading = (markdown) => markdown.trim().replace(/^# .+\n+/, "")
2+
3+
const nestHeadings = (markdown) => markdown.trim().replace(/^(#{1,4}) /gm, (_, hashes) => `${"#".repeat(Math.min(6, hashes.length + 2))} `)
4+
5+
const resolveRegistration = (markdown, definition) =>
6+
markdown.replaceAll("{{SOURCE_ID}}", definition.id).replaceAll("{{WORKER_ID}}", definition.worker_id)
7+
8+
export function assembleStandaloneDemoPrompt({
9+
basePrompt,
10+
domainPrompt,
11+
definition,
12+
snapshotSchema,
13+
uiBlocksSchema,
14+
domainSchema,
15+
llmContract,
16+
privacyContract,
17+
}) {
18+
const workerInstructions = resolveRegistration(stripHeading(basePrompt), definition)
19+
.replace(/^Copy this contract together with one domain prompt[^\n]*\n+/, "")
20+
.replace("- the domain prompt supplied with this contract", "- the embedded domain instructions in this document")
21+
const domainInstructions = resolveRegistration(stripHeading(domainPrompt), definition)
22+
.replace(/^Use with \[`base-worker\.md`\]\(base-worker\.md\)\.\n+/, "")
23+
.replace(
24+
`Set ${definition.id} to ${definition.id} and ${definition.worker_id} to ${definition.worker_id}.`,
25+
`This document is registered for source ${definition.id} and worker ${definition.worker_id}.`,
26+
)
27+
const embeddedLlmContract = llmContract.replace(/^Use \[`prompts\/base-worker\.md`\][^\n]*\n+/m, "")
28+
const json = (value) => JSON.stringify(value, null, 2)
29+
30+
return `# ${definition.label} scheduled-task prompt
31+
32+
> This is a standalone Zaati OS prompt. The worker instructions, registered source, permission boundary, and current executable schemas are embedded below; no linked local prompt file is required.
33+
34+
## Replace before scheduling
35+
36+
Replace these three environment-specific placeholders everywhere they appear:
37+
38+
- \`{{CODE_REPOSITORY}}\` — the public Zaati OS code repository
39+
- \`{{DATA_REPOSITORY}}\` — the private repository that owns real snapshots
40+
- \`{{TIMEZONE}}\` — the user's IANA timezone, such as \`Asia/Karachi\`
41+
42+
The source and worker identifiers are already resolved to \`${definition.id}\` and \`${definition.worker_id}\`. Review the embedded permission boundary before giving the workflow access to any source.
43+
44+
## Worker instructions
45+
46+
${workerInstructions}
47+
48+
## Domain instructions
49+
50+
${domainInstructions}
51+
52+
## Registered source and permission boundary
53+
54+
\`\`\`json
55+
${json(definition)}
56+
\`\`\`
57+
58+
## Snapshot envelope schema
59+
60+
\`\`\`json
61+
${json(snapshotSchema)}
62+
\`\`\`
63+
64+
## Safe UI blocks schema
65+
66+
\`\`\`json
67+
${json(uiBlocksSchema)}
68+
\`\`\`
69+
70+
## Registered domain schema
71+
72+
\`\`\`json
73+
${json(domainSchema)}
74+
\`\`\`
75+
76+
## LLM contract
77+
78+
The embedded contract is the copy-time baseline. The worker must still read and obey the current default-branch files on every run and stop if they are missing or incompatible.
79+
80+
${nestHeadings(embeddedLlmContract)}
81+
82+
## Privacy contract
83+
84+
${nestHeadings(privacyContract)}
85+
`
86+
}

src/App.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -672,11 +672,28 @@ function DashboardPage({
672672
}
673673

674674
function PromptDrawer({ prompt, sourceLabel }: { prompt: string; sourceLabel: string }) {
675-
const [copied, setCopied] = useState(false)
675+
const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">("idle")
676676
const copyPrompt = async () => {
677-
await navigator.clipboard.writeText(prompt)
678-
setCopied(true)
679-
window.setTimeout(() => setCopied(false), 1800)
677+
try {
678+
await Promise.race([
679+
navigator.clipboard.writeText(prompt),
680+
new Promise<never>((_, reject) => window.setTimeout(() => reject(new Error("Clipboard permission timed out.")), 600)),
681+
])
682+
setCopyState("copied")
683+
} catch {
684+
const field = document.createElement("textarea")
685+
field.value = prompt
686+
field.setAttribute("readonly", "")
687+
field.style.position = "fixed"
688+
field.style.opacity = "0"
689+
document.body.append(field)
690+
field.focus()
691+
field.select()
692+
const copied = document.execCommand("copy")
693+
field.remove()
694+
setCopyState(copied ? "copied" : "failed")
695+
}
696+
window.setTimeout(() => setCopyState("idle"), 2400)
680697
}
681698
return (
682699
<Dialog>
@@ -691,12 +708,13 @@ function PromptDrawer({ prompt, sourceLabel }: { prompt: string; sourceLabel: st
691708
<div>
692709
<DialogTitle className="text-base font-semibold">{sourceLabel} scheduled-task prompt</DialogTitle>
693710
<DialogDescription className="mt-1 text-sm leading-6 text-muted-foreground">
694-
Replace every placeholder, review the permission boundary, then paste this Markdown into your LLM workflow.
711+
One standalone Markdown prompt with the worker, source registration, permissions, and current schemas included. Replace the
712+
three environment placeholders, review it, then paste the complete document into your LLM workflow.
695713
</DialogDescription>
696714
</div>
697715
<Button aria-live="polite" onClick={() => void copyPrompt()} size="sm" variant="secondary">
698-
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
699-
{copied ? "Copied" : "Copy"}
716+
{copyState === "copied" ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
717+
{copyState === "copied" ? "Complete prompt copied" : copyState === "failed" ? "Select and copy below" : "Copy complete prompt"}
700718
</Button>
701719
</div>
702720
<div className="min-h-0 flex-1 overflow-auto bg-muted/35 p-4 sm:p-5">

tests/contracts.test.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,49 @@
11
import assert from "node:assert/strict"
22
import { readFile } from "node:fs/promises"
33
import test from "node:test"
4+
import { assembleStandaloneDemoPrompt } from "../scripts/lib/demo-prompt.mjs"
45
import { validateSnapshotPolicy } from "../scripts/lib/snapshot-policy.mjs"
56
import { validateCustomTheme } from "../scripts/lib/theme-contrast.mjs"
67

78
const readJson = async (file) => JSON.parse(await readFile(file, "utf8"))
9+
test("demo scheduled-task prompts are standalone and embed the complete contract", async () => {
10+
const registry = await readJson("config/sources.json")
11+
const definition = registry.sources.find((source) => source.id === "money:pulse")
12+
const [basePrompt, domainPrompt, snapshotSchema, uiBlocksSchema, domainSchema, llmContract, privacyContract] = await Promise.all([
13+
readFile("prompts/base-worker.md", "utf8"),
14+
readFile(definition.prompt, "utf8"),
15+
readJson("schemas/snapshot.schema.json"),
16+
readJson("schemas/ui-blocks.schema.json"),
17+
readJson(definition.schema_ref),
18+
readFile("docs/llm-contract.md", "utf8"),
19+
readFile("docs/privacy.md", "utf8"),
20+
])
21+
const prompt = assembleStandaloneDemoPrompt({
22+
basePrompt,
23+
domainPrompt,
24+
definition,
25+
snapshotSchema,
26+
uiBlocksSchema,
27+
domainSchema,
28+
llmContract,
29+
privacyContract,
30+
})
31+
32+
assert.match(prompt, /This is a standalone Zaati OS prompt/)
33+
assert.match(prompt, /You are a registered Zaati OS snapshot worker/)
34+
assert.match(prompt, /Use only user-approved normalized totals/)
35+
assert.match(prompt, /registered for source money:pulse and worker money-pulse-daily/)
36+
assert.doesNotMatch(prompt, /Set money:pulse to money:pulse/)
37+
assert.match(prompt, /"id": "money:pulse"/)
38+
assert.match(prompt, /"worker_id": "money-pulse-daily"/)
39+
assert.match(prompt, /"target_path": "data\/snapshots\/money\/pulse/)
40+
assert.ok(prompt.includes(JSON.stringify(snapshotSchema, null, 2)))
41+
assert.ok(prompt.includes(JSON.stringify(uiBlocksSchema, null, 2)))
42+
assert.ok(prompt.includes(JSON.stringify(domainSchema, null, 2)))
43+
assert.doesNotMatch(prompt, /base-worker\.md/)
44+
assert.doesNotMatch(prompt, /\{\{SOURCE_ID\}\}|\{\{WORKER_ID\}\}/)
45+
assert.deepEqual([...new Set(prompt.match(/\{\{[A-Z_]+\}\}/g))].sort(), ["{{CODE_REPOSITORY}}", "{{DATA_REPOSITORY}}", "{{TIMEZONE}}"])
46+
})
847
test("safe UI contract exposes only audited block kinds", async () => {
948
const schema = await readJson("schemas/ui-blocks.schema.json")
1049
const kinds = schema.$defs.block.oneOf.map((item) => item.$ref.split("/").at(-1)).sort()

0 commit comments

Comments
 (0)