feat: require complete data context header evidence (AEG-V16-018)
Make projection and watermark visible requirements, expose stale state accessibly, and add component regression coverage. Status remains IN_PROGRESS pending predecessor and UX evidence.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# AEG-V16-018 — DataContextHeader
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-018 / REQ-V16-FEC-02 / UI-V16-FEC-02 / T-V16-FEC-02
|
||||
- **Classification:** one shared FE component slice; no server, API, schema, policy, or provider change.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, `frontend/src/shared/contracts/versionSet.ts`, `frontend/src/shared/ui/EvidenceVersionSet.vue`, and existing `KsDataContextHeader.vue`.
|
||||
- **Assumption:** consumers pass values from an approved server-side evidence/projection context; this display component neither creates nor validates an evidence decision.
|
||||
- **Unknown:** the WBS tracker lacks approved predecessor evidence for AEG-V16-017. This slice preserves that dependency as a completion blocker.
|
||||
- **Decision Required:** a UI/UX owner must approve visual and accessibility evidence before this item is marked COMPLETED at MVP-A.
|
||||
|
||||
## Acceptance mapping
|
||||
|
||||
| Acceptance requirement | Implementation evidence |
|
||||
| --- | --- |
|
||||
| as-of / VersionSet / Projection / Watermark | all four are required props and rendered in the header. |
|
||||
| stale state is visible and accessible | `data-stale`, visible `STALE`, and an assertive status label are rendered together. |
|
||||
| reproducible component behavior | `KsDataContextHeader.spec.ts` verifies evidence propagation and stale output. |
|
||||
|
||||
## Execution evidence — 2026-08-08
|
||||
|
||||
- `frontend: pnpm test -- --run src/shared/ui/components/tests/KsDataContextHeader.spec.ts`: 1 file / 2 tests passed.
|
||||
- `frontend: pnpm typecheck`: passed.
|
||||
- `frontend: pnpm build`: passed. The build reported a 501.13 kB gzip main chunk warning; no unapproved performance threshold is inferred from that warning.
|
||||
@@ -9,6 +9,7 @@ AEG-X-007,S0,Cross,Serilog/OTel correlation 고도화,COMPLETED,2026-08-06,"test
|
||||
AEG-V16-017,S6,Cross,FieldShell 표준,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-017_FIELDSHELL_SLICE_NOTE.md; frontend/src/shared/ui/components/FieldShell.vue; frontend/src/shared/ui/components/tests/FieldShell.spec.ts","FE Lead","2026-08-08: FieldShell now owns label/error/help/ARIA relationships for KsTextField, KsTextArea, KsSelect, KsDateField, and KsNumberField. Actual evidence: frontend pnpm typecheck PASS; pnpm test PASS (19 files, 42 tests); pnpm build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending WBS Master/tracker reconciliation and AEG-V16-016 vendor-boundary acceptance evidence."
|
||||
AEG-V16-016,S0,VS-00,Vendor boundary fitness,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-016_VENDOR_BOUNDARY_SLICE_NOTE.md; tools/validate_v16.py; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts","FE Lead","2026-08-08: Removed stale fixed WBS row-count assertion; validator now verifies WBS ID integrity and reports vendor imports outside the approved adapter boundary. Actual evidence: python tools/validate_v16.py PASS=1 WARN=2 FAIL=0; targeted adapter tests 4/4 PASS; frontend typecheck PASS. COMPLETED is blocked because dependency AEG-V16-015 has no approved acceptance evidence in the tracker."
|
||||
AEG-V16-015,S0,VS-00,Adapter rollback runbook,IN_PROGRESS,TBD,"docs/CURRENT/ui-provider-switch.md","FE Lead","2026-08-08: Created startup-only provider switch and rollback runbook with fail-closed configuration, immutable-artifact rollback, and append-only evidence record. Visual/a11y/performance rehearsal evidence is not present and is explicitly required before completion; dependency AEG-V16-014 is also not evidenced in the tracker."
|
||||
AEG-V16-018,S6,Cross,DataContextHeader,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-018_DATA_CONTEXT_HEADER_SLICE_NOTE.md; frontend/src/shared/ui/components/KsDataContextHeader.vue; frontend/src/shared/ui/components/tests/KsDataContextHeader.spec.ts","FE Lead","2026-08-08: Made projectionVersion and watermark required so stale/rebuildable read-model context cannot be omitted; added visible and accessible stale state plus VersionSet propagation tests. Actual evidence: targeted Vitest 2/2 PASS, frontend typecheck PASS, production build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending predecessor AEG-V16-017 acceptance and UX/a11y evidence."
|
||||
AEG-X-008,S0,Cross,OpenAPI artifact 고도화,COMPLETED,2026-08-04,.gitea/workflows/openapi-gate.yml + docs/api/openapi.json,BE/FE Architect,"✅ OpenAPI diff gate implemented: CI/CD automation detects breaking changes (3 checks: parameter removal, status code removal, field removal), blocks merge without approval, auto-comments on PR"
|
||||
AEG-VS-00-01,S0,VS-00,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-06,"docs/CURRENT/SLICE_SPECS/VS-00-SLICE_SPEC.md + commit e7913db",PM/Architect,"✅ SLICE_SPEC produced: VS-00-SLICE_SPEC.md (state transitions, RBAC, governance gates, DQ rules, compliance). Commit e7913db. 249/253 tests PASS."
|
||||
AEG-VS-00-02,S0,VS-00,데이터 시점·스키마·정합성 계약,COMPLETED,2026-08-06,"contracts/data/platform-data-contract.v1.json + commit e7913db",Data Architect/DBA,"✅ DATA_CONTRACT v1.0 produced: PIT envelope (published_at/correlation_id/revision), 5 table schemas, DQ rules/lineage, GDPR/PCI-DSS compliance. JSON schema + validation. 249/253 tests PASS."
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import type { VersionSet } from '../../contracts/versionSet'
|
||||
import EvidenceVersionSet from '../EvidenceVersionSet.vue'
|
||||
const props = defineProps<{
|
||||
title: string; asOf: string; datasetId: string; dataHash: string; modelVersion: string; configVersion: string;
|
||||
codeSha: string; contractVersion: string; projectionVersion?: string; watermark?: string;
|
||||
codeSha: string; contractVersion: string; projectionVersion: string; watermark: string;
|
||||
stale?: boolean; correlationId?: string
|
||||
}>()
|
||||
const versionSet = computed<VersionSet>(() => ({
|
||||
@@ -18,11 +18,11 @@ const versionSet = computed<VersionSet>(() => ({
|
||||
</script>
|
||||
<template>
|
||||
<header class="ks-data-context" :data-stale="stale || undefined">
|
||||
<div><h1>{{ title }}</h1><p>As-of {{ asOf }} <strong v-if="stale">STALE</strong></p></div>
|
||||
<div><h1>{{ title }}</h1><p>As-of {{ asOf }} <strong v-if="stale" role="status" aria-label="Data is stale">STALE</strong></p></div>
|
||||
<EvidenceVersionSet :value="versionSet" compact />
|
||||
<dl v-if="projectionVersion || watermark || correlationId">
|
||||
<template v-if="projectionVersion"><dt>Projection</dt><dd>{{ projectionVersion }}</dd></template>
|
||||
<template v-if="watermark"><dt>Watermark</dt><dd>{{ watermark }}</dd></template>
|
||||
<dl>
|
||||
<dt>Projection</dt><dd>{{ projectionVersion }}</dd>
|
||||
<dt>Watermark</dt><dd>{{ watermark }}</dd>
|
||||
<template v-if="correlationId"><dt>Correlation</dt><dd>{{ correlationId }}</dd></template>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
@@ -30,7 +30,10 @@ __VLS_asFunctionalElement1(__VLS_intrinsics.h1, __VLS_intrinsics.h1)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
|
||||
(__VLS_ctx.asOf);
|
||||
if (__VLS_ctx.stale) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({
|
||||
role: "status",
|
||||
'aria-label': "Data is stale",
|
||||
});
|
||||
}
|
||||
const __VLS_0 = EvidenceVersionSet;
|
||||
// @ts-ignore
|
||||
@@ -42,26 +45,20 @@ const __VLS_2 = __VLS_1({
|
||||
value: (__VLS_ctx.versionSet),
|
||||
compact: true,
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
|
||||
if (__VLS_ctx.projectionVersion || __VLS_ctx.watermark || __VLS_ctx.correlationId) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dl, __VLS_intrinsics.dl)({});
|
||||
if (__VLS_ctx.projectionVersion) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.projectionVersion);
|
||||
}
|
||||
if (__VLS_ctx.watermark) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.watermark);
|
||||
}
|
||||
if (__VLS_ctx.correlationId) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.correlationId);
|
||||
}
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dl, __VLS_intrinsics.dl)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.projectionVersion);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.watermark);
|
||||
if (__VLS_ctx.correlationId) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.correlationId);
|
||||
}
|
||||
// @ts-ignore
|
||||
[stale, stale, title, asOf, versionSet, projectionVersion, projectionVersion, projectionVersion, watermark, watermark, watermark, correlationId, correlationId, correlationId,];
|
||||
[stale, stale, title, asOf, versionSet, projectionVersion, watermark, correlationId, correlationId,];
|
||||
const __VLS_export = (await import('vue')).defineComponent({
|
||||
__typeProps: {},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import KsDataContextHeader from '../KsDataContextHeader.vue';
|
||||
const props = {
|
||||
title: 'Decision context',
|
||||
asOf: '2026-08-08T03:00:00Z',
|
||||
datasetId: 'dataset-20260808',
|
||||
dataHash: 'data-sha',
|
||||
modelVersion: 'model-v4',
|
||||
configVersion: 'config-v2',
|
||||
codeSha: 'code-sha',
|
||||
contractVersion: '4.0',
|
||||
projectionVersion: 'projection-v7',
|
||||
watermark: 'watermark-42',
|
||||
correlationId: 'corr-123'
|
||||
};
|
||||
function mountHeader(overrides = {}) {
|
||||
return mount(KsDataContextHeader, {
|
||||
props: { ...props, ...overrides },
|
||||
global: {
|
||||
stubs: {
|
||||
EvidenceVersionSet: { props: ['value', 'compact'], template: '<output :data-value="JSON.stringify(value)" />' }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
describe('KsDataContextHeader', () => {
|
||||
it('renders the required point-in-time and projection context', () => {
|
||||
const wrapper = mountHeader();
|
||||
expect(wrapper.text()).toContain('As-of 2026-08-08T03:00:00Z');
|
||||
expect(wrapper.text()).toContain('Projectionprojection-v7');
|
||||
expect(wrapper.text()).toContain('Watermarkwatermark-42');
|
||||
expect(wrapper.text()).toContain('Correlationcorr-123');
|
||||
expect(wrapper.get('output').attributes('data-value')).toContain('dataset-20260808');
|
||||
expect(wrapper.get('output').attributes('data-value')).toContain('code-sha');
|
||||
});
|
||||
it('exposes stale data both visually and to assistive technology', () => {
|
||||
const wrapper = mountHeader({ stale: true });
|
||||
expect(wrapper.get('header').attributes('data-stale')).toBe('true');
|
||||
expect(wrapper.get('[role="status"]').attributes('aria-label')).toBe('Data is stale');
|
||||
expect(wrapper.get('[role="status"]').text()).toBe('STALE');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import KsDataContextHeader from '../KsDataContextHeader.vue'
|
||||
|
||||
const props = {
|
||||
title: 'Decision context',
|
||||
asOf: '2026-08-08T03:00:00Z',
|
||||
datasetId: 'dataset-20260808',
|
||||
dataHash: 'data-sha',
|
||||
modelVersion: 'model-v4',
|
||||
configVersion: 'config-v2',
|
||||
codeSha: 'code-sha',
|
||||
contractVersion: '4.0',
|
||||
projectionVersion: 'projection-v7',
|
||||
watermark: 'watermark-42',
|
||||
correlationId: 'corr-123'
|
||||
}
|
||||
|
||||
type HeaderProps = typeof props & { stale?: boolean }
|
||||
|
||||
function mountHeader(overrides: Partial<HeaderProps> = {}) {
|
||||
return mount(KsDataContextHeader, {
|
||||
props: { ...props, ...overrides },
|
||||
global: {
|
||||
stubs: {
|
||||
EvidenceVersionSet: { props: ['value', 'compact'], template: '<output :data-value="JSON.stringify(value)" />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('KsDataContextHeader', () => {
|
||||
it('renders the required point-in-time and projection context', () => {
|
||||
const wrapper = mountHeader()
|
||||
|
||||
expect(wrapper.text()).toContain('As-of 2026-08-08T03:00:00Z')
|
||||
expect(wrapper.text()).toContain('Projectionprojection-v7')
|
||||
expect(wrapper.text()).toContain('Watermarkwatermark-42')
|
||||
expect(wrapper.text()).toContain('Correlationcorr-123')
|
||||
expect(wrapper.get('output').attributes('data-value')).toContain('dataset-20260808')
|
||||
expect(wrapper.get('output').attributes('data-value')).toContain('code-sha')
|
||||
})
|
||||
|
||||
it('exposes stale data both visually and to assistive technology', () => {
|
||||
const wrapper = mountHeader({ stale: true })
|
||||
|
||||
expect(wrapper.get('header').attributes('data-stale')).toBe('true')
|
||||
expect(wrapper.get('[role="status"]').attributes('aria-label')).toBe('Data is stale')
|
||||
expect(wrapper.get('[role="status"]').text()).toBe('STALE')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user