wicked-interactive 0.4.0
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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/ensure-siblings.mjs +94 -0
- package/bin/wi-watch.mjs +111 -0
- package/bin/wicked-interactive.js +96 -0
- package/frontend/dist/assets/index-Df5rc-Mm.js +41 -0
- package/frontend/dist/assets/index-Dq_AQHYX.css +1 -0
- package/frontend/dist/index.html +13 -0
- package/package.json +40 -0
- package/src/core/feedback-schema.js +124 -0
- package/src/core/instrument.js +116 -0
- package/src/core/regenerate.js +140 -0
- package/src/core/theme.js +79 -0
- package/src/core/versions.js +109 -0
- package/src/index.js +7 -0
- package/src/service/bus.js +30 -0
- package/src/service/demo.js +411 -0
- package/src/service/export.js +124 -0
- package/src/service/fsstore.js +26 -0
- package/src/service/generation.js +105 -0
- package/src/service/preflight.js +84 -0
- package/src/service/server.js +580 -0
- package/src/service/structural.js +103 -0
- package/src/service/theme-source.js +63 -0
- package/src/service/workspace.js +141 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// structural.js — delegate structural-change edits to the supervising agent (ADR-0010).
|
|
2
|
+
//
|
|
3
|
+
// The service writes requests/_v{n}.request.json; the agent edits each fragment
|
|
4
|
+
// (preserving data-wid) and writes requests/_v{n}.response.json; the service applies the
|
|
5
|
+
// response through the INV-2 gate, producing a follow-on version (write-once, INV-4).
|
|
6
|
+
|
|
7
|
+
import { readFileSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import * as cheerio from "cheerio";
|
|
10
|
+
import { regenerate } from "../core/regenerate.js";
|
|
11
|
+
import { instrument } from "../core/instrument.js";
|
|
12
|
+
import { themed } from "./theme-source.js";
|
|
13
|
+
import { recordVersion, nextVersionNumber } from "../core/versions.js";
|
|
14
|
+
import { atomicWrite, loadManifest, saveManifest, readVersionHtml } from "./fsstore.js";
|
|
15
|
+
|
|
16
|
+
export const REQUESTS_DIR = "requests";
|
|
17
|
+
|
|
18
|
+
/** Partition feedback items into the two engine paths. */
|
|
19
|
+
export function splitItems(items) {
|
|
20
|
+
const deterministic = [];
|
|
21
|
+
const structural = [];
|
|
22
|
+
for (const it of items) (it.type === "structural-change" ? structural : deterministic).push(it);
|
|
23
|
+
return { deterministic, structural };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The serialized current outerHTML of the element a structural item targets. */
|
|
27
|
+
export function extractFragment(html, selector) {
|
|
28
|
+
const $ = cheerio.load(html, null, false);
|
|
29
|
+
const el = $(`[data-wid="${selector}"]`);
|
|
30
|
+
return el.length ? $.html(el) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rootWid = (fragmentHtml) => {
|
|
34
|
+
const $ = cheerio.load(fragmentHtml, null, false);
|
|
35
|
+
return $("[data-wid]").first().attr("data-wid") || null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write the structural work request for version `version` (the partial). Each item gets
|
|
40
|
+
* its current fragment extracted from the partial HTML so the agent edits the real markup.
|
|
41
|
+
* @returns {{ requestFile: string, count: number }}
|
|
42
|
+
*/
|
|
43
|
+
export function writeStructuralRequest(dir, { version, baseHtmlFile, structural, documentId = dir }) {
|
|
44
|
+
mkdirSync(join(dir, REQUESTS_DIR), { recursive: true });
|
|
45
|
+
const html = readFileSync(join(dir, baseHtmlFile), "utf-8");
|
|
46
|
+
const items = structural.map((it) => ({
|
|
47
|
+
selector: it.selector,
|
|
48
|
+
instruction: it.instruction,
|
|
49
|
+
fragment: extractFragment(html, it.selector),
|
|
50
|
+
}));
|
|
51
|
+
const requestFile = `_v${version}.request.json`;
|
|
52
|
+
const body = { document_id: documentId, version, base_html: baseHtmlFile, items };
|
|
53
|
+
atomicWrite(join(dir, REQUESTS_DIR, requestFile), JSON.stringify(body, null, 2));
|
|
54
|
+
return { requestFile, count: items.length };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply an agent response: finalize the structural edits as a follow-on version.
|
|
59
|
+
* Response shape: { version, results: [{ selector, fragment }] }.
|
|
60
|
+
* The INV-2 gate (in regenerate) rejects any fragment that dropped a data-wid.
|
|
61
|
+
* @returns {Promise<{version:number, parent:number, applied:string[], rejected:object[]}>}
|
|
62
|
+
*/
|
|
63
|
+
export async function applyStructuralResponse(dir, responseFile, opts = {}) {
|
|
64
|
+
const resp = JSON.parse(readFileSync(join(dir, REQUESTS_DIR, responseFile), "utf-8"));
|
|
65
|
+
const parent = resp.version;
|
|
66
|
+
const bySelector = new Map(resp.results.map((r) => [r.selector, r.fragment]));
|
|
67
|
+
|
|
68
|
+
const baseHtml = readVersionHtml(dir, parent);
|
|
69
|
+
const feedback = {
|
|
70
|
+
items: resp.results.map((r) => (r.remove
|
|
71
|
+
? { selector: r.selector, type: "remove" }
|
|
72
|
+
: { selector: r.selector, type: "structural-change", instruction: "(delegated)" })),
|
|
73
|
+
};
|
|
74
|
+
const llm = async (fragmentBefore) => {
|
|
75
|
+
const sel = rootWid(fragmentBefore);
|
|
76
|
+
const frag = bySelector.get(sel);
|
|
77
|
+
if (frag == null) throw new Error(`no agent result for ${sel}`);
|
|
78
|
+
return frag;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const { html: regenerated, applied, rejected } = await regenerate(baseHtml, feedback, { llm });
|
|
82
|
+
// Re-instrument so any new h2/p/li in the structural fragment pick up a data-wid.
|
|
83
|
+
// Without this, new content from the agent stays unclickable in the editor — INV-1
|
|
84
|
+
// still preserves existing wids, INV-2 already passed inside regenerate, so this is
|
|
85
|
+
// strictly additive.
|
|
86
|
+
// Re-apply the base theme (idempotent) so agent-produced versions stay themed (ADR-0016
|
|
87
|
+
// Slice C). Anchor-free, so the INV-2 gate that just passed is unaffected.
|
|
88
|
+
const html = themed(instrument(regenerated).html, opts);
|
|
89
|
+
|
|
90
|
+
let manifest = loadManifest(dir);
|
|
91
|
+
const version = nextVersionNumber(manifest);
|
|
92
|
+
atomicWrite(join(dir, `_v${version}.html`), html);
|
|
93
|
+
({ manifest } = recordVersion(manifest, { version, parent, feedbackFile: responseFile }));
|
|
94
|
+
saveManifest(dir, manifest);
|
|
95
|
+
|
|
96
|
+
if (typeof opts.emit === "function") {
|
|
97
|
+
opts.emit("HTML_UPDATED", {
|
|
98
|
+
document_id: opts.documentId ?? dir,
|
|
99
|
+
version, html_file: `_v${version}.html`, prev_version: parent, ts: new Date().toISOString(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return { version, parent, applied, rejected };
|
|
103
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// theme-source.js — resolve theme tokens from the wicked-prezzie plugin cache (ADR-0016).
|
|
2
|
+
//
|
|
3
|
+
// wicked-prezzie is a required sibling plugin but is NOT importable as a library (it's a
|
|
4
|
+
// plugin/skill). We locate its `skills/theme/themes/<name>.json` under the same plugin search
|
|
5
|
+
// paths the preflight uses and read the tokens. On any miss we fall back to the bundled
|
|
6
|
+
// DEFAULT_THEME from core — resilience against prezzie's on-disk layout shifting, not
|
|
7
|
+
// plugin-optional behavior (the preflight still requires prezzie to be installed).
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { pluginSearchPaths } from "./preflight.js";
|
|
12
|
+
import { DEFAULT_THEME, applyTheme } from "../core/theme.js";
|
|
13
|
+
|
|
14
|
+
/** Locate prezzie's `skills/theme/themes` dir under any resolved plugin search path. */
|
|
15
|
+
export function prezzieThemesDir() {
|
|
16
|
+
for (const base of pluginSearchPaths()) {
|
|
17
|
+
const root = join(base, "wicked-prezzie");
|
|
18
|
+
if (!existsSync(root)) continue;
|
|
19
|
+
// Direct (test-fixture layout) then the nested pkg/version cache layout.
|
|
20
|
+
const candidates = [join(root, "skills/theme/themes")];
|
|
21
|
+
let pkgs = [];
|
|
22
|
+
try { pkgs = readdirSync(root); } catch { pkgs = []; }
|
|
23
|
+
for (const pkg of pkgs) {
|
|
24
|
+
const pkgDir = join(root, pkg);
|
|
25
|
+
candidates.push(join(pkgDir, "skills/theme/themes"));
|
|
26
|
+
let vers = [];
|
|
27
|
+
try { vers = readdirSync(pkgDir); } catch { vers = []; }
|
|
28
|
+
for (const ver of vers) candidates.push(join(pkgDir, ver, "skills/theme/themes"));
|
|
29
|
+
}
|
|
30
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a theme token object by name. Reads prezzie's JSON if present; falls back to the
|
|
37
|
+
* bundled DEFAULT_THEME. Never throws — a missing/corrupt file degrades to the default so a
|
|
38
|
+
* version is always produced.
|
|
39
|
+
* @param {string} [name]
|
|
40
|
+
* @param {object} [opts] { themesDir?: string } — themesDir overrides discovery (tests)
|
|
41
|
+
*/
|
|
42
|
+
export function resolveThemeTokens(name = "corporate-light", opts = {}) {
|
|
43
|
+
const dir = opts.themesDir || prezzieThemesDir();
|
|
44
|
+
if (dir) {
|
|
45
|
+
const file = join(dir, `${name}.json`);
|
|
46
|
+
try {
|
|
47
|
+
if (existsSync(file)) return JSON.parse(readFileSync(file, "utf-8"));
|
|
48
|
+
} catch { /* fall through to default */ }
|
|
49
|
+
}
|
|
50
|
+
return DEFAULT_THEME;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the theme and apply it to an HTML string in one step — the seam used at every
|
|
55
|
+
* version-creation point. `opts.theme === false` skips theming; a string picks the theme by
|
|
56
|
+
* name; otherwise the default (`corporate-light`) is used.
|
|
57
|
+
*/
|
|
58
|
+
export function themed(html, opts = {}) {
|
|
59
|
+
if (opts.theme === false) return html;
|
|
60
|
+
const name = typeof opts.theme === "string" ? opts.theme : "corporate-light";
|
|
61
|
+
const tokens = resolveThemeTokens(name, opts);
|
|
62
|
+
return applyTheme(html, { tokens });
|
|
63
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// workspace.js — a document workspace on disk and the feedback->regenerate pipeline.
|
|
2
|
+
//
|
|
3
|
+
// Layout (one directory per document):
|
|
4
|
+
// _v0.html, _v1.html, ... version artifacts (write-once, INV-4)
|
|
5
|
+
// _v1.md, _v2.md, ... feedback files (no _v0.md — v0 is the initial build)
|
|
6
|
+
// versions.json parent-pointer manifest (ADR-0008)
|
|
7
|
+
// requests/ structural-change delegation to the agent (ADR-0010)
|
|
8
|
+
//
|
|
9
|
+
// The service is the SINGLE writer of feedback files (ADR-0002): writes are atomic so the
|
|
10
|
+
// watcher never reads a half-written file. Deterministic edits apply immediately;
|
|
11
|
+
// structural edits are delegated to the supervising agent (ADR-0010).
|
|
12
|
+
|
|
13
|
+
import { readFileSync, mkdirSync, readdirSync, copyFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { instrument } from "../core/instrument.js";
|
|
16
|
+
import { parseFeedback, serializeFeedback } from "../core/feedback-schema.js";
|
|
17
|
+
import { regenerate } from "../core/regenerate.js";
|
|
18
|
+
import { initManifest, recordVersion, getVersion, nextVersionNumber } from "../core/versions.js";
|
|
19
|
+
import { atomicWrite, loadManifest, saveManifest, readVersionHtml } from "./fsstore.js";
|
|
20
|
+
import { splitItems, writeStructuralRequest } from "./structural.js";
|
|
21
|
+
import { themed } from "./theme-source.js";
|
|
22
|
+
|
|
23
|
+
// Re-export the store reads so existing callers (server, tests) keep their import path.
|
|
24
|
+
export { loadManifest, readVersionHtml } from "./fsstore.js";
|
|
25
|
+
|
|
26
|
+
const versionFromHtmlFile = (f) => Number(/_v(\d+)\.html$/.exec(f)?.[1]);
|
|
27
|
+
|
|
28
|
+
// Highest _v{n}.{md,html} on disk — so rapid writes reserve distinct numbers even before
|
|
29
|
+
// the manifest is updated (two quick UPDATEs must not both grab _v1.md).
|
|
30
|
+
function highestVersionOnDisk(dir) {
|
|
31
|
+
let max = -1;
|
|
32
|
+
for (const f of readdirSync(dir)) {
|
|
33
|
+
const m = /^_v(\d+)\.(md|html)$/.exec(f);
|
|
34
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
35
|
+
}
|
|
36
|
+
return max;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialise a workspace from an HTML draft. Instruments it with data-wid (unless
|
|
41
|
+
* opts.instrument === false), writes _v0.html, and seeds the manifest.
|
|
42
|
+
*/
|
|
43
|
+
export function initWorkspace(dir, html, opts = {}) {
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
const anchored = opts.instrument === false ? html : instrument(html).html;
|
|
46
|
+
// Apply the prezzie-derived base theme so every version (including v0) shares a consistent
|
|
47
|
+
// look (ADR-0016 Slice C). Idempotent + anchor-free, so INV-1/INV-2 are unaffected.
|
|
48
|
+
const prepared = themed(anchored, opts);
|
|
49
|
+
atomicWrite(join(dir, "_v0.html"), prepared);
|
|
50
|
+
const manifest = initManifest("_v0.html", { kind: opts.kind });
|
|
51
|
+
saveManifest(dir, manifest);
|
|
52
|
+
return { manifest };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Write a feedback file as the single writer. Allocates the next version number,
|
|
57
|
+
* validates the feedback (round-trips through the schema), and writes _v{n}.md atomically.
|
|
58
|
+
* Does NOT touch the manifest — the version becomes real only once its HTML is produced.
|
|
59
|
+
*/
|
|
60
|
+
export function writeFeedback(dir, { items, author }) {
|
|
61
|
+
const manifest = loadManifest(dir);
|
|
62
|
+
const base = getVersion(manifest, manifest.head);
|
|
63
|
+
const version = Math.max(nextVersionNumber(manifest), highestVersionOnDisk(dir) + 1);
|
|
64
|
+
const feedback = {
|
|
65
|
+
frontmatter: {
|
|
66
|
+
version, base_html: base.html_file, timestamp: new Date().toISOString(),
|
|
67
|
+
...(author ? { author } : {}),
|
|
68
|
+
},
|
|
69
|
+
items,
|
|
70
|
+
};
|
|
71
|
+
const md = serializeFeedback(feedback);
|
|
72
|
+
parseFeedback(md); // validate by round-trip; throws on invalid schema
|
|
73
|
+
const file = `_v${version}.md`;
|
|
74
|
+
atomicWrite(join(dir, file), md);
|
|
75
|
+
return { version, file };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fork from an existing version (AC-21): non-destructive "start again from here". Copies
|
|
80
|
+
* _v{from}.html to a new write-once version whose parent is `from`; the new version becomes
|
|
81
|
+
* the head. Nothing is removed (AC-22).
|
|
82
|
+
* @returns {{ version: number, parent: number }}
|
|
83
|
+
*/
|
|
84
|
+
export function forkVersion(dir, from) {
|
|
85
|
+
let manifest = loadManifest(dir);
|
|
86
|
+
if (getVersion(manifest, from) == null) throw new Error(`fork: v${from} does not exist`);
|
|
87
|
+
const version = Math.max(nextVersionNumber(manifest), highestVersionOnDisk(dir) + 1);
|
|
88
|
+
copyFileSync(join(dir, `_v${from}.html`), join(dir, `_v${version}.html`));
|
|
89
|
+
({ manifest } = recordVersion(manifest, { version, parent: from, feedbackFile: null }));
|
|
90
|
+
saveManifest(dir, manifest);
|
|
91
|
+
return { version, parent: from };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Process a feedback file: apply the DETERMINISTIC edits immediately (cheerio), write the
|
|
96
|
+
* partial _v{n}.html, record the version, and emit. STRUCTURAL items are delegated to the
|
|
97
|
+
* supervising agent via a request file (ADR-0010) and finalized later as a follow-on
|
|
98
|
+
* version. Idempotent on (version).
|
|
99
|
+
* @returns {Promise<{version,html_file,applied,rejected,stale,awaiting_structural}>}
|
|
100
|
+
*/
|
|
101
|
+
export async function processFeedbackFile(dir, mdFile, opts = {}) {
|
|
102
|
+
const md = readFileSync(join(dir, mdFile), "utf-8");
|
|
103
|
+
const feedback = parseFeedback(md);
|
|
104
|
+
const version = feedback.frontmatter.version;
|
|
105
|
+
const parent = versionFromHtmlFile(feedback.frontmatter.base_html);
|
|
106
|
+
|
|
107
|
+
let manifest = loadManifest(dir);
|
|
108
|
+
if (getVersion(manifest, version) != null) {
|
|
109
|
+
const existing = getVersion(manifest, version);
|
|
110
|
+
return { version, html_file: existing.html_file, applied: [], rejected: [], stale: [], awaiting_structural: 0, idempotent: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { deterministic, structural } = splitItems(feedback.items);
|
|
114
|
+
const prevHtml = readVersionHtml(dir, parent);
|
|
115
|
+
const { html: regenerated, applied, rejected, stale } = await regenerate(prevHtml, { items: deterministic }, {});
|
|
116
|
+
// Re-instrument so any new h2/p/li introduced by the regen pick up a data-wid.
|
|
117
|
+
// instrument() preserves existing wids (INV-1), only ADDS for untagged blocks; safe to
|
|
118
|
+
// run after INV-2 has already passed. Without this, content added via structural-change
|
|
119
|
+
// stays unclickable in the editor.
|
|
120
|
+
// Re-apply the base theme (idempotent — replaces the inherited block) so the version stays
|
|
121
|
+
// themed after regeneration (ADR-0016 Slice C).
|
|
122
|
+
const html = themed(instrument(regenerated).html, opts);
|
|
123
|
+
atomicWrite(join(dir, `_v${version}.html`), html);
|
|
124
|
+
({ manifest } = recordVersion(manifest, { version, parent, feedbackFile: mdFile }));
|
|
125
|
+
saveManifest(dir, manifest);
|
|
126
|
+
|
|
127
|
+
if (typeof opts.emit === "function") {
|
|
128
|
+
opts.emit("HTML_UPDATED", {
|
|
129
|
+
document_id: opts.documentId ?? dir,
|
|
130
|
+
version, html_file: `_v${version}.html`, prev_version: parent, ts: new Date().toISOString(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let awaiting_structural = 0;
|
|
135
|
+
if (structural.length) {
|
|
136
|
+
({ count: awaiting_structural } = writeStructuralRequest(dir, {
|
|
137
|
+
version, baseHtmlFile: `_v${version}.html`, structural, documentId: opts.documentId ?? dir,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
return { version, html_file: `_v${version}.html`, applied, rejected, stale, awaiting_structural };
|
|
141
|
+
}
|