Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions src/transform/render-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import {
table,
tableRow,
tableCell,
carrierDiv,
} from './ast-helpers.js';
import { readMetric } from './resolved-store.js';
import type { ArtifactResolver } from '../loader.js';
import type { ProseParser } from './prose.js';
import { fileExt, parseTableData, type TableData } from './parse-table-data.js';
Expand Down Expand Up @@ -184,12 +186,15 @@ export function renderOneOutput(
opts?: EvidenceRenderOptions,
): any[] {
const resultPath = results(artifactId);
const identifier = `output-${artifactId}`;
if (!resultPath) {
return [pendingOutput(artifactId)];
// Wrap the pending admonition in the identifier-bearing carrier div (the
// same contract as decisions/findings since #11): cross-references and
// rich-theme joins resolve even before the artifact is produced — the
// store entry exists (label, description, provenance) without it.
return [carrierDiv([pendingOutput(artifactId)], identifier)];
}

const identifier = `output-${artifactId}`;

switch (output.type) {
case 'figure':
return figureBlock(
Expand All @@ -215,8 +220,14 @@ export function renderOneOutput(
fallback.label = identifier;
return [fallback];
}
case 'metric':
// One `div` carrier holds the whole neutral fallback (a readable
// "label: value ± uncertainty unit" sentence), so a rich theme that
// overrides the carrier renders its big-stat from `store.metric` and
// replaces the fallback wholesale — the #11 pattern.
return [carrierDiv(metricFallback(output, artifactId, resultPath, opts?.vfile), identifier)];
default: {
// metric / data / report: render inline, then tag the first node with
// data / report: render inline, then tag the first node with
// the `output-<id>` carrier so cross-references resolve to it.
const nodes = renderInlineArtifact(output, artifactId, resultPath, undefined, opts?.vfile);
if (nodes.length > 0 && !nodes[0].identifier) {
Expand All @@ -228,6 +239,36 @@ export function renderOneOutput(
}
}

/**
* The neutral fallback for a produced metric output: a plain sentence
* ("**Summary metric:** 1.5 ± 0.3 Mpc") built from the parsed artifact —
* readable on a stock theme, entirely replaceable by a rich one. A JSON that
* doesn't parse as a metric shape (scalar / [value, uncertainty] / {value,…})
* falls back to the tabular rendering the other inline artifact types use.
*/
function metricFallback(
output: Output,
artifactId: string,
resultPath: string,
vfile?: any,
): any[] {
const metric = readMetric(resultPath);
if (metric?.value !== undefined) {
const parts: any[] = [
strong([text(`${output.label ?? artifactId}: `)]),
text(String(metric.value)),
];
const uncertainty = metric.uncertainty ?? metric.error;
if (uncertainty !== undefined && uncertainty !== '') {
parts.push(text(` \u00B1 ${uncertainty}`));
}
const unit = metric.unit ?? metric.units;
if (unit) parts.push(text(` ${unit}`));
return [paragraph(parts)];
}
return renderTabularFile(resultPath, artifactId, output.label ?? artifactId, vfile);
}

function renderArtifactEvidence(
evidence: Evidence,
results: ArtifactResolver,
Expand Down
22 changes: 20 additions & 2 deletions tests/plugin-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,26 @@ describe('directive — elements', () => {
expect(findFirst(nodes, (n) => n.type === 'table')).toBeDefined();
});

it('outputs.<id> metric → carrier tagged astra-output--metric', () => {
expect(hasClass(byIdentifier(runAstra('outputs.summary_metric'), 'output-summary_metric'), 'astra-output--metric')).toBe(true);
it('outputs.<id> metric → div carrier tagged astra-output--metric, sentence fallback nested inside', () => {
const nodes = runAstra('outputs.summary_metric');
const carrier = byIdentifier(nodes, 'output-summary_metric');
expect(carrier?.type).toBe('div');
expect(hasClass(carrier, 'astra-output--metric')).toBe(true);
// the neutral fallback is a readable sentence built from the artifact,
// nested inside the carrier (the #11 contract) — not a details/table
expect(textOf(carrier!.children as Node[])).toBe('Summary metric: 1.5 \u00B1 0.3 Mpc');
expect(findFirst(carrier!.children as Node[], (n) => n.type === 'details')).toBeUndefined();
});

it('an unproduced output embed → div carrier with identifier, pending admonition nested', () => {
const nodes = runAstra('outputs.unproduced_metric');
const carrier = byIdentifier(nodes, 'output-unproduced_metric');
expect(carrier?.type).toBe('div');
expect(hasClass(carrier, 'astra-output--metric')).toBe(true);
// the pending state stays visible on neutral themes…
expect(findFirst(carrier!.children as Node[], (n) => n.type === 'admonition')).toBeDefined();
// …while the identifier keeps cross-references and store joins working
expect(textOf(carrier!.children as Node[])).toContain('has not been produced yet');
});

it('aliased output (from: sub.sub_plot) resolves the source type → figure', () => {
Expand Down
Loading