@satanwagen/reviewkit 0.1.2 → 0.1.3
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 +105 -0
- package/README.md +134 -63
- package/dist/activate.cjs +150 -0
- package/dist/activate.cjs.map +1 -0
- package/dist/activate.d.cts +59 -0
- package/dist/activate.d.ts +59 -0
- package/dist/activate.js +21 -0
- package/dist/activate.js.map +1 -0
- package/dist/{chunk-4YNLMSCK.js → chunk-DG6O5XIC.js} +36 -10
- package/dist/chunk-DG6O5XIC.js.map +1 -0
- package/dist/chunk-ETAGGIDN.js +119 -0
- package/dist/chunk-ETAGGIDN.js.map +1 -0
- package/dist/{chunk-YWBFAV57.js → chunk-NSXRFM75.js} +853 -1544
- package/dist/chunk-NSXRFM75.js.map +1 -0
- package/dist/chunk-Q7U43PXE.js +36 -0
- package/dist/chunk-Q7U43PXE.js.map +1 -0
- package/dist/client/index.cjs +4530 -4237
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +49 -3
- package/dist/client/index.js.map +1 -1
- package/dist/effects-EWKHKUDP.js +851 -0
- package/dist/effects-EWKHKUDP.js.map +1 -0
- package/dist/index-BUSmAweg.d.ts +77 -0
- package/dist/index-Dl2Fl0qr.d.cts +77 -0
- package/dist/index.cjs +4544 -4256
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/lazy.cjs +8988 -0
- package/dist/lazy.cjs.map +1 -0
- package/dist/lazy.d.cts +7 -0
- package/dist/lazy.d.ts +7 -0
- package/dist/lazy.js +8 -0
- package/dist/lazy.js.map +1 -0
- package/dist/next.cjs +8989 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +4 -0
- package/dist/next.d.ts +4 -0
- package/dist/next.js +9 -0
- package/dist/next.js.map +1 -0
- package/dist/schema.cjs +53 -16
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +20 -5
- package/dist/schema.d.ts +20 -5
- package/dist/schema.js +30 -2
- package/dist/schema.js.map +1 -1
- package/package.json +21 -8
- package/cli/emblema-sync.mjs +0 -675
- package/dist/chunk-4YNLMSCK.js.map +0 -1
- package/dist/chunk-YWBFAV57.js.map +0 -1
- package/dist/index-BbucFgZi.d.ts +0 -49
- package/dist/index-CBlJrKkm.d.cts +0 -49
package/dist/next.d.cts
ADDED
package/dist/next.d.ts
ADDED
package/dist/next.js
ADDED
package/dist/next.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/schema.cjs
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/schema.ts
|
|
21
|
+
var schema_exports = {};
|
|
22
|
+
__export(schema_exports, {
|
|
23
|
+
ITEM_STATUSES: () => ITEM_STATUSES,
|
|
24
|
+
KNOWN_ITEM_TYPES: () => KNOWN_ITEM_TYPES,
|
|
25
|
+
REVIEW_DISPATCH_VERSION: () => REVIEW_DISPATCH_VERSION,
|
|
26
|
+
SCHEMA_VERSION: () => SCHEMA_VERSION,
|
|
27
|
+
isCommentItem: () => isCommentItem,
|
|
28
|
+
isCopyItem: () => isCopyItem,
|
|
29
|
+
isDeleteItem: () => isDeleteItem,
|
|
30
|
+
isImageItem: () => isImageItem,
|
|
31
|
+
isKnownItemType: () => isKnownItemType,
|
|
32
|
+
isMoveItem: () => isMoveItem,
|
|
33
|
+
isStyleItem: () => isStyleItem,
|
|
34
|
+
parseSession: () => parseSession,
|
|
35
|
+
parseSessionJson: () => parseSessionJson
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(schema_exports);
|
|
2
38
|
|
|
3
39
|
// src/integrations/emblema.ts
|
|
4
40
|
var REVIEW_DISPATCH_VERSION = 1;
|
|
@@ -112,19 +148,20 @@ function parseSessionJson(json) {
|
|
|
112
148
|
}
|
|
113
149
|
return parseSession(value);
|
|
114
150
|
}
|
|
115
|
-
|
|
116
|
-
exports
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
152
|
+
0 && (module.exports = {
|
|
153
|
+
ITEM_STATUSES,
|
|
154
|
+
KNOWN_ITEM_TYPES,
|
|
155
|
+
REVIEW_DISPATCH_VERSION,
|
|
156
|
+
SCHEMA_VERSION,
|
|
157
|
+
isCommentItem,
|
|
158
|
+
isCopyItem,
|
|
159
|
+
isDeleteItem,
|
|
160
|
+
isImageItem,
|
|
161
|
+
isKnownItemType,
|
|
162
|
+
isMoveItem,
|
|
163
|
+
isStyleItem,
|
|
164
|
+
parseSession,
|
|
165
|
+
parseSessionJson
|
|
166
|
+
});
|
|
130
167
|
//# sourceMappingURL=schema.cjs.map
|
package/dist/schema.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/integrations/emblema.ts","../src/schema.ts"],"names":[],"mappings":"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","file":"schema.cjs","sourcesContent":["/**\n * ReviewKit → Emblema dispatch (Workstream B of INTEGRATION-EMBLEMA.md).\n *\n * This file is the AUTHORITATIVE definition of the EmblemaReviewDispatch v1\n * contract — Emblema vendors a copy of the type (its\n * src/lib/review-intake-contract.ts points back here). Do not rename or add\n * payload fields without bumping REVIEW_DISPATCH_VERSION; the receiving end\n * rejects any other `v`.\n *\n * Transport per the spec: GET /intake/ping to find a live app, then one\n * POST /intake/review with the whole batch. One-way in v1 — Emblema shows an\n * approval card; nothing flows back to the page. No queueing: if the app is\n * not running the caller shows a toast and gives up.\n */\n\nimport type { ReviewItem, ReviewSession } from '../schema';\n\nexport const REVIEW_DISPATCH_VERSION = 1;\n\n/** Fixed intake ports, in probe order (spec §3.1: 48752, fallbacks if busy). */\nexport const EMBLEMA_INTAKE_PORTS = [48752, 48753, 48754, 48755] as const;\n\n/** ReviewKitConfig.emblema — enables the \"Send to Emblema\" menu entry. */\nexport interface EmblemaConfig {\n /** Absolute path of the repo Emblema's agents should edit. */\n repoPath: string;\n /** Pin the intake to one port instead of probing the default list. */\n port?: number;\n}\n\n/** The v1 payload of POST /intake/review — field names are frozen. */\nexport interface EmblemaReviewDispatch {\n v: typeof REVIEW_DISPATCH_VERSION;\n source: 'review-kit';\n batchId: string;\n /** ISO 8601 */\n sentAt: string;\n /** Absolute repo path from ReviewKitConfig.emblema.repoPath. */\n repoPath: string;\n site: {\n origin: string;\n reviewUrl: string;\n };\n sessionAuthor: string;\n /** 1..200 verbatim ReviewItems (spec sends only actionable statuses). */\n items: ReviewItem[];\n}\n\nexport type SendResult =\n | { ok: true; batchId: string; sent: number; port: number }\n | { ok: false; reason: 'offline' | 'rejected' | 'no-items'; detail?: string };\n\n/** Max items per batch (spec §3.2). */\nexport const MAX_DISPATCH_ITEMS = 200;\n\nfunction batchId(): string {\n return `rk-batch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction portsFor(config: EmblemaConfig): number[] {\n return config.port !== undefined ? [config.port] : [...EMBLEMA_INTAKE_PORTS];\n}\n\n/**\n * Order items the way Emblema's route-grouping consumes them (spec §3.3):\n * grouped by route, `must` before `nice` inside a group, then oldest first.\n * The payload stays a flat array — grouping itself happens on the Emblema side.\n */\nexport function orderForDispatch(items: ReviewItem[]): ReviewItem[] {\n const routeOrder: string[] = [];\n for (const item of items) if (!routeOrder.includes(item.route)) routeOrder.push(item.route);\n return [...items].sort((a, b) => {\n const byRoute = routeOrder.indexOf(a.route) - routeOrder.indexOf(b.route);\n if (byRoute !== 0) return byRoute;\n if (a.priority !== b.priority) return a.priority === 'must' ? -1 : 1;\n return a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0;\n });\n}\n\n/** Build the v1 payload. Caller pre-filters items to the statuses it wants sent. */\nexport function buildDispatch(\n session: ReviewSession | null,\n items: ReviewItem[],\n config: EmblemaConfig,\n): EmblemaReviewDispatch {\n const origin = window.location.origin;\n return {\n v: REVIEW_DISPATCH_VERSION,\n source: 'review-kit',\n batchId: batchId(),\n sentAt: new Date().toISOString(),\n repoPath: config.repoPath,\n site: {\n origin,\n reviewUrl: window.location.href,\n },\n sessionAuthor: session?.author ?? '',\n items: orderForDispatch(items).slice(0, MAX_DISPATCH_ITEMS),\n };\n}\n\nasync function fetchJson(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const abort = new AbortController();\n const timer = window.setTimeout(() => abort.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: abort.signal });\n } finally {\n window.clearTimeout(timer);\n }\n}\n\n/** Find a live intake: first port whose /intake/ping answers as Emblema. */\nexport async function pingEmblema(config: EmblemaConfig): Promise<number | null> {\n for (const port of portsFor(config)) {\n try {\n const res = await fetchJson(`http://127.0.0.1:${port}/intake/ping`, { method: 'GET' }, 1200);\n if (!res.ok) continue;\n const body: unknown = await res.json();\n if (typeof body === 'object' && body !== null && (body as { app?: unknown }).app === 'emblema') {\n return port;\n }\n } catch {\n /* connection refused / timeout — try the next port */\n }\n }\n return null;\n}\n\n/** Ping, then POST the batch. Never queues; never retries. */\nexport async function sendToEmblema(\n session: ReviewSession | null,\n items: ReviewItem[],\n config: EmblemaConfig,\n): Promise<SendResult> {\n if (items.length === 0) return { ok: false, reason: 'no-items' };\n const port = await pingEmblema(config);\n if (port === null) return { ok: false, reason: 'offline' };\n const payload = buildDispatch(session, items, config);\n try {\n const res = await fetchJson(\n `http://127.0.0.1:${port}/intake/review`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n 10_000,\n );\n if (!res.ok) return { ok: false, reason: 'rejected', detail: `HTTP ${res.status}` };\n const body: unknown = await res.json().catch(() => null);\n const returnedId =\n typeof body === 'object' && body !== null && typeof (body as { batchId?: unknown }).batchId === 'string'\n ? ((body as { batchId: string }).batchId)\n : payload.batchId;\n return { ok: true, batchId: returnedId, sent: payload.items.length, port };\n } catch {\n return { ok: false, reason: 'offline' };\n }\n}\n\n/* ---------------- pairing (contract v2) ---------------- */\n\n/** Local sync agent (reviewkit-emblema serve) management API ports. */\nexport const EMBLEMA_SYNC_PORTS = [48770, 48771, 48772, 48773] as const;\n\nexport interface EmblemaSyncState {\n ok: true;\n paired: boolean;\n revoked: boolean;\n id: string;\n name: string;\n url: string;\n lastAck: string | null;\n findings: number;\n}\n\nasync function localFetch(path: string, init?: RequestInit): Promise<Response | null> {\n for (const port of EMBLEMA_SYNC_PORTS) {\n try {\n const abort = new AbortController();\n const timer = window.setTimeout(() => abort.abort(), 1200);\n const res = await fetch(`http://127.0.0.1:${port}${path}`, { ...init, signal: abort.signal });\n window.clearTimeout(timer);\n return res;\n } catch {\n /* next port */\n }\n }\n return null;\n}\n\n/** State of the local pairing agent; null when it isn't running. */\nexport async function fetchSyncState(): Promise<EmblemaSyncState | null> {\n const res = await localFetch('/api/emblema/local/state');\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n return typeof body === 'object' && body !== null && (body as { ok?: unknown }).ok === true\n ? (body as EmblemaSyncState)\n : null;\n}\n\n/** Generate (or rotate — same operation) the pairing key; returns emk_… */\nexport async function generateSyncKey(): Promise<string | null> {\n const res = await localFetch('/api/emblema/local/generate', { method: 'POST' });\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n const key = (body as { key?: unknown } | null)?.key;\n return typeof key === 'string' ? key : null;\n}\n\n/** Current key without rotating (Copy button). */\nexport async function fetchSyncKey(): Promise<string | null> {\n const res = await localFetch('/api/emblema/local/key');\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n const key = (body as { key?: unknown } | null)?.key;\n return typeof key === 'string' ? key : null;\n}\n\nexport async function revokeSyncKey(): Promise<boolean> {\n const res = await localFetch('/api/emblema/local/revoke', { method: 'POST' });\n return res !== null && res.ok;\n}\n","/**\n * review-kit session schema — the contract everything else depends on.\n *\n * Forward compatibility rules:\n * - Every object carries an index signature, so unknown fields parsed from a\n * file are kept in memory and survive a load → save round-trip.\n * - Item `type` is open-ended: unknown types are preserved as\n * {@link UnknownReviewItem} and never dropped.\n *\n * Kept in sync with `schema/review-session.schema.json`.\n */\n\nexport const SCHEMA_VERSION = 1;\n\nexport type ItemStatus = 'open' | 'accepted' | 'rejected' | 'applied';\nexport type ItemPriority = 'must' | 'nice';\nexport type KnownItemType = 'copy' | 'image' | 'comment' | 'delete' | 'move' | 'style';\n\nexport const ITEM_STATUSES: readonly ItemStatus[] = ['open', 'accepted', 'rejected', 'applied'];\nexport const KNOWN_ITEM_TYPES: readonly KnownItemType[] = [\n 'copy',\n 'image',\n 'comment',\n 'delete',\n 'move',\n 'style',\n];\n\n/** Extra fields from future schema versions are preserved, never dropped. */\nexport interface Extensible {\n [extra: string]: unknown;\n}\n\nexport interface BoundingBox extends Extensible {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface TargetAttrs extends Extensible {\n id?: string;\n classList?: string[];\n alt?: string;\n src?: string;\n href?: string;\n ariaLabel?: string;\n /** All `data-*` attributes, keyed without the `data-` prefix. */\n data?: Record<string, string>;\n}\n\n/**\n * Everything needed to find the element again — by the plugin (to re-pin it)\n * and by a coding agent (to locate it in source).\n */\nexport interface ReviewTarget extends Extensible {\n /** Stable CSS selector path (id-anchored where possible, no hashed classes). */\n selector: string;\n tagName: string;\n /** Trimmed, whitespace-collapsed text content, first ~120 chars. */\n textSnippet: string;\n attrs: TargetAttrs;\n /** nth-of-type index chain from <body> down to the element (1-based). */\n nthOfType: number[];\n /** Viewport-relative bounding box at capture time. */\n boundingBox: BoundingBox;\n /** Reserved for phase 4 (source mapping). */\n sourceFile?: string;\n sourceLine?: number;\n /** Fractional point inside the element's box (0..1) the item refers to. */\n anchor?: { ox: number; oy: number } & Extensible;\n}\n\nexport interface CopyPayload extends Extensible {\n before: string;\n after: string;\n}\n\nexport interface ImagePayload extends Extensible {\n beforeSrc: string;\n /**\n * Replacement image by reference — an external URL that couldn't be\n * embedded (CORS) or exceeded the embed cap. Exclusive with afterDataUrl.\n */\n afterSrc?: string;\n /** Replacement image embedded inline as a data: URL (2 MB cap at capture). */\n afterDataUrl?: string;\n description?: string;\n}\n\nexport interface CommentPayload extends Extensible {\n text: string;\n}\n\nexport interface DeletePayload extends Extensible {\n reason?: string;\n}\n\nexport interface MovePayload extends Extensible {\n direction?: 'up' | 'down' | 'first' | 'last';\n targetDescription?: string;\n}\n\nexport interface StylePayload extends Extensible {\n description: string;\n}\n\nexport interface ReviewItemBase extends Extensible {\n id: string;\n /** ISO 8601 */\n createdAt: string;\n /** Who created the item (additive; older sessions may lack it). */\n author?: { id: string; name: string; color?: string } & Extensible;\n type: string;\n status: ItemStatus;\n /** Pathname where the item was captured. */\n route: string;\n target: ReviewTarget;\n /** Optional free text from the reviewer, in addition to the payload. */\n note?: string;\n priority: ItemPriority;\n}\n\nexport interface CopyItem extends ReviewItemBase {\n type: 'copy';\n payload: CopyPayload;\n}\nexport interface ImageItem extends ReviewItemBase {\n type: 'image';\n payload: ImagePayload;\n}\nexport interface CommentItem extends ReviewItemBase {\n type: 'comment';\n payload: CommentPayload;\n}\nexport interface DeleteItem extends ReviewItemBase {\n type: 'delete';\n payload: DeletePayload;\n}\nexport interface MoveItem extends ReviewItemBase {\n type: 'move';\n payload: MovePayload;\n}\nexport interface StyleItem extends ReviewItemBase {\n type: 'style';\n payload: StylePayload;\n}\n\nexport type KnownReviewItem = CopyItem | ImageItem | CommentItem | DeleteItem | MoveItem | StyleItem;\n\n/** An item of a type this build doesn't know about — carried through untouched. */\nexport interface UnknownReviewItem extends ReviewItemBase {\n payload?: unknown;\n}\n\nexport type ReviewItem = KnownReviewItem | UnknownReviewItem;\n\nexport interface SiteInfo extends Extensible {\n origin: string;\n /** Pathname of the route the session was started on. */\n route: string;\n viewport: { width: number; height: number } & Extensible;\n userAgent: string;\n}\n\nexport interface ReviewSession extends Extensible {\n schemaVersion: number;\n id: string;\n /** ISO 8601 */\n createdAt: string;\n /** ISO 8601 */\n updatedAt: string;\n site: SiteInfo;\n /** Free-text reviewer name, typed once. */\n author: string;\n items: ReviewItem[];\n}\n\n/* ------------------------------------------------------------------ */\n/* Type guards */\n/* ------------------------------------------------------------------ */\n\nexport function isKnownItemType(type: string): type is KnownItemType {\n return (KNOWN_ITEM_TYPES as readonly string[]).includes(type);\n}\n\nexport function isCopyItem(item: ReviewItem): item is CopyItem {\n return item.type === 'copy';\n}\nexport function isImageItem(item: ReviewItem): item is ImageItem {\n return item.type === 'image';\n}\nexport function isCommentItem(item: ReviewItem): item is CommentItem {\n return item.type === 'comment';\n}\nexport function isDeleteItem(item: ReviewItem): item is DeleteItem {\n return item.type === 'delete';\n}\nexport function isMoveItem(item: ReviewItem): item is MoveItem {\n return item.type === 'move';\n}\nexport function isStyleItem(item: ReviewItem): item is StyleItem {\n return item.type === 'style';\n}\n\n/* ------------------------------------------------------------------ */\n/* Validation */\n/* ------------------------------------------------------------------ */\n\nexport type ParseResult =\n | { ok: true; session: ReviewSession }\n | { ok: false; error: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction fail(error: string): ParseResult {\n return { ok: false, error };\n}\n\n/**\n * Validate an unknown value (already JSON-parsed) into a ReviewSession.\n * Tolerant of unknown fields and unknown item types (forward compatibility);\n * strict about the structural minimum each item needs to be useful.\n * Missing `status` defaults to 'open', missing `priority` to 'nice'.\n */\nexport function parseSession(value: unknown): ParseResult {\n if (!isRecord(value)) return fail('Not a JSON object.');\n if (typeof value.schemaVersion !== 'number') {\n return fail('Missing or invalid \"schemaVersion\" (expected a number).');\n }\n if (typeof value.id !== 'string' || value.id.length === 0) {\n return fail('Missing or invalid session \"id\".');\n }\n if (!isRecord(value.site) || typeof value.site.origin !== 'string') {\n return fail('Missing or invalid \"site\" (expected { origin, route, viewport, userAgent }).');\n }\n if (!Array.isArray(value.items)) {\n return fail('Missing or invalid \"items\" (expected an array).');\n }\n\n const items: ReviewItem[] = [];\n for (let i = 0; i < value.items.length; i++) {\n const raw: unknown = value.items[i];\n if (!isRecord(raw)) return fail(`Item ${i + 1} is not an object.`);\n if (typeof raw.id !== 'string' || raw.id.length === 0) {\n return fail(`Item ${i + 1} is missing an \"id\".`);\n }\n if (typeof raw.type !== 'string' || raw.type.length === 0) {\n return fail(`Item ${i + 1} (\"${raw.id}\") is missing a \"type\".`);\n }\n if (!isRecord(raw.target) || typeof raw.target.selector !== 'string') {\n return fail(`Item ${i + 1} (\"${raw.id}\") is missing a valid \"target\".`);\n }\n const status: ItemStatus = (ITEM_STATUSES as readonly string[]).includes(String(raw.status))\n ? (raw.status as ItemStatus)\n : 'open';\n const priority: ItemPriority = raw.priority === 'must' ? 'must' : 'nice';\n items.push({\n ...raw,\n status,\n priority,\n createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : new Date(0).toISOString(),\n route: typeof raw.route === 'string' ? raw.route : '/',\n target: raw.target as ReviewTarget,\n } as ReviewItem);\n }\n\n const site = value.site;\n const viewport = isRecord(site.viewport) ? site.viewport : {};\n const session: ReviewSession = {\n ...value,\n schemaVersion: value.schemaVersion,\n id: value.id,\n createdAt: typeof value.createdAt === 'string' ? value.createdAt : new Date(0).toISOString(),\n updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : new Date(0).toISOString(),\n author: typeof value.author === 'string' ? value.author : '',\n site: {\n ...site,\n origin: typeof site.origin === 'string' ? site.origin : '',\n route: typeof site.route === 'string' ? site.route : '/',\n viewport: {\n ...viewport,\n width: typeof viewport.width === 'number' ? viewport.width : 0,\n height: typeof viewport.height === 'number' ? viewport.height : 0,\n },\n userAgent: typeof site.userAgent === 'string' ? site.userAgent : '',\n },\n items,\n };\n return { ok: true, session };\n}\n\n/** Parse a raw JSON string into a session, with a readable error on failure. */\nexport function parseSessionJson(json: string): ParseResult {\n let value: unknown;\n try {\n value = JSON.parse(json);\n } catch {\n return fail('File is not valid JSON.');\n }\n return parseSession(value);\n}\n\n// Emblema integration contract (authoritative definition lives in\n// src/integrations/emblema.ts; re-exported here so review-kit/schema carries it).\nexport type { EmblemaReviewDispatch, EmblemaConfig } from './integrations/emblema';\nexport { REVIEW_DISPATCH_VERSION } from './integrations/emblema';\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts","../src/integrations/emblema.ts"],"sourcesContent":["/**\n * review-kit session schema — the contract everything else depends on.\n *\n * Forward compatibility rules:\n * - Every object carries an index signature, so unknown fields parsed from a\n * file are kept in memory and survive a load → save round-trip.\n * - Item `type` is open-ended: unknown types are preserved as\n * {@link UnknownReviewItem} and never dropped.\n *\n * Kept in sync with `schema/review-session.schema.json`.\n */\n\nexport const SCHEMA_VERSION = 1;\n\nexport type ItemStatus = 'open' | 'accepted' | 'rejected' | 'applied';\nexport type ItemPriority = 'must' | 'nice';\nexport type KnownItemType = 'copy' | 'image' | 'comment' | 'delete' | 'move' | 'style';\n\nexport const ITEM_STATUSES: readonly ItemStatus[] = ['open', 'accepted', 'rejected', 'applied'];\nexport const KNOWN_ITEM_TYPES: readonly KnownItemType[] = [\n 'copy',\n 'image',\n 'comment',\n 'delete',\n 'move',\n 'style',\n];\n\n/** Extra fields from future schema versions are preserved, never dropped. */\nexport interface Extensible {\n [extra: string]: unknown;\n}\n\nexport interface BoundingBox extends Extensible {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface TargetAttrs extends Extensible {\n id?: string;\n classList?: string[];\n alt?: string;\n src?: string;\n href?: string;\n ariaLabel?: string;\n /** All `data-*` attributes, keyed without the `data-` prefix. */\n data?: Record<string, string>;\n}\n\n/**\n * Everything needed to find the element again — by the plugin (to re-pin it)\n * and by a coding agent (to locate it in source).\n */\nexport interface ReviewTarget extends Extensible {\n /** Stable CSS selector path (id-anchored where possible, no hashed classes). */\n selector: string;\n tagName: string;\n /** Trimmed, whitespace-collapsed text content, first ~120 chars. */\n textSnippet: string;\n attrs: TargetAttrs;\n /** nth-of-type index chain from <body> down to the element (1-based). */\n nthOfType: number[];\n /** Viewport-relative bounding box at capture time. */\n boundingBox: BoundingBox;\n /** Reserved for phase 4 (source mapping). */\n sourceFile?: string;\n sourceLine?: number;\n /** Fractional point inside the element's box (0..1) the item refers to. */\n anchor?: { ox: number; oy: number } & Extensible;\n}\n\nexport interface CopyPayload extends Extensible {\n before: string;\n after: string;\n}\n\nexport interface ImagePayload extends Extensible {\n beforeSrc: string;\n /**\n * Replacement image by reference — an external URL that couldn't be\n * embedded (CORS) or exceeded the embed cap. Exclusive with afterDataUrl.\n */\n afterSrc?: string;\n /** Replacement image embedded inline as a data: URL (2 MB cap at capture). */\n afterDataUrl?: string;\n description?: string;\n}\n\nexport interface CommentPayload extends Extensible {\n text: string;\n}\n\nexport interface DeletePayload extends Extensible {\n reason?: string;\n}\n\nexport interface MovePayload extends Extensible {\n direction?: 'up' | 'down' | 'first' | 'last';\n targetDescription?: string;\n}\n\nexport interface StylePayload extends Extensible {\n description: string;\n}\n\nexport interface ReviewItemBase extends Extensible {\n id: string;\n /** ISO 8601 */\n createdAt: string;\n /** Who created the item (additive; older sessions may lack it). */\n author?: { id: string; name: string; color?: string } & Extensible;\n type: string;\n status: ItemStatus;\n /** Pathname where the item was captured. */\n route: string;\n target: ReviewTarget;\n /** Optional free text from the reviewer, in addition to the payload. */\n note?: string;\n priority: ItemPriority;\n}\n\nexport interface CopyItem extends ReviewItemBase {\n type: 'copy';\n payload: CopyPayload;\n}\nexport interface ImageItem extends ReviewItemBase {\n type: 'image';\n payload: ImagePayload;\n}\nexport interface CommentItem extends ReviewItemBase {\n type: 'comment';\n payload: CommentPayload;\n}\nexport interface DeleteItem extends ReviewItemBase {\n type: 'delete';\n payload: DeletePayload;\n}\nexport interface MoveItem extends ReviewItemBase {\n type: 'move';\n payload: MovePayload;\n}\nexport interface StyleItem extends ReviewItemBase {\n type: 'style';\n payload: StylePayload;\n}\n\nexport type KnownReviewItem = CopyItem | ImageItem | CommentItem | DeleteItem | MoveItem | StyleItem;\n\n/** An item of a type this build doesn't know about — carried through untouched. */\nexport interface UnknownReviewItem extends ReviewItemBase {\n payload?: unknown;\n}\n\nexport type ReviewItem = KnownReviewItem | UnknownReviewItem;\n\nexport interface SiteInfo extends Extensible {\n origin: string;\n /** Pathname of the route the session was started on. */\n route: string;\n viewport: { width: number; height: number } & Extensible;\n userAgent: string;\n}\n\nexport interface ReviewSession extends Extensible {\n schemaVersion: number;\n id: string;\n /** ISO 8601 */\n createdAt: string;\n /** ISO 8601 */\n updatedAt: string;\n site: SiteInfo;\n /** Free-text reviewer name, typed once. */\n author: string;\n items: ReviewItem[];\n}\n\n/* ------------------------------------------------------------------ */\n/* Type guards */\n/* ------------------------------------------------------------------ */\n\nexport function isKnownItemType(type: string): type is KnownItemType {\n return (KNOWN_ITEM_TYPES as readonly string[]).includes(type);\n}\n\nexport function isCopyItem(item: ReviewItem): item is CopyItem {\n return item.type === 'copy';\n}\nexport function isImageItem(item: ReviewItem): item is ImageItem {\n return item.type === 'image';\n}\nexport function isCommentItem(item: ReviewItem): item is CommentItem {\n return item.type === 'comment';\n}\nexport function isDeleteItem(item: ReviewItem): item is DeleteItem {\n return item.type === 'delete';\n}\nexport function isMoveItem(item: ReviewItem): item is MoveItem {\n return item.type === 'move';\n}\nexport function isStyleItem(item: ReviewItem): item is StyleItem {\n return item.type === 'style';\n}\n\n/* ------------------------------------------------------------------ */\n/* Validation */\n/* ------------------------------------------------------------------ */\n\nexport type ParseResult =\n | { ok: true; session: ReviewSession }\n | { ok: false; error: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction fail(error: string): ParseResult {\n return { ok: false, error };\n}\n\n/**\n * Validate an unknown value (already JSON-parsed) into a ReviewSession.\n * Tolerant of unknown fields and unknown item types (forward compatibility);\n * strict about the structural minimum each item needs to be useful.\n * Missing `status` defaults to 'open', missing `priority` to 'nice'.\n */\nexport function parseSession(value: unknown): ParseResult {\n if (!isRecord(value)) return fail('Not a JSON object.');\n if (typeof value.schemaVersion !== 'number') {\n return fail('Missing or invalid \"schemaVersion\" (expected a number).');\n }\n if (typeof value.id !== 'string' || value.id.length === 0) {\n return fail('Missing or invalid session \"id\".');\n }\n if (!isRecord(value.site) || typeof value.site.origin !== 'string') {\n return fail('Missing or invalid \"site\" (expected { origin, route, viewport, userAgent }).');\n }\n if (!Array.isArray(value.items)) {\n return fail('Missing or invalid \"items\" (expected an array).');\n }\n\n const items: ReviewItem[] = [];\n for (let i = 0; i < value.items.length; i++) {\n const raw: unknown = value.items[i];\n if (!isRecord(raw)) return fail(`Item ${i + 1} is not an object.`);\n if (typeof raw.id !== 'string' || raw.id.length === 0) {\n return fail(`Item ${i + 1} is missing an \"id\".`);\n }\n if (typeof raw.type !== 'string' || raw.type.length === 0) {\n return fail(`Item ${i + 1} (\"${raw.id}\") is missing a \"type\".`);\n }\n if (!isRecord(raw.target) || typeof raw.target.selector !== 'string') {\n return fail(`Item ${i + 1} (\"${raw.id}\") is missing a valid \"target\".`);\n }\n const status: ItemStatus = (ITEM_STATUSES as readonly string[]).includes(String(raw.status))\n ? (raw.status as ItemStatus)\n : 'open';\n const priority: ItemPriority = raw.priority === 'must' ? 'must' : 'nice';\n items.push({\n ...raw,\n status,\n priority,\n createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : new Date(0).toISOString(),\n route: typeof raw.route === 'string' ? raw.route : '/',\n target: raw.target as ReviewTarget,\n } as ReviewItem);\n }\n\n const site = value.site;\n const viewport = isRecord(site.viewport) ? site.viewport : {};\n const session: ReviewSession = {\n ...value,\n schemaVersion: value.schemaVersion,\n id: value.id,\n createdAt: typeof value.createdAt === 'string' ? value.createdAt : new Date(0).toISOString(),\n updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : new Date(0).toISOString(),\n author: typeof value.author === 'string' ? value.author : '',\n site: {\n ...site,\n origin: typeof site.origin === 'string' ? site.origin : '',\n route: typeof site.route === 'string' ? site.route : '/',\n viewport: {\n ...viewport,\n width: typeof viewport.width === 'number' ? viewport.width : 0,\n height: typeof viewport.height === 'number' ? viewport.height : 0,\n },\n userAgent: typeof site.userAgent === 'string' ? site.userAgent : '',\n },\n items,\n };\n return { ok: true, session };\n}\n\n/** Parse a raw JSON string into a session, with a readable error on failure. */\nexport function parseSessionJson(json: string): ParseResult {\n let value: unknown;\n try {\n value = JSON.parse(json);\n } catch {\n return fail('File is not valid JSON.');\n }\n return parseSession(value);\n}\n\n// Emblema integration contract (authoritative definition lives in\n// src/integrations/emblema.ts; re-exported here so review-kit/schema carries it).\nexport type { EmblemaReviewDispatch, EmblemaConfig } from './integrations/emblema';\nexport { REVIEW_DISPATCH_VERSION } from './integrations/emblema';\n","/**\n * ReviewKit → Emblema dispatch (Workstream B of INTEGRATION-EMBLEMA.md).\n *\n * This file is the AUTHORITATIVE definition of the EmblemaReviewDispatch v1\n * contract — Emblema vendors a copy of the type (its\n * src/lib/review-intake-contract.ts points back here). Do not rename or add\n * payload fields without bumping REVIEW_DISPATCH_VERSION; the receiving end\n * rejects any other `v`.\n *\n * Transport per the spec: GET /intake/ping to find a live app, then one\n * POST /intake/review with the whole batch. One-way in v1 — Emblema shows an\n * approval card; nothing flows back to the page. No queueing: if the app is\n * not running the caller shows a toast and gives up.\n */\n\nimport type { ReviewItem, ReviewSession } from '../schema';\n\nexport const REVIEW_DISPATCH_VERSION = 1;\n\n/** Fixed intake ports, in probe order (spec §3.1: 48752, fallbacks if busy). */\nexport const EMBLEMA_INTAKE_PORTS = [48752, 48753, 48754, 48755] as const;\n\n/**\n * ReviewKitConfig.emblema — enables the Emblema bridge. `{}` is a valid\n * value: the repo Emblema should edit is Emblema's own local knowledge (it\n * knows which project it opened; the `reviewkit-emblema` agent has it in\n * `.reviewkit/emblema-sync.json`), so an absolute path from the reviewer's\n * machine never has to appear in the host site's code.\n */\nexport interface EmblemaConfig {\n /**\n * Override the repo path sent with each batch. Normally omitted: the\n * intake advertises its own `repoPath` in the `/intake/ping` answer, and\n * when it does not, the field is left out and Emblema falls back to its\n * active project.\n */\n repoPath?: string;\n /** Pin the intake to one port instead of probing the default list. */\n port?: number;\n}\n\n/** The v1 payload of POST /intake/review — field names are frozen. */\nexport interface EmblemaReviewDispatch {\n v: typeof REVIEW_DISPATCH_VERSION;\n source: 'review-kit';\n batchId: string;\n /** ISO 8601 */\n sentAt: string;\n /**\n * Absolute repo path — `emblema.repoPath` if configured, else the path the\n * intake advertised on ping. Omitted when neither is known; the receiver\n * then uses its active project. (Optional since 0.1.3; still `v: 1`.)\n */\n repoPath?: string;\n site: {\n origin: string;\n reviewUrl: string;\n };\n sessionAuthor: string;\n /** 1..200 verbatim ReviewItems (spec sends only actionable statuses). */\n items: ReviewItem[];\n}\n\nexport type SendResult =\n | { ok: true; batchId: string; sent: number; port: number }\n | { ok: false; reason: 'offline' | 'rejected' | 'no-items'; detail?: string };\n\n/** Max items per batch (spec §3.2). */\nexport const MAX_DISPATCH_ITEMS = 200;\n\nfunction batchId(): string {\n return `rk-batch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction portsFor(config: EmblemaConfig): number[] {\n return config.port !== undefined ? [config.port] : [...EMBLEMA_INTAKE_PORTS];\n}\n\n/**\n * Order items the way Emblema's route-grouping consumes them (spec §3.3):\n * grouped by route, `must` before `nice` inside a group, then oldest first.\n * The payload stays a flat array — grouping itself happens on the Emblema side.\n */\nexport function orderForDispatch(items: ReviewItem[]): ReviewItem[] {\n const routeOrder: string[] = [];\n for (const item of items) if (!routeOrder.includes(item.route)) routeOrder.push(item.route);\n return [...items].sort((a, b) => {\n const byRoute = routeOrder.indexOf(a.route) - routeOrder.indexOf(b.route);\n if (byRoute !== 0) return byRoute;\n if (a.priority !== b.priority) return a.priority === 'must' ? -1 : 1;\n return a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0;\n });\n}\n\n/** Build the v1 payload. Caller pre-filters items to the statuses it wants sent. */\nexport function buildDispatch(\n session: ReviewSession | null,\n items: ReviewItem[],\n config: EmblemaConfig,\n advertisedRepoPath?: string,\n): EmblemaReviewDispatch {\n const origin = window.location.origin;\n const repoPath = config.repoPath ?? advertisedRepoPath;\n return {\n v: REVIEW_DISPATCH_VERSION,\n source: 'review-kit',\n batchId: batchId(),\n sentAt: new Date().toISOString(),\n ...(repoPath ? { repoPath } : {}),\n site: {\n origin,\n reviewUrl: window.location.href,\n },\n sessionAuthor: session?.author ?? '',\n items: orderForDispatch(items).slice(0, MAX_DISPATCH_ITEMS),\n };\n}\n\nasync function fetchJson(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const abort = new AbortController();\n const timer = window.setTimeout(() => abort.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: abort.signal });\n } finally {\n window.clearTimeout(timer);\n }\n}\n\nexport interface EmblemaProbe {\n port: number;\n /** Repo the intake is bound to, when the ping answer advertises one. */\n repoPath?: string;\n}\n\n/**\n * Find a live intake: first port whose /intake/ping answers as Emblema.\n * The answer may carry `repoPath` (the project Emblema has open for this\n * pairing); it becomes the batch's `repoPath` unless the config overrides it.\n */\nexport async function probeEmblema(config: EmblemaConfig): Promise<EmblemaProbe | null> {\n for (const port of portsFor(config)) {\n try {\n const res = await fetchJson(`http://127.0.0.1:${port}/intake/ping`, { method: 'GET' }, 1200);\n if (!res.ok) continue;\n const body: unknown = await res.json();\n if (typeof body === 'object' && body !== null && (body as { app?: unknown }).app === 'emblema') {\n const repoPath = (body as { repoPath?: unknown }).repoPath;\n return typeof repoPath === 'string' && repoPath !== '' ? { port, repoPath } : { port };\n }\n } catch {\n /* connection refused / timeout — try the next port */\n }\n }\n return null;\n}\n\n/** Port of a live intake, or null. Thin wrapper over `probeEmblema`. */\nexport async function pingEmblema(config: EmblemaConfig): Promise<number | null> {\n const probe = await probeEmblema(config);\n return probe?.port ?? null;\n}\n\n/** Ping, then POST the batch. Never queues; never retries. */\nexport async function sendToEmblema(\n session: ReviewSession | null,\n items: ReviewItem[],\n config: EmblemaConfig,\n): Promise<SendResult> {\n if (items.length === 0) return { ok: false, reason: 'no-items' };\n const probe = await probeEmblema(config);\n if (probe === null) return { ok: false, reason: 'offline' };\n const { port } = probe;\n const payload = buildDispatch(session, items, config, probe.repoPath);\n try {\n const res = await fetchJson(\n `http://127.0.0.1:${port}/intake/review`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n 10_000,\n );\n if (!res.ok) return { ok: false, reason: 'rejected', detail: `HTTP ${res.status}` };\n const body: unknown = await res.json().catch(() => null);\n const returnedId =\n typeof body === 'object' && body !== null && typeof (body as { batchId?: unknown }).batchId === 'string'\n ? ((body as { batchId: string }).batchId)\n : payload.batchId;\n return { ok: true, batchId: returnedId, sent: payload.items.length, port };\n } catch {\n return { ok: false, reason: 'offline' };\n }\n}\n\n/* ---------------- pairing (contract v2) ---------------- */\n\n/** Local sync agent (reviewkit-emblema serve) management API ports. */\nexport const EMBLEMA_SYNC_PORTS = [48770, 48771, 48772, 48773] as const;\n\nexport interface EmblemaSyncState {\n ok: true;\n paired: boolean;\n revoked: boolean;\n id: string;\n name: string;\n url: string;\n lastAck: string | null;\n findings: number;\n}\n\nasync function localFetch(path: string, init?: RequestInit): Promise<Response | null> {\n for (const port of EMBLEMA_SYNC_PORTS) {\n try {\n const abort = new AbortController();\n const timer = window.setTimeout(() => abort.abort(), 1200);\n const res = await fetch(`http://127.0.0.1:${port}${path}`, { ...init, signal: abort.signal });\n window.clearTimeout(timer);\n return res;\n } catch {\n /* next port */\n }\n }\n return null;\n}\n\n/** State of the local pairing agent; null when it isn't running. */\nexport async function fetchSyncState(): Promise<EmblemaSyncState | null> {\n const res = await localFetch('/api/emblema/local/state');\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n return typeof body === 'object' && body !== null && (body as { ok?: unknown }).ok === true\n ? (body as EmblemaSyncState)\n : null;\n}\n\n/** Generate (or rotate — same operation) the pairing key; returns emk_… */\nexport async function generateSyncKey(): Promise<string | null> {\n const res = await localFetch('/api/emblema/local/generate', { method: 'POST' });\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n const key = (body as { key?: unknown } | null)?.key;\n return typeof key === 'string' ? key : null;\n}\n\n/** Current key without rotating (Copy button). */\nexport async function fetchSyncKey(): Promise<string | null> {\n const res = await localFetch('/api/emblema/local/key');\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n const key = (body as { key?: unknown } | null)?.key;\n return typeof key === 'string' ? key : null;\n}\n\nexport async function revokeSyncKey(): Promise<boolean> {\n const res = await localFetch('/api/emblema/local/revoke', { method: 'POST' });\n return res !== null && res.ok;\n}\n"],"mappings":"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","names":[]}
|
package/dist/schema.d.cts
CHANGED
|
@@ -14,10 +14,21 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
declare const REVIEW_DISPATCH_VERSION = 1;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* ReviewKitConfig.emblema — enables the Emblema bridge. `{}` is a valid
|
|
19
|
+
* value: the repo Emblema should edit is Emblema's own local knowledge (it
|
|
20
|
+
* knows which project it opened; the `reviewkit-emblema` agent has it in
|
|
21
|
+
* `.reviewkit/emblema-sync.json`), so an absolute path from the reviewer's
|
|
22
|
+
* machine never has to appear in the host site's code.
|
|
23
|
+
*/
|
|
18
24
|
interface EmblemaConfig {
|
|
19
|
-
/**
|
|
20
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Override the repo path sent with each batch. Normally omitted: the
|
|
27
|
+
* intake advertises its own `repoPath` in the `/intake/ping` answer, and
|
|
28
|
+
* when it does not, the field is left out and Emblema falls back to its
|
|
29
|
+
* active project.
|
|
30
|
+
*/
|
|
31
|
+
repoPath?: string;
|
|
21
32
|
/** Pin the intake to one port instead of probing the default list. */
|
|
22
33
|
port?: number;
|
|
23
34
|
}
|
|
@@ -28,8 +39,12 @@ interface EmblemaReviewDispatch {
|
|
|
28
39
|
batchId: string;
|
|
29
40
|
/** ISO 8601 */
|
|
30
41
|
sentAt: string;
|
|
31
|
-
/**
|
|
32
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Absolute repo path — `emblema.repoPath` if configured, else the path the
|
|
44
|
+
* intake advertised on ping. Omitted when neither is known; the receiver
|
|
45
|
+
* then uses its active project. (Optional since 0.1.3; still `v: 1`.)
|
|
46
|
+
*/
|
|
47
|
+
repoPath?: string;
|
|
33
48
|
site: {
|
|
34
49
|
origin: string;
|
|
35
50
|
reviewUrl: string;
|
package/dist/schema.d.ts
CHANGED
|
@@ -14,10 +14,21 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
declare const REVIEW_DISPATCH_VERSION = 1;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* ReviewKitConfig.emblema — enables the Emblema bridge. `{}` is a valid
|
|
19
|
+
* value: the repo Emblema should edit is Emblema's own local knowledge (it
|
|
20
|
+
* knows which project it opened; the `reviewkit-emblema` agent has it in
|
|
21
|
+
* `.reviewkit/emblema-sync.json`), so an absolute path from the reviewer's
|
|
22
|
+
* machine never has to appear in the host site's code.
|
|
23
|
+
*/
|
|
18
24
|
interface EmblemaConfig {
|
|
19
|
-
/**
|
|
20
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Override the repo path sent with each batch. Normally omitted: the
|
|
27
|
+
* intake advertises its own `repoPath` in the `/intake/ping` answer, and
|
|
28
|
+
* when it does not, the field is left out and Emblema falls back to its
|
|
29
|
+
* active project.
|
|
30
|
+
*/
|
|
31
|
+
repoPath?: string;
|
|
21
32
|
/** Pin the intake to one port instead of probing the default list. */
|
|
22
33
|
port?: number;
|
|
23
34
|
}
|
|
@@ -28,8 +39,12 @@ interface EmblemaReviewDispatch {
|
|
|
28
39
|
batchId: string;
|
|
29
40
|
/** ISO 8601 */
|
|
30
41
|
sentAt: string;
|
|
31
|
-
/**
|
|
32
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Absolute repo path — `emblema.repoPath` if configured, else the path the
|
|
44
|
+
* intake advertised on ping. Omitted when neither is known; the receiver
|
|
45
|
+
* then uses its active project. (Optional since 0.1.3; still `v: 1`.)
|
|
46
|
+
*/
|
|
47
|
+
repoPath?: string;
|
|
33
48
|
site: {
|
|
34
49
|
origin: string;
|
|
35
50
|
reviewUrl: string;
|
package/dist/schema.js
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ITEM_STATUSES,
|
|
3
|
+
KNOWN_ITEM_TYPES,
|
|
4
|
+
REVIEW_DISPATCH_VERSION,
|
|
5
|
+
SCHEMA_VERSION,
|
|
6
|
+
isCommentItem,
|
|
7
|
+
isCopyItem,
|
|
8
|
+
isDeleteItem,
|
|
9
|
+
isImageItem,
|
|
10
|
+
isKnownItemType,
|
|
11
|
+
isMoveItem,
|
|
12
|
+
isStyleItem,
|
|
13
|
+
parseSession,
|
|
14
|
+
parseSessionJson
|
|
15
|
+
} from "./chunk-DG6O5XIC.js";
|
|
16
|
+
export {
|
|
17
|
+
ITEM_STATUSES,
|
|
18
|
+
KNOWN_ITEM_TYPES,
|
|
19
|
+
REVIEW_DISPATCH_VERSION,
|
|
20
|
+
SCHEMA_VERSION,
|
|
21
|
+
isCommentItem,
|
|
22
|
+
isCopyItem,
|
|
23
|
+
isDeleteItem,
|
|
24
|
+
isImageItem,
|
|
25
|
+
isKnownItemType,
|
|
26
|
+
isMoveItem,
|
|
27
|
+
isStyleItem,
|
|
28
|
+
parseSession,
|
|
29
|
+
parseSessionJson
|
|
30
|
+
};
|
|
3
31
|
//# sourceMappingURL=schema.js.map
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@satanwagen/reviewkit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Drop-in visual review & feedback layer for React sites. Reviewers annotate the live page; nothing is ever applied
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Drop-in visual review & feedback layer for React sites. Reviewers annotate the live page; nothing is ever applied \u2014 changes are recorded as a portable session for a coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -19,6 +19,21 @@
|
|
|
19
19
|
"import": "./dist/client/index.js",
|
|
20
20
|
"require": "./dist/client/index.cjs"
|
|
21
21
|
},
|
|
22
|
+
"./lazy": {
|
|
23
|
+
"types": "./dist/lazy.d.ts",
|
|
24
|
+
"import": "./dist/lazy.js",
|
|
25
|
+
"require": "./dist/lazy.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./next": {
|
|
28
|
+
"types": "./dist/next.d.ts",
|
|
29
|
+
"import": "./dist/next.js",
|
|
30
|
+
"require": "./dist/next.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./activate": {
|
|
33
|
+
"types": "./dist/activate.d.ts",
|
|
34
|
+
"import": "./dist/activate.js",
|
|
35
|
+
"require": "./dist/activate.cjs"
|
|
36
|
+
},
|
|
22
37
|
"./schema": {
|
|
23
38
|
"types": "./dist/schema.d.ts",
|
|
24
39
|
"import": "./dist/schema.js",
|
|
@@ -31,14 +46,15 @@
|
|
|
31
46
|
"schema",
|
|
32
47
|
"README.md",
|
|
33
48
|
"LICENSE",
|
|
34
|
-
"
|
|
49
|
+
"CHANGELOG.md"
|
|
35
50
|
],
|
|
36
51
|
"scripts": {
|
|
37
52
|
"build": "tsup",
|
|
38
53
|
"typecheck": "tsc --noEmit",
|
|
39
54
|
"lint": "eslint src",
|
|
40
|
-
"test": "node --test \"cli/**/*.test.mjs\"",
|
|
41
|
-
"prepublishOnly": "npm run typecheck && npm run lint && npm test
|
|
55
|
+
"test": "node --test \"cli/**/*.test.mjs\" \"test/**/*.test.mjs\"",
|
|
56
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm test",
|
|
57
|
+
"pretest": "npm run build"
|
|
42
58
|
},
|
|
43
59
|
"publishConfig": {
|
|
44
60
|
"access": "public"
|
|
@@ -65,8 +81,5 @@
|
|
|
65
81
|
"tsup": "^8.3.6",
|
|
66
82
|
"typescript": "^5.7.3",
|
|
67
83
|
"typescript-eslint": "^8.24.0"
|
|
68
|
-
},
|
|
69
|
-
"bin": {
|
|
70
|
-
"reviewkit-emblema": "./cli/emblema-sync.mjs"
|
|
71
84
|
}
|
|
72
85
|
}
|