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
13 changes: 11 additions & 2 deletions pages/dashboard/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- [x] **Provenance, freshness, data states** — Section 8 including unavailable, empty, partial, and stale states.
- [x] **Links and findings** — Section 9 link objects and the `href` channel semantics.
- [x] **Custom pages** — Section 11 metric, table, and chart views with the temporal line and bar defaults.
- [ ] **Built-in pages** — Section 10, one page per increment, each expressed as declarative page definitions built from the custom-view primitives.
- [ ] **Built-in pages** — Section 10, one page per increment, each expressed as declarative page definitions built from the custom-view primitives and visibly rendered in the browser prototype.
- [x] Slice: `DLS-PAGE-001` built-in page title default validation.
- [x] Slice: `DLS-PAGE-001` canonical explicit title validation for built-in pages.
- [x] Slice: `DLS-PAGE-002` and `DLS-PAGE-006` conservative required-source validation for built-in page definitions.
Expand All @@ -26,7 +26,7 @@
## Specification questions

- 2026-08-28: Section 10 requires every built-in page to be expressed as declarative page definitions built from the custom-view primitives, but Section 4.2 and Section 10 define no YAML vocabulary for embedding those declarative built-in definitions alongside `kind: built-in` / `page`. The current validator implements the most conservative reading available in this slice by accepting an implementation-local `definition.views` sequence on built-in pages so required source, field, and run-link coverage can be validated, but this key is not yet specification-backed and may need to change if the YAML vocabulary is clarified.
- 2026-08-28: `DLS-PAGE-014` says every built-in page must expose availability, completeness, and freshness independently, but Section 10 does not define a declarative YAML shape for asserting that exposure inside a built-in page definition. The current validator uses a conservative implementation-local `definition.data-state` marker with canonical boolean `true` for each axis; this is only a coverage declaration, not presenter behavior.
- 2026-08-28: `DLS-PAGE-014` says every built-in page must expose availability, completeness, and freshness independently, but Section 10 does not define a declarative YAML shape for asserting that exposure inside a built-in page definition. The current validator uses a conservative implementation-local `definition.data-state` marker with canonical boolean `true` for each axis. The presenter prototype now renders independent page-level summaries from runtime source metadata, but the exact normative YAML vocabulary for binding built-in definitions to those summaries remains unspecified.
- 2026-08-28: Section 4.3 requires `language-version` to be the quoted string `"0.1.0"`, but YAML parsing does not preserve whether a scalar was quoted. The current validator enforces string type and exact canonical value, which is the most conservative check available without relying on parser-specific CST details.
- 2026-08-28: Section 8 defines required logical-source metadata outside the dashboard YAML, while Section 4.2 omits any YAML vocabulary for carrying that metadata inside a dashboard document. The current validator now accepts a conservative `data.source-metadata` structure so Section 8 metadata shape can be validated in-document, but the presenter-side runtime contract and the exact source of truth between YAML and external inputs remain ambiguous.
- 2026-08-28: Section 11.2 says `data.order-by.field` resolves against the post-aggregation output grain, but the specification does not fully define how to derive that grain from arbitrary encodings before the presenter exists. The current validator uses the most conservative reading available in this slice: it accepts aggregate output identifiers and bare source fields only when they are canonical entity identifier fields for the selected source, and rejects other unresolved references with `DLS-E010`.
Expand All @@ -38,6 +38,15 @@

## Run log

### 2026-08-29 (built-in overview provenance-and-freshness render slice)

- Extended the Built-in pages milestone with a narrow `DLS-PAGE-002` and `DLS-PAGE-014` presenter increment for `overview`, rendering independent availability, completeness, and freshness summaries plus per-source provenance from runtime source metadata.
- Added `src/presenter.js`, a tiny browser presenter prototype that renders built-in pages with page headings, independent data-state text, and source provenance derived from built-in `definition.views` plus runtime logical-source metadata.
- Replaced the static browser smoke test with a Playwright browser test in `test/e2e/smoke.spec.js` that renders a built-in `overview` page and verifies independent `availability`, `completeness`, and `freshness` text alongside per-source provenance entries.
- Added a validator acceptance test in `test/unit/validator.test.js` showing that `overview` built-in definitions can conservatively carry source-metadata-bearing declarative views covering provenance and freshness obligations without inventing new dashboard semantics.
- Verified `npm install`, `npm run typecheck`, `npm run lint`, and `npm test`; `npm run test:e2e` remains blocked in this environment because the Playwright Chromium executable is not provisioned (`browserType.launch: Executable doesn't exist`).
- Next milestone: Built-in pages, next slice for rendering one additional Section 10 built-in page from the declarative definitions instead of only validating its coverage.

### 2026-08-29 (built-in overview linked-findings and operational-value timeline slice)

- Extended the Built-in pages milestone with a narrow `DLS-PAGE-002` validator increment for conservative `overview` coverage of recent linked findings and operational-value timelines.
Expand Down
2 changes: 1 addition & 1 deletion pages/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ The current built-in-pages slice adds a conservative implementation-local built-

The latest built-in-pages increment also adds a conservative implementation-local `definition.data-state` marker for `DLS-PAGE-014`, requiring declarative independent coverage of `availability`, `completeness`, and `freshness` on built-in pages.

The current built-in-pages slice extends conservative Section 10 coverage for `overview`, requiring declarative built-in definitions to expose recent linked findings through relation-specific finding links and to expose operational-value timelines with definition-aware series coverage.
The current built-in-pages slice extends conservative Section 10 coverage for `overview`, requiring declarative built-in definitions to expose recent linked findings through relation-specific finding links, to expose operational-value timelines with definition-aware series coverage, and to render page-level provenance plus independent freshness/completeness/availability summaries in the browser prototype.
227 changes: 227 additions & 0 deletions pages/dashboard/src/presenter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/**
* Tiny presenter prototype for built-in and custom dashboard pages.
*/

import { h } from './dom.js';

/**
* @typedef {{ availability: 'available'|'empty'|'unavailable', completeness: 'complete'|'partial'|'unknown', freshness: 'fresh'|'stale'|'unknown' }} DataState
*/

/**
* @typedef {{ 'source-id': string, 'source-kind': string, 'as-of': string, 'retrieved-at': string, completeness: DataState['completeness'], freshness: DataState['freshness'], availability?: DataState['availability'] }} SourceMetadata
*/

/**
* @typedef {{ source: string, rows: Array<Record<string, unknown>>, metadata: SourceMetadata }} LogicalSourceInput
*/

/**
* @typedef {{ document: import('./validator.js').DashboardDocument, sources: Record<string, LogicalSourceInput> }} PresentationInput
*/

/**
* @param {PresentationInput} input
* @returns {HTMLElement}
*/
export function renderDashboard(input) {
const { document, sources } = input;
const title = document.dashboard.title;

return h(
'main',
{ className: 'dashboard-prototype' },
h('h1', null, title),
h(
'div',
{ className: 'dashboard-pages' },
document.dashboard.pages.map((page) => renderPage(page, sources))
)
);
}

/**
* @param {import('./validator.js').BuiltInPage | import('./validator.js').CustomPage} page
* @param {Record<string, LogicalSourceInput>} sources
* @returns {HTMLElement}
*/
function renderPage(page, sources) {
const title = typeof page.title === 'string' && page.title.length > 0
? page.title
: titleCase(page.id);

if (page.kind === 'built-in') {
return renderBuiltInPage(page, title, sources);
}

return h(
'section',
{ className: 'dashboard-page', 'data-page-kind': 'custom', 'data-page-id': page.id },
h('h2', null, title),
h('p', null, 'Custom page rendering is not implemented in this increment.')
);
}

/**
* @param {import('./validator.js').BuiltInPage & { definition?: { views?: Array<unknown>, ['data-state']?: Record<string, boolean> } }} page
* @param {string} title
* @param {Record<string, LogicalSourceInput>} sources
* @returns {HTMLElement}
*/
function renderBuiltInPage(page, title, sources) {
const viewDefinitions = Array.isArray(page.definition?.views) ? page.definition.views : [];
/** @type {Map<string, LogicalSourceInput>} */
const pageSources = new Map();

for (const view of viewDefinitions) {
const sourceName = getViewSource(view);
if (!sourceName) {
continue;
}
const sourceInput = sources[sourceName];
if (sourceInput) {
pageSources.set(sourceName, sourceInput);
}
}

const effectiveState = summarizeDataState(pageSources);
const provenanceItems = [...pageSources.entries()].map(([sourceName, sourceInput]) => {
const metadata = sourceInput.metadata;
return h(
'li',
null,
`${sourceName}: ${metadata['source-id']} (${metadata['source-kind']}) — as of ${metadata['as-of']}`
);
});

return h(
'section',
{ className: 'dashboard-page', 'data-page-kind': 'built-in', 'data-page-name': page.page, 'data-page-id': page.id },
h('h2', null, title),
h(
'dl',
{ className: 'data-state-summary' },
h('dt', null, 'Availability'),
h('dd', { 'data-state-axis': 'availability' }, effectiveState.availability),
h('dt', null, 'Completeness'),
h('dd', { 'data-state-axis': 'completeness' }, effectiveState.completeness),
h('dt', null, 'Freshness'),
h('dd', { 'data-state-axis': 'freshness' }, effectiveState.freshness)
),
h('h3', null, 'Provenance'),
h(
'ul',
{ className: 'provenance-list' },
provenanceItems.length > 0
? provenanceItems
: [h('li', null, 'No source provenance available for this page.')]
)
);
}

/**
* @param {Map<string, LogicalSourceInput>} pageSources
* @returns {DataState}
*/
function summarizeDataState(pageSources) {
/** @type {DataState['availability'][]} */
const availabilities = [];
/** @type {DataState['completeness'][]} */
const completenessValues = [];
/** @type {DataState['freshness'][]} */
const freshnessValues = [];

for (const sourceInput of pageSources.values()) {
const metadata = sourceInput.metadata;
availabilities.push(metadata.availability ?? inferAvailability(sourceInput.rows));
completenessValues.push(metadata.completeness);
freshnessValues.push(metadata.freshness);
}

return {
availability: combineAvailability(availabilities),
completeness: combineCompleteness(completenessValues),
freshness: combineFreshness(freshnessValues)
};
}

/**
* @param {Array<Record<string, unknown>>} rows
* @returns {DataState['availability']}
*/
function inferAvailability(rows) {
return rows.length > 0 ? 'available' : 'empty';
}

/**
* @param {DataState['availability'][]} values
* @returns {DataState['availability']}
*/
function combineAvailability(values) {
if (values.includes('unavailable')) {
return 'unavailable';
}
if (values.length === 0 || values.every((value) => value === 'empty')) {
return 'empty';
}
return 'available';
}

/**
* @param {DataState['completeness'][]} values
* @returns {DataState['completeness']}
*/
function combineCompleteness(values) {
if (values.includes('partial')) {
return 'partial';
}
if (values.length === 0 || values.includes('unknown')) {
return 'unknown';
}
return 'complete';
}

/**
* @param {DataState['freshness'][]} values
* @returns {DataState['freshness']}
*/
function combineFreshness(values) {
if (values.includes('stale')) {
return 'stale';
}
if (values.length === 0 || values.includes('unknown')) {
return 'unknown';
}
return 'fresh';
}

/**
* @param {unknown} view
* @returns {string | null}
*/
function getViewSource(view) {
if (!isPlainObject(view) || !isPlainObject(view.data) || typeof view.data.source !== 'string') {
return null;
}
return view.data.source;
}

/**
* @param {string} value
* @returns {string}
*/
function titleCase(value) {
return value
.split('-')
.filter(Boolean)
.map((part) => part[0] ? `${part[0].toUpperCase()}${part.slice(1)}` : part)
.join(' ');
}

/**
* @param {unknown} value
* @returns {value is Record<string, any>}
*/
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
Loading
Loading