@jini-ai/vibecoding 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +154 -0
  3. package/dist/core/apply.d.ts +69 -0
  4. package/dist/core/apply.js +69 -0
  5. package/dist/core/history.d.ts +144 -0
  6. package/dist/core/history.js +132 -0
  7. package/dist/core/index.d.ts +19 -0
  8. package/dist/core/index.js +3 -0
  9. package/dist/core/target.d.ts +92 -0
  10. package/dist/core/target.js +2 -0
  11. package/dist/core/types.d.ts +86 -0
  12. package/dist/core/types.js +19 -0
  13. package/dist/html/index.d.ts +12 -0
  14. package/dist/html/index.js +2 -0
  15. package/dist/html/node/index.d.ts +10 -0
  16. package/dist/html/node/index.js +10 -0
  17. package/dist/html/node/parse5-region-parser.d.ts +12 -0
  18. package/dist/html/node/parse5-region-parser.js +213 -0
  19. package/dist/html/regions.d.ts +137 -0
  20. package/dist/html/regions.js +165 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +10 -0
  23. package/dist/react/components/DocumentPreview.d.ts +59 -0
  24. package/dist/react/components/DocumentPreview.js +44 -0
  25. package/dist/react/components/PartsViewer.d.ts +49 -0
  26. package/dist/react/components/PartsViewer.js +61 -0
  27. package/dist/react/components/VibecodingWorkbench.d.ts +38 -0
  28. package/dist/react/components/VibecodingWorkbench.js +96 -0
  29. package/dist/react/index.d.ts +29 -0
  30. package/dist/react/index.js +7 -0
  31. package/dist/react/session.d.ts +108 -0
  32. package/dist/react/session.js +160 -0
  33. package/dist/react/tools.d.ts +91 -0
  34. package/dist/react/tools.js +246 -0
  35. package/dist/react/use-vibecoding-session.d.ts +30 -0
  36. package/dist/react/use-vibecoding-session.js +44 -0
  37. package/package.json +99 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @module core/target
3
+ *
4
+ * `EditTarget` — the one seam between this package's loop and whatever is being authored.
5
+ *
6
+ * Four operations cover the artifact (list / read / replace / snapshot), plus `restore` as
7
+ * snapshot's counterpart and `validate` as the guard around `replace`. That set was arrived at by
8
+ * tracing two shipped AI app-builders end to end and then trying to break it: creation, deletion,
9
+ * ordering, cross-part moves and binary parts were all predicted to leak through and none of them
10
+ * did. `replace` being an upsert absorbs creation; deletion and ordering turn out to be host
11
+ * concerns that no model-facing edit protocol needs to express.
12
+ *
13
+ * ## Why `validate` exists, and why it takes the whole prospective artifact
14
+ *
15
+ * Neither reference implementation validates writes at all. They get away with it because their
16
+ * unit is a whole file, and **a malformed file cannot corrupt another file's syntax**. That
17
+ * property does not survive the move to sub-document parts: a tagged region lives inside one
18
+ * shared document, so a single unbalanced tag corrupts everything after it. Nothing in either
19
+ * codebase solves this, so there was nothing to copy — it is the one genuinely new piece here.
20
+ *
21
+ * It receives the **whole prospective artifact**, not the part in isolation, because a fragment
22
+ * that is well-formed on its own can still break the document it lands in — a table cell outside a
23
+ * table, a region that closes a parent's tag. Validating the part alone would pass both.
24
+ *
25
+ * ## What is deliberately absent
26
+ *
27
+ * There are no verbs for running processes, installing dependencies, or building. Those are real
28
+ * needs for a file-tree host and they belong to a separate execution layer — putting them here
29
+ * would make the port undescribable for hosts that have no such tier, which is precisely the
30
+ * over-fitting this seam exists to prevent. Their absence is a design decision, not a gap.
31
+ */
32
+ import type { PartId, PartRef, Snapshot, ValidationResult } from "./types.js";
33
+ /**
34
+ * The host-supplied artifact port.
35
+ *
36
+ * Implementations are expected to be small: a file-tree host maps parts to files, a
37
+ * single-document host maps them to addressable regions. Nothing here assumes a filesystem, a
38
+ * DOM, or a framework.
39
+ */
40
+ export interface EditTarget {
41
+ /**
42
+ * Every part a model may currently be asked to rewrite.
43
+ *
44
+ * This is the allowlist. A part omitted here is structurally unaddressable rather than merely
45
+ * discouraged — which is where scope discipline actually comes from. Prompt severity is not a
46
+ * substitute, and measurably is not one: a shipped builder with three prompt variants enforces
47
+ * identical scope rules with wildly different tone, because the constraint lives in the
48
+ * addressing, not the wording.
49
+ */
50
+ listParts(): Promise<readonly PartRef[]>;
51
+ /** Current content of one part. Rejects if `id` was not published by `listParts`. */
52
+ readPart(args: {
53
+ readonly id: PartId;
54
+ }): Promise<string>;
55
+ /**
56
+ * Write one part's content, creating it if absent — this is an **upsert**.
57
+ *
58
+ * Callers must not invoke this directly when a `validate` implementation exists; go through the
59
+ * apply loop, which validates first. Implementations should assume `content` is model-authored
60
+ * and therefore arbitrary.
61
+ */
62
+ replacePart(args: {
63
+ readonly id: PartId;
64
+ readonly content: string;
65
+ }): Promise<void>;
66
+ /** Capture every part's content. See `Snapshot` — data only, never execution state. */
67
+ snapshot(): Promise<Snapshot>;
68
+ /**
69
+ * Restore previously captured content.
70
+ *
71
+ * Returns once the data is back. Any downstream execution state is the host's to re-sync
72
+ * afterwards; this call makes no claim about it.
73
+ */
74
+ restore(args: {
75
+ readonly snapshot: Snapshot;
76
+ }): Promise<void>;
77
+ /**
78
+ * Decide whether a prospective change may be committed.
79
+ *
80
+ * Called with the artifact **as it would be** if `id` were replaced by `content`. A host whose
81
+ * parts cannot corrupt one another (a file tree, typically) may return `{ ok: true }`
82
+ * unconditionally; the cost of the hook is then a function call.
83
+ *
84
+ * A rejection's `reason` is fed to the model as its next turn, so it should describe the defect
85
+ * in terms the model can act on.
86
+ */
87
+ validate(candidate: {
88
+ readonly id: PartId;
89
+ readonly content: string;
90
+ }): Promise<ValidationResult>;
91
+ }
92
+ //# sourceMappingURL=target.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=target.js.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module core/types
3
+ *
4
+ * The vocabulary of a conversational authoring session, deliberately independent of *what* is
5
+ * being authored.
6
+ *
7
+ * The whole design rests on one observation: every AI app-builder differs at exactly one place —
8
+ * the artifact being edited. One tool edits a file tree; another edits a single markup document.
9
+ * The conversation, the streaming, the undo, and the preview are the same in all of them. So this
10
+ * package owns the loop and knows nothing about the target, which reaches it as a small port
11
+ * (see `./target.ts`).
12
+ *
13
+ * Terminology: an artifact is made of **parts**. A part is the unit a model may be asked to
14
+ * rewrite — a file for a file-tree host, a tagged region for a single-document host. Parts are
15
+ * addressed by an opaque `PartId` that the host publishes; a caller never supplies a selector or a
16
+ * path of its own devising.
17
+ */
18
+ /**
19
+ * An opaque handle to one editable part, minted and published by the host.
20
+ *
21
+ * It is an **allowlist entry, not a query**. A model or caller may only name an id the host has
22
+ * already listed, which is what keeps a model-authored string from becoming a path traversal or an
23
+ * arbitrary DOM selector. Hosts that expose filesystem paths or CSS selectors as ids are
24
+ * responsible for validating them at the boundary before publishing them.
25
+ */
26
+ export type PartId = string;
27
+ /** One editable part as advertised to the model, without its content. */
28
+ export interface PartRef {
29
+ /** The host-published handle used to read or replace this part. */
30
+ readonly id: PartId;
31
+ /**
32
+ * Optional host-meaningful category (e.g. a language, a region role). Purely descriptive —
33
+ * the loop never branches on it, it exists so a host can shape its own prompt.
34
+ */
35
+ readonly kind?: string;
36
+ /**
37
+ * Optional human-facing label. Host-authored and therefore **untrusted** for display: treat it
38
+ * as text, never as markup.
39
+ */
40
+ readonly label?: string;
41
+ }
42
+ /**
43
+ * A point-in-time copy of every part's content.
44
+ *
45
+ * Deliberately a flat id → content map and nothing more. Two consequences worth stating, because
46
+ * the reference implementation this shape was taken from conflates them:
47
+ *
48
+ * 1. **A snapshot restores DATA, never an execution environment.** A host with downstream state —
49
+ * a dev server, a build, an installed dependency tree — re-syncs itself *after* `restore`
50
+ * returns. If this type implied "and everything depending on these parts works again," it would
51
+ * be promising something it cannot deliver for any host but the simplest.
52
+ * 2. It carries no ordering and no tree structure. A host that needs those reconstructs them from
53
+ * its own ids, which it minted and therefore understands.
54
+ */
55
+ export interface Snapshot {
56
+ /** Identifies this snapshot within a session. Assigned by the caller that took it. */
57
+ readonly id: string;
58
+ /** Every part's content at capture time, keyed by `PartId`. */
59
+ readonly parts: Readonly<Record<PartId, string>>;
60
+ }
61
+ /** A host's verdict on a prospective change, returned by `EditTarget.validate`. */
62
+ export type ValidationResult = {
63
+ readonly ok: true;
64
+ } | {
65
+ readonly ok: false;
66
+ /**
67
+ * Why the change was refused, phrased for the **model** rather than for a log. This string
68
+ * is fed back as the next turn's input, so it should name the defect concretely
69
+ * (e.g. "unclosed <section> — the document no longer parses") rather than say "invalid".
70
+ */
71
+ readonly reason: string;
72
+ };
73
+ /** What happened to one `replace` attempt. */
74
+ export type ApplyOutcome = {
75
+ readonly status: "applied";
76
+ readonly id: PartId;
77
+ } | {
78
+ readonly status: "rejected";
79
+ readonly id: PartId;
80
+ readonly reason: string;
81
+ } | {
82
+ readonly status: "failed";
83
+ readonly id: PartId;
84
+ readonly error: Error;
85
+ };
86
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module core/types
3
+ *
4
+ * The vocabulary of a conversational authoring session, deliberately independent of *what* is
5
+ * being authored.
6
+ *
7
+ * The whole design rests on one observation: every AI app-builder differs at exactly one place —
8
+ * the artifact being edited. One tool edits a file tree; another edits a single markup document.
9
+ * The conversation, the streaming, the undo, and the preview are the same in all of them. So this
10
+ * package owns the loop and knows nothing about the target, which reaches it as a small port
11
+ * (see `./target.ts`).
12
+ *
13
+ * Terminology: an artifact is made of **parts**. A part is the unit a model may be asked to
14
+ * rewrite — a file for a file-tree host, a tagged region for a single-document host. Parts are
15
+ * addressed by an opaque `PartId` that the host publishes; a caller never supplies a selector or a
16
+ * path of its own devising.
17
+ */
18
+ export {};
19
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @module html
3
+ *
4
+ * The single-document `EditTarget`: parts are tagged regions of one HTML document.
5
+ *
6
+ * Framework-free like `/core` — the HTML parser arrives as an injected port rather than a
7
+ * dependency, so this entry point stays usable from a server, a worker or a browser without
8
+ * dragging a parser into any of them.
9
+ */
10
+ export type { HtmlDocumentStore, HtmlRegionParser, HtmlRegionTargetDeps, HtmlRegionTargetOptions, ParsedRegion, } from "./regions.js";
11
+ export { AGENT_ELEMENT_ATTRIBUTE, createHtmlRegionTarget, isValidRegionHandle, } from "./regions.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { AGENT_ELEMENT_ATTRIBUTE, createHtmlRegionTarget, isValidRegionHandle, } from "./regions.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module html/node
3
+ *
4
+ * The Node-only `HtmlRegionParser` implementation for `../regions.js`'s injected port — parse5,
5
+ * because that dependency is the one thing keeping `./html` itself framework- and runtime-free. See
6
+ * `./parse5-region-parser.ts` for the implementation and its findings.
7
+ * See docs/decisions/DR-001-conservative-region-detection.md.
8
+ */
9
+ export { createParse5RegionParser, isValidRegionHandle } from "./parse5-region-parser.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module html/node
3
+ *
4
+ * The Node-only `HtmlRegionParser` implementation for `../regions.js`'s injected port — parse5,
5
+ * because that dependency is the one thing keeping `./html` itself framework- and runtime-free. See
6
+ * `./parse5-region-parser.ts` for the implementation and its findings.
7
+ * See docs/decisions/DR-001-conservative-region-detection.md.
8
+ */
9
+ export { createParse5RegionParser, isValidRegionHandle } from "./parse5-region-parser.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ import { isValidRegionHandle, type HtmlRegionParser } from "../regions.js";
2
+ /**
3
+ * Build the parse5-backed `HtmlRegionParser`. Stateless — a single instance is safe to share as a
4
+ * module-level singleton across every call, per `./regions.js`'s injected-port contract.
5
+ *
6
+ * @returns a parser ready to hand to `createHtmlRegionTarget`, with `checkWellFormed` always
7
+ * present even though the generic parser port allows implementations to omit it.
8
+ * @complexity 1 (delegates entirely to `analyzeDocument`)
9
+ */
10
+ export declare function createParse5RegionParser(): HtmlRegionParser & Required<Pick<HtmlRegionParser, "checkWellFormed">>;
11
+ export { isValidRegionHandle };
12
+ //# sourceMappingURL=parse5-region-parser.d.ts.map
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @module html/node/parse5-region-parser
3
+ *
4
+ * The `HtmlRegionParser` (`../regions.js`) has no implementation anywhere until this file — this is
5
+ * it: a spec-compliant WHATWG HTML parser (parse5, the same one jsdom depends on), Node-only because
6
+ * parse5 is Node-only, which is why this lives behind `./html/node` rather than the dependency-free
7
+ * `./html` entry `regions.ts` itself ships from.
8
+ *
9
+ * ## Fragment, not document
10
+ *
11
+ * A host's "one HTML document" (per `regions.ts`'s own module doc) may genuinely be a whole page,
12
+ * but the shape this was built against is inner content only — a string meant to be spliced into an
13
+ * existing container, not a standalone `<html>` page. `parse5.parseFragment` (the same algorithm
14
+ * browsers use for `Element.innerHTML`) is the correct entry point for that shape, parsed against a
15
+ * `<div>` context element: a generic block container, matching how a real host embeds this content.
16
+ * Getting this choice right is not cosmetic — the fragment context materially changes recovery
17
+ * behavior (verified empirically; see the section below), so a `<template>` context (parse5's
18
+ * own default when none is given) would UNDER-detect a real failure mode this module exists to
19
+ * catch.
20
+ *
21
+ * ## — a tagged element the parser could not reliably locate must never be silently dropped
22
+ *
23
+ * `regions.ts`'s own contract states the requirement (`findRegions`'s doc: "must report ALL of
24
+ * them... a parser that filters them out silently defeats the allowlist check"). Silently omitting
25
+ * an unlocatable region is actively dangerous, not merely incomplete: the omitted handle vanishes
26
+ * from `listParts`'s next call, and the region's own multiset check (`regions.ts`'s `validate`) then
27
+ * reports the *next*, unrelated edit to that document as "removes existing handle" — a permanent
28
+ * lockout with a reason that misdescribes the actual defect, because nothing was removed; the parser
29
+ * lost track of something still in the DOM.
30
+ *
31
+ * **This was verified empirically against real parse5 output before being implemented — not
32
+ * inferred from parse5's docs alone.** Two distinct mechanisms were confirmed reachable:
33
+ *
34
+ * 1. **A located-but-incomplete node.** parse5's own docs state `sourceCodeLocation` is `undefined`
35
+ * for elements the parser implicitly created during tree correction; concretely, this shows up as
36
+ * a tagged element whose `sourceCodeLocation.endTag` is missing — an implicitly-closed element at
37
+ * EOF, or (structurally, always) a tagged void element (`<img>`, `<br>`,...), which has no
38
+ * content model and therefore can never have a closing tag to locate.
39
+ * 2. **A node dropped from the tree entirely — no location to inspect because no node exists.**
40
+ * Confirmed with a tagged `<td>` used outside a `<table>`, parsed with a `<div>` fragment context:
41
+ * the HTML5 "in body" insertion mode's table-tag handling is a spec-mandated "parse error, ignore
42
+ * the token" — the element (and its `data-agent-element` attribute) never enters the tree at all.
43
+ * Neither `sourceCodeLocationInfo` nor `onParseError` flags this case on its own (verified: this
44
+ * fixture produces zero `onParseError` entries), which is why this module cross-checks the raw
45
+ * source text's occurrence count against what the tree walk actually found, independent of
46
+ * parse5's own error stream.
47
+ *
48
+ * Both mechanisms are handled uniformly: `findRegions` throws (never returns a silently-shortened
49
+ * list), and `checkWellFormed` reports the same problem through the port's ordinary rejection
50
+ * channel so `regions.ts`'s `validate` can surface it as a model-facing reason before `findRegions`
51
+ * is ever reached a second time for the same prospective document.
52
+ *
53
+ * ## `checkWellFormed` is a courtesy diagnostic, not the safety mechanism
54
+ *
55
+ * Per the port's own doc: "omit the method entirely if the parser cannot distinguish a malformed
56
+ * document from a recovered one." parse5 genuinely cannot always tell, so `checkWellFormed` is
57
+ * honest about that — ordinary spec-compliant recovery that produces no `onParseError` entry (e.g.
58
+ * an implied `</p>` before a following block element) is allowed to pass. Only two things fail it:
59
+ * a location problem (above), or at least one genuine `onParseError` entry, phrased for the
60
+ * model. The handle-multiset check in `regions.ts`'s `validate` remains the actual security
61
+ * property; this method exists to give the model a better reason sooner, not to replace that check.
62
+ * See docs/decisions/DR-001-conservative-region-detection.md.
63
+ */
64
+ import { defaultTreeAdapter, html, parseFragment } from "parse5";
65
+ import { AGENT_ELEMENT_ATTRIBUTE, isValidRegionHandle } from "../regions.js";
66
+ /**
67
+ * HTML5 void elements — no content model, so they can never have a closing tag for parse5 to
68
+ * locate. Tagging one with `data-agent-element` is a structural impossibility for a "region" (a
69
+ * span of editable inner content), not a parser-recovery accident, so it gets a distinct message.
70
+ */
71
+ const VOID_ELEMENTS = new Set([
72
+ "area",
73
+ "base",
74
+ "br",
75
+ "col",
76
+ "embed",
77
+ "hr",
78
+ "img",
79
+ "input",
80
+ "link",
81
+ "meta",
82
+ "param",
83
+ "source",
84
+ "track",
85
+ "wbr",
86
+ ]);
87
+ /** Matches `data-agent-element` as an attribute *name* only — deliberately not the value or its
88
+ * quoting style, so it survives single-quoted, double-quoted and unquoted attribute forms alike.
89
+ *
90
+ * This is a plain substring/regex scan of the raw source, the same cost/precision tradeoff
91
+ * `withFormGuardrail` ( Decision 8) makes for its own guardrail: a comment or script-text
92
+ * mention of the attribute name can inflate the count and cause an over-eager rejection, but that
93
+ * false positive is a safe failure mode (the model is asked to retry), whereas the false negative
94
+ * this check exists to catch — a genuinely dropped region — is not.
95
+ * See docs/decisions/DR-001-conservative-region-detection.md.
96
+ */
97
+ const RAW_HANDLE_ATTRIBUTE_RE = new RegExp(`\\b${AGENT_ELEMENT_ATTRIBUTE}\\s*=`, "gi");
98
+ function attributeValue(element, name) {
99
+ return element.attrs.find((a) => a.name === name)?.value;
100
+ }
101
+ /** Yields every element node in the tree, in document order, recursing into `<template>` content. */
102
+ function* walkElements(node) {
103
+ if ("tagName" in node)
104
+ yield node;
105
+ const children = "content" in node ? node.content.childNodes : "childNodes" in node ? node.childNodes : undefined;
106
+ if (children === undefined)
107
+ return;
108
+ for (const child of children)
109
+ yield* walkElements(child);
110
+ }
111
+ /** Fresh `<div>` fragment-context element — see this module's doc for why `<div>`, not the default. */
112
+ function divFragmentContext() {
113
+ return defaultTreeAdapter.createElement("div", html.NS.HTML, []);
114
+ }
115
+ /** A parse-error entry, phrased for the model rather than left as parse5's bare kebab-case code. */
116
+ function describeParseError(error) {
117
+ return `${error.code.replace(/-/g, " ")} near character ${error.startOffset}`;
118
+ }
119
+ /**
120
+ * One parse pass, shared by `findRegions` and `checkWellFormed` so both methods judge the same
121
+ * parse5 run rather than parsing the document twice with a chance of disagreeing.
122
+ *
123
+ * @complexity Linear in document size: one parse5 pass plus one tree walk plus one regex scan.
124
+ */
125
+ function analyzeDocument(html_) {
126
+ const parseErrorReasons = [];
127
+ const fragment = parseFragment(divFragmentContext(), html_, {
128
+ sourceCodeLocationInfo: true,
129
+ onParseError: (error) => parseErrorReasons.push(describeParseError(error)),
130
+ });
131
+ const regions = [];
132
+ let locationProblem;
133
+ for (const element of walkElements(fragment)) {
134
+ const handle = attributeValue(element, AGENT_ELEMENT_ATTRIBUTE);
135
+ if (handle === undefined)
136
+ continue;
137
+ if (locationProblem !== undefined)
138
+ continue; // Already failed; stop accumulating regions.
139
+ const location = element.sourceCodeLocation;
140
+ if (location === null || location === undefined || location.startTag === undefined) {
141
+ locationProblem =
142
+ `a "${AGENT_ELEMENT_ATTRIBUTE}" region ("${handle}") could not be located in the document — ` +
143
+ "the parser's error-recovery rules likely dropped or repositioned it. Rewrite the document " +
144
+ "so this element is reachable in ordinary document structure (for example, not a table-only " +
145
+ "tag used outside a <table>).";
146
+ continue;
147
+ }
148
+ if (location.endTag === undefined) {
149
+ locationProblem = VOID_ELEMENTS.has(element.tagName)
150
+ ? `"${handle}" tags a <${element.tagName}> element, which is void and has no inner content — ` +
151
+ `void elements cannot be a ${AGENT_ELEMENT_ATTRIBUTE} region`
152
+ : `a "${AGENT_ELEMENT_ATTRIBUTE}" region ("${handle}") has no closing tag the parser could ` +
153
+ "locate — its content boundaries cannot be determined safely. Close the tag explicitly.";
154
+ continue;
155
+ }
156
+ regions.push({
157
+ handle,
158
+ innerStart: location.startTag.endOffset,
159
+ innerEnd: location.endTag.startOffset,
160
+ ...(attributeValue(element, "data-agent-role") === undefined
161
+ ? {}
162
+ : { role: attributeValue(element, "data-agent-role") }),
163
+ ...(attributeValue(element, "data-agent-label") === undefined
164
+ ? {}
165
+ : { label: attributeValue(element, "data-agent-label") }),
166
+ });
167
+ }
168
+ if (locationProblem === undefined) {
169
+ const rawMentions = html_.match(RAW_HANDLE_ATTRIBUTE_RE)?.length ?? 0;
170
+ if (rawMentions > regions.length) {
171
+ locationProblem =
172
+ `the source mentions ${AGENT_ELEMENT_ATTRIBUTE} ${rawMentions} time(s) but only ` +
173
+ `${regions.length} could be located in the parsed document — the parser's error-recovery ` +
174
+ "rules likely dropped one entirely (a common cause: a table-only tag, such as <td>, used " +
175
+ "outside a <table>). Rewrite the document so every tagged element sits in ordinary, " +
176
+ "non-table-recovery document structure.";
177
+ }
178
+ }
179
+ return locationProblem === undefined
180
+ ? { regions, parseErrorReasons }
181
+ : { regions, locationProblem, parseErrorReasons };
182
+ }
183
+ /**
184
+ * Build the parse5-backed `HtmlRegionParser`. Stateless — a single instance is safe to share as a
185
+ * module-level singleton across every call, per `./regions.js`'s injected-port contract.
186
+ *
187
+ * @returns a parser ready to hand to `createHtmlRegionTarget`, with `checkWellFormed` always
188
+ * present even though the generic parser port allows implementations to omit it.
189
+ * @complexity 1 (delegates entirely to `analyzeDocument`)
190
+ */
191
+ export function createParse5RegionParser() {
192
+ return {
193
+ findRegions({ html: html_ }) {
194
+ const { regions, locationProblem } = analyzeDocument(html_);
195
+ if (locationProblem !== undefined)
196
+ throw new Error(locationProblem);
197
+ return regions;
198
+ },
199
+ checkWellFormed({ html: html_ }) {
200
+ const { locationProblem, parseErrorReasons } = analyzeDocument(html_);
201
+ if (locationProblem !== undefined)
202
+ return { ok: false, reason: locationProblem };
203
+ if (parseErrorReasons.length > 0) {
204
+ return { ok: false, reason: `the document does not parse cleanly: ${parseErrorReasons.join("; ")}` };
205
+ }
206
+ return { ok: true };
207
+ },
208
+ };
209
+ }
210
+ // Re-exported so a caller can reuse the same grammar this parser's handles are checked against
211
+ // without importing `../regions.js` directly for one symbol.
212
+ export { isValidRegionHandle };
213
+ //# sourceMappingURL=parse5-region-parser.js.map
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @module html/regions
3
+ *
4
+ * An `EditTarget` whose parts are **tagged regions of one HTML document** — the shape a
5
+ * single-document host needs, as opposed to the file tree a multi-file host edits.
6
+ *
7
+ * This is the target the port was designed against but never had an implementation for, and it is
8
+ * where `validate` stops being a formality: a file cannot corrupt another file's syntax, but a
9
+ * region shares one document with every other region, so one unbalanced tag corrupts everything
10
+ * after it.
11
+ *
12
+ * ## Addressing reuses `data-agent-element`, deliberately
13
+ *
14
+ * Handles are the existing `data-agent-element` convention (`@jini-ai/agentic`'s
15
+ * `element-handles.ts`), including its handle grammar — lowercase words joined by single hyphens,
16
+ * bounded length, and no quote, bracket, backslash or whitespace. That grammar exists so a handle
17
+ * cannot break out of the attribute selector it gets interpolated into, and it serves equally well
18
+ * here: a `PartId` a model can name is a string a model authored.
19
+ *
20
+ * Minting a new product-specific attribute was considered and rejected twice over — the convention
21
+ * already exists with `region` as a defined role, and a product-named attribute could not live in
22
+ * this workspace anyway.
23
+ *
24
+ * ## THE SECURITY PROPERTY: a model may not extend its own allowlist
25
+ *
26
+ * `listParts` is an allowlist, and an allowlist a model can write to is not an allowlist. If a model
27
+ * may edit region `hero`, and it writes `data-agent-element="anything"` inside `hero`, then the very
28
+ * next `listParts()` publishes `anything` as addressable — the model has granted itself a new part
29
+ * without the host ever deciding to. Every subsequent turn inherits the expanded surface.
30
+ *
31
+ * So `validate` refuses any candidate whose prospective document does not carry **exactly the same
32
+ * multiset of handles** as the current one. That covers all three ways this breaks: a handle
33
+ * invented, a handle deleted, and a handle duplicated (which would make an id ambiguous and let one
34
+ * write land somewhere the host did not intend). Neither reference implementation has an equivalent
35
+ * check, because neither has sub-document parts — there was nothing to copy.
36
+ *
37
+ * ## Writes are byte-preserving outside the edited region
38
+ *
39
+ * `replacePart` is a string splice across the region's inner offsets: everything outside those
40
+ * offsets is carried through untouched, byte for byte. This is a deliberate departure from the
41
+ * visual editor studied for this design, which re-parses, regenerates and pretty-prints the whole
42
+ * file on every write — semantically faithful, but it reformats the document out from under the
43
+ * author, and it does so even to files it failed to parse. A host that stores author-written HTML
44
+ * cannot accept that.
45
+ *
46
+ * ## The parser is injected, not imported
47
+ *
48
+ * This package is `"runtime": "universal"`, and no HTML parser exists anywhere in this workspace, so
49
+ * hard-wiring one would either make this module Node-only or push a parser into every browser
50
+ * bundle. It arrives as a port instead — the same choice, for the same reason, that `EditTarget`
51
+ * itself is a port. A browser host can satisfy it with `DOMParser`; a server host with whatever it
52
+ * already depends on.
53
+ */
54
+ import type { EditTarget } from "../core/target.js";
55
+ /** The attribute that publishes a region's handle. Mirrors `@jini-ai/agentic`'s convention. */
56
+ export declare const AGENT_ELEMENT_ATTRIBUTE = "data-agent-element";
57
+ /** Whether `handle` is a syntactically valid region handle. */
58
+ export declare function isValidRegionHandle({ handle }: {
59
+ readonly handle: string;
60
+ }): boolean;
61
+ /** One tagged region located in a document, as reported by the injected parser. */
62
+ export interface ParsedRegion {
63
+ /** The `data-agent-element` value. */
64
+ readonly handle: string;
65
+ /** The `data-agent-role` value, if the markup carries one. */
66
+ readonly role?: string;
67
+ /** The `data-agent-label` value, if present. Page-authored and therefore untrusted for display. */
68
+ readonly label?: string;
69
+ /** Index of the first character of the region's INNER content. */
70
+ readonly innerStart: number;
71
+ /** Index one past the last character of the region's inner content. */
72
+ readonly innerEnd: number;
73
+ }
74
+ /**
75
+ * The host-supplied HTML parser.
76
+ *
77
+ * Implementations must report regions in document order with accurate inner offsets — every write
78
+ * this module performs is a splice across those offsets, so an incorrect offset corrupts the
79
+ * document silently.
80
+ */
81
+ export interface HtmlRegionParser {
82
+ /**
83
+ * Locate every element carrying `data-agent-element`.
84
+ *
85
+ * Must report ALL of them, including duplicates and handles that fail `isValidRegionHandle` —
86
+ * this module needs to *see* invalid and duplicated handles in order to refuse them. A parser
87
+ * that filters them out silently defeats the allowlist check.
88
+ */
89
+ findRegions(args: {
90
+ readonly html: string;
91
+ }): readonly ParsedRegion[];
92
+ /**
93
+ * Optional structural check on a whole document.
94
+ *
95
+ * Return a reason phrased for the MODEL, since it is fed back as the next turn's input. Omit the
96
+ * method entirely if the parser cannot distinguish a malformed document from a recovered one —
97
+ * that is honest, and the handle-set check below still catches the damage that matters most.
98
+ */
99
+ checkWellFormed?(args: {
100
+ readonly html: string;
101
+ }): {
102
+ readonly ok: true;
103
+ } | {
104
+ readonly ok: false;
105
+ readonly reason: string;
106
+ };
107
+ }
108
+ /** Where the document itself lives. The host owns storage; this module never assumes one. */
109
+ export interface HtmlDocumentStore {
110
+ read(): Promise<string>;
111
+ write(args: {
112
+ readonly html: string;
113
+ }): Promise<void>;
114
+ }
115
+ export interface HtmlRegionTargetOptions {
116
+ /**
117
+ * Reject a proposed region body larger than this many characters.
118
+ *
119
+ * Content is model-authored and arrives over a network boundary; without a bound, one turn can
120
+ * grow the stored document without limit. Defaults to 256 KiB, generous for a page region.
121
+ */
122
+ readonly maxPartLength?: number;
123
+ }
124
+ export interface HtmlRegionTargetDeps {
125
+ readonly store: HtmlDocumentStore;
126
+ readonly parser: HtmlRegionParser;
127
+ }
128
+ /**
129
+ * Build an `EditTarget` over the tagged regions of one HTML document.
130
+ *
131
+ * @param deps - document storage and an HTML parser, both host-supplied.
132
+ * @param options - optional region size limit, separate from the required ports.
133
+ * @returns a target ready to hand to `applyEdit`/`applyEdits` or to `createEditHistory`.
134
+ * @complexity 4
135
+ */
136
+ export declare function createHtmlRegionTarget(deps: HtmlRegionTargetDeps, options?: HtmlRegionTargetOptions): EditTarget;
137
+ //# sourceMappingURL=regions.d.ts.map