@pygmalionjs/pygmalion 0.6.33 → 0.6.35

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.
@@ -27,6 +27,8 @@ export interface CaptureSupplyFrame {
27
27
  recipe?: RoutePreviewFrameRecipe;
28
28
  }
29
29
  export interface CaptureSupplyPlan {
30
+ /** Canvas that owned the viewport when this plan was built. */
31
+ activeCanvas?: string;
30
32
  /** Chunks of the active canvas, nearest first. */
31
33
  active: CaptureSupplyFrame[][];
32
34
  /** Chunks of the other canvases, in the order the frames were given. */
@@ -27,6 +27,34 @@ export declare function mirrorBootRetryDelayMs(attempt: number): number;
27
27
  export interface MirrorGateStatus {
28
28
  state: string;
29
29
  }
30
+ /** The idle status fields that can identify an immutable source before checkout. */
31
+ export interface IdleMirrorSourceStatus extends MirrorGateStatus {
32
+ sourceRef: string | null;
33
+ }
34
+ /** One listed ref whose exact revision can start a lazy checkout. */
35
+ export interface IdleMirrorListedSourceRef {
36
+ name: string;
37
+ fullName?: string;
38
+ sourceRevision?: string;
39
+ }
40
+ /** The ref-list fields needed to resolve an idle named source. */
41
+ export interface IdleMirrorSourceRefs {
42
+ refs: readonly IdleMirrorListedSourceRef[];
43
+ defaultRef: string | null;
44
+ worktreeRef?: string | null;
45
+ }
46
+ /**
47
+ * Returns an immutable source revision that can safely create the first lazy
48
+ * preview demand.
49
+ *
50
+ * An idle mirror has no checked-out `commit` yet. When it was configured with
51
+ * a full Git object id, however, that ref already names the exact revision the
52
+ * catalog endpoint will verify after preparation. A named ref can also create
53
+ * that first demand when the ref-list endpoint supplies its full object id.
54
+ * The abbreviated display commit is intentionally ignored: it can be
55
+ * ambiguous and cannot satisfy the exact source lease.
56
+ */
57
+ export declare function idleMirrorSourceRevision(status: IdleMirrorSourceStatus, sourceRefs?: IdleMirrorSourceRefs): string | null;
30
58
  export interface SettleMirrorStatusOptions<S extends MirrorGateStatus> {
31
59
  /** Status of the initial request; returned as-is when already settled. */
32
60
  initial: S;
@@ -22,7 +22,10 @@ export interface PygmalionDevMirrorStatus {
22
22
  /** One revision the mirror can be repointed at. */
23
23
  export interface PygmalionSourceRef {
24
24
  name: string;
25
+ /** Abbreviated commit used for compact branch-tip labels. */
25
26
  commit: string;
27
+ /** Full immutable object id used for preview and artifact identity. */
28
+ sourceRevision?: string;
26
29
  fullName: string;
27
30
  kind: 'local' | 'remote';
28
31
  }
@@ -5,6 +5,7 @@ export interface RoutePreviewArtifactStatusInput {
5
5
  resolution: RoutePreviewArtifactResolution;
6
6
  captureStatus?: RoutePreviewArtifactV3Status;
7
7
  diagnostic?: string;
8
+ liveInteractionReady?: boolean;
8
9
  }
9
10
  export interface RoutePreviewArtifactStatusPresentation {
10
11
  title: string;
@@ -45,4 +46,4 @@ export declare function resolveStaticRoutePreviewSurface({ hasScreenshot, hasDom
45
46
  preferDom?: boolean;
46
47
  }): StaticRoutePreviewSurface;
47
48
  /** Turns artifact freshness and capture state into one unambiguous frame badge. */
48
- export declare function resolveRoutePreviewArtifactStatus({ stale, resolution, captureStatus, diagnostic, }: RoutePreviewArtifactStatusInput): RoutePreviewArtifactStatusPresentation | null;
49
+ export declare function resolveRoutePreviewArtifactStatus({ stale, resolution, captureStatus, diagnostic, liveInteractionReady, }: RoutePreviewArtifactStatusInput): RoutePreviewArtifactStatusPresentation | null;
@@ -350,15 +350,21 @@ function escapeRegExp(value) {
350
350
  * of asking for a ref string.
351
351
  */
352
352
  export async function listSourceRefs(repoRoot, remote = 'origin') {
353
- const format = '%(refname:short)%09%(objectname:short=8)%09%(refname)';
353
+ const format =
354
+ '%(refname:short)%09%(objectname:short=8)%09%(objectname)%09%(refname)';
354
355
  const read = async (...patterns) => {
355
356
  const raw = await git(repoRoot, 'for-each-ref', `--format=${format}`, ...patterns);
356
357
  if (!raw) return [];
357
358
  return raw
358
359
  .split('\n')
359
360
  .map((line) => line.split('\t'))
360
- .filter((parts) => parts.length === 3 && parts[0])
361
- .map(([name, commit, fullName]) => ({ name, commit, fullName }));
361
+ .filter((parts) => parts.length === 4 && parts[0])
362
+ .map(([name, commit, sourceRevision, fullName]) => ({
363
+ name,
364
+ commit,
365
+ sourceRevision,
366
+ fullName,
367
+ }));
362
368
  };
363
369
  const remotes = (await read(`refs/remotes/${remote}`))
364
370
  // `%(refname:short)` may abbreviate refs/remotes/origin/HEAD to just
@@ -375,15 +381,19 @@ export async function listSourceRefs(repoRoot, remote = 'origin') {
375
381
  // `dev@a0d5fdbf → 3e8cca5b`, an invitation to go backwards.
376
382
  const remoteTipPrefix = `refs/remotes/${remote}/`;
377
383
  const remoteTips = new Map(
378
- remotes.map((item) => [item.fullName.slice(remoteTipPrefix.length), item.commit]),
384
+ remotes.map((item) => [item.fullName.slice(remoteTipPrefix.length), item]),
379
385
  );
380
- const locals = (await read('refs/heads')).map((item) => ({
381
- ...item,
382
- // A branch the remote does not have resolves locally (the fetch fails and
383
- // becomes a warning), so its own tip is the honest answer there.
384
- commit: remoteTips.get(item.name) ?? item.commit,
385
- kind: 'local',
386
- }));
386
+ const locals = (await read('refs/heads')).map((item) => {
387
+ const remoteTip = remoteTips.get(item.name);
388
+ return {
389
+ ...item,
390
+ // A branch the remote does not have resolves locally (the fetch fails and
391
+ // becomes a warning), so its own tip is the honest answer there.
392
+ commit: remoteTip?.commit ?? item.commit,
393
+ sourceRevision: remoteTip?.sourceRevision ?? item.sourceRevision,
394
+ kind: 'local',
395
+ };
396
+ });
387
397
  const seen = new Set();
388
398
  return [...locals, ...remotes].filter((item) => {
389
399
  if (seen.has(item.name)) return false;
@@ -843,6 +853,9 @@ export async function acquireExactDevMirrorSourceLease({
843
853
  return {
844
854
  sourceRoot: exactSourceRoot,
845
855
  sourceRevision: expectedRevision,
856
+ // Synchronization and reaping honor the heartbeat lease until release,
857
+ // so retention may safely inspect this root while the caller holds it.
858
+ sourceRootStable: true,
846
859
  release,
847
860
  };
848
861
  } catch (error) {
@@ -788,6 +788,12 @@ export function pygmalionPreviewArtifactPlugin({
788
788
  return { sourceRevision, sourceRoot: lease.sourceRoot };
789
789
  }
790
790
 
791
+ function retentionSourceContextFromLease(lease, sourceRevision) {
792
+ return lease?.sourceRootStable === true
793
+ ? sourceContextFromLease(lease, sourceRevision)
794
+ : undefined;
795
+ }
796
+
791
797
  /**
792
798
  * Refines a conservative cache decision only while the requested checkout is
793
799
  * pinned. Fingerprint hits, recipe mismatches, and frames without observed
@@ -872,6 +878,11 @@ export function pygmalionPreviewArtifactPlugin({
872
878
  const progress = startCaptureProgress();
873
879
  try {
874
880
  const generated = await withCaptureLease(sourceRevision, async (lease) => {
881
+ const sourceContext = sourceContextFromLease(lease, sourceRevision);
882
+ const retentionSourceContext = retentionSourceContextFromLease(
883
+ lease,
884
+ sourceRevision,
885
+ );
875
886
  const captured = await generateArtifact(
876
887
  {
877
888
  namespace,
@@ -889,10 +900,10 @@ export function pygmalionPreviewArtifactPlugin({
889
900
  'Generated preview artifact identity does not match the request.',
890
901
  );
891
902
  }
892
- const observedDependenciesByFrame = lease?.sourceRoot
903
+ const observedDependenciesByFrame = sourceContext
893
904
  ? await artifactStore.observeArtifactDependencies(
894
905
  captured,
895
- lease.sourceRoot,
906
+ sourceContext.sourceRoot,
896
907
  )
897
908
  : undefined;
898
909
  await validateSourceLease(lease, sourceRevision);
@@ -900,6 +911,9 @@ export function pygmalionPreviewArtifactPlugin({
900
911
  await artifactStore.publishArtifact(captured, {
901
912
  sourceRevision,
902
913
  recordRevision: true,
914
+ ...(retentionSourceContext
915
+ ? { sourceContext: retentionSourceContext }
916
+ : {}),
903
917
  ...(observedDependenciesByFrame
904
918
  ? { observedDependenciesByFrame }
905
919
  : {}),
@@ -941,6 +955,10 @@ export function pygmalionPreviewArtifactPlugin({
941
955
  try {
942
956
  return await withCaptureLease(sourceRevision, async (lease) => {
943
957
  const sourceContext = sourceContextFromLease(lease, sourceRevision);
958
+ const retentionSourceContext = retentionSourceContextFromLease(
959
+ lease,
960
+ sourceRevision,
961
+ );
944
962
  const before = await artifactStore.readFrameSelection(
945
963
  namespace,
946
964
  sourceRevision,
@@ -976,10 +994,10 @@ export function pygmalionPreviewArtifactPlugin({
976
994
  'Generated preview artifact frames do not match the request.',
977
995
  );
978
996
  }
979
- const observedDependenciesByFrame = lease?.sourceRoot
997
+ const observedDependenciesByFrame = sourceContext
980
998
  ? await artifactStore.observeArtifactDependencies(
981
999
  captured,
982
- lease.sourceRoot,
1000
+ sourceContext.sourceRoot,
983
1001
  )
984
1002
  : undefined;
985
1003
  await validateSourceLease(lease, sourceRevision);
@@ -988,6 +1006,9 @@ export function pygmalionPreviewArtifactPlugin({
988
1006
  sourceRevision,
989
1007
  recordRevision: false,
990
1008
  frameRequests: remaining,
1009
+ ...(retentionSourceContext
1010
+ ? { sourceContext: retentionSourceContext }
1011
+ : {}),
991
1012
  ...(observedDependenciesByFrame
992
1013
  ? { observedDependenciesByFrame }
993
1014
  : {}),
@@ -596,7 +596,10 @@ export function createPreviewArtifactStore({
596
596
  * is what keeps a concurrent reader from losing a bundle it is about to
597
597
  * fetch.
598
598
  */
599
- const evictToBudget = async (manifest, { at, sourceContext } = {}) => {
599
+ const evictToBudget = async (
600
+ manifest,
601
+ { at, sourceContext, admittedFrameObjects = new Set() } = {},
602
+ ) => {
600
603
  const referenced = new Map();
601
604
  const remember = async (hash) => {
602
605
  if (!referenced.has(hash)) referenced.set(hash, await objectByteSize(hash));
@@ -627,7 +630,13 @@ export function createPreviewArtifactStore({
627
630
  candidates.push({
628
631
  kind: 'frame',
629
632
  entry,
630
- rank: EVICTION_RANK[await classifyFrameEntry(entry, sourceContext)],
633
+ // A caller that supplied normalized observations recorded from the
634
+ // artifact's own snapshot has already paid to produce this object.
635
+ // Protect that admission from older unknown entries without rereading
636
+ // a source root that may not be physically pinned.
637
+ rank: admittedFrameObjects.has(entry.object)
638
+ ? EVICTION_RANK.valid
639
+ : EVICTION_RANK[await classifyFrameEntry(entry, sourceContext)],
631
640
  // Protection within a rank, never across it: the last copy of a frame
632
641
  // is worth more than a surplus copy, but an invalid sole entry is still
633
642
  // garbage and goes before a valid one.
@@ -872,6 +881,7 @@ export function createPreviewArtifactStore({
872
881
  ...(stored?.frames ?? []),
873
882
  ].map((entry) => entry.object),
874
883
  );
884
+ const admittedFrameObjects = new Set();
875
885
  if (recordRevision && sourceRevision) {
876
886
  const wholeObject = await writeObject(artifact);
877
887
  const nextRevision = {
@@ -922,6 +932,7 @@ export function createPreviewArtifactStore({
922
932
  generation,
923
933
  ...(observed ? { sourceFiles: observed } : {}),
924
934
  };
935
+ if (observed) admittedFrameObjects.add(object);
925
936
  const alreadyRecorded = frames.some(
926
937
  (entry) =>
927
938
  entry.id === nextFrame.id &&
@@ -941,6 +952,7 @@ export function createPreviewArtifactStore({
941
952
  const trimmed = await evictToBudget(manifest, {
942
953
  at,
943
954
  sourceContext: exactSourceContext,
955
+ admittedFrameObjects,
944
956
  });
945
957
  manifest = trimmed.dropped
946
958
  ? { ...trimmed.manifest, lastEviction: trimmed.dropped }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.6.33",
3
+ "version": "0.6.35",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {