Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c107d1f
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
0dd2804
fix: reject impossible Weekly VOC dates
seonghobae Aug 20, 2026
f4a5b55
chore: restack Weekly VOC on current foundation head
seonghobae Aug 20, 2026
debd51b
chore: restack Weekly VOC on current foundation head
seonghobae Aug 20, 2026
b6c3b02
chore: restack Weekly VOC on current Buyer root
seonghobae Aug 20, 2026
0d756b6
chore: restack Weekly VOC on current Buyer root
seonghobae Aug 20, 2026
86fa07b
chore: reconcile remote Weekly VOC restack
seonghobae Aug 20, 2026
a6b87a6
chore: restack Weekly VOC on final Buyer root
seonghobae Aug 20, 2026
ffffd2d
chore: restack Weekly VOC on final Buyer root
seonghobae Aug 20, 2026
043505c
chore: restack Weekly VOC on final Buyer root
seonghobae Aug 20, 2026
f960ee9
Merge remote-tracking branch 'origin/feat/analysis-run-name-evidence-…
seonghobae Aug 20, 2026
ed2282b
Merge remote-tracking branch 'origin/feat/analysis-run-name-evidence-…
seonghobae Aug 20, 2026
d208529
Merge commit '480d2a1964c536ade5cebdf4d1dd5dab017f6c79' of https://gi…
seonghobae Aug 20, 2026
4fdd027
fix: avoid stacked ADR number collision
seonghobae Aug 20, 2026
49f99dc
Merge remote-tracking branch 'origin/feat/analysis-run-name-evidence-…
seonghobae Aug 20, 2026
4dfde04
fix: align embedded image accessibility assertion
seonghobae Aug 20, 2026
f5de56d
merge: retain exact embedded image accessibility contract
seonghobae Aug 20, 2026
c0e9043
Merge remote-tracking branch 'origin/feat/analysis-run-name-evidence-…
seonghobae Aug 20, 2026
868559e
test: align queue schema and orchestrator contracts
seonghobae Aug 20, 2026
74718d6
chore: sync lock metadata with release version
seonghobae Aug 20, 2026
3bcea53
Merge current analysis-run base into board lineage
seonghobae Aug 20, 2026
79545af
merge: propagate current analysis-run lineage base
seonghobae Aug 20, 2026
431a642
fix: derive Weekly VOC from authorized newest post
seonghobae Aug 20, 2026
727512a
merge: synchronize weekly VOC stack with current parent
seonghobae Aug 20, 2026
0f93b03
merge: propagate current protected analysis-run base
seonghobae Aug 20, 2026
1f2cb3f
Merge remote-tracking branch 'origin/feat/board-weekly-voc-open-event…
seonghobae Aug 20, 2026
9b61751
merge: retain concurrent Weekly VOC parent sync
seonghobae Aug 20, 2026
dfd95d9
merge: propagate current semantic-source parent
seonghobae Aug 20, 2026
7b5c1e9
Merge branch 'feat/analysis-run-name-evidence-lineage' of https://git…
seonghobae Aug 20, 2026
e573875
fix(board): keep weekly VOC filters complete
seonghobae Aug 20, 2026
8268b14
fix(board): fetch posts for selected ISO week
seonghobae Aug 20, 2026
6f31284
test(api): prove ISO week scope excludes other corps
seonghobae Aug 20, 2026
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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ adjudication does -- never a raw LLM API. Demo TEPP seed goes through
envelope is Failed (`tepp_not_available` / `tepp_result_not_persisted`),
never a fabricated theta or a local psychometric substitute.

Buyer Board **Weekly VOC** is an ISO-8601 week list filter (ADR 0092).
Opening that filtered post focuses Event Lineage (ADR 0093). Do not
invent a week, a theta, or a cutoff body.


## Tests

```bash
Expand Down
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ Keycloak (`src/main.tsx`'s `AuthProvider`) -- no mocked auth, no static
HTML. `src/api.ts` calls the FastAPI backend directly with the token
Keycloak issued; `src/App.tsx` renders a git-branch SVG of
`GET /api/lineage` (click a node to open that post; `post_admin` can
rebuild), the post list, and a full detail popup: Korean
rebuild), the post list with a named Weekly VOC ISO-8601 week filter
(ADR 0092; opening that filtered post focuses Event Lineage, ADR 0093),
and a full detail popup: Korean
summary/key-events/R&R, VOC evidence excerpts, an Event Lineage panel
(direct vs. indirect links; a link opens that post), the Keyman
affiliate tree (resolved ancestors plus unresolved org roots), Keyman +
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.d/2.12.0-weekly-voc-iso-week.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 2.12.0 Weekly VOC ISO-week list filter

Weekly VOC on Board keeps Voice of Customer posts for the latest ISO-8601
week (UTC Thursday rule). Other VOC types and older weeks drop out. The
Board names Event Lineage as the next read. No TEPP theta is invented.
6 changes: 6 additions & 0 deletions CHANGELOG.d/2.13.0-weekly-voc-open-event-lineage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 2.13.0 Opening Weekly VOC focuses Event Lineage

Open a Voice of Customer post from an active Weekly VOC filter and the
popup Event Lineage heading takes focus. The popup names that post as
current and to read Keyman and evaluation next. Home-list opens do not.
No TEPP theta is invented.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to this project are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.13.0] - 2026-08-19

### Added

- Opening a Voice of Customer post from an active Weekly VOC filter now
focuses Event Lineage and names Keyman and evaluation as the next read.
Home-list opens do not add that focus or copy. No TEPP theta is
invented. No cutoff body is invented (ADR 0093 / ADR 0016).

## [2.12.0] - 2026-08-19

### Added

- Board now names Weekly VOC as an ISO-8601 week list filter. The control
keeps Voice of Customer posts for the latest week present in the loaded
list (UTC Thursday rule) and tells the buyer to open a post to read
Event Lineage. Reset filters returns every VOC type and every week.
No TEPP theta is invented (ADR 0092).

## [2.10.0] - 2026-08-18

### Added
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,11 @@ cited source. After that next action, the popup lands the first cited
evidence. Changing the week first still
focuses the report period field. Mean θ stays on the period-report
panel.

## Weekly VOC (v2.12.0 / v2.13.0)

On Board, click **Weekly VOC**. Voice of Customer posts for the latest
ISO-8601 week stay; other VOC types and older weeks drop out. The Board
names Event Lineage as the next read (ADR 0092). Open a remaining post:
Event Lineage takes focus and names Keyman and evaluation next
(ADR 0093). A home-list open does not. Do not invent a theta.
51 changes: 36 additions & 15 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ async def _lookup_post_labels(conn: asyncpg.Connection, rows: list[asyncpg.Recor

async def _post_filter_options(
conn: asyncpg.Connection, corporate_entity_ids: frozenset[str]
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
) -> tuple[list[dict[str, str]], list[dict[str, str]], list[str]]:
"""Return every authorized filter value, not only values on the current page."""
visibility_sql = f"""
select distinct post.visibility_code as code,
Expand Down Expand Up @@ -569,6 +569,15 @@ async def _post_filter_options(
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
order by display_order, code
"""
week_sql = f"""
select distinct to_char(post.created_at at time zone 'UTC', 'IYYY-"W"IW') as iso_week
from source_post post
where (post.visibility_code = 'public'
or post.corporate_entity_id::text = any($1::text[]))
and post.created_at is not null
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
order by iso_week desc
"""
# Safe SQL: both query strings are closed lookup statements; entity ids remain asyncpg parameters.
visibility_rows = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli
visibility_sql, list(corporate_entity_ids)
Expand All @@ -577,9 +586,14 @@ async def _post_filter_options(
type_rows = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli
type_sql, list(corporate_entity_ids)
)
# Safe SQL: the ISO-week query is closed; entity ids remain an asyncpg parameter.
week_rows = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli
week_sql, list(corporate_entity_ids)
)
return (
[{"code": row["code"], "label": row["label"]} for row in type_rows],
[{"code": row["code"], "label": row["label"]} for row in visibility_rows],
[row["iso_week"] for row in week_rows],
)


Expand Down Expand Up @@ -1093,6 +1107,7 @@ async def list_posts(
search: str | None = Query(None, max_length=200),
voc_type: list[str] | None = Query(None, max_length=80),
visibility: str | None = Query(None, max_length=80),
iso_week: str | None = Query(None, max_length=8, pattern=r"^\d{4}-W\d{2}$"),
sort: Literal["newest", "oldest", "title"] = Query("newest"),
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
Expand All @@ -1101,7 +1116,7 @@ async def list_posts(
_require_post_read(account)
search_term = search.strip() if search and search.strip() else None
async with pool.acquire() as conn:
voc_type_options, visibility_options = await _post_filter_options(
voc_type_options, visibility_options, iso_week_options = await _post_filter_options(
conn, account.corporate_entity_ids
)
body_search_ids: list[str] = []
Expand Down Expand Up @@ -1147,7 +1162,7 @@ async def list_posts(
case
when $1::text is null then 0
when lower(coalesce(post.post_title, '')) like '%' || lower($1) || '%' then 0
when post.post_id = any($5::uuid[]) then 1
when post.post_id = any($6::uuid[]) then 1
else 2
end as search_priority,
count(*) over() as total_count
Expand Down Expand Up @@ -1211,7 +1226,7 @@ async def list_posts(
) >= 0.45
)
)
or post.post_id = any($5::uuid[])
or post.post_id = any($6::uuid[])
or exists (
select 1 from post_project_mention project
where project.post_id = post.post_id
Expand Down Expand Up @@ -1278,18 +1293,22 @@ async def list_posts(
)
and ($3::text[] is null or post.voc_type_code = any($3::text[]))
and ($4::text is null or post.visibility_code = $4)
and (
$5::text is null
or to_char(post.created_at at time zone 'UTC', 'IYYY-"W"IW') = $5
)
order by
search_priority asc,
case
when $1::text is not null and post.post_id = any($5::uuid[])
then array_position($5::uuid[], post.post_id)
when $1::text is not null and post.post_id = any($6::uuid[])
then array_position($6::uuid[], post.post_id)
end asc,
case when $8::text = 'title' then lower(coalesce(post.post_title, '')) end asc,
case when $8::text = 'oldest' then post.created_at end asc,
case when $8::text in ('newest', 'title') then post.created_at end desc,
case when $9::text = 'title' then lower(coalesce(post.post_title, '')) end asc,
case when $9::text = 'oldest' then post.created_at end asc,
case when $9::text in ('newest', 'title') then post.created_at end desc,
post.post_id desc
offset $6
limit $7
offset $7
limit $8
)
select page.*,
case
Expand Down Expand Up @@ -1336,17 +1355,18 @@ async def list_posts(
case when $1::text is not null then page.search_priority end asc,
case
when $1::text is not null and page.search_priority = 1
then array_position($5::uuid[], page.post_id)
then array_position($6::uuid[], page.post_id)
end asc,
case when $8::text = 'title' then lower(coalesce(page.post_title, '')) end asc,
case when $8::text = 'oldest' then page.created_at end asc,
case when $8::text in ('newest', 'title') then page.created_at end desc,
case when $9::text = 'title' then lower(coalesce(page.post_title, '')) end asc,
case when $9::text = 'oldest' then page.created_at end asc,
case when $9::text in ('newest', 'title') then page.created_at end desc,
page.post_id desc
""",
search_term,
list(account.corporate_entity_ids),
[code.strip() for code in voc_type if code.strip()] if voc_type else None,
visibility.strip() if visibility and visibility.strip() else None,
iso_week,
body_search_ids,
offset,
limit,
Expand All @@ -1362,6 +1382,7 @@ async def list_posts(
"offset": offset,
"voc_type_options": voc_type_options,
"visibility_options": visibility_options,
"iso_week_options": iso_week_options,
}


Expand Down
18 changes: 17 additions & 1 deletion backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,12 @@ def _insert_post(
"Ada West at Test Corp followed up with Priya Nair at Northridge Grid about the delayed shipment. "
"The weather in Gwangju was irrelevant.",
)
other_private_post_id = _insert_post("Other-corp private post", other_corp_id, "private")
other_private_post_id = _insert_post(
"Other-corp private post",
other_corp_id,
"private",
created_at="2026-02-03T12:00:00Z",
)
late_own_private_post_id = _insert_post(
"Late own-corp private post",
own_corp_id,
Expand Down Expand Up @@ -1371,6 +1376,8 @@ def test_post_list_includes_public_and_own_corp_but_excludes_other_corp(client,
assert {option["code"] for option in payload["voc_type_options"]} == {"voc"}
assert {option["code"] for option in payload["visibility_options"]} == {"public", "private"}
assert next(option for option in payload["visibility_options"] if option["code"] == "public")["label"] == "Public"
assert set(payload["iso_week_options"]) == {"2026-W02", "2026-W04"}
assert "2026-W06" not in payload["iso_week_options"]


def test_post_list_supports_bounded_offset_pages(client, demo_analyst_token, seeded_db) -> None:
Expand All @@ -1390,6 +1397,15 @@ def test_post_list_supports_bounded_offset_pages(client, demo_analyst_token, see
assert title_sorted.status_code == 200, title_sorted.text
assert title_sorted.json()["posts"][0]["post_title"] == "Edited own-corp private post"

week_filtered = client.get(
"/api/posts?iso_week=2026-W04",
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert week_filtered.status_code == 200, week_filtered.text
assert [post["post_title"] for post in week_filtered.json()["posts"]] == [
"Late own-corp private post"
]

invalid_sort = client.get(
"/api/posts?sort=unsupported",
headers={"Authorization": f"Bearer {demo_analyst_token}"},
Expand Down
35 changes: 35 additions & 0 deletions docs/adr/0092-weekly-voc-iso-week-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ADR 0092: Weekly VOC is an ISO-8601 week list filter

- Status: Accepted
- Date: 2026-08-19

## Context

Board already exposes checkbox VOC-type filters (ADR 0060). Buyers still need
one named control that shows this week's Voice of Customer posts without
inventing a measurement or collapsing other VOC types into a guessed default.
PR #259 stacked a `<select>` week filter on a stale Board that used a single
string type filter; current Board keeps `typeFilter` as `string[]`.

## Decision

Weekly VOC is a Board list filter, not a report and not ADR 0051 named hints.

- The **Weekly VOC** control sets VOC type to `voc` only and selects the
latest ISO-8601 week (`YYYY-Www`) present among authorized Voice of
Customer posts.
- Week membership uses the UTC date of `created_at`. Thursday decides the
ISO week-year (ISO 8601 week date).
- An explicit ISO-week `<select>` remains available beside the named
control. Reset filters returns both the checkboxes and the week to All.
- The Board names the next action: Voice of Customer posts for that week
are current; open a post to read Event Lineage.
- No TEPP theta is invented. No cutoff body is invented (ADR 0016).

## Consequences

- Weekly VOC composes with the existing checkbox VOC vocabulary instead of
replacing it.
- Posts whose `created_at` cannot be parsed contribute no week and cannot
be selected by this filter.
- Home-list and Customer-master opens are unchanged until ADR 0093.
33 changes: 33 additions & 0 deletions docs/adr/0093-weekly-voc-open-focuses-event-lineage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ADR 0093: Opening a Weekly VOC post focuses Event Lineage

- Status: Accepted
- Date: 2026-08-19

## Context

ADR 0092 names Weekly VOC as an ISO-week list filter and tells the buyer to
open a post to read Event Lineage. Report-member opens already focus the
popup Event Lineage heading. A home-list open must not steal that focus or
add that next-action copy.

## Decision

Opening a Board post while Weekly VOC is active (`voc` only and a concrete
ISO week) is a `fromWeeklyVoc` open. That open reuses the existing Event
Lineage focus path used by report-member opens:

- The popup Event Lineage heading takes focus.
- The popup names the opened post as current in Event Lineage and tells
the buyer to read Keyman and evaluation next.

A Board open from the unfiltered home list, a reset filter list, or any
path that did not set `fromWeeklyVoc` does not focus Event Lineage and
does not add that copy. Closing the popup clears the Weekly VOC open flag.

No TEPP theta is invented. No cutoff body is invented (ADR 0016).

## Consequences

- Weekly VOC and report-member opens share one focus contract.
- Changing VOC checkboxes or the ISO week so Weekly VOC is no longer
active makes the next Board open a home-list open.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ADR 0031: Preserve authorized internal evidence for relation verification
# ADR 0094: Preserve authorized internal evidence for relation verification

- Status: Accepted
- Date: 2026-08-18
Expand Down
2 changes: 1 addition & 1 deletion docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Product, technical, and gap baseline

**Snapshot:** 2026-08-20 17:08 (Asia/Seoul)
**Protected-main baseline:** `origin/main`, product version `2.12.5`
**Protected-main baseline:** `origin/main`, product version `2.12.5`
**Audited PR head:** #258 at `bf599aca` (integrated timeout boundary, fixture fix, worker repair, and SQL review evidence)
**Active PR update:** ADR 0101 and the enrichment-timeout changes are pushed to
PR #258; protected-main runtime evidence remains pending.
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "2.12.6",
"version": "2.13.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@
min-height: 0;
}

.board-weekly-voc[aria-pressed="true"] {
border-color: var(--text);
font-weight: 700;
}

.board-next-action {
margin: 0 0 1rem;
}

.board-controls input,
.board-controls select {
min-height: var(--size-control-min);
Expand Down
Loading