@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0 — 2026-10-02
4
+
5
+ ### BREAKING
6
+
7
+ - Pure kernel containment and isolated HTML/React entries remain; shared ports and public object-argument contracts replace local vocabulary.
8
+ - Distribution includes runtime output, release documentation and required assets only. Process records and per-job neutrality checks are no longer part of the package surface.
9
+
10
+ ## Unreleased
11
+
12
+ - Fixed compilation against the current core contracts: tool input errors use `{ message }`,
13
+ and registry integration tests use object arguments for registry creation and listing.
14
+ The parse5 factory return type now exposes its always-present `checkWellFormed` method.
15
+ Error messages and parser behavior are unchanged.
16
+
17
+ - Integration follow-up: reconciled the `./core`, `./html`, `./html/node` and `./react`
18
+ surfaces, including HTML parser/storage ports, session/history, discoverable tool registrations,
19
+ direct tool runner, React hook and preview components. Export metadata and optional React peers
20
+ were already present and are retained. No version change or new dependency is required.
21
+ - BREAKING: `createVibecodingSession({ target }, { historyOptions })` separates required ports
22
+ from optional tuning. `VibecodingSessionOptions` now contains only history tuning; the required
23
+ target lives in the new `VibecodingSessionArgs` type. `useVibecodingSession`,
24
+ `createVibecodingToolRegistrations` and `createVibecodingToolRunner` receive `{ session }`.
25
+ - BREAKING: session subscriptions, part reads, edit batches and snapshot rewinds take named
26
+ objects; labels are in the optional second object. Hook actions share these method types.
27
+ The runner uses `run({ toolId, input })`. Tool wire schemas and React component props are unchanged.
28
+ - Fixed session caching for repeated part ids and reverse-order undo. Selected previews now
29
+ refresh after history replay. Added regression, composition, export-map and whole-source
30
+ neutrality tests, and migrated the existing React test fixtures and README API examples.
31
+ - Integration status supersedes the earlier React ownership note below: the cleared React
32
+ directory and its dependent calls are now migrated. Tests, typechecks, builds and pack checks
33
+ remain **not run (owner directive)**; no runtime correctness or release readiness is claimed.
34
+
35
+ - BREAKING: `applyEdit`, `applyEdits`, `correctionsFor`, `createEditHistory`, and
36
+ `isValidRegionHandle` accept named required arguments. `createHtmlRegionTarget`
37
+ receives optional settings as its second object, separate from storage and parser ports.
38
+ - BREAKING: `EditTarget.readPart`, `replacePart`, and `restore`, `EditHistory.transaction`
39
+ and `restore`, `HtmlRegionParser.findRegions` and `checkWellFormed`, and
40
+ `HtmlDocumentStore.write` accept named object arguments. Transaction labels belong
41
+ to the optional second object. Export names and edit/history behavior are preserved.
42
+ - Updated core/HTML behavior tests and added a package neutrality guard. Verification
43
+ is deferred by owner directive. The pre-existing React worktree remains unchanged;
44
+ its dependent calls require migration before package verification or release.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # @jini-ai/vibecoding
2
+
3
+ Conversational artifact authoring — the loop that turns "make the hero blue" into a validated,
4
+ undoable change, without knowing what kind of artifact it is editing.
5
+
6
+ ## The idea
7
+
8
+ Every AI app-builder differs at exactly one place: **what is being edited**. One edits a file tree.
9
+ Another edits a single markup document. The conversation, the streaming, the undo and the preview
10
+ are the same in all of them.
11
+
12
+ So this package owns the loop, and the artifact reaches it through one small port:
13
+
14
+ ```ts
15
+ interface EditTarget {
16
+ listParts(): Promise<readonly PartRef[]>; // the allowlist
17
+ readPart({ id }): Promise<string>;
18
+ replacePart({ id, content }): Promise<void>; // upsert
19
+ snapshot(): Promise<Snapshot>;
20
+ restore({ snapshot }): Promise<void>;
21
+ validate(candidate): Promise<ValidationResult>;
22
+ }
23
+ ```
24
+
25
+ A file-tree host maps parts to files. A single-document host maps them to addressable regions.
26
+ Neither reimplements the loop.
27
+
28
+ ## Layout
29
+
30
+ | entry | runtime | contents |
31
+ | --- | --- | --- |
32
+ | `.` | universal | re-exports `./core` |
33
+ | `./core` | universal | the loop and the `EditTarget` port — no React, no Node, no DOM |
34
+ | `./html` | universal | the tagged-region, single-HTML-document `EditTarget` (parser injected) |
35
+ | `./html/node` | node | a parse5-backed `HtmlRegionParser` for `./html`, for a Node host |
36
+ | `./react` | browser | `VibecodingSession`, the `@jini-ai/core`-registrable tool set, and the file-viewer/renderer components — see `src/react/README` section below |
37
+
38
+ `./node` (filesystem targets) is planned and deliberately not present yet — building it is a
39
+ separate `EditTarget` implementation, not a `./react` concern. React is an **optional** peer of
40
+ `./react`: keeping the framework-free half importable without a UI dependency is the point of the
41
+ split, not a stylistic preference, and importing `.`/`./core`/`./html` never pulls React in.
42
+
43
+ ### `./react`
44
+
45
+ The chat + preview surface over any `EditTarget`: `createVibecodingSession` (a subscribable
46
+ wrapper over `./core`'s loop, built for `useSyncExternalStore`), `createVibecodingToolRegistrations`
47
+ /`createVibecodingToolRunner` (the same session exposed as `@jini-ai/core` `ToolDescriptor`s a chat
48
+ surface discovers through a registry rather than a hardcoded list), and three React pieces —
49
+ `PartsViewer` (the file viewer), `DocumentPreview` (the live renderer, a sandboxed iframe — not an
50
+ editor, and not `@jini-ai/sandbox`'s job; see that component's own doc), and `VibecodingWorkbench`
51
+ (both composed). See `src/react/index.ts` and each module's own doc comment for the design
52
+ rationale behind every one of those pieces.
53
+
54
+ ## Three decisions worth knowing before changing anything
55
+
56
+ **1. `listParts` is an allowlist, and that is where scope discipline comes from.**
57
+ A part not listed is structurally unaddressable rather than merely discouraged. Prompt severity is
58
+ not a substitute for this — one shipped builder ships three prompt variants that enforce identical
59
+ scope rules with wildly different tone, over an identical mechanism. The constraint lives in the
60
+ addressing.
61
+
62
+ **2. `validate` receives the whole prospective artifact, and it is the one piece with no upstream.**
63
+ Neither reference implementation validates writes at all, because their unit is a whole file and a
64
+ malformed file cannot corrupt another file's syntax. Sub-document parts lose that property: one
65
+ unbalanced tag corrupts everything after it. It takes the whole prospective artifact rather than the
66
+ part alone because a fragment that is well-formed in isolation can still break the document it lands
67
+ in. A rejection's `reason` is written for the model and fed back as its next turn — that is what
68
+ closes the loop between validation and correction.
69
+
70
+ **3. `snapshot` restores data, never an execution environment.**
71
+ The reference implementation this shape came from also replays setup commands on rewind to restart
72
+ its dev server. That is execution resync, and it is the host's job, after `restore` returns. Folding
73
+ it in here would promise something this package cannot deliver for any host but the simplest.
74
+
75
+ There are deliberately **no verbs for running processes, installing dependencies, or building**.
76
+ Those are real needs for a file-tree host and belong to a separate execution layer. Their absence is
77
+ a decision, not a gap.
78
+
79
+ ## Usage
80
+
81
+ ```ts
82
+ import { applyEdits, correctionsFor } from "@jini-ai/vibecoding/core";
83
+
84
+ const outcomes = await applyEdits({ target, edits: proposals });
85
+ const corrections = correctionsFor({ outcomes });
86
+ if (corrections.length > 0) {
87
+ // Feed these back as the model's next turn.
88
+ }
89
+ ```
90
+
91
+ Every proposal gets an outcome — a rejection or a write failure does not abandon the batch, so a
92
+ host can report precisely which parts landed. Write failures surface as `failed` rather than being
93
+ logged and forgotten.
94
+
95
+ ## Object API
96
+
97
+ Functions with inputs take a required object followed, when needed, by an optional object.
98
+ Dependencies are explicit ports in the required object. Methods with no inputs stay zero-argument;
99
+ React components keep their props and framework callbacks keep the framework's signature.
100
+
101
+ | API | Call shape |
102
+ | --- | --- |
103
+ | Single edit | `applyEdit({ target, edit })` |
104
+ | Batch edits and feedback | `applyEdits({ target, edits })`, `correctionsFor({ outcomes })` |
105
+ | History | `createEditHistory({ target }, { limit })` |
106
+ | Transaction / snapshot rewind | `history.transaction({ work }, { label })`, `history.restore({ snapshot })` |
107
+ | HTML target | `createHtmlRegionTarget({ store, parser }, { maxPartLength })` |
108
+ | Handle check / parser | `isValidRegionHandle({ handle })`, `createParse5RegionParser()` |
109
+ | Parser port | `parser.findRegions({ html })`, `parser.checkWellFormed?.({ html })` |
110
+ | Document store port | `store.read()`, `store.write({ html })` |
111
+ | Session | `createVibecodingSession({ target }, { historyOptions })` |
112
+ | Session methods | `session.subscribe({ listener })`, `session.readPart({ id })`, `session.applyEdits({ edits }, { label })`, `session.restoreSnapshot({ snapshot })` |
113
+ | React hook | `useVibecodingSession({ session })`; its `readPart` and `applyEdits` actions use the session method shapes |
114
+ | Registered tools | `createVibecodingToolRegistrations({ session }, { policy })` |
115
+ | Direct tool runner | `createVibecodingToolRunner({ session })`, `runner.run({ toolId, input })` |
116
+
117
+ `createParse5RegionParser()` always provides `checkWellFormed({ html })`; optional chaining
118
+ is only needed when working with the generic `HtmlRegionParser` port.
119
+
120
+ `VibecodingSessionArgs` holds the required target; `VibecodingSessionOptions` now holds only
121
+ optional `historyOptions`. `UseVibecodingSessionArgs` and `VibecodingToolArgs` describe the required
122
+ session port. All are exported from `./react`. Tool input JSON remains unchanged: `edits` and
123
+ `label` still belong to the same tool input object.
124
+
125
+ ```ts
126
+ import { createHtmlRegionTarget, type HtmlDocumentStore } from '@jini-ai/vibecoding/html';
127
+ import { createParse5RegionParser } from '@jini-ai/vibecoding/html/node';
128
+ import {
129
+ createVibecodingSession, createVibecodingToolRunner, VIBECODING_TOOL_IDS,
130
+ } from '@jini-ai/vibecoding/react';
131
+
132
+ let html = '<section data-agent-element="hero">Hello</section>';
133
+ const store: HtmlDocumentStore = {
134
+ read: async () => html,
135
+ write: async ({ html: next }) => { html = next; },
136
+ };
137
+ const target = createHtmlRegionTarget({ store, parser: createParse5RegionParser() });
138
+ const session = createVibecodingSession({ target }, { historyOptions: { limit: 25 } });
139
+ const runner = createVibecodingToolRunner({ session });
140
+ await runner.run({
141
+ toolId: VIBECODING_TOOL_IDS.PROPOSE_EDITS,
142
+ input: { edits: [{ id: 'hero', content: 'Hello again' }], label: 'Greeting' },
143
+ });
144
+ await session.undo();
145
+ ```
146
+
147
+ The parser import above is for a Node host. Browser hosts inject a browser-compatible
148
+ `HtmlRegionParser` through the same port. Storage paths, persistence and product configuration
149
+ belong to the host. Integration verification is deferred by owner directive; see
150
+ [INTEGRATION-REPORT.md](./INTEGRATION-REPORT.md) for the source review and pending commands.
151
+
152
+ ## Design decisions
153
+
154
+ - [Ambiguous region detection refuses rather than dropping a handle](docs/decisions/DR-001-conservative-region-detection.md).
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @module core/apply
3
+ *
4
+ * Validate-then-commit: the only sanctioned path from a model's proposed content to a real write.
5
+ *
6
+ * Two failure behaviours here are deliberate corrections of what the reference implementations do,
7
+ * and both were observed in their source rather than inferred:
8
+ *
9
+ * - **A write failure is never swallowed.** One builder wraps both its directory-create and its
10
+ * file-write in `try/catch` blocks that only log, then returns normally — so the action is
11
+ * marked complete whether or not anything reached disk, while its *shell* actions do surface
12
+ * failures. That asymmetry looks accidental. Here a failed write becomes a `failed` outcome the
13
+ * caller must handle.
14
+ * - **Per-part outcomes are collected and returned, not aggregated into a single boolean.** This is
15
+ * the one thing the second reference implementation does better than the first, and it is what
16
+ * lets a host stream partial progress and tell the model precisely which parts landed.
17
+ */
18
+ import type { EditTarget } from "./target.js";
19
+ import type { ApplyOutcome, PartId } from "./types.js";
20
+ /** One proposed change: replace `id`'s content with `content`. */
21
+ export interface ProposedEdit {
22
+ readonly id: PartId;
23
+ readonly content: string;
24
+ }
25
+ /**
26
+ * Validate one proposed edit and, if the host accepts it, commit it.
27
+ *
28
+ * @param requiredArgs - the host's artifact port and the model's proposed replacement.
29
+ * @returns what happened — applied, rejected by `validate`, or failed while validating or writing.
30
+ * @complexity 3
31
+ */
32
+ export declare function applyEdit({ target, edit }: {
33
+ readonly target: EditTarget;
34
+ readonly edit: ProposedEdit;
35
+ }): Promise<ApplyOutcome>;
36
+ /**
37
+ * Apply several proposed edits in order, one at a time.
38
+ *
39
+ * Sequential on purpose: `validate` is defined over the whole prospective artifact, so two edits
40
+ * validated concurrently would each be judged against a state that never existed. A rejected or
41
+ * failed edit does **not** halt the rest — every proposal gets an outcome, so the caller can report
42
+ * precisely which parts landed rather than abandoning the batch at the first problem.
43
+ *
44
+ * @param requiredArgs - the host's artifact port and proposals, applied in the given order.
45
+ * @returns one outcome per proposal, positionally aligned with `edits`.
46
+ * @complexity 2
47
+ */
48
+ export declare function applyEdits({ target, edits }: {
49
+ readonly target: EditTarget;
50
+ readonly edits: readonly ProposedEdit[];
51
+ }): Promise<readonly ApplyOutcome[]>;
52
+ /**
53
+ * The subset of outcomes a host should feed back to the model as its next turn.
54
+ *
55
+ * Rejections carry a reason written for the model; write failures carry an error message. Both are
56
+ * things the model can act on, and neither reaches it unless the host asks for them here — which
57
+ * is what closes the loop between validation and correction.
58
+ *
59
+ * @param requiredArgs - results from `applyEdits`.
60
+ * @returns one `{ id, reason }` per non-applied outcome, in input order.
61
+ * @complexity 3
62
+ */
63
+ export declare function correctionsFor({ outcomes }: {
64
+ readonly outcomes: readonly ApplyOutcome[];
65
+ }): readonly {
66
+ readonly id: PartId;
67
+ readonly reason: string;
68
+ }[];
69
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1,69 @@
1
+ /** Normalizes an unknown thrown value into an `Error`, preserving a non-Error as its message. */
2
+ function toError(thrown) {
3
+ return thrown instanceof Error ? thrown : new Error(String(thrown));
4
+ }
5
+ /**
6
+ * Validate one proposed edit and, if the host accepts it, commit it.
7
+ *
8
+ * @param requiredArgs - the host's artifact port and the model's proposed replacement.
9
+ * @returns what happened — applied, rejected by `validate`, or failed while validating or writing.
10
+ * @complexity 3
11
+ */
12
+ export async function applyEdit({ target, edit }) {
13
+ try {
14
+ // Validation is a host port too: a failed parser/read must produce an outcome so the batch
15
+ // can continue, and must never reach the write for this proposal.
16
+ const verdict = await target.validate({ id: edit.id, content: edit.content });
17
+ if (!verdict.ok) {
18
+ return { status: "rejected", id: edit.id, reason: verdict.reason };
19
+ }
20
+ await target.replacePart({ id: edit.id, content: edit.content });
21
+ }
22
+ catch (thrown) {
23
+ return { status: "failed", id: edit.id, error: toError(thrown) };
24
+ }
25
+ return { status: "applied", id: edit.id };
26
+ }
27
+ /**
28
+ * Apply several proposed edits in order, one at a time.
29
+ *
30
+ * Sequential on purpose: `validate` is defined over the whole prospective artifact, so two edits
31
+ * validated concurrently would each be judged against a state that never existed. A rejected or
32
+ * failed edit does **not** halt the rest — every proposal gets an outcome, so the caller can report
33
+ * precisely which parts landed rather than abandoning the batch at the first problem.
34
+ *
35
+ * @param requiredArgs - the host's artifact port and proposals, applied in the given order.
36
+ * @returns one outcome per proposal, positionally aligned with `edits`.
37
+ * @complexity 2
38
+ */
39
+ export async function applyEdits({ target, edits }) {
40
+ const outcomes = [];
41
+ for (const edit of edits) {
42
+ outcomes.push(await applyEdit({ target, edit }));
43
+ }
44
+ return outcomes;
45
+ }
46
+ /**
47
+ * The subset of outcomes a host should feed back to the model as its next turn.
48
+ *
49
+ * Rejections carry a reason written for the model; write failures carry an error message. Both are
50
+ * things the model can act on, and neither reaches it unless the host asks for them here — which
51
+ * is what closes the loop between validation and correction.
52
+ *
53
+ * @param requiredArgs - results from `applyEdits`.
54
+ * @returns one `{ id, reason }` per non-applied outcome, in input order.
55
+ * @complexity 3
56
+ */
57
+ export function correctionsFor({ outcomes }) {
58
+ const corrections = [];
59
+ for (const outcome of outcomes) {
60
+ if (outcome.status === "rejected") {
61
+ corrections.push({ id: outcome.id, reason: outcome.reason });
62
+ }
63
+ else if (outcome.status === "failed") {
64
+ corrections.push({ id: outcome.id, reason: outcome.error.message });
65
+ }
66
+ }
67
+ return corrections;
68
+ }
69
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @module core/history
3
+ *
4
+ * Operation-level undo and redo, layered strictly **on top of** `EditTarget` — it adds no verb to
5
+ * that port. A history records the before/after content of every part a transaction touched, which
6
+ * it learns by calling `readPart` immediately before each `replacePart`. Nothing here needs the
7
+ * host to implement anything new.
8
+ *
9
+ * ## Why this tier exists at all, given `snapshot`/`restore`
10
+ *
11
+ * `snapshot`/`restore` is a whole-artifact rewind: it answers "put everything back the way it was."
12
+ * It cannot answer "undo the last thing," which is the operation a person actually reaches for, and
13
+ * it has no notion of redo. One shipped builder has exactly this gap — a rewind-to-snapshot feature
14
+ * and no undo stack at all, so stepping back one edit means stepping back every edit since the last
15
+ * capture. A second builder has both tiers, with transaction batching, and is the better model.
16
+ *
17
+ * ## Three decisions worth stating, because each could reasonably have gone the other way
18
+ *
19
+ * 1. **Undo and redo bypass `validate`.** `validate` judges a candidate against the whole
20
+ * prospective artifact (see `./target.ts`). Rewinding a transaction that touched three parts
21
+ * necessarily passes through two intermediate states that never existed as committed states, and
22
+ * validating those would reject a legitimate rewind — a half-restored document is exactly the
23
+ * kind of thing `validate` is built to refuse. The state being restored was already committed
24
+ * once, so it needs no second opinion. Applying a *new* edit still goes through `applyEdit`, and
25
+ * that path is unchanged.
26
+ *
27
+ * 2. **A rewind is itself undoable, and so is a snapshot restore.** `restore` below diffs the
28
+ * target snapshot against current content and records the result as an ordinary transaction, so
29
+ * the two tiers share one stack and a mis-aimed rewind is recoverable. Borrowed deliberately:
30
+ * the alternative — a restore that silently discards whatever it overwrote — turns the safety
31
+ * feature into the most destructive button in the product.
32
+ *
33
+ * 3. **A transaction that throws still commits its partial entry, then rethrows.** Whatever reached
34
+ * the artifact before the throw is real, and the only thing worse than a partial write is a
35
+ * partial write nobody can undo. Note that `applyEdits` does not throw — it returns `failed`
36
+ * outcomes — so this path is genuinely exceptional rather than routine.
37
+ *
38
+ * ## The one thing this tier cannot do
39
+ *
40
+ * `replacePart` is an upsert, and `EditTarget` deliberately has no delete verb, so **undoing the
41
+ * creation of a part cannot remove it** — the best available inverse is writing empty content back.
42
+ * Each change records `existedBefore` so a host that *can* delete may consult the entry and finish
43
+ * the job properly. Pretending otherwise would be the more dangerous choice: a caller would believe
44
+ * a part had gone away while it was still listed and still readable.
45
+ */
46
+ import type { EditTarget } from "./target.js";
47
+ import type { PartId, Snapshot } from "./types.js";
48
+ /** What one part looked like before and after a single transaction touched it. */
49
+ export interface PartChange {
50
+ readonly id: PartId;
51
+ /** Content prior to the change. Empty when the part did not exist — see `existedBefore`. */
52
+ readonly before: string;
53
+ /** Content written by the change. */
54
+ readonly after: string;
55
+ /**
56
+ * Whether the part existed before this change.
57
+ *
58
+ * `false` means the transaction created it, and undo therefore cannot fully invert it — see this
59
+ * module's doc. A host with its own delete capability can use this flag to do better.
60
+ */
61
+ readonly existedBefore: boolean;
62
+ }
63
+ /** One undoable step: every part change a single transaction produced, in the order applied. */
64
+ export interface HistoryEntry {
65
+ /** Optional caller-supplied description, e.g. the model turn that produced it. Untrusted text. */
66
+ readonly label?: string;
67
+ /** Part changes in application order. Undo replays these in reverse. */
68
+ readonly changes: readonly PartChange[];
69
+ }
70
+ /** Tuning for `createEditHistory`. */
71
+ export interface EditHistoryOptions {
72
+ /**
73
+ * Maximum number of undoable entries retained. Oldest are dropped first.
74
+ *
75
+ * Bounded because every entry holds full copies of the content it touched, so an unbounded stack
76
+ * in a long authoring session grows with total bytes written rather than with artifact size.
77
+ * Defaults to 50.
78
+ */
79
+ readonly limit?: number;
80
+ }
81
+ /** An operation-level undo/redo stack over one `EditTarget`. */
82
+ export interface EditHistory {
83
+ /**
84
+ * Run `work` against a recording view of the target, committing everything it changed as one
85
+ * undoable entry.
86
+ *
87
+ * The target handed to `work` is a transparent wrapper: pass it to `applyEdit`/`applyEdits`
88
+ * exactly as you would the real one. Grouping is what makes a multi-part model turn undo as a
89
+ * single step rather than as N unrelated ones.
90
+ *
91
+ * A transaction that changes nothing records nothing, so a rejected model turn leaves no empty
92
+ * step for a user to click past. Committing also clears the redo stack, which is the standard
93
+ * rule: once you branch off the redo path, the abandoned future is no longer reachable.
94
+ */
95
+ transaction<T>(requiredArgs: {
96
+ readonly work: (recording: EditTarget) => Promise<T>;
97
+ }, optionalArgs?: {
98
+ readonly label?: string;
99
+ }): Promise<T>;
100
+ /** Whether `undo` would do anything. */
101
+ canUndo(): boolean;
102
+ /** Whether `redo` would do anything. */
103
+ canRedo(): boolean;
104
+ /**
105
+ * Revert the most recent entry, writing each part's `before` content in reverse order.
106
+ *
107
+ * @returns the entry that was reverted, or `null` when there is nothing to undo.
108
+ */
109
+ undo(): Promise<HistoryEntry | null>;
110
+ /**
111
+ * Re-apply the most recently undone entry, writing each part's `after` content in order.
112
+ *
113
+ * @returns the entry that was re-applied, or `null` when there is nothing to redo.
114
+ */
115
+ redo(): Promise<HistoryEntry | null>;
116
+ /**
117
+ * Restore a snapshot as an **undoable** step rather than a destructive one.
118
+ *
119
+ * Current content is captured first and recorded as the `before` side, so the rewind can itself
120
+ * be undone. Parts present now but absent from the snapshot are left alone — there is no delete
121
+ * verb to remove them with (see this module's doc).
122
+ *
123
+ * @returns the entry recorded, or `null` when the snapshot already matches current content.
124
+ */
125
+ restore(args: {
126
+ readonly snapshot: Snapshot;
127
+ }): Promise<HistoryEntry | null>;
128
+ /** Undoable entries, oldest first. For rendering a history UI; do not mutate. */
129
+ entries(): readonly HistoryEntry[];
130
+ /** Discard both stacks. */
131
+ clear(): void;
132
+ }
133
+ /**
134
+ * Create an undo/redo stack over one target.
135
+ *
136
+ * @param requiredArgs - the host's artifact port; used for reads and replaying changes.
137
+ * @param options - see `EditHistoryOptions`.
138
+ * @returns a history whose `transaction` wrapper is the only thing callers need to adopt.
139
+ * @complexity 4
140
+ */
141
+ export declare function createEditHistory({ target }: {
142
+ readonly target: EditTarget;
143
+ }, options?: EditHistoryOptions): EditHistory;
144
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1,132 @@
1
+ /** Reads a part's current content, reporting absence rather than throwing. */
2
+ async function readCurrent(target, id) {
3
+ try {
4
+ return { content: await target.readPart({ id }), existed: true };
5
+ }
6
+ catch {
7
+ return { content: "", existed: false };
8
+ }
9
+ }
10
+ /**
11
+ * Create an undo/redo stack over one target.
12
+ *
13
+ * @param requiredArgs - the host's artifact port; used for reads and replaying changes.
14
+ * @param options - see `EditHistoryOptions`.
15
+ * @returns a history whose `transaction` wrapper is the only thing callers need to adopt.
16
+ * @complexity 4
17
+ */
18
+ export function createEditHistory({ target }, options = {}) {
19
+ const limit = options.limit ?? 50;
20
+ const undoStack = [];
21
+ const redoStack = [];
22
+ let transactionOpen = false;
23
+ function push(entry) {
24
+ undoStack.push(entry);
25
+ while (undoStack.length > limit)
26
+ undoStack.shift();
27
+ redoStack.length = 0;
28
+ }
29
+ /**
30
+ * Replay part contents directly, deliberately skipping `validate` — see this module's doc for
31
+ * why an intermediate state of a multi-part rewind must not be judged.
32
+ */
33
+ async function replay(changes, side) {
34
+ for (const change of changes) {
35
+ await target.replacePart({ id: change.id, content: side === "before" ? change.before : change.after });
36
+ }
37
+ }
38
+ return {
39
+ async transaction({ work }, { label } = {}) {
40
+ if (transactionOpen) {
41
+ throw new Error("a transaction is already open — overlapping transactions would interleave their changes into one another's entries");
42
+ }
43
+ transactionOpen = true;
44
+ const changes = [];
45
+ const recording = {
46
+ listParts: () => target.listParts(),
47
+ readPart: (args) => target.readPart(args),
48
+ snapshot: () => target.snapshot(),
49
+ restore: (args) => target.restore(args),
50
+ validate: (candidate) => target.validate(candidate),
51
+ async replacePart({ id, content }) {
52
+ const current = await readCurrent(target, id);
53
+ await target.replacePart({ id, content });
54
+ // A write that changed nothing is not a step a person can meaningfully undo.
55
+ if (current.existed && current.content === content)
56
+ return;
57
+ changes.push({
58
+ id,
59
+ before: current.content,
60
+ after: content,
61
+ existedBefore: current.existed,
62
+ });
63
+ },
64
+ };
65
+ try {
66
+ return await work(recording);
67
+ }
68
+ finally {
69
+ transactionOpen = false;
70
+ // Committed even when `work` threw: partial writes already reached the artifact, and the
71
+ // caller needs them to be undoable.
72
+ if (changes.length > 0)
73
+ push(label === undefined ? { changes } : { label, changes });
74
+ }
75
+ },
76
+ canUndo: () => undoStack.length > 0,
77
+ canRedo: () => redoStack.length > 0,
78
+ async undo() {
79
+ // Peeked, not popped: a `replacePart` that throws partway through leaves the earlier parts
80
+ // already rewound, and popping first would strand that half-applied entry in NEITHER stack —
81
+ // the artifact changed and the history API could no longer reach it. Same principle decision
82
+ // 3 in this module's doc states for `transaction`. Moving between stacks only after a clean
83
+ // replay means a failed undo is simply retryable: replaying `before` again rewrites the
84
+ // parts that already landed with the content they already hold (a no-op) and retries the one
85
+ // that failed.
86
+ const entry = undoStack[undoStack.length - 1];
87
+ if (!entry)
88
+ return null;
89
+ await replay([...entry.changes].reverse(), "before");
90
+ undoStack.pop();
91
+ redoStack.push(entry);
92
+ return entry;
93
+ },
94
+ async redo() {
95
+ // Peeked for the same reason as `undo` above, in the other direction.
96
+ const entry = redoStack[redoStack.length - 1];
97
+ if (!entry)
98
+ return null;
99
+ await replay(entry.changes, "after");
100
+ redoStack.pop();
101
+ undoStack.push(entry);
102
+ return entry;
103
+ },
104
+ async restore({ snapshot }) {
105
+ const changes = [];
106
+ for (const [id, after] of Object.entries(snapshot.parts)) {
107
+ const current = await readCurrent(target, id);
108
+ if (current.existed && current.content === after)
109
+ continue;
110
+ changes.push({ id, before: current.content, after, existedBefore: current.existed });
111
+ }
112
+ if (changes.length === 0)
113
+ return null;
114
+ // Recorded BEFORE the replay, not after. A restore that throws on its second part has still
115
+ // changed the first, and pushing afterwards left those writes with no undo record at all —
116
+ // the destructive-restore failure mode decision 2 in this module's doc exists to prevent,
117
+ // reached by a different route. The `before` sides were captured above and are correct
118
+ // regardless of how far the replay got: undoing a partially-applied restore rewrites every
119
+ // listed part with the content it had before, which is a no-op for the ones never reached.
120
+ const entry = { label: `restore ${snapshot.id}`, changes };
121
+ push(entry);
122
+ await replay(changes, "after");
123
+ return entry;
124
+ },
125
+ entries: () => [...undoStack],
126
+ clear() {
127
+ undoStack.length = 0;
128
+ redoStack.length = 0;
129
+ },
130
+ };
131
+ }
132
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module core
3
+ *
4
+ * The framework-free half of `@jini-ai/vibecoding`: the authoring loop and the artifact port it
5
+ * drives. No React, no Node built-ins, no DOM — so a server, a worker or a CLI can import this
6
+ * without acquiring a UI dependency.
7
+ *
8
+ * That separation is enforced by the same rule that governs the rest of this workspace, and it is
9
+ * not theoretical: a sibling consolidation was measured and abandoned precisely because a Node
10
+ * HTTP server imports a framework-free chat package directly, and folding a React layer into it
11
+ * would have forced React onto that server.
12
+ */
13
+ export type { ApplyOutcome, PartId, PartRef, Snapshot, ValidationResult, } from "./types.js";
14
+ export type { EditTarget } from "./target.js";
15
+ export type { ProposedEdit } from "./apply.js";
16
+ export { applyEdit, applyEdits, correctionsFor } from "./apply.js";
17
+ export type { EditHistory, EditHistoryOptions, HistoryEntry, PartChange } from "./history.js";
18
+ export { createEditHistory } from "./history.js";
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { applyEdit, applyEdits, correctionsFor } from "./apply.js";
2
+ export { createEditHistory } from "./history.js";
3
+ //# sourceMappingURL=index.js.map