From 9c5848b6073171ee1bb29366aba57bea6b5fbd80 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:18:12 +0000 Subject: [PATCH 1/2] Add built-in overview presenter prototype --- pages/dashboard/PLAN.md | 13 +- pages/dashboard/README.md | 2 +- pages/dashboard/src/presenter.js | 227 ++++++++++++++++++++ pages/dashboard/test/e2e/smoke.spec.js | 130 ++++++++++- pages/dashboard/test/unit/validator.test.js | 133 ++++++++++++ 5 files changed, 498 insertions(+), 7 deletions(-) create mode 100644 pages/dashboard/src/presenter.js diff --git a/pages/dashboard/PLAN.md b/pages/dashboard/PLAN.md index 14b9dae..e43befe 100644 --- a/pages/dashboard/PLAN.md +++ b/pages/dashboard/PLAN.md @@ -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. @@ -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`. @@ -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. diff --git a/pages/dashboard/README.md b/pages/dashboard/README.md index b46b1db..d94f790 100644 --- a/pages/dashboard/README.md +++ b/pages/dashboard/README.md @@ -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. diff --git a/pages/dashboard/src/presenter.js b/pages/dashboard/src/presenter.js new file mode 100644 index 0000000..2b833bf --- /dev/null +++ b/pages/dashboard/src/presenter.js @@ -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>, metadata: SourceMetadata }} LogicalSourceInput + */ + +/** + * @typedef {{ document: import('./validator.js').DashboardDocument, sources: Record }} 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} 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, ['data-state']?: Record } }} page + * @param {string} title + * @param {Record} sources + * @returns {HTMLElement} + */ +function renderBuiltInPage(page, title, sources) { + const viewDefinitions = Array.isArray(page.definition?.views) ? page.definition.views : []; + /** @type {Map} */ + 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} 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>} 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} + */ +function isPlainObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/pages/dashboard/test/e2e/smoke.spec.js b/pages/dashboard/test/e2e/smoke.spec.js index c37a6a6..9fc2fb1 100644 --- a/pages/dashboard/test/e2e/smoke.spec.js +++ b/pages/dashboard/test/e2e/smoke.spec.js @@ -1,7 +1,129 @@ +import { readFileSync } from 'node:fs'; import { test, expect } from '@playwright/test'; -test('DLS-CONF-004 scaffold browser harness loads a static document', async ({ page }) => { - await page.setContent('

Dashboard scaffold

Browser harness ready.

'); - await expect(page.getByRole('heading', { name: 'Dashboard scaffold' })).toBeVisible(); - await expect(page.getByText('Browser harness ready.')).toBeVisible(); +test('DLS-PAGE-002 DLS-PAGE-014 built-in overview page renders independent data state and provenance in browser', async ({ page }) => { + const domSource = readFileSync(new URL('../../src/dom.js', import.meta.url), 'utf8'); + const presenterSource = readFileSync(new URL('../../src/presenter.js', import.meta.url), 'utf8'); + const domModuleUrl = `data:text/javascript;charset=utf-8,${encodeURIComponent(domSource)}`; + const presenterModuleUrl = `data:text/javascript;charset=utf-8,${encodeURIComponent(presenterSource.replace("'./dom.js'", JSON.stringify(domModuleUrl)))}`; + + await page.setContent(` +
+ + `); + + await expect(page.getByRole('heading', { name: 'Built In Overview Render' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Overview' })).toBeVisible(); + await expect(page.locator('[data-state-axis="availability"]')).toHaveText('unavailable'); + await expect(page.locator('[data-state-axis="completeness"]')).toHaveText('partial'); + await expect(page.locator('[data-state-axis="freshness"]')).toHaveText('stale'); + await expect(page.locator('.provenance-list li')).toContainText([ + 'workflows: workflows-fixture (fixture) — as of 2026-08-29T12:00:00Z', + 'runs: runs-fixture (fixture) — as of 2026-08-29T12:00:00Z', + 'usage: usage-fixture (fixture) — as of 2026-08-29T12:00:00Z', + 'findings: findings-fixture (fixture) — as of 2026-08-29T12:00:00Z', + 'operational-values: operational-value-fixture (fixture) — as of 2026-08-29T12:00:00Z' + ]); }); diff --git a/pages/dashboard/test/unit/validator.test.js b/pages/dashboard/test/unit/validator.test.js index bcee770..13427d9 100644 --- a/pages/dashboard/test/unit/validator.test.js +++ b/pages/dashboard/test/unit/validator.test.js @@ -959,6 +959,139 @@ dashboard: expect(result.ok).toBe(true); }); + it('DLS-PAGE-002 DLS-PAGE-014 accepts built-in overview page definitions that conservatively expose provenance and freshness coverage through source metadata-bearing views', () => { + const result = validateDashboardDocument(`language-version: "0.1.0" +dashboard: + id: overview-provenance-freshness + title: Overview Provenance Freshness + pages: + - id: overview + kind: built-in + page: overview + title: Overview + definition: + data-state: + availability: true + completeness: true + freshness: true + views: + - id: workflow-inventory + data: + source: workflows + source-metadata: + source-id: workflows-fixture + source-kind: fixture + as-of: '2026-08-29T12:00:00Z' + retrieved-at: '2026-08-29T12:05:00Z' + completeness: complete + freshness: fresh + availability: available + mark: table + encoding: + columns: + - field: workflow-active + - field: rollout-mode + - id: run-trends + data: + source: runs + source-metadata: + source-id: runs-fixture + source-kind: fixture + as-of: '2026-08-29T12:00:00Z' + retrieved-at: '2026-08-29T12:05:00Z' + completeness: partial + freshness: stale + availability: empty + mark: chart + encoding: + x: + field: started-at + type: temporal + time-unit: day + y: + field: run + aggregate: count + color: + field: run-conclusion + - id: run-rankings + data: + source: runs + source-metadata: + source-id: runs-fixture + source-kind: fixture + as-of: '2026-08-29T12:00:00Z' + retrieved-at: '2026-08-29T12:05:00Z' + completeness: partial + freshness: stale + availability: empty + mark: table + encoding: + columns: + - field: repository + - field: workflow + - field: run-status + - field: run-conclusion + - id: usage-metric + data: + source: usage + source-metadata: + source-id: usage-fixture + source-kind: fixture + as-of: '2026-08-29T12:00:00Z' + retrieved-at: '2026-08-29T12:05:00Z' + completeness: complete + freshness: fresh + availability: available + mark: metric + encoding: + value: + field: aic + aggregate: sum + - id: recent-findings + data: + source: findings + source-metadata: + source-id: findings-fixture + source-kind: fixture + as-of: '2026-08-29T12:00:00Z' + retrieved-at: '2026-08-29T12:05:00Z' + completeness: complete + freshness: fresh + availability: available + mark: table + encoding: + columns: + - field: observed-at + - field: issue-link + - field: pull-request-link + - field: run-link + - id: operational-value-timeline + data: + source: operational-values + source-metadata: + source-id: operational-values-fixture + source-kind: fixture + as-of: '2026-08-29T12:00:00Z' + retrieved-at: '2026-08-29T12:05:00Z' + completeness: unknown + freshness: fresh + availability: unavailable + mark: chart + encoding: + x: + field: observed-at + type: temporal + time-unit: day + y: + field: operational-value + aggregate: max + color: + field: operational-value-definition +`); + + expect(result.ok).toBe(true); + }); + it('DLS-SEM-017 accepts every canonical Section 5.1 source name', () => { const result = validateDashboardDocument(`language-version: "0.1.0" dashboard: From 3f420baf2a662d4ba6e82ef4e5fc20f56c96cfad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:28:25 +0000 Subject: [PATCH 2/2] Fix overview browser test fixture Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pages/dashboard/test/e2e/smoke.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/dashboard/test/e2e/smoke.spec.js b/pages/dashboard/test/e2e/smoke.spec.js index 9fc2fb1..8428c02 100644 --- a/pages/dashboard/test/e2e/smoke.spec.js +++ b/pages/dashboard/test/e2e/smoke.spec.js @@ -12,7 +12,7 @@ test('DLS-PAGE-002 DLS-PAGE-014 built-in overview page renders independent data `); await expect(page.getByRole('heading', { name: 'Built In Overview Render' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Overview' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Overview', exact: true })).toBeVisible(); await expect(page.locator('[data-state-axis="availability"]')).toHaveText('unavailable'); await expect(page.locator('[data-state-axis="completeness"]')).toHaveText('partial'); await expect(page.locator('[data-state-axis="freshness"]')).toHaveText('stale');