diff --git a/.github/workflows/daily-dashboard-language-renderer.lock.yml b/.github/workflows/daily-dashboard-language-renderer.lock.yml index 544cc08..53353da 100644 --- a/.github/workflows/daily-dashboard-language-renderer.lock.yml +++ b/.github/workflows/daily-dashboard-language-renderer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ac7111e8df972f9d8985fc584af314cc0c8006868a03962902651184472e8ccb","body_hash":"856ee6ecef2a23787ca8d85a37522d58b600aac3442b434a679640d7f4203f28","compiler_version":"v0.87.8","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.84.3"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ae6e8e7ee8b19d403298a72438b499e2c5a85d4010f5f148934dc6b9783b1457","body_hash":"856ee6ecef2a23787ca8d85a37522d58b600aac3442b434a679640d7f4203f28","compiler_version":"v0.87.8","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.84.3"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"1aa033c7bf25ac9428fe521065b90c30a7070c4e","version":"v0.87.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.7","digest":"sha256:40a1e30b1b8d70642d4292485146cd5af612730d7a6a2e12706ddd13df375059","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.7@sha256:40a1e30b1b8d70642d4292485146cd5af612730d7a6a2e12706ddd13df375059"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.7","digest":"sha256:4f209dd4cbc74d47a6c7379956143de293429d1b1b2fb2647776cdcbf65836a1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.7@sha256:4f209dd4cbc74d47a6c7379956143de293429d1b1b2fb2647776cdcbf65836a1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.7","digest":"sha256:ebc8758c9b085ca244234e3e3ee22300d150095f9bfe7312ca8a61c5acb34a78","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.7@sha256:ebc8758c9b085ca244234e3e3ee22300d150095f9bfe7312ca8a61c5acb34a78"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.7","digest":"sha256:fb362a08d4d2f0da6c036e3f5d3b2fd87931e857fec3ca4a241cd2f2b61131f9","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.7@sha256:fb362a08d4d2f0da6c036e3f5d3b2fd87931e857fec3ca4a241cd2f2b61131f9"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.12","digest":"sha256:92d5377b6bd32cd5b9306b2a553f7ef3549bccff9207e46f931e7249bc718713","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.12@sha256:92d5377b6bd32cd5b9306b2a553f7ef3549bccff9207e46f931e7249bc718713"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"},{"image":"mcr.microsoft.com/playwright/mcp"}],"mcp_servers":[{"name":"playwright","tools":["browser_click","browser_close","browser_console_messages","browser_drag","browser_evaluate","browser_file_upload","browser_fill_form","browser_handle_dialog","browser_hover","browser_install","browser_navigate","browser_navigate_back","browser_network_requests","browser_press_key","browser_resize","browser_select_option","browser_snapshot","browser_tabs","browser_take_screenshot","browser_type","browser_wait_for"]},{"name":"safeoutputs","tools":["create_pull_request","missing_data","missing_tool","noop"]}]} # This file was automatically generated by gh-aw (v0.87.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -548,7 +548,7 @@ jobs: env: GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\"pages/dashboard/**\"],\"draft\":true,\"if_no_changes\":\"warn\",\"labels\":[\"dashboard-language-renderer\",\"ai-generated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[dashboard-language] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\"pages/dashboard/README.md\",\"pages/dashboard/PLAN.md\",\"pages/dashboard/**\"],\"draft\":true,\"if_no_changes\":\"warn\",\"labels\":[\"dashboard-language-renderer\",\"ai-generated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[dashboard-language] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" with: script: | const path = require('path'); @@ -2110,7 +2110,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.google.com,*.googleapis.com,*.gvt1.com,api.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,esm.sh,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\"pages/dashboard/**\"],\"draft\":true,\"if_no_changes\":\"warn\",\"labels\":[\"dashboard-language-renderer\",\"ai-generated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[dashboard-language] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\"pages/dashboard/README.md\",\"pages/dashboard/PLAN.md\",\"pages/dashboard/**\"],\"draft\":true,\"if_no_changes\":\"warn\",\"labels\":[\"dashboard-language-renderer\",\"ai-generated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[dashboard-language] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/daily-dashboard-language-renderer.md b/.github/workflows/daily-dashboard-language-renderer.md index 2b0af97..995abe6 100644 --- a/.github/workflows/daily-dashboard-language-renderer.md +++ b/.github/workflows/daily-dashboard-language-renderer.md @@ -57,6 +57,8 @@ safe-outputs: draft: true if-no-changes: warn allowed-files: + - "pages/dashboard/README.md" + - "pages/dashboard/PLAN.md" - "pages/dashboard/**" noop: features: diff --git a/pages/dashboard/PLAN.md b/pages/dashboard/PLAN.md index ef0cff4..8809e61 100644 --- a/pages/dashboard/PLAN.md +++ b/pages/dashboard/PLAN.md @@ -16,6 +16,7 @@ - [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. - [x] Slice: `DLS-PAGE-003` through `DLS-PAGE-013` conservative required-field coverage validation for built-in page definitions. + - [x] Slice: `DLS-PAGE-014` conservative built-in data-state exposure validation via implementation-local declarative markers. - [ ] **Security, privacy, accessibility** — Section 13 including escaping, redaction, and keyboard and screen-reader behavior verified with Playwright. - [ ] **Compliance suite** — Section 14 test suite, the compliance checklist, Appendix A as a passing fixture, and Appendix C as failing fixtures. - [ ] **Parity** — inventory the features of the existing dashboard in `.github/scripts/pages-report/report.mjs`, record them in `PLAN.md` as a parity checklist, then express each one as YAML configuration plus data fixtures, closing the checklist incrementally. @@ -23,6 +24,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` mapping on built-in pages so the required-source 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: 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`. @@ -34,6 +36,14 @@ ## Run log +### 2026-08-28 (built-in data-state exposure slice) + +- Extended the Built-in pages milestone with a narrow `DLS-PAGE-014` validator increment for independent availability, completeness, and freshness exposure on built-in pages. +- Updated `src/specification.js` and `src/validator.js` to accept a conservative implementation-local built-in `definition.data-state` mapping and require canonical boolean `true` markers for `availability`, `completeness`, and `freshness`, while still rejecting unknown declarative data-state axes. +- Expanded `test/unit/validator.test.js` coverage for accepted built-in definitions carrying independent data-state markers plus rejected built-in definitions with missing or non-canonical data-state declarations. +- 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 conservative validation of Section 10 run-link coverage on the `runs` built-in page without inventing presenter semantics. + ### 2026-08-28 (built-in required-field coverage slice) - Extended the Built-in pages milestone with a narrow Section 10 validator increment that conservatively checks built-in declarative definitions for field-level coverage, not just logical-source presence. diff --git a/pages/dashboard/README.md b/pages/dashboard/README.md index d48f51b..b9342de 100644 --- a/pages/dashboard/README.md +++ b/pages/dashboard/README.md @@ -25,3 +25,5 @@ The current aggregation slice adds conservative Section 7 and Section 11 field-d The latest provenance/freshness/data-states slice audited Section 8 and recorded a specification gap: required logical-source metadata is defined outside the dashboard YAML, but no Section 4.2 document vocabulary admits it yet. The validator therefore continues to reject attempted inline `source-metadata` keys conservatively rather than inventing undeclared YAML semantics. The current built-in-pages slice adds a conservative implementation-local built-in `definition.views` shape so Section 10 pages can declare custom-view-style source and field coverage without inventing presenter behavior beyond the specification's built-in page names and required-source catalog. + +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. diff --git a/pages/dashboard/src/specification.js b/pages/dashboard/src/specification.js index ea0497f..6631247 100644 --- a/pages/dashboard/src/specification.js +++ b/pages/dashboard/src/specification.js @@ -98,7 +98,9 @@ export const BUILT_IN_PAGE_VALUES = [ 'findings' ]; -export const BUILT_IN_PAGE_DEFINITION_KEYS = ['views']; +export const BUILT_IN_PAGE_DEFINITION_KEYS = ['views', 'data-state']; + +export const BUILT_IN_PAGE_DATA_STATE_KEYS = ['availability', 'completeness', 'freshness']; export const BUILT_IN_PAGE_REQUIRED_SOURCES = { overview: ['workflows', 'runs', 'usage', 'findings', 'operational-values'], diff --git a/pages/dashboard/src/validator.js b/pages/dashboard/src/validator.js index 77b67c2..7ba68dd 100644 --- a/pages/dashboard/src/validator.js +++ b/pages/dashboard/src/validator.js @@ -10,6 +10,8 @@ import { DATASET_COMPLETENESS_VALUES, DATASET_FRESHNESS_VALUES, DATASET_METADATA_KEYS, + BUILT_IN_PAGE_DATA_STATE_KEYS, + BUILT_IN_PAGE_DEFINITION_KEYS, DEFAULTS_KEYS, ERROR_CODES, LINK_FIELD_NAMES, @@ -372,6 +374,9 @@ function validateBuiltInPageContent(pageName, page, path, errors) { * @param {ValidationError[]} errors */ function validateBuiltInPageDefinition(pageName, definition, path, errors) { + validateBuiltInPageDefinitionKeys(definition, path, errors); + validateBuiltInPageDataState(definition['data-state'], path, errors); + if (!Array.isArray(definition.views) || definition.views.length === 0) { errors.push(createError( ERROR_CODES.missingOrInvalidRequiredField, @@ -448,6 +453,60 @@ function getBuiltInRequiredFields(pageName, sourceName) { return /** @type {string[]} */ (pageFields[/** @type {keyof typeof pageFields} */ (sourceName)]); } +/** + * @param {Record} definition + * @param {string} path + * @param {ValidationError[]} errors + */ +function validateBuiltInPageDefinitionKeys(definition, path, errors) { + for (const key of Object.keys(definition)) { + if (!BUILT_IN_PAGE_DEFINITION_KEYS.includes(key)) { + errors.push(createError( + ERROR_CODES.unknownOrDuplicateKey, + `Unknown key "${key}" is not allowed at ${path}.definition.`, + `${path}.definition.${key}` + )); + } + } +} + +/** + * @param {unknown} dataState + * @param {string} path + * @param {ValidationError[]} errors + */ +function validateBuiltInPageDataState(dataState, path, errors) { + const dataStatePath = `${path}.definition.data-state`; + if (!isPlainObject(dataState)) { + errors.push(createError( + ERROR_CODES.missingOrInvalidRequiredField, + 'built-in page definition must expose independent availability, completeness, and freshness state.', + dataStatePath + )); + return; + } + + for (const key of Object.keys(dataState)) { + if (!BUILT_IN_PAGE_DATA_STATE_KEYS.includes(key)) { + errors.push(createError( + ERROR_CODES.unknownOrDuplicateKey, + `Unknown key "${key}" is not allowed at ${dataStatePath}.`, + `${dataStatePath}.${key}` + )); + } + } + + for (const key of BUILT_IN_PAGE_DATA_STATE_KEYS) { + if (dataState[key] !== true) { + errors.push(createError( + ERROR_CODES.missingOrInvalidRequiredField, + `built-in page definition must expose independent ${key} state with canonical boolean true.`, + `${dataStatePath}.${key}` + )); + } + } +} + /** * @param {unknown} encoding * @param {Set | undefined} coveredFields diff --git a/pages/dashboard/test/unit/validator.test.js b/pages/dashboard/test/unit/validator.test.js index cfe5e54..7f1f5a6 100644 --- a/pages/dashboard/test/unit/validator.test.js +++ b/pages/dashboard/test/unit/validator.test.js @@ -227,7 +227,7 @@ dashboard: } }); - it('DLS-PAGE-001 DLS-PAGE-010 accepts an omitted built-in page title when the page name is canonical', () => { + it('DLS-PAGE-001 DLS-PAGE-010 DLS-PAGE-014 accepts an omitted built-in page title when the page name is canonical', () => { const result = validateDashboardDocument(`language-version: "0.1.0" dashboard: id: built-in-title-default @@ -237,6 +237,10 @@ dashboard: kind: built-in page: usage definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: usage-summary data: @@ -295,6 +299,10 @@ dashboard: page: organizations title: Organizations definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: organizations-view data: @@ -335,7 +343,7 @@ dashboard: } }); - it('DLS-PAGE-001 DLS-PAGE-011 accepts an explicit built-in page title when it matches the canonical title default', () => { + it('DLS-PAGE-001 DLS-PAGE-011 DLS-PAGE-014 accepts an explicit built-in page title when it matches the canonical title default', () => { const result = validateDashboardDocument(`language-version: "0.1.0" dashboard: id: explicit-built-in-title-default @@ -346,6 +354,10 @@ dashboard: page: engines-models title: Engines Models definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: runs-view data: @@ -378,7 +390,6 @@ dashboard: - field: reasoning-tokens - field: aic `); - expect(result.ok).toBe(true); }); @@ -457,7 +468,7 @@ dashboard: } }); - it('DLS-PAGE-006 rejects a runs built-in page definition that omits required run fields with DLS-E003', () => { + it('DLS-PAGE-006 DLS-PAGE-014 rejects a runs built-in page definition that omits required run fields with DLS-E003', () => { const result = validateDashboardDocument(`language-version: "0.1.0" dashboard: id: incomplete-runs-page @@ -468,6 +479,10 @@ dashboard: page: runs title: Runs definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: run-table data: @@ -529,7 +544,119 @@ dashboard: } }); - it('DLS-PAGE-006 DLS-PAGE-010 DLS-PAGE-011 DLS-PAGE-012 DLS-PAGE-013 accepts built-in definitions that conservatively cover required fields', () => { + it('DLS-PAGE-014 rejects a built-in page definition that does not expose independent availability, completeness, and freshness', () => { + const result = validateDashboardDocument(`language-version: "0.1.0" +dashboard: + id: missing-built-in-data-state + title: Missing Built In Data State + pages: + - id: usage + kind: built-in + page: usage + title: Usage + definition: + views: + - id: usage-table + data: + source: usage + mark: table + encoding: + columns: + - field: input-tokens + - field: output-tokens + - field: cache-read-tokens + - field: cache-write-tokens + - field: reasoning-tokens + - field: aic + - field: engine + - field: requested-model + - field: resolved-model + - field: organization + - field: repository + - field: workflow + - field: rollout-mode + - field: observed-at +`); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: 'DLS-E003', + path: '$.dashboard.pages[0].definition.data-state', + message: 'built-in page definition must expose independent availability, completeness, and freshness state.' + }) + ]) + ); + } + }); + + it('DLS-PAGE-014 rejects a built-in page definition with non-canonical independent data-state markers', () => { + const result = validateDashboardDocument(`language-version: "0.1.0" +dashboard: + id: invalid-built-in-data-state + title: Invalid Built In Data State + pages: + - id: usage + kind: built-in + page: usage + title: Usage + definition: + data-state: + availability: available + completeness: false + freshness: maybe + extra-axis: true + views: + - id: usage-table + data: + source: usage + mark: table + encoding: + columns: + - field: input-tokens + - field: output-tokens + - field: cache-read-tokens + - field: cache-write-tokens + - field: reasoning-tokens + - field: aic + - field: engine + - field: requested-model + - field: resolved-model + - field: organization + - field: repository + - field: workflow + - field: rollout-mode + - field: observed-at +`); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: 'DLS-E004', path: '$.dashboard.pages[0].definition.data-state.extra-axis' }), + expect.objectContaining({ + code: 'DLS-E003', + path: '$.dashboard.pages[0].definition.data-state.availability', + message: 'built-in page definition must expose independent availability state with canonical boolean true.' + }), + expect.objectContaining({ + code: 'DLS-E003', + path: '$.dashboard.pages[0].definition.data-state.completeness', + message: 'built-in page definition must expose independent completeness state with canonical boolean true.' + }), + expect.objectContaining({ + code: 'DLS-E003', + path: '$.dashboard.pages[0].definition.data-state.freshness', + message: 'built-in page definition must expose independent freshness state with canonical boolean true.' + }) + ]) + ); + } + }); + + it('DLS-PAGE-006 DLS-PAGE-010 DLS-PAGE-011 DLS-PAGE-012 DLS-PAGE-013 DLS-PAGE-014 accepts built-in definitions that conservatively cover required fields', () => { const result = validateDashboardDocument(`language-version: "0.1.0" dashboard: id: built-in-field-coverage @@ -540,6 +667,10 @@ dashboard: page: runs title: Runs definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: run-table data: @@ -563,6 +694,10 @@ dashboard: page: usage title: Usage definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: usage-table data: @@ -589,6 +724,10 @@ dashboard: page: operational-value title: Operational Value definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: operational-value-table data: @@ -613,6 +752,10 @@ dashboard: page: findings title: Findings definition: + data-state: + availability: true + completeness: true + freshness: true views: - id: findings-table data: diff --git a/tests/unit/workflow-contract.test.mjs b/tests/unit/workflow-contract.test.mjs index abbc527..2ec17bb 100644 --- a/tests/unit/workflow-contract.test.mjs +++ b/tests/unit/workflow-contract.test.mjs @@ -1181,7 +1181,7 @@ test("daily dashboard renderer builds incrementally inside its own directory", ( assert.match(source, /^timeout-minutes: 60$/m); assert.match(source, /^max-turns: 500$/m); assert.match(source, /playwright:\n\s+mode: mcp/); - assert.match(source, /create-pull-request:[\s\S]*?allowed-files:\n\s+- "pages\/dashboard\/\*\*"/); + assert.match(source, /create-pull-request:[\s\S]*?allowed-files:\n\s+- "pages\/dashboard\/README\.md"\n\s+- "pages\/dashboard\/PLAN\.md"\n\s+- "pages\/dashboard\/\*\*"/); assert.match(source, /skip-if-match: "is:pr is:open label:dashboard-language-renderer"/); assert.doesNotMatch(source, /push-to-pull-request-branch:/); assert.match(source, /pages\/dashboard\/PLAN\.md/);