Skip to content

Commit 7e9319a

Browse files
committed
fix: harden pending runtime sidecars [AI: openai/gpt-5.6-sol via OpenCode]
1 parent 08bacb2 commit 7e9319a

8 files changed

Lines changed: 145 additions & 8 deletions

File tree

includes/class-static-site-importer-diagnostic-loss-classes.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Static_Site_Importer_Diagnostic_Loss_Classes {
1717
public const NATIVE_CONVERSION = 'native_conversion';
1818
public const EDITABLE_APPROXIMATION = 'editable_approximation';
1919
public const PRESERVED_RUNTIME_ISLAND = 'preserved_runtime_island';
20+
public const RUNTIME_RESUME_REQUIRED = 'runtime_resume_required';
2021
public const UNSUPPORTED_LOSS = 'unsupported_loss';
2122
public const IMPORTER_MATERIALIZATION_BUG = 'importer_materialization_bug';
2223

@@ -42,6 +43,10 @@ public static function classify( array $diagnostic ): string {
4243
$selector = self::scalar( $diagnostic, array( 'selector', 'target_selector', 'runtime_target_selector' ) );
4344
$haystack = strtolower( implode( ' ', array( $type, $category, $repair, $reason, $stage, $block_name, $element, $selector ) ) );
4445

46+
if ( self::contains_any( $haystack, array( 'runtime_resume_required', 'runtime_resume', 'resume_required' ) ) ) {
47+
return self::RUNTIME_RESUME_REQUIRED;
48+
}
49+
4550
if (
4651
self::contains_any(
4752
$haystack,
@@ -137,6 +142,7 @@ public static function classes(): array {
137142
self::NATIVE_CONVERSION,
138143
self::EDITABLE_APPROXIMATION,
139144
self::PRESERVED_RUNTIME_ISLAND,
145+
self::RUNTIME_RESUME_REQUIRED,
140146
self::UNSUPPORTED_LOSS,
141147
self::IMPORTER_MATERIALIZATION_BUG,
142148
);

lib/fixture-matrix/collectors/run-intake.mjs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ function normalizeCollectedFixtureResult({ fixture, payloads, fixtureArtifactsDi
133133

134134
function materializationSidecarPayload(fixtureId, sidecar) {
135135
const receipt = objectValue(sidecar.receipt);
136-
const status = receipt.status === 'pending_runtime'
137-
? 'pending_runtime'
138-
: sidecar.command_result?.status === 'failed' ? 'failed' : 'passed';
136+
const status = sidecarStatus(receipt.status);
139137
return {
140138
fixture_id: fixtureId,
141139
status,
@@ -148,6 +146,10 @@ function materializationSidecarPayload(fixtureId, sidecar) {
148146
};
149147
}
150148

149+
function sidecarStatus(status) {
150+
return status === 'completed' ? 'passed' : status;
151+
}
152+
151153
function pendingRuntimeDiagnostics(payload) {
152154
if (payload.status !== 'pending_runtime') {
153155
return [];
@@ -231,7 +233,7 @@ function inferredDiagnosticBoundary(diagnostic) {
231233
const kind = String(diagnostic.kind || diagnostic.code || '').toLowerCase();
232234
const lossClass = String(diagnostic.loss_class || diagnostic.lossClass || '').toLowerCase();
233235
if (['editor_block_invalid', 'invalid_block_content', 'low_native_conversion', 'runtime_target_gap'].includes(kind) || ['editor_block_invalid', 'invalid_block_content', 'low_native_conversion', 'runtime_target_gap'].includes(lossClass)) return 'transform';
234-
if (['importer_materialization_bug', 'missing_asset', 'dropped_images'].includes(kind) || ['importer_materialization_bug', 'missing_asset'].includes(lossClass)) return 'materialization';
236+
if (['importer_materialization_bug', 'missing_asset', 'dropped_images', 'runtime_resume_required'].includes(kind) || ['importer_materialization_bug', 'missing_asset', 'runtime_resume_required'].includes(lossClass)) return 'materialization';
235237
return '';
236238
}
237239

@@ -730,7 +732,11 @@ function validSidecar(row) {
730732
if (Object.keys(row).some((key) => !allowed.includes(key))) return false;
731733
if (row.schema !== MATERIALIZATION_SIDECAR_SCHEMA || !safeToken(row.fixture_id, 80) || !safeToken(row.run_id, 160) || row.step_id !== 'import' || !safeToken(row.attempt_id, 80) || !sha256(row.artifact_sha256) || !sha256(row.content_sha256)) return false;
732734
if (!validProvenance(row.provenance) || !validDurability(row.durability) || !validReceipt(receipt) || (row.command_result !== undefined && !validCommandResult(row.command_result)) || (row.front_page_options !== undefined && !validFrontPageOptions(row.front_page_options))) return false;
733-
return true;
735+
return statusesAgree(row.provenance.provider_status, receipt.status, row.command_result?.status);
736+
}
737+
738+
function statusesAgree(provenanceStatus, receiptStatus, commandStatus) {
739+
return provenanceStatus === receiptStatus && (commandStatus === undefined || commandStatus === receiptStatus);
734740
}
735741

736742
function validDurability(value) {

lib/fixture-matrix/findings.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,14 @@ function applyEvidenceAttribution(finding, raw, result) {
230230
const requestedBoundary = attributionBoundary(raw, finding);
231231
const transformReady = evidence.readiness === 'verified' && ['transformer_package', 'transformer_version', 'transformer_reference', 'wordpress_site_plan'].every((key) => !missing.includes(key));
232232
const materializationReady = transformReady && !missing.includes('materialization_receipt');
233+
const lifecycleResume = finding.loss_class === 'runtime_resume_required';
233234
const diagnosticEvidence = objectValue(raw.attribution_evidence || raw.attributionEvidence);
234235
const provider = objectValue(diagnosticEvidence.provider_adapter || diagnosticEvidence.providerAdapter);
235236
const capture = objectValue(diagnosticEvidence.capture);
236237
const correlationMatches = evidenceCorrelationMatches(raw, diagnosticEvidence);
237238
const candidates = [
238239
attributionCandidate('transform', 'blocks-engine', transformReady && boundaryMatches(requestedBoundary, 'transform'), boundaryMissing(requestedBoundary, 'transform', ['transformer_package', 'transformer_version', 'transformer_reference', 'wordpress_site_plan'].filter((key) => missing.includes(key)))),
239-
attributionCandidate('materialization', 'static-site-importer', materializationReady && boundaryMatches(requestedBoundary, 'materialization'), boundaryMissing(requestedBoundary, 'materialization', ['transformer_package', 'transformer_version', 'transformer_reference', 'wordpress_site_plan', 'materialization_receipt'].filter((key) => missing.includes(key)))),
240+
attributionCandidate('materialization', 'static-site-importer', (materializationReady || lifecycleResume) && boundaryMatches(requestedBoundary, 'materialization'), lifecycleResume ? [] : boundaryMissing(requestedBoundary, 'materialization', ['transformer_package', 'transformer_version', 'transformer_reference', 'wordpress_site_plan', 'materialization_receipt'].filter((key) => missing.includes(key)))),
240241
attributionCandidate('provider', 'static-site-importer', provider.status === 'failed' && correlationMatches && boundaryMatches(requestedBoundary, 'provider'), boundaryMissing(requestedBoundary, 'provider', provider.status ? (correlationMatches ? [] : ['provider_adapter_correlation']) : ['provider_adapter_result'])),
241242
attributionCandidate('capture', 'static-site-importer', capture.status === 'failed' && correlationMatches && boundaryMatches(requestedBoundary, 'capture'), boundaryMissing(requestedBoundary, 'capture', capture.status ? (correlationMatches ? [] : ['capture_contract_correlation']) : ['capture_contract'])),
242243
attributionCandidate('unknown', '', !requestedBoundary, requestedBoundary ? [] : ['attribution_boundary']),
@@ -275,7 +276,7 @@ function attributionBoundary(raw, finding) {
275276
if (['editor_block_invalid', 'invalid_block_content', 'low_native_conversion', 'runtime_target_gap'].includes(finding.loss_class)) {
276277
return 'transform';
277278
}
278-
if (['importer_materialization_bug', 'missing_asset'].includes(finding.loss_class)) {
279+
if (['importer_materialization_bug', 'missing_asset', 'runtime_resume_required'].includes(finding.loss_class)) {
279280
return 'materialization';
280281
}
281282
return '';
@@ -460,6 +461,9 @@ function classifyLossClass({ raw, kind, group_key, repair_bucket, message, resul
460461
if (kind === 'fixture_not_run' || group_key === 'fixture_not_run') {
461462
return 'fixture_not_run';
462463
}
464+
if (kind === 'runtime_resume_required' || group_key === 'runtime_resume_required') {
465+
return 'runtime_resume_required';
466+
}
463467
if (kind === 'visual_evidence_missing' || group_key === 'visual_evidence_missing') {
464468
return 'visual_evidence_missing';
465469
}
@@ -663,6 +667,7 @@ export function normalizeLossClass(value) {
663667
missing_output: 'missing_output',
664668
fixture_not_run: 'fixture_not_run',
665669
not_run: 'fixture_not_run',
670+
runtime_resume_required: 'runtime_resume_required',
666671
fixture_failed: 'fixture_failed',
667672
runtime_execution_failed: 'runtime_execution_failed',
668673
recipe_step_failure: 'runtime_execution_failed',

lib/fixture-matrix/result.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,7 @@ function taxonomyRollup(key) {
976976
fixture_count: 0,
977977
passed: 0,
978978
failed: 0,
979+
pending_runtime: 0,
979980
not_run: 0,
980981
finding_count: 0,
981982
acceptable_finding_count: 0,
@@ -1050,6 +1051,7 @@ function classRollup(key) {
10501051
fixture_count: 0,
10511052
passed: 0,
10521053
failed: 0,
1054+
pending_runtime: 0,
10531055
not_run: 0,
10541056
finding_count: 0,
10551057
acceptable_finding_count: 0,
@@ -1071,6 +1073,7 @@ function qualityBudgetSummaries(classRollups) {
10711073
fixture_count: row.fixture_count,
10721074
passed: row.passed,
10731075
failed: row.failed,
1076+
pending_runtime: row.pending_runtime,
10741077
not_run: row.not_run,
10751078
finding_count: row.finding_count,
10761079
acceptable_finding_count: row.acceptable_finding_count,

lib/fixture-matrix/shared/constants.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ export const VISUAL_PARITY_DETERMINISTIC_CSS = `
119119
export const VISUAL_PARITY_GATE_SIGNAL_KEYS = ['gate', 'visual_parity_gate', 'visualParityGate'];
120120

121121
export const DEFAULT_FINDING_GROUPS = {
122+
runtime_resume_required: {
123+
patterns: [/runtime_resume_required/i, /runtime.*resume.*required/i, /fresh wordpress runtime.*resume/i],
124+
candidate_repo: 'static-site-importer',
125+
repair_mode: 'runtime-resume',
126+
},
122127
// Low native-conversion-rate gate findings route to the transformer's
123128
// native-conversion bucket. Listed first so the message ("native conversion
124129
// rate ... below ... minimum") classifies here rather than matching the
@@ -198,6 +203,7 @@ export const ACCEPTABLE_LOSS_CLASSES = new Set([
198203
]);
199204

200205
export const UNACCEPTABLE_LOSS_CLASSES = new Set([
206+
'runtime_resume_required',
201207
'unsupported_loss',
202208
'importer_materialization_bug',
203209
'invalid_block_output',

static-site-importer.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ function static_site_importer_cli_pending_materialization_summary( array $result
597597
'schema' => 'static-site-importer/materialization-receipt/v1',
598598
'status' => 'pending_runtime',
599599
'reason_code' => static_site_importer_cli_sidecar_token_value( $result['reason_code'] ?? 'runtime_resume_required', 80 ),
600-
'message' => static_site_importer_cli_sidecar_token_value( $result['message'] ?? 'Validation requires a fresh WordPress runtime before materialization can resume.', 240 ),
600+
'message' => static_site_importer_cli_sidecar_message_value( $result['message'] ?? 'Validation requires a fresh WordPress runtime before materialization can resume.', 240 ),
601601
);
602602
}
603603

@@ -684,3 +684,18 @@ function static_site_importer_cli_sidecar_token_value( $value, int $maximum ): s
684684
$value = is_scalar( $value ) ? (string) $value : '';
685685
return static_site_importer_cli_sidecar_token( $value, $maximum ) ? $value : '';
686686
}
687+
688+
/**
689+
* Preserve an actionable receipt message as bounded JSON-safe free text.
690+
*
691+
* @param mixed $value Candidate message.
692+
* @param int $maximum Maximum serialized byte length.
693+
*/
694+
function static_site_importer_cli_sidecar_message_value( $value, int $maximum ): string {
695+
$value = is_scalar( $value ) ? (string) $value : '';
696+
$value = wp_check_invalid_utf8( $value, true );
697+
$value = preg_replace( '/[\x00-\x1F\x7F]/', ' ', $value );
698+
$value = is_string( $value ) ? trim( preg_replace( '/[ \t]+/', ' ', $value ) ) : '';
699+
$value = substr( $value, 0, max( 0, $maximum ) );
700+
return wp_check_invalid_utf8( $value, true );
701+
}

tests/smoke-diagnostic-loss-classes.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ function sanitize_key( $key ) { // phpcs:ignore Universal.NamingConventions.NoRe
5353
),
5454
'expected' => 'preserved_runtime_island',
5555
),
56+
'resume-required' => array(
57+
'diagnostic' => array(
58+
'kind' => 'runtime_resume_required',
59+
),
60+
'expected' => 'runtime_resume_required',
61+
),
5662
'preserved-dom-markup' => array(
5763
'diagnostic' => array(
5864
'type' => 'dom',
@@ -93,6 +99,7 @@ function sanitize_key( $key ) { // phpcs:ignore Universal.NamingConventions.NoRe
9399
$assert( 1 === ( $counts['native_conversion'] ?? 0 ), 'counts-native' );
94100
$assert( 2 === ( $counts['editable_approximation'] ?? 0 ), 'counts-editable' );
95101
$assert( 2 === ( $counts['preserved_runtime_island'] ?? 0 ), 'counts-runtime' );
102+
$assert( 1 === ( $counts['runtime_resume_required'] ?? 0 ), 'counts-resume-required' );
96103
$assert( 1 === ( $counts['unsupported_loss'] ?? 0 ), 'counts-unsupported' );
97104
$assert( 1 === ( $counts['importer_materialization_bug'] ?? 0 ), 'counts-importer' );
98105

tools/fixture-matrix.test.mjs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,16 @@ test('validate-artifact sidecar contract preserves legacy calls and rejects part
888888
assert.deepEqual(JSON.parse(result.stdout), [false, true, 'static_site_importer_sidecar_contract_partial']);
889889
});
890890

891+
test('pending sidecar messages preserve bounded actionable JSON-safe text', () => {
892+
const plugin = readFileSync(path.join(packageRoot, 'static-site-importer.php'), 'utf8');
893+
const start = plugin.indexOf('function static_site_importer_cli_sidecar_message_value');
894+
const helper = plugin.slice(start);
895+
const code = `function wp_check_invalid_utf8($value, $strip) { return $value; } ${helper} $message = static_site_importer_cli_sidecar_message_value("Resume after\tcompanion\x01 initialization", 240); $bounded = static_site_importer_cli_sidecar_message_value(str_repeat('x', 300), 240); echo json_encode(array($message, strlen($bounded)));`;
896+
const result = spawnSync('php', ['-r', code], { encoding: 'utf8' });
897+
assert.equal(result.status, 0, result.stderr);
898+
assert.deepEqual(JSON.parse(result.stdout), ['Resume after companion initialization', 240]);
899+
});
900+
891901
test('gates visual capture on complete generated SVG font evidence after import', () => {
892902
const matrix = createFixtureMatrix({ fixture_root: fixtureRoot, id: 'svg-font-evidence-recipe-test' });
893903
const recipe = buildFixtureMatrixRecipe({
@@ -1456,6 +1466,55 @@ test('failed sidecars retain the bounded terminal import result and front-page o
14561466
assert.equal(result.fixtures[0].matrix_evidence.materialization_receipt.status, 'failed');
14571467
});
14581468

1469+
test('legacy failed sidecars without command results remain failed', () => {
1470+
const outputDirectory = mkdtempSync(path.join(tmpdir(), 'ssi-sidecar-legacy-failed-'));
1471+
const matrix = createFixtureMatrix({ fixture_root: fixtureRoot, id: 'legacy-failed-sidecar' });
1472+
const directory = path.join(outputDirectory, 'simple-site');
1473+
mkdirSync(directory, { recursive: true });
1474+
writeFileSync(path.join(directory, 'artifact.json'), JSON.stringify({ fixture: 'simple-site' }));
1475+
writeMaterializationSidecar({
1476+
directory,
1477+
fixtureId: 'simple-site',
1478+
runId: matrix.id,
1479+
providerStatus: 'failed',
1480+
receipt: { schema: 'static-site-importer/materialization-receipt/v1', status: 'failed', page_count: 0, file_count: 0, operation_count: 0, loss_count: 1, failure_code: 'import_failed' },
1481+
});
1482+
1483+
const result = collectFixtureMatrixRunResults({ matrix, outputDirectory });
1484+
assert.equal(result.fixtures[0].status, 'failed');
1485+
assert.equal(result.summary.failed, 1);
1486+
assert.equal(result.summary.succeeded, 0);
1487+
});
1488+
1489+
test('materialization sidecars reject contradictory terminal status provenance', () => {
1490+
for (const [name, providerStatus, receiptStatus, commandStatus] of [
1491+
['provenance-receipt', 'completed', 'failed', undefined],
1492+
['receipt-command', 'failed', 'failed', 'completed'],
1493+
['pending-command', 'pending_runtime', 'pending_runtime', 'failed'],
1494+
]) {
1495+
const outputDirectory = mkdtempSync(path.join(tmpdir(), `ssi-sidecar-contradictory-${name}-`));
1496+
const matrix = createFixtureMatrix({ fixture_root: fixtureRoot, id: `contradictory-${name}` });
1497+
const directory = path.join(outputDirectory, 'simple-site');
1498+
mkdirSync(directory, { recursive: true });
1499+
writeFileSync(path.join(directory, 'artifact.json'), JSON.stringify({ fixture: 'simple-site' }));
1500+
const receipt = receiptStatus === 'pending_runtime'
1501+
? { schema: 'static-site-importer/materialization-receipt/v1', status: receiptStatus, reason_code: 'runtime_resume_required', message: 'Resume after runtime initialization.' }
1502+
: receiptStatus === 'failed'
1503+
? { schema: 'static-site-importer/materialization-receipt/v1', status: receiptStatus, page_count: 0, file_count: 0, operation_count: 0, loss_count: 1, failure_code: 'import_failed' }
1504+
: boundedSidecarReceipt();
1505+
writeMaterializationSidecar({
1506+
directory,
1507+
fixtureId: 'simple-site',
1508+
runId: matrix.id,
1509+
providerStatus,
1510+
receipt,
1511+
commandResult: commandStatus === undefined ? undefined : { status: commandStatus, success: commandStatus === 'completed', error_code: commandStatus === 'failed' ? 'import_failed' : '', error_hash: 'a'.repeat(64) },
1512+
});
1513+
const result = collectFixtureMatrixRunResults({ matrix, outputDirectory });
1514+
assert.equal(result.fixtures[0].matrix_evidence.materialization_sidecar.status, 'malformed', name);
1515+
}
1516+
});
1517+
14591518
test('pending runtime sidecars preserve resumable validation without materialization claims', () => {
14601519
const outputDirectory = mkdtempSync(path.join(tmpdir(), 'ssi-sidecar-pending-runtime-'));
14611520
const matrix = createFixtureMatrix({ fixture_root: fixtureRoot, id: 'pending-runtime-evidence' });
@@ -1505,6 +1564,36 @@ test('pending runtime sidecars preserve resumable validation without materializa
15051564
assert.equal(Object.hasOwn(fixture.artifacts, 'generated_theme'), false);
15061565
assert.ok(fixture.diagnostics.some((diagnostic) => diagnostic.kind === 'runtime_resume_required' && diagnostic.reason_code === 'companion_plugin_init_pending'));
15071566
assert.ok(result.findings.some((finding) => finding.kind === 'runtime_resume_required' && finding.reason_code === 'companion_plugin_init_pending'));
1567+
const finding = result.findings.find((candidate) => candidate.kind === 'runtime_resume_required');
1568+
assert.equal(finding.loss_class, 'runtime_resume_required');
1569+
assert.equal(finding.repair_bucket, 'runtime_resume_required');
1570+
assert.equal(finding.candidate_repo, 'static-site-importer');
1571+
});
1572+
1573+
test('pending runtime counts remain in every aggregate projection', () => {
1574+
const root = mkdtempSync(path.join(tmpdir(), 'ssi-pending-runtime-rollups-'));
1575+
for (const [id, fixture] of Object.entries({
1576+
pending: { fixture_class: 'marketing/static', capabilities: ['hero'], risk_profile: 'medium' },
1577+
passed: { fixture_class: 'marketing/static', capabilities: ['hero'], risk_profile: 'medium' },
1578+
})) {
1579+
const directory = path.join(root, id);
1580+
mkdirSync(directory, { recursive: true });
1581+
writeFileSync(path.join(directory, 'index.html'), `<main>${id}</main>`);
1582+
writeFileSync(path.join(directory, 'fixture.json'), JSON.stringify(fixture));
1583+
}
1584+
const matrix = createFixtureMatrix({ fixture_root: root, id: 'pending-runtime-rollups' });
1585+
const result = normalizeFixtureMatrixResult({
1586+
matrix,
1587+
results: [
1588+
{ fixture_id: 'pending', status: 'pending_runtime' },
1589+
{ fixture_id: 'passed', status: 'passed' },
1590+
],
1591+
});
1592+
assert.equal(result.summary.pending_runtime, 1);
1593+
assert.equal(result.summary.classes['marketing/static'].pending_runtime, 1);
1594+
assert.equal(result.summary.quality_budgets['marketing/static'].pending_runtime, 1);
1595+
assert.equal(result.summary.capabilities.hero.pending_runtime, 1);
1596+
assert.equal(result.summary.risk_profiles.medium.pending_runtime, 1);
15081597
});
15091598

15101599
test('materialization sidecars reject malformed, stale, cross-fixture, and hash-mismatched evidence', () => {

0 commit comments

Comments
 (0)