Skip to content

Commit 9ce37a1

Browse files
authored
Add shared resource plan identity (#817)
Persist a deterministic shared-resource digest and invalidate dependent pending batches when shared inputs change. AI assistance: OpenAI gpt-5.6-sol via OpenCode implemented, tested, linted, and prepared this change. Chris Huber reviewed and remains responsible for it.
1 parent 27fdd55 commit 9ce37a1

5 files changed

Lines changed: 551 additions & 311 deletions

homeboy-test-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"tests/smoke-website-artifact-import-input.php": { "environment": "standalone-php" },
3434
"tests/smoke-wordpress-site-plan-materializer.php": { "environment": "standalone-php" },
3535
"tests/smoke-artifact-run-primitives.php": { "environment": "standalone-php" },
36+
"tests/smoke-shared-resource-plan.php": { "environment": "standalone-php" },
3637
"tests/smoke-figma-workspace-lifecycle.php": { "environment": "standalone-php" },
3738
"tests/smoke-url-batch-import.php": { "environment": "standalone-php" },
3839
"tests/smoke-url-site-collector.php": { "environment": "standalone-php" }
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
/** Immutable shared-resource plan storage for resumable artifact runs. @package StaticSiteImporter */
3+
if ( ! defined( 'ABSPATH' ) ) {
4+
exit;
5+
}
6+
7+
/**
8+
* Retains source-agnostic, non-page artifact resources shared by a site run.
9+
* Prepared batches can only be reused when they name this verified digest.
10+
*/
11+
final class Static_Site_Importer_Shared_Resource_Plan {
12+
private const SCHEMA = 'static-site-importer/shared-resource-plan/v1';
13+
private Static_Site_Importer_Artifact_Run_Workspace $workspace;
14+
15+
public function __construct( Static_Site_Importer_Artifact_Run_Workspace $workspace ) {
16+
$this->workspace = $workspace;
17+
}
18+
19+
/** @return array<string,mixed>|null */
20+
public function load(): ?array {
21+
$raw = $this->workspace->read_raw( 'shared-resource-plan.json' );
22+
$plan = is_string( $raw ) ? json_decode( $raw, true ) : null;
23+
if ( ! is_array( $plan ) || self::SCHEMA !== ( $plan['schema'] ?? null ) || ! is_array( $plan['resources'] ?? null ) ) {
24+
return null;
25+
}
26+
return hash_equals( (string) ( $plan['digest'] ?? '' ), self::digest( $plan['resources'] ) ) ? $plan : null;
27+
}
28+
29+
/** @return array<string,mixed>|WP_Error */
30+
public function establish( array $artifact, ?array $paths = null ): array|WP_Error {
31+
$resources = self::resources( $artifact, $paths );
32+
$plan = array(
33+
'schema' => self::SCHEMA,
34+
'digest' => self::digest( $resources ),
35+
'resources' => $resources,
36+
'verified' => true,
37+
'created_at' => gmdate( 'c' ),
38+
);
39+
$stored = $this->workspace->publish_json( 'shared-resource-plan.json', $plan );
40+
if ( is_wp_error( $stored ) ) {
41+
return $stored;
42+
}
43+
$verified = $this->load();
44+
return is_array( $verified ) && $verified['digest'] === $plan['digest'] ? $verified : new WP_Error( 'static_site_importer_shared_plan_verification_failed', 'The retained shared resource plan could not be verified.' );
45+
}
46+
47+
/** @return array{digest:string,changed:bool,plan:array<string,mixed>|WP_Error} */
48+
public function reconcile( array $artifact ): array {
49+
$existing = $this->load();
50+
if ( ! is_array( $existing ) ) {
51+
$plan = $this->establish( $artifact );
52+
return array(
53+
'digest' => is_array( $plan ) ? $plan['digest'] : '',
54+
'changed' => false,
55+
'plan' => $plan,
56+
);
57+
}
58+
$current = self::resources( $artifact, array_column( $existing['resources'], 'path' ) );
59+
$digest = self::digest( $current );
60+
if ( hash_equals( $existing['digest'], $digest ) ) {
61+
return array(
62+
'digest' => $digest,
63+
'changed' => false,
64+
'plan' => $existing,
65+
);
66+
}
67+
$plan = $this->establish( $artifact, array_column( $existing['resources'], 'path' ) );
68+
return array(
69+
'digest' => is_array( $plan ) ? $plan['digest'] : '',
70+
'changed' => true,
71+
'plan' => $plan,
72+
);
73+
}
74+
75+
/** @return array<int,array<string,mixed>> */
76+
private static function resources( array $artifact, ?array $paths = null ): array {
77+
$resources = array();
78+
foreach ( $artifact['files'] ?? array() as $file ) {
79+
if ( ! is_array( $file ) || 'text/html' === strtolower( (string) ( $file['mime_type'] ?? '' ) ) ) {
80+
continue;
81+
}
82+
$path = (string) ( $file['path'] ?? '' );
83+
if ( '' === $path || ( null !== $paths && ! in_array( $path, $paths, true ) ) ) {
84+
continue;
85+
}
86+
$resources[] = array_filter(
87+
array(
88+
'path' => $path,
89+
'mime_type' => (string) ( $file['mime_type'] ?? '' ),
90+
'content' => $file['content'] ?? null,
91+
'content_base64' => $file['content_base64'] ?? null,
92+
),
93+
static fn( $value ): bool => null !== $value
94+
);
95+
}
96+
usort( $resources, static fn( array $left, array $right ): int => strcmp( $left['path'], $right['path'] ) );
97+
return $resources;
98+
}
99+
100+
private static function digest( array $resources ): string {
101+
$json = wp_json_encode( $resources, JSON_UNESCAPED_SLASHES );
102+
return hash( 'sha256', false === $json ? '' : $json );
103+
}
104+
}

0 commit comments

Comments
 (0)