@pygmalionjs/pygmalion 0.8.1 → 0.10.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/README.md +34 -0
- package/binding-source.d.ts +25 -0
- package/dist-lib/{CameraLayer-B2D29VXQ.js → CameraLayer-BUOlJcUa.js} +17 -17
- package/dist-lib/pygmalion.js +24524 -12555
- package/dist-lib/{runtime-BAy8W1mJ.js → runtime-BOfDlGPR.js} +12438 -11118
- package/dist-lib/style.css +1 -1
- package/dist-lib/testing.js +19 -19
- package/dist-lib/types/binding/catalog.d.ts +10 -0
- package/dist-lib/types/binding/contracts.d.ts +202 -0
- package/dist-lib/types/binding/documentCommands.d.ts +38 -0
- package/dist-lib/types/binding/duplicateMetadata.d.ts +8 -0
- package/dist-lib/types/binding/index.d.ts +7 -0
- package/dist-lib/types/binding/preview.d.ts +37 -0
- package/dist-lib/types/binding/source.d.ts +109 -0
- package/dist-lib/types/binding/useDataProjection.d.ts +9 -0
- package/dist-lib/types/binding/validate.d.ts +8 -0
- package/dist-lib/types/core/runtime.d.ts +6 -0
- package/dist-lib/types/document/appearance.d.ts +112 -0
- package/dist-lib/types/document/assets.d.ts +204 -0
- package/dist-lib/types/document/components.d.ts +169 -0
- package/dist-lib/types/document/contracts.d.ts +171 -0
- package/dist-lib/types/document/engine.d.ts +20 -0
- package/dist-lib/types/document/exportSettings.d.ts +45 -0
- package/dist-lib/types/document/geometry.d.ts +11 -0
- package/dist-lib/types/document/layout.d.ts +53 -0
- package/dist-lib/types/document/prototype.d.ts +424 -0
- package/dist-lib/types/document/prototypeReferences.d.ts +3 -0
- package/dist-lib/types/document/richText.d.ts +185 -0
- package/dist-lib/types/document/styles.d.ts +216 -0
- package/dist-lib/types/document/svgImport.d.ts +17 -0
- package/dist-lib/types/document/vector.d.ts +222 -0
- package/dist-lib/types/editor/designCompiler.d.ts +7 -0
- package/dist-lib/types/editor/designImport.d.ts +7 -0
- package/dist-lib/types/editor/inspect.d.ts +8 -0
- package/dist-lib/types/editor/revisionCatalogInstall.d.ts +1 -1
- package/dist-lib/types/editor/screenInteractions.d.ts +3 -3
- package/dist-lib/types/editor/store.d.ts +1 -1
- package/dist-lib/types/host/project.d.ts +44 -0
- package/dist-lib/types/host/registryConnections.d.ts +11 -0
- package/dist-lib/types/host/sourceFilePlan.d.ts +15 -0
- package/dist-lib/types/host/sourceWriteback.d.ts +50 -0
- package/dist-lib/types/lib.d.ts +103 -3
- package/dist-lib/types/token-library/bindings.d.ts +62 -0
- package/dist-lib/types/token-library/contracts.d.ts +117 -0
- package/dist-lib/types/token-library/engine.d.ts +13 -0
- package/dist-lib/types/token-library/valueResolver.d.ts +19 -0
- package/dist-lib/types/workspace/WorkspaceShell.d.ts +9 -0
- package/dist-lib/types/workspace/application/ApplicationWorkspace.d.ts +3 -0
- package/dist-lib/types/workspace/configuration.d.ts +47 -0
- package/dist-lib/types/workspace/contracts.d.ts +64 -0
- package/dist-lib/types/workspace/controller.d.ts +35 -0
- package/dist-lib/types/workspace/documentSessions.d.ts +42 -0
- package/dist-lib/types/workspace/edit/AppearancePanel.d.ts +23 -0
- package/dist-lib/types/workspace/edit/ArrangeLayers.d.ts +10 -0
- package/dist-lib/types/workspace/edit/AssetsPanel.d.ts +16 -0
- package/dist-lib/types/workspace/edit/CanvasGuides.d.ts +19 -0
- package/dist-lib/types/workspace/edit/ComponentPanel.d.ts +11 -0
- package/dist-lib/types/workspace/edit/DataPanel.d.ts +16 -0
- package/dist-lib/types/workspace/edit/DesignWorkspace.d.ts +10 -0
- package/dist-lib/types/workspace/edit/LayerTree.d.ts +14 -0
- package/dist-lib/types/workspace/edit/LayoutPanel.d.ts +13 -0
- package/dist-lib/types/workspace/edit/PrototypePanel.d.ts +12 -0
- package/dist-lib/types/workspace/edit/ScopeTransferDialog.d.ts +40 -0
- package/dist-lib/types/workspace/edit/SelectionHandles.d.ts +52 -0
- package/dist-lib/types/workspace/edit/StylesPanel.d.ts +8 -0
- package/dist-lib/types/workspace/edit/TextEditor.d.ts +50 -0
- package/dist-lib/types/workspace/edit/VectorEditor.d.ts +89 -0
- package/dist-lib/types/workspace/edit/VectorToolbar.d.ts +5 -0
- package/dist-lib/types/workspace/edit/canvasGuideModel.d.ts +102 -0
- package/dist-lib/types/workspace/edit/clipboard.d.ts +35 -0
- package/dist-lib/types/workspace/edit/duplicateMetadata.d.ts +2 -0
- package/dist-lib/types/workspace/edit/geometryHandles.d.ts +224 -0
- package/dist-lib/types/workspace/edit/layerArrangement.d.ts +39 -0
- package/dist-lib/types/workspace/edit/layerMoves.d.ts +44 -0
- package/dist-lib/types/workspace/edit/projection.d.ts +12 -0
- package/dist-lib/types/workspace/edit/scopeTransfer.d.ts +96 -0
- package/dist-lib/types/workspace/edit/textStyles.d.ts +75 -0
- package/dist-lib/types/workspace/edit/vectorTools.d.ts +136 -0
- package/dist-lib/types/workspace/legacyModeAdapter.d.ts +13 -0
- package/dist-lib/types/workspace/source/ResponsiveLayoutPanel.d.ts +20 -0
- package/dist-lib/types/workspace/source/SourceReview.d.ts +11 -0
- package/dist-lib/types/workspace/source/appearanceSource.d.ts +19 -0
- package/dist-lib/types/workspace/source/compiler.d.ts +5 -0
- package/dist-lib/types/workspace/source/contracts.d.ts +156 -0
- package/dist-lib/types/workspace/source/controller.d.ts +50 -0
- package/dist-lib/types/workspace/source/documentRevision.d.ts +2 -0
- package/dist-lib/types/workspace/source/draftPreviewClient.d.ts +13 -0
- package/dist-lib/types/workspace/source/draftRecipe.d.ts +30 -0
- package/dist-lib/types/workspace/source/flowContracts.d.ts +72 -0
- package/dist-lib/types/workspace/source/prototypeSource.d.ts +4 -0
- package/dist-lib/types/workspace/source/responsive.d.ts +13 -0
- package/dist-lib/types/workspace/source/responsiveContracts.d.ts +78 -0
- package/dist-lib/types/workspace/source/structure.d.ts +21 -0
- package/dist-lib/types/workspace/source/textSource.d.ts +28 -0
- package/dist-lib/types/workspace/tokens/TokenLibraryContext.d.ts +11 -0
- package/dist-lib/types/workspace/tokens/TokenLibraryWorkspace.d.ts +14 -0
- package/dist-lib/types/workspace/tokens/contracts.d.ts +74 -0
- package/dist-lib/types/workspace/tokens/controller.d.ts +44 -0
- package/dist-lib/types/workspace/view/PrototypePlayer.d.ts +17 -0
- package/dist-lib/types/workspace/view/StoryboardWorkspace.d.ts +3 -0
- package/dist-lib/types/workspace/view/prototypePlayback.d.ts +46 -0
- package/draft-preview.d.ts +54 -0
- package/host-runtime.d.ts +73 -0
- package/inspect.d.ts +2 -0
- package/jsx-source-reconciler.d.ts +35 -0
- package/node/design-session.mjs +3 -0
- package/node/host-runtime.mjs +4 -0
- package/node/inspect-plugin.mjs +3 -1
- package/node/inspect-writeback.mjs +18 -2
- package/node/jsx-source-reconciler.mjs +155 -0
- package/node/preview-artifact-store.mjs +10 -7
- package/node/revision-catalog-resolver.mjs +159 -0
- package/node/source-archive-runtime.mjs +63 -0
- package/node/source-archive.mjs +88 -0
- package/node/source-bindings.mjs +65 -0
- package/node/source-file-lock.mjs +15 -0
- package/node/source-file-plan.mjs +162 -0
- package/node/source-file-proofs.mjs +44 -0
- package/node/source-module.mjs +17 -0
- package/node/source-responsive-codec.d.mts +17 -0
- package/node/source-responsive-codec.mjs +133 -0
- package/node/source-responsive.mjs +189 -0
- package/node/source-service-inputs.mjs +81 -0
- package/node/source-service-plugin.mjs +57 -0
- package/node/source-service.mjs +4 -0
- package/node/source-structure-reconcile.mjs +186 -0
- package/node/source-structure-wrapper-codec.d.mts +20 -0
- package/node/source-structure-wrapper-codec.mjs +205 -0
- package/node/source-structure.mjs +404 -0
- package/node/token-source-references.mjs +136 -0
- package/node/token-source-service.mjs +151 -0
- package/node/workspace-draft-entry.mjs +52 -0
- package/node/workspace-draft-preview-plugin.mjs +70 -0
- package/node/workspace-draft-preview.mjs +232 -0
- package/node/workspace-draft-process.mjs +129 -0
- package/node/workspace-draft-runtime.mjs +67 -0
- package/node/workspace-source-identity.d.mts +1 -0
- package/node/workspace-source-identity.mjs +21 -0
- package/node/workspace-source-plan.mjs +195 -0
- package/node/workspace-source-session.mjs +187 -0
- package/package.json +90 -6
- package/source-files.d.ts +35 -0
- package/source-module.d.ts +16 -0
- package/source-responsive.d.ts +10 -0
- package/source-service.d.ts +92 -0
- package/source-structure.d.ts +155 -0
- package/workspace-draft-entry.d.ts +15 -0
- package/workspace-source-plan.d.ts +44 -0
- package/docs/coverage-contracts.md +0 -444
- package/docs/screen-state-contract.md +0 -252
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TokenLibraryEngine } from '../../token-library/contracts.js';
|
|
2
|
+
import type { InspectApplyResult, InspectPreviewResult } from '../../editor/host.js';
|
|
3
|
+
import type { WorkspaceTarget } from '../contracts.js';
|
|
4
|
+
import type { WorkspaceTokenLibraryBaseline, WorkspaceTokenLibraryCapabilities, WorkspaceTokenSourceRequest } from './contracts.js';
|
|
5
|
+
export interface WorkspaceTokenLibraryState {
|
|
6
|
+
status: 'idle' | 'loading' | 'ready' | 'reviewing' | 'reviewed' | 'applying' | 'applied' | 'error';
|
|
7
|
+
baseline?: WorkspaceTokenLibraryBaseline;
|
|
8
|
+
engine?: TokenLibraryEngine;
|
|
9
|
+
request?: WorkspaceTokenSourceRequest;
|
|
10
|
+
preview?: InspectPreviewResult;
|
|
11
|
+
result?: InspectApplyResult;
|
|
12
|
+
savedFingerprint?: string;
|
|
13
|
+
saving?: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
/** A library owns its draft and review independently from visual screen sessions. */
|
|
17
|
+
export declare class WorkspaceTokenLibraryController {
|
|
18
|
+
private state;
|
|
19
|
+
private listeners;
|
|
20
|
+
private epoch;
|
|
21
|
+
private pending;
|
|
22
|
+
private pendingSave;
|
|
23
|
+
private saveGeneration;
|
|
24
|
+
private disposed;
|
|
25
|
+
private invalidating;
|
|
26
|
+
private applying;
|
|
27
|
+
private unsubscribe;
|
|
28
|
+
private writer;
|
|
29
|
+
readonly capabilities: WorkspaceTokenLibraryCapabilities;
|
|
30
|
+
readonly sourceRevision: string;
|
|
31
|
+
constructor(sourceRevision: string, capabilities: WorkspaceTokenLibraryCapabilities);
|
|
32
|
+
getSnapshot: () => WorkspaceTokenLibraryState;
|
|
33
|
+
subscribe: (listener: () => void) => () => void;
|
|
34
|
+
private update;
|
|
35
|
+
private invalidate;
|
|
36
|
+
private current;
|
|
37
|
+
private subscribeEngine;
|
|
38
|
+
load(): Promise<boolean>;
|
|
39
|
+
saveDraft(): Promise<boolean>;
|
|
40
|
+
review(target: WorkspaceTarget): Promise<boolean>;
|
|
41
|
+
apply(): Promise<boolean>;
|
|
42
|
+
reset(): boolean;
|
|
43
|
+
dispose(): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { DesignDocument } from '../../document/contracts.js';
|
|
3
|
+
import { type PrototypeState } from '../../document/prototype.js';
|
|
4
|
+
import { type PrototypeClock } from './prototypePlayback.js';
|
|
5
|
+
|
|
6
|
+
export interface PrototypePlayerProps {
|
|
7
|
+
document: DesignDocument;
|
|
8
|
+
/** Uses the same document/component renderer as Edit, with authoring controls disabled. */
|
|
9
|
+
renderScene(rootId: string, state: PrototypeState): ReactNode;
|
|
10
|
+
sceneSize?(rootId: string, state: PrototypeState): {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
} | undefined;
|
|
14
|
+
onClose(): void;
|
|
15
|
+
clock?: PrototypeClock;
|
|
16
|
+
}
|
|
17
|
+
export declare function PrototypePlayer(props: PrototypePlayerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DesignDocument, DocumentNode, ResolvedDocumentNode } from '../../document/contracts.js';
|
|
2
|
+
import { type PrototypeEffect, type PrototypeEvent, type PrototypeStartOptions, type PrototypeState } from '../../document/prototype.js';
|
|
3
|
+
export interface PrototypeClock {
|
|
4
|
+
now(): number;
|
|
5
|
+
schedule(callback: () => void, delayMs: number): unknown;
|
|
6
|
+
cancel(handle: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
export interface PrototypePlayerSnapshot {
|
|
9
|
+
state: PrototypeState;
|
|
10
|
+
previous: PrototypeState | null;
|
|
11
|
+
effects: readonly PrototypeEffect[];
|
|
12
|
+
playing: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface PrototypePlayerOptions extends PrototypeStartOptions {
|
|
15
|
+
clock: PrototypeClock;
|
|
16
|
+
/** Effects describe intended links and scrolling; the adapter decides how to execute them. */
|
|
17
|
+
onEffect?(effect: PrototypeEffect): void;
|
|
18
|
+
}
|
|
19
|
+
export interface PrototypePlayerController {
|
|
20
|
+
getSnapshot(): PrototypePlayerSnapshot;
|
|
21
|
+
subscribe(listener: () => void): () => void;
|
|
22
|
+
dispatch(event: PrototypeEvent): void;
|
|
23
|
+
pause(): void;
|
|
24
|
+
resume(): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
/** One document snapshot and one clock. Playback cannot mutate the document engine or its history. */
|
|
28
|
+
export declare function createPrototypePlayer(document: DesignDocument, options: PrototypePlayerOptions): PrototypePlayerController;
|
|
29
|
+
export interface PrototypePresentationScene {
|
|
30
|
+
id: string;
|
|
31
|
+
rootId: string;
|
|
32
|
+
root: ResolvedDocumentNode;
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
overlay: boolean;
|
|
36
|
+
backdrop: {
|
|
37
|
+
color: string;
|
|
38
|
+
opacity: number;
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
export declare function prototypeScene(document: DesignDocument, state: PrototypeState): PrototypePresentationScene[];
|
|
42
|
+
export declare function prototypeFlowOptions(document: DesignDocument): {
|
|
43
|
+
id: string;
|
|
44
|
+
label: string;
|
|
45
|
+
node: DocumentNode;
|
|
46
|
+
}[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { WorkspaceSourceRequest } from './dist-lib/types/workspace/source/flowContracts.js';
|
|
3
|
+
import type { WorkspaceTokenSourceRequest } from './dist-lib/types/workspace/tokens/contracts.js';
|
|
4
|
+
import type { WorkspaceApplicationSurface } from './dist-lib/types/workspace/contracts.js';
|
|
5
|
+
|
|
6
|
+
export type WorkspaceDraftSourceRequest = WorkspaceSourceRequest | WorkspaceTokenSourceRequest;
|
|
7
|
+
|
|
8
|
+
export interface WorkspaceDraftPreviewServiceOptions {
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
appDirectory?: string;
|
|
11
|
+
/** Existing node_modules directory reused by the trusted isolated build. */
|
|
12
|
+
dependenciesRoot?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Stage proven working-copy inputs in the disposable archive. Full request proofs are checked afterward.
|
|
15
|
+
* `appRoot` holds exactly `sourceRevision` (the request target), materialized from the repository
|
|
16
|
+
* history rather than the working tree, so another revision than the checkout can be built honestly.
|
|
17
|
+
*/
|
|
18
|
+
prepareSource?(context: { appRoot: string; request: WorkspaceDraftSourceRequest; sourceRevision: string; signal: AbortSignal }): Promise<void>;
|
|
19
|
+
/** Must stop and settle when aborted; outputDirectory must contain a completed static app. */
|
|
20
|
+
build(context: { appRoot: string; outputDirectory: string; request: WorkspaceDraftSourceRequest; sourceRevision: string; signal: AbortSignal }): Promise<void>;
|
|
21
|
+
/** Return true after handling a host API request. False or void allows static routing. */
|
|
22
|
+
handleRequest?(request: IncomingMessage, response: ServerResponse, context: { request: WorkspaceDraftSourceRequest; sourceRevision: string }): boolean | void | Promise<boolean | void>;
|
|
23
|
+
/** Active builds and ready origins count toward this bound. Default: 3. */
|
|
24
|
+
maxRuns?: number;
|
|
25
|
+
maxOutputFiles?: number;
|
|
26
|
+
maxOutputBytes?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare function createWorkspaceDraftPreviewService(options: WorkspaceDraftPreviewServiceOptions): {
|
|
30
|
+
/** The host must validate file-plan requests with its canonical compiler before preparing a build. */
|
|
31
|
+
prepare(input: { request: WorkspaceDraftSourceRequest; documentRevision: string; route: string }, options?: { signal?: AbortSignal }): Promise<WorkspaceApplicationSurface>;
|
|
32
|
+
/** Abort builds, close origins, and remove every temporary archive. */
|
|
33
|
+
dispose(): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Minimal Vite-compatible boundary; headless service consumers do not require Vite types. */
|
|
37
|
+
export declare function workspaceDraftPreviewPlugin(options: {
|
|
38
|
+
service: ReturnType<typeof createWorkspaceDraftPreviewService>;
|
|
39
|
+
endpoint?: string;
|
|
40
|
+
/** The built app reports its recipe result through the workspace ready-message protocol. */
|
|
41
|
+
waitForReady?: boolean;
|
|
42
|
+
/** Validate file-plan source semantics with the host compiler, not only browser-supplied fingerprints. */
|
|
43
|
+
resolveRequest(input: unknown, options: { signal: AbortSignal }):
|
|
44
|
+
{ request: WorkspaceDraftSourceRequest; documentRevision: string; route: string } |
|
|
45
|
+
Promise<{ request: WorkspaceDraftSourceRequest; documentRevision: string; route: string }>;
|
|
46
|
+
}): {
|
|
47
|
+
name: string;
|
|
48
|
+
apply: 'serve';
|
|
49
|
+
configureServer(server: {
|
|
50
|
+
httpServer?: { once(event: 'close', listener: () => void): unknown } | null;
|
|
51
|
+
config: { logger: { error(message: string): void } };
|
|
52
|
+
middlewares: { use(handler: (request: IncomingMessage, response: ServerResponse, next: () => void) => void | Promise<void>): unknown };
|
|
53
|
+
}): void;
|
|
54
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { SpawnOptions } from 'node:child_process';
|
|
2
|
+
import type { PygmalionRevisionCatalog } from './dist-lib/types/editor/revisionCatalog.js';
|
|
3
|
+
|
|
4
|
+
export interface WorkspaceDraftProcessOptions extends Omit<SpawnOptions, 'signal' | 'detached' | 'stdio' | 'timeout' | 'killSignal'> {
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
/** Milliseconds before cancellation. Zero disables the deadline. Default: 240,000. */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Milliseconds between SIGTERM and SIGKILL. Default: 1,000. */
|
|
9
|
+
terminationGraceMs?: number;
|
|
10
|
+
/** Maximum bytes per output stream. Default: 8 MiB. */
|
|
11
|
+
maxBuffer?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** macOS/Linux: waits for process-group cleanup on success, failure, cancellation, and timeout. */
|
|
15
|
+
export declare function executeWorkspaceDraftProcess(
|
|
16
|
+
command: string, args: readonly string[], options?: WorkspaceDraftProcessOptions,
|
|
17
|
+
): Promise<{ stdout: string; stderr: string }>;
|
|
18
|
+
|
|
19
|
+
export interface RevisionCatalogResolver {
|
|
20
|
+
/** Only complete SHA-1 or SHA-256 commits are accepted; returned catalogs are frozen copies. */
|
|
21
|
+
resolve(sourceRevision: string, signal?: AbortSignal): Promise<PygmalionRevisionCatalog>;
|
|
22
|
+
currentRevision(signal?: AbortSignal): Promise<string>;
|
|
23
|
+
/** Least recently used first; expired and current-build entries are omitted. */
|
|
24
|
+
retained(): string[];
|
|
25
|
+
/** Cancels owned generation, discards cached results, and rejects subsequent requests. */
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RevisionCatalogResolverOptions {
|
|
30
|
+
projectRoot?: string;
|
|
31
|
+
/** Repository-relative application directory. Default: the repository root. */
|
|
32
|
+
appDirectory?: string;
|
|
33
|
+
currentCatalog(signal?: AbortSignal): unknown | Promise<unknown>;
|
|
34
|
+
generate(context: { archiveRoot: string; sourceRevision: string; signal: AbortSignal }): unknown | Promise<unknown>;
|
|
35
|
+
/** Defaults to commits reachable from HEAD or repository branches, remotes, and tags. */
|
|
36
|
+
reachable?(sourceRevision: string, signal: AbortSignal): boolean | Promise<boolean>;
|
|
37
|
+
/** Custom extraction must await its callback and stop workers before removing source files. */
|
|
38
|
+
archive?<T>(sourceRevision: string, signal: AbortSignal,
|
|
39
|
+
operation: (context: Readonly<{ appRoot: string; sourceRevision: string; signal: AbortSignal }>) => Promise<T>): Promise<T>;
|
|
40
|
+
/** Defaults to 4 archived catalogs; zero disables retention. */
|
|
41
|
+
maxEntries?: number;
|
|
42
|
+
/** Absolute lifetime since generation, in milliseconds. Default: 30 minutes. */
|
|
43
|
+
maxAgeMs?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Shares requests for each revision and serializes archive/build work across revisions. */
|
|
47
|
+
export declare function createRevisionCatalogResolver(options: RevisionCatalogResolverOptions): RevisionCatalogResolver;
|
|
48
|
+
|
|
49
|
+
/** Returns dependency search paths from the first byte-identical lockfile, or [] for an archive-local install. */
|
|
50
|
+
export declare function resolveSourceArchiveDependencies(options: {
|
|
51
|
+
archiveRoot: string; dependencyRoots: readonly string[]; lockfile?: string;
|
|
52
|
+
}): Promise<string[]>;
|
|
53
|
+
|
|
54
|
+
/** Rejects host worktrees, Git metadata, and missing or linked required directories. */
|
|
55
|
+
export declare function assertIsolatedSourceRoot(options: {
|
|
56
|
+
archiveRoot: string; hostRoot: string; requiredDirectories?: readonly string[];
|
|
57
|
+
}): Promise<string>;
|
|
58
|
+
|
|
59
|
+
/** Converts SIGTERM/SIGINT into cancellation and exit status; dispose removes its process listeners. */
|
|
60
|
+
export declare function createWorkspaceDraftLifecycle(): Readonly<{
|
|
61
|
+
signal: AbortSignal;
|
|
62
|
+
dispose(): void;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
/** Requires canonical source/output directories belonging to one SDK draft-preview temporary root. */
|
|
66
|
+
export declare function assertWorkspaceDraftDirectories(options: {
|
|
67
|
+
appRoot: string; outputDirectory: string; appDirectory?: string;
|
|
68
|
+
}): Promise<Readonly<{ appRoot: string; outputDirectory: string; temporaryRoot: string }>>;
|
|
69
|
+
|
|
70
|
+
/** Imports the installed package's explicit exports['.'].import entry; no parent-install fallback. */
|
|
71
|
+
export declare function importIsolatedPackage<T = Record<string, unknown>>(options: {
|
|
72
|
+
appRoot: string; name: string;
|
|
73
|
+
}): Promise<T>;
|
package/inspect.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { InspectApplyPayload } from './dist-lib/types/lib';
|
|
2
2
|
|
|
3
3
|
export interface PygmalionInspectWritebackOptions {
|
|
4
|
+
/** If supplied, source proofs must use this exact source revision. */
|
|
5
|
+
sourceRevision?: string;
|
|
4
6
|
sourceDirectory?: string;
|
|
5
7
|
normalizeValue?: (property: string, value: string) => string;
|
|
6
8
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { WorkspaceTarget } from './dist-lib/types/workspace/contracts.js';
|
|
2
|
+
import type { WorkspaceSourceBaseline, WorkspaceSourceReconcileInput, WorkspaceSourceReconcileResult } from './dist-lib/types/workspace/source/flowContracts.js';
|
|
3
|
+
import type { compileWorkspaceDocumentSource } from './dist-lib/types/workspace/source/structure.js';
|
|
4
|
+
import type { JsxStructureAnchor, JsxStructureIdentity, JsxStructureSourceNode, JsxStructureWrapperEvidence } from './source-structure.js';
|
|
5
|
+
|
|
6
|
+
export type ReconcileSourceInputs = Readonly<Record<string, string | null>>;
|
|
7
|
+
export interface ReconcileSourceProjection { anchors: JsxStructureAnchor[]; nodes: JsxStructureSourceNode[] }
|
|
8
|
+
export interface JsxSourceReconcilerOptions {
|
|
9
|
+
file: string;
|
|
10
|
+
identityFile: string;
|
|
11
|
+
rootId: string;
|
|
12
|
+
definitionId: string;
|
|
13
|
+
screenId: string;
|
|
14
|
+
documentId: string;
|
|
15
|
+
opaqueNodeIds: readonly string[];
|
|
16
|
+
/** The owner validates original expressions and allowed source structure. */
|
|
17
|
+
projectSource(input: { source: string; identity: JsxStructureIdentity | null; sourceInputs: ReconcileSourceInputs }): ReconcileSourceProjection;
|
|
18
|
+
resolveWrapper?(inputs: ReconcileSourceInputs): JsxStructureWrapperEvidence | undefined;
|
|
19
|
+
/** Pass the SDK's document compiler; injecting it avoids loading the browser bundle in Node. */
|
|
20
|
+
compileDocumentSource: typeof compileWorkspaceDocumentSource;
|
|
21
|
+
/** Explicit policy for a unique byte-identical island; changed or duplicate islands remain conflicts. Default: false. */
|
|
22
|
+
allowUnchangedIslandRelocation?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface JsxReconcileInputs {
|
|
25
|
+
baselineInputs: ReconcileSourceInputs;
|
|
26
|
+
externalInputs: ReconcileSourceInputs;
|
|
27
|
+
baselineIdentity?: JsxStructureIdentity;
|
|
28
|
+
}
|
|
29
|
+
export interface JsxSourceReconciler {
|
|
30
|
+
projectInputs(input: JsxReconcileInputs): { identity: JsxStructureIdentity; projectedStructure: ReconcileSourceProjection };
|
|
31
|
+
reconcile(input: WorkspaceSourceReconcileInput & JsxReconcileInputs & {
|
|
32
|
+
loadBaseline(input: { target: WorkspaceTarget; sourceInputs: ReconcileSourceInputs; projectedStructure: ReconcileSourceProjection }, signal?: AbortSignal): Promise<WorkspaceSourceBaseline>;
|
|
33
|
+
}, signal?: AbortSignal): Promise<WorkspaceSourceReconcileResult>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createJsxSourceReconciler(options: JsxSourceReconcilerOptions): JsxSourceReconciler;
|
package/node/design-session.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { normalizeViteMode } from './dev-mirror.mjs';
|
|
|
12
12
|
import { writeStyleEdit, writeTextEdit } from './inspect-plugin.mjs';
|
|
13
13
|
import { createUnifiedDiff } from './source-diff.mjs';
|
|
14
14
|
import { writeSourceOperations } from './source-operations.mjs';
|
|
15
|
+
import { validateInspectSourceProofs } from './source-file-proofs.mjs';
|
|
15
16
|
import { resolvePygmalionPreviewViteCacheDir } from './preview-vite-cache.mjs';
|
|
16
17
|
import { sendJson as sendJsonEnvelope } from './envelope.mjs';
|
|
17
18
|
|
|
@@ -696,6 +697,7 @@ export function pygmalionDesignSessionPlugin(options) {
|
|
|
696
697
|
if (dirty) throw new Error(`There are unconfirmed changes in the edit session task folder:${dirty.split('\n')[0]}`);
|
|
697
698
|
|
|
698
699
|
const sessionAppRoot = appRootForWorktree(session.worktreeRoot);
|
|
700
|
+
await validateInspectSourceProofs(sessionAppRoot, input.changes, { sourceRevision: session.baseDevSha });
|
|
699
701
|
const files = collectVisualFiles(sessionAppRoot, changes);
|
|
700
702
|
const before = new Map();
|
|
701
703
|
for (const [relativeFile, absoluteFile] of files) {
|
|
@@ -748,6 +750,7 @@ export function pygmalionDesignSessionPlugin(options) {
|
|
|
748
750
|
if (dirty) throw new Error(`There are unconfirmed changes in the edit session task folder:${dirty.split('\n')[0]}`);
|
|
749
751
|
|
|
750
752
|
const sessionAppRoot = appRootForWorktree(session.worktreeRoot);
|
|
753
|
+
await validateInspectSourceProofs(sessionAppRoot, input.changes, { sourceRevision: session.baseDevSha });
|
|
751
754
|
const files = collectVisualFiles(sessionAppRoot, { styles, texts, operations });
|
|
752
755
|
const backups = new Map();
|
|
753
756
|
for (const [relativeFile, absoluteFile] of files) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { executeWorkspaceDraftProcess } from './workspace-draft-process.mjs';
|
|
2
|
+
export { createRevisionCatalogResolver } from './revision-catalog-resolver.mjs';
|
|
3
|
+
export { assertIsolatedSourceRoot, resolveSourceArchiveDependencies } from './source-archive-runtime.mjs';
|
|
4
|
+
export { createWorkspaceDraftLifecycle, assertWorkspaceDraftDirectories, importIsolatedPackage } from './workspace-draft-runtime.mjs';
|
package/node/inspect-plugin.mjs
CHANGED
|
@@ -24,8 +24,10 @@ export { writeSourceOperations } from './source-operations.mjs';
|
|
|
24
24
|
// POST /__pygmalion-inspect/impact — Check dedependency closure of changed files.
|
|
25
25
|
// The editor doesn't know the endpoint URL — the onInspectApply callback in design-editor.tsx POSTs here.
|
|
26
26
|
|
|
27
|
+
// Only a real import statement at the start of a line registers; the same text inside a
|
|
28
|
+
// string literal or comment must not create a registration for an undefined identifier.
|
|
27
29
|
const CSS_MODULE_IMPORT_RE =
|
|
28
|
-
|
|
30
|
+
/^[ \t]*import\s+([A-Za-z_$][\w$]*)\s+from\s+['"]([^'"]+\.module\.scss)['"]\s*;?/gm;
|
|
29
31
|
|
|
30
32
|
export const PYGMALION_INSPECT_CONTROL = PYGMALION_ENDPOINTS.inspectControl;
|
|
31
33
|
|
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { writeStyleEdit, writeTextEdit } from './inspect-plugin.mjs';
|
|
5
5
|
import { createUnifiedDiff } from './source-diff.mjs';
|
|
6
6
|
import { writeSourceOperations } from './source-operations.mjs';
|
|
7
|
+
import { validateInspectSourceProofs } from './source-file-proofs.mjs';
|
|
8
|
+
import { withSourceFileLock } from './source-file-lock.mjs';
|
|
7
9
|
|
|
8
10
|
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts']);
|
|
9
11
|
|
|
@@ -100,8 +102,9 @@ async function applyInspectChanges(root, changes, options) {
|
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
async function previewInspectChangesUnlocked(root, input, options = {}) {
|
|
104
106
|
const appRoot = path.resolve(root);
|
|
107
|
+
await validateInspectSourceProofs(appRoot, input, options);
|
|
105
108
|
const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
|
|
106
109
|
const changes = normalizeInspectChanges(input);
|
|
107
110
|
const files = collectInspectFiles(appRoot, changes, sourceDirectory);
|
|
@@ -136,8 +139,9 @@ export async function previewInspectChanges(root, input, options = {}) {
|
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
|
|
142
|
+
async function writeInspectChangesUnlocked(root, input, options = {}) {
|
|
140
143
|
const appRoot = path.resolve(root);
|
|
144
|
+
await validateInspectSourceProofs(appRoot, input, options);
|
|
141
145
|
const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
|
|
142
146
|
const changes = normalizeInspectChanges(input);
|
|
143
147
|
const files = collectInspectFiles(appRoot, changes, sourceDirectory);
|
|
@@ -161,3 +165,15 @@ export async function writeInspectChanges(root, input, options = {}) {
|
|
|
161
165
|
throw error;
|
|
162
166
|
}
|
|
163
167
|
}
|
|
168
|
+
|
|
169
|
+
export async function previewInspectChanges(root, input, options = {}) {
|
|
170
|
+
const snapshot = structuredClone(input);
|
|
171
|
+
const configuration = { ...options };
|
|
172
|
+
return withSourceFileLock(root, base => previewInspectChangesUnlocked(base, snapshot, configuration));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function writeInspectChanges(root, input, options = {}) {
|
|
176
|
+
const snapshot = structuredClone(input);
|
|
177
|
+
const configuration = { ...options };
|
|
178
|
+
return withSourceFileLock(root, base => writeInspectChangesUnlocked(base, snapshot, configuration));
|
|
179
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { workspaceSourceFingerprint as fingerprint } from './workspace-source-identity.mjs';
|
|
3
|
+
import { createJsxStructureIdentity, reconcileJsxStructurePlan, reimportJsxStructureSource } from './source-structure.mjs';
|
|
4
|
+
|
|
5
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
6
|
+
const same = (left, right) => fingerprint(left) === fingerprint(right);
|
|
7
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
function freeze(value) {
|
|
9
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(freeze); Object.freeze(value); }
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
const capture = value => freeze(JSON.parse(fingerprint(value)));
|
|
13
|
+
function fail(code, message, file) {
|
|
14
|
+
throw Object.assign(new Error(message), { diagnostics: [{ code: `source-reconcile-${code}`, message, ...(file ? { file } : {}) }] });
|
|
15
|
+
}
|
|
16
|
+
const sourceLayout = layout => layout ? { direction: layout.direction, gap: layout.gap, padding: layout.padding, ...(layout.align === undefined ? {} : { align: layout.align }) } : null;
|
|
17
|
+
|
|
18
|
+
/** Reconciles one explicitly declared, unchanged source island while delegating product ownership to its projector. */
|
|
19
|
+
export function createJsxSourceReconciler({
|
|
20
|
+
file, identityFile, rootId, definitionId, screenId, documentId, opaqueNodeIds,
|
|
21
|
+
projectSource, resolveWrapper = () => undefined, compileDocumentSource,
|
|
22
|
+
allowUnchangedIslandRelocation = false,
|
|
23
|
+
}) {
|
|
24
|
+
if ([file, identityFile, rootId, definitionId, screenId, documentId].some(value => typeof value !== 'string' || !value)
|
|
25
|
+
|| file === identityFile || !Array.isArray(opaqueNodeIds) || new Set(opaqueNodeIds).size !== opaqueNodeIds.length
|
|
26
|
+
|| opaqueNodeIds.some(value => typeof value !== 'string' || !value)
|
|
27
|
+
|| ![projectSource, resolveWrapper, compileDocumentSource].every(value => typeof value === 'function')) {
|
|
28
|
+
throw new TypeError('A source reconciler requires explicit files, node identities, and projection/compiler callbacks.');
|
|
29
|
+
}
|
|
30
|
+
opaqueNodeIds = [...opaqueNodeIds];
|
|
31
|
+
function capturedInputs(value) {
|
|
32
|
+
if (!record(value) || Object.keys(value).length > 500 || !Object.hasOwn(value, identityFile)
|
|
33
|
+
|| typeof value[file] !== 'string' || !value[file]) fail('inputs-invalid', 'Complete source and identity inputs are required.');
|
|
34
|
+
let size = 0;
|
|
35
|
+
for (const [inputFile, content] of Object.entries(value)) {
|
|
36
|
+
if (content === null && inputFile === identityFile) continue;
|
|
37
|
+
if (typeof content !== 'string' || Buffer.from(content).toString('utf8') !== content) fail('inputs-invalid', 'Source inputs must be UTF-8 strings.', inputFile);
|
|
38
|
+
size += Buffer.byteLength(content);
|
|
39
|
+
}
|
|
40
|
+
if (size > 32 * 1024 * 1024) fail('inputs-invalid', 'Source inputs exceed the reconciliation size limit.');
|
|
41
|
+
}
|
|
42
|
+
function sourceIdentity(inputs, baselineIdentity) {
|
|
43
|
+
const source = inputs[file];
|
|
44
|
+
const supplied = baselineIdentity ?? (inputs[identityFile] === null ? null : JSON.parse(inputs[identityFile]));
|
|
45
|
+
const wrapper = resolveWrapper(inputs);
|
|
46
|
+
const projectedStructure = capture(projectSource({ source, identity: supplied, sourceInputs: inputs }));
|
|
47
|
+
const identity = supplied ?? createJsxStructureIdentity({
|
|
48
|
+
file, source, rootId, anchors: projectedStructure.anchors, opaqueNodeIds, ...(wrapper ? { wrapper } : {}),
|
|
49
|
+
});
|
|
50
|
+
return { identity, projectedStructure, wrapper };
|
|
51
|
+
}
|
|
52
|
+
function projectInputs(input) {
|
|
53
|
+
const { baselineInputs, externalInputs, baselineIdentity } = capture({
|
|
54
|
+
baselineInputs: input.baselineInputs, externalInputs: input.externalInputs,
|
|
55
|
+
...(input.baselineIdentity === undefined ? {} : { baselineIdentity: input.baselineIdentity }),
|
|
56
|
+
});
|
|
57
|
+
capturedInputs(baselineInputs); capturedInputs(externalInputs);
|
|
58
|
+
if (!same(Object.keys(baselineInputs).sort(), Object.keys(externalInputs).sort())) fail('input-set-conflict', 'The complete source input sets differ.');
|
|
59
|
+
for (const inputFile of Object.keys(baselineInputs)) {
|
|
60
|
+
if (![file, identityFile].includes(inputFile) && baselineInputs[inputFile] !== externalInputs[inputFile]) {
|
|
61
|
+
fail('dependency-conflict', 'A source dependency changed and requires separate review.', inputFile);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const original = sourceIdentity(baselineInputs, baselineIdentity);
|
|
65
|
+
const oldRoot = original.projectedStructure.anchors.find(anchor => anchor.nodeId === rootId);
|
|
66
|
+
if (!oldRoot) fail('island-conflict', 'The declared source island is missing.', file);
|
|
67
|
+
const source = externalInputs[file];
|
|
68
|
+
const start = source.indexOf(oldRoot.expected);
|
|
69
|
+
if (start < 0 || source.indexOf(oldRoot.expected, start + oldRoot.expected.length) !== -1) {
|
|
70
|
+
fail('island-conflict', 'The source island changed or no longer has a unique unchanged match.', file);
|
|
71
|
+
}
|
|
72
|
+
if (source !== baselineInputs[file] && !allowUnchangedIslandRelocation) {
|
|
73
|
+
fail('relocation-disabled', 'The source owner has not allowed unchanged island relocation.', file);
|
|
74
|
+
}
|
|
75
|
+
const offset = start - oldRoot.start;
|
|
76
|
+
const anchors = original.projectedStructure.anchors.map(anchor => ({ ...anchor, start: anchor.start + offset, end: anchor.end + offset }));
|
|
77
|
+
const wrapper = resolveWrapper(externalInputs);
|
|
78
|
+
const identity = createJsxStructureIdentity({ file, source, rootId, anchors, opaqueNodeIds: original.identity.opaqueNodeIds, ...(wrapper ? { wrapper } : {}) });
|
|
79
|
+
if (externalInputs[identityFile] !== baselineInputs[identityFile]
|
|
80
|
+
&& (externalInputs[identityFile] === null || !same(JSON.parse(externalInputs[identityFile]), identity))) {
|
|
81
|
+
fail('ledger-conflict', 'The external identity ledger differs from the proven current source correspondence.', identityFile);
|
|
82
|
+
}
|
|
83
|
+
return capture({ identity, projectedStructure: projectSource({ source, identity, sourceInputs: externalInputs }) });
|
|
84
|
+
}
|
|
85
|
+
function verifyProofs(mapping, inputs) {
|
|
86
|
+
if (!Array.isArray(mapping?.files) || !mapping.files.length) fail('proofs-invalid', 'Baseline source proofs are required.');
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const proof of mapping.files) {
|
|
89
|
+
if (!proof || typeof proof.file !== 'string' || seen.has(proof.file) || !Object.hasOwn(inputs, proof.file)
|
|
90
|
+
|| proof.sha256 !== (inputs[proof.file] === null ? null : hash(inputs[proof.file]))) {
|
|
91
|
+
fail('proofs-invalid', 'Baseline proofs do not match the source bytes.', proof?.file);
|
|
92
|
+
}
|
|
93
|
+
seen.add(proof.file);
|
|
94
|
+
}
|
|
95
|
+
if (!seen.has(file) || !seen.has(identityFile)) fail('proofs-invalid', 'Source and identity proofs are both required.');
|
|
96
|
+
}
|
|
97
|
+
function verifyProjection(document, projection) {
|
|
98
|
+
for (const node of projection.nodes) {
|
|
99
|
+
const current = document.nodes[node.nodeId];
|
|
100
|
+
const parentId = node.nodeId === rootId ? definitionId : node.parentId;
|
|
101
|
+
if (!current || current.parentId !== parentId || !same(current.children, node.children) || !same(sourceLayout(current.layout), sourceLayout(node.layout))) {
|
|
102
|
+
fail('document-conflict', 'The reconciled source hierarchy differs from the edited document.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function reconcile(input, signal) {
|
|
107
|
+
try {
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
const loadBaseline = input?.loadBaseline;
|
|
110
|
+
if (typeof loadBaseline !== 'function') fail('loader-invalid', 'A source baseline loader is required.');
|
|
111
|
+
const { target, baseline, document, baselineInputs, externalInputs, baselineIdentity } = capture({
|
|
112
|
+
target: input.target, baseline: input.baseline, document: input.document,
|
|
113
|
+
baselineInputs: input.baselineInputs, externalInputs: input.externalInputs,
|
|
114
|
+
...(input.baselineIdentity === undefined ? {} : { baselineIdentity: input.baselineIdentity }),
|
|
115
|
+
});
|
|
116
|
+
if (target?.screenId !== screenId || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(target.sourceRevision ?? '')
|
|
117
|
+
|| baseline?.document?.id !== documentId || document?.id !== documentId
|
|
118
|
+
|| baseline.document.sourceRevision !== target.sourceRevision || document.sourceRevision !== target.sourceRevision
|
|
119
|
+
|| !same(baseline.mapping?.target, target)) fail('target-conflict', 'Screen, document, and exact source revision must match.');
|
|
120
|
+
capturedInputs(baselineInputs); capturedInputs(externalInputs);
|
|
121
|
+
verifyProofs(baseline.mapping, baselineInputs);
|
|
122
|
+
const original = sourceIdentity(baselineInputs, baselineIdentity);
|
|
123
|
+
const latest = projectInputs({ baselineInputs, externalInputs, ...(baselineIdentity === undefined ? {} : { baselineIdentity }) });
|
|
124
|
+
const verifiedBaseline = capture(await loadBaseline(freeze({ target, sourceInputs: baselineInputs, projectedStructure: original.projectedStructure }), signal));
|
|
125
|
+
signal?.throwIfAborted();
|
|
126
|
+
if (!same(verifiedBaseline, baseline)) fail('baseline-conflict', 'The supplied baseline differs from the document projected from its source.');
|
|
127
|
+
const planned = compileDocumentSource({ target, before: baseline.document, after: document, mapping: baseline.mapping });
|
|
128
|
+
if (planned.status === 'blocked') return freeze({ status: 'conflict', diagnostics: planned.diagnostics });
|
|
129
|
+
const wrapper = original.wrapper;
|
|
130
|
+
const stylesheetFile = wrapper?.contract.stylesheet;
|
|
131
|
+
const reconciled = reconcileJsxStructurePlan({
|
|
132
|
+
file, baseline: { sourceRevision: target.sourceRevision, source: baselineInputs[file], identity: original.identity },
|
|
133
|
+
external: { sourceRevision: target.sourceRevision, source: externalInputs[file], sha256: hash(externalInputs[file]), identity: latest.identity },
|
|
134
|
+
operations: planned.operations, allowedFiles: wrapper ? [file, stylesheetFile] : [file],
|
|
135
|
+
...(wrapper ? { wrapper: { contract: wrapper.contract, baselineSource: baselineInputs[stylesheetFile], externalSource: externalInputs[stylesheetFile], externalSha256: hash(externalInputs[stylesheetFile]) } } : {}),
|
|
136
|
+
});
|
|
137
|
+
if (reconciled.status === 'conflict') return reconciled;
|
|
138
|
+
const projection = reimportJsxStructureSource({ file, source: reconciled.source, identity: reconciled.identity,
|
|
139
|
+
...(wrapper ? { wrapper: { contract: wrapper.contract, source: reconciled.stylesheetSource ?? externalInputs[stylesheetFile] } } : {}) });
|
|
140
|
+
verifyProjection(document, projection);
|
|
141
|
+
const current = capture(await loadBaseline(freeze({ target, sourceInputs: externalInputs, projectedStructure: latest.projectedStructure }), signal));
|
|
142
|
+
signal?.throwIfAborted();
|
|
143
|
+
verifyProofs(current.mapping, externalInputs);
|
|
144
|
+
if (!same(current.document, baseline.document)) fail('source-document-conflict', 'The current source projection changed the original document contract.');
|
|
145
|
+
const checked = compileDocumentSource({ target, before: current.document, after: document, mapping: current.mapping });
|
|
146
|
+
if (checked.status === 'blocked') return freeze({ status: 'conflict', diagnostics: checked.diagnostics });
|
|
147
|
+
signal?.throwIfAborted();
|
|
148
|
+
return freeze({ status: 'ready', baseline: JSON.parse(fingerprint(current)), document });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
signal?.throwIfAborted();
|
|
151
|
+
return freeze({ status: 'conflict', diagnostics: error?.diagnostics ?? [{ code: 'source-reconcile-invalid', message: error instanceof Error ? error.message : String(error) }] });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return Object.freeze({ projectInputs, reconcile });
|
|
155
|
+
}
|
|
@@ -1230,7 +1230,6 @@ export function createPreviewArtifactStore({
|
|
|
1230
1230
|
) => {
|
|
1231
1231
|
const manifest = await readManifest(namespace);
|
|
1232
1232
|
let aggregate = null;
|
|
1233
|
-
const contentFreshIds = new Set();
|
|
1234
1233
|
const sourcePendingIds = new Set();
|
|
1235
1234
|
if (manifest) {
|
|
1236
1235
|
// Index once per read. The earlier path filtered the complete manifest for
|
|
@@ -1272,12 +1271,21 @@ export function createPreviewArtifactStore({
|
|
|
1272
1271
|
artifact.frames[request.id]
|
|
1273
1272
|
) {
|
|
1274
1273
|
const frame = artifact.frames[request.id];
|
|
1274
|
+
const acceptedAsFresh = exactSet.has(entry);
|
|
1275
1275
|
const withProvenance = {
|
|
1276
1276
|
...artifact,
|
|
1277
1277
|
frames: {
|
|
1278
1278
|
...artifact.frames,
|
|
1279
1279
|
[request.id]: {
|
|
1280
1280
|
...frame,
|
|
1281
|
+
// A matching recipe plus unchanged observed source files is
|
|
1282
|
+
// a content proof for the requested fingerprint. Serve that
|
|
1283
|
+
// proof under the requested identity so browser bootstrap
|
|
1284
|
+
// reaches the same exact verdict as the manifest resolver.
|
|
1285
|
+
// The original capture revision remains truthful provenance.
|
|
1286
|
+
...(acceptedAsFresh && request.fingerprint != null
|
|
1287
|
+
? { fingerprint: request.fingerprint }
|
|
1288
|
+
: {}),
|
|
1281
1289
|
...(entry.sourceRevision == null
|
|
1282
1290
|
? {}
|
|
1283
1291
|
: { sourceRevision: entry.sourceRevision }),
|
|
@@ -1288,8 +1296,7 @@ export function createPreviewArtifactStore({
|
|
|
1288
1296
|
aggregate,
|
|
1289
1297
|
withProvenance,
|
|
1290
1298
|
);
|
|
1291
|
-
if (
|
|
1292
|
-
contentFreshIds.add(request.id);
|
|
1299
|
+
if (acceptedAsFresh) {
|
|
1293
1300
|
sourcePendingIds.delete(request.id);
|
|
1294
1301
|
}
|
|
1295
1302
|
break;
|
|
@@ -1304,10 +1311,6 @@ export function createPreviewArtifactStore({
|
|
|
1304
1311
|
);
|
|
1305
1312
|
return {
|
|
1306
1313
|
...selection,
|
|
1307
|
-
// The wire-level selector only knows fingerprints. A frame whose recipe
|
|
1308
|
-
// and observed files were verified against the requested source context is
|
|
1309
|
-
// also current, even when its coarse source fingerprint changed.
|
|
1310
|
-
stale: selection.stale.filter((id) => !contentFreshIds.has(id)),
|
|
1311
1314
|
...(sourcePendingIds.size
|
|
1312
1315
|
? { sourcePending: [...sourcePendingIds] }
|
|
1313
1316
|
: {}),
|