@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.
- package/CHANGELOG.md +44 -0
- package/README.md +154 -0
- package/dist/core/apply.d.ts +69 -0
- package/dist/core/apply.js +69 -0
- package/dist/core/history.d.ts +144 -0
- package/dist/core/history.js +132 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +3 -0
- package/dist/core/target.d.ts +92 -0
- package/dist/core/target.js +2 -0
- package/dist/core/types.d.ts +86 -0
- package/dist/core/types.js +19 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.js +2 -0
- package/dist/html/node/index.d.ts +10 -0
- package/dist/html/node/index.js +10 -0
- package/dist/html/node/parse5-region-parser.d.ts +12 -0
- package/dist/html/node/parse5-region-parser.js +213 -0
- package/dist/html/regions.d.ts +137 -0
- package/dist/html/regions.js +165 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/react/components/DocumentPreview.d.ts +59 -0
- package/dist/react/components/DocumentPreview.js +44 -0
- package/dist/react/components/PartsViewer.d.ts +49 -0
- package/dist/react/components/PartsViewer.js +61 -0
- package/dist/react/components/VibecodingWorkbench.d.ts +38 -0
- package/dist/react/components/VibecodingWorkbench.js +96 -0
- package/dist/react/index.d.ts +29 -0
- package/dist/react/index.js +7 -0
- package/dist/react/session.d.ts +108 -0
- package/dist/react/session.js +160 -0
- package/dist/react/tools.d.ts +91 -0
- package/dist/react/tools.js +246 -0
- package/dist/react/use-vibecoding-session.d.ts +30 -0
- package/dist/react/use-vibecoding-session.js +44 -0
- 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,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,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
|