Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
ce4663c
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
03a1167
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
d25eeda
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
0aaf992
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
3e5bfca
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
8966294
ci: repair analysis-run DAG cutoff context
seonghobae Aug 19, 2026
943ab63
ci: run buyer-surface cutoff repair test-first
seonghobae Aug 19, 2026
8e5be3e
ci: cover direct and DAG cutoff navigation
seonghobae Aug 19, 2026
3f4c41e
ci: restore canonical tests workflow
seonghobae Aug 19, 2026
4b80c87
ci: remove temporary cutoff repair workflow
seonghobae Aug 19, 2026
c96fa28
ci: repair analysis-run DAG cutoff context
seonghobae Aug 19, 2026
c23b81a
ci: repair analysis-run cutoff navigation
seonghobae Aug 19, 2026
b78b8c5
chore: remove one-shot repair workflow
seonghobae Aug 19, 2026
be0a544
chore: remove duplicate cutoff repair workflow
seonghobae Aug 19, 2026
aad290b
ci: execute PR 264 cutoff repair
seonghobae Aug 19, 2026
8cf3491
ci: run PR 264 cutoff repair on branch push
seonghobae Aug 19, 2026
4e4dae5
ci: repair ontology-grounded semantic paths on PR 264
seonghobae Aug 19, 2026
1b73e66
ci: run cutoff repair through canonical tests workflow
seonghobae Aug 19, 2026
796af57
ci: stage deterministic PR 264 cutoff repair
seonghobae Aug 19, 2026
a409895
ci: execute deterministic cutoff repair
seonghobae Aug 19, 2026
821432a
fix(ui): preserve analysis-run cutoff across DAG navigation
github-actions[bot] Aug 19, 2026
c616a4a
ci: guard PR 264 against the unrelated ontology writer
seonghobae Aug 19, 2026
f74377f
ci: restore canonical PR 264 verification
seonghobae Aug 19, 2026
3e37f02
ci: add direct ontology semantic-path repair
seonghobae Aug 19, 2026
f010d9b
ci: remove completed cutoff repair helper
seonghobae Aug 19, 2026
6cf7f3b
ci: replace stale semantic repair workflow
seonghobae Aug 19, 2026
f68b8d7
ci: execute ontology semantic-path repair
seonghobae Aug 19, 2026
af9d25a
test: stage ontology semantic-path repair
seonghobae Aug 19, 2026
06a5b37
ci: run test-first ontology semantic-path repair
seonghobae Aug 19, 2026
8431998
ci: move semantic-path repair out of PR 264
seonghobae Aug 19, 2026
4e601f6
ci: remove relocated semantic-path repair helper
seonghobae Aug 19, 2026
757757b
ci: consolidate PR 264 Knowledge Graph semantic repair
seonghobae Aug 19, 2026
2234fc7
ci: keep semantic repair exact-head safe
seonghobae Aug 19, 2026
0664679
test: align semantic repair fixtures with production schema
seonghobae Aug 19, 2026
4c3e99a
chore: stage project-history RED-GREEN payload (1/4)
seonghobae Aug 20, 2026
7bb9a36
chore: stage project-history RED-GREEN payload (2/4)
seonghobae Aug 20, 2026
0814949
chore: stage project-history RED-GREEN payload (3/4)
seonghobae Aug 20, 2026
ad1d5fa
chore: stage project-history RED-GREEN payload (4/4)
seonghobae Aug 20, 2026
c75f37b
chore: run project-history RED-GREEN verification
seonghobae Aug 20, 2026
c910478
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
85db4e1
fix: reject impossible Weekly VOC dates
seonghobae Aug 20, 2026
8daa419
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
156a507
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
5267614
fix: keep Calendar scroll with Event Lineage
seonghobae Aug 20, 2026
dc00316
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
15c82c7
test: keep customer master router specific
seonghobae Aug 20, 2026
8fb0fea
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
7f36683
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
a731551
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
4d6494d
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
860d07b
ci: make project-history repair observable on the stacked PR
seonghobae Aug 20, 2026
176dc74
merge: restack project lifecycle on current PR 264
seonghobae Aug 20, 2026
219acfb
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
4fc6e7c
fix: reject impossible Weekly VOC dates
seonghobae Aug 20, 2026
ba3a62a
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
fda93f3
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
9aac2b7
fix: keep Calendar scroll with Event Lineage
seonghobae Aug 20, 2026
acb56ab
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
159627c
test: keep customer master router specific
seonghobae Aug 20, 2026
59963dc
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
d104a88
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
cd793b9
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
e85b0d2
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
fc1bcbc
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
31b0dea
fix: reject impossible Weekly VOC dates
seonghobae Aug 20, 2026
621e0e3
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
02e06e9
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
88c6ae5
fix: keep Calendar scroll with Event Lineage
seonghobae Aug 20, 2026
c52cdcc
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
41c0161
test: keep customer master router specific
seonghobae Aug 20, 2026
f5021b1
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
04c043e
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
338d8f8
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
7bd992d
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
dfcd442
merge: restack project lifecycle on latest PR 264
seonghobae Aug 20, 2026
1d3f87b
merge: restack project lifecycle on production PR 264 head
seonghobae Aug 20, 2026
287e630
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
e854c93
fix: reject impossible Weekly VOC dates
seonghobae Aug 20, 2026
a5c948d
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
f007b80
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
fc39af1
fix: keep Calendar scroll with Event Lineage
seonghobae Aug 20, 2026
527661b
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
2114cc5
test: keep customer master router specific
seonghobae Aug 20, 2026
b2340c6
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
f6a61ac
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
58d5eeb
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
e829ac9
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
d8bd836
merge: restack project lifecycle on exact PR 264 head
seonghobae Aug 20, 2026
6044965
chore: restack project history on current Event Lineage head
seonghobae Aug 20, 2026
3a1f025
ci: pin project history to the live parent head
seonghobae Aug 20, 2026
2c957e7
test(projects): define evidence-bound project history contract
seonghobae Aug 20, 2026
ec840ba
chore(projects): remove cancelled one-shot project-history workflow
seonghobae Aug 20, 2026
5477377
chore(projects): remove cancelled project-history payload part 1
seonghobae Aug 20, 2026
3760e72
chore(projects): remove cancelled project-history payload part 2
seonghobae Aug 20, 2026
fc7d138
test(ui): define accessible project-history timeline contract
seonghobae Aug 20, 2026
106bc00
feat: add evidence-honest Global Ask knowledge cutoff (v2.23.0)
seonghobae Aug 20, 2026
32084a4
test(projects): classify every visible VOC record
seonghobae Aug 20, 2026
ef4bd59
feat(projects): port evidence-bound history core for RED repair
seonghobae Aug 20, 2026
d1dcaca
fix: match Global Ask cutoff candidates on retained revisions
seonghobae Aug 20, 2026
aa76282
fix(projects): keep lifecycle projection evidence-bound
seonghobae Aug 20, 2026
105d098
fix(ui): keep project-history selection and tab semantics current
seonghobae Aug 20, 2026
4f2d4ad
fix(ui): make project-history evidence and time semantics explicit
seonghobae Aug 20, 2026
30182c1
merge: restack onto feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
4cae152
Merge commit 'b065fb80b05ef37c550c5983e4606825e64e0842' into HEAD
seonghobae Aug 20, 2026
95a8ac5
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
4e3e2f9
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
093b816
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
f6a02bc
Merge current event-lineage base into knowledge-cutoff feature
seonghobae Aug 20, 2026
76a8b92
fix: keep ADR numbers unique after restack
seonghobae Aug 20, 2026
7422282
Merge current event-lineage base into knowledge-cutoff feature
seonghobae Aug 20, 2026
bb009da
docs: assign unique ADR number to Valkey operation decision
seonghobae Aug 20, 2026
29d80b8
merge: propagate current protected Event Lineage base
seonghobae Aug 20, 2026
1a25993
merge: propagate current Event Lineage branch head
seonghobae Aug 20, 2026
8febf13
merge: retain concurrent project-history updates
seonghobae Aug 20, 2026
93537f8
merge: propagate current Event Lineage branch head
seonghobae Aug 20, 2026
59ccdf9
merge: propagate current semantic-source knowledge-cutoff base
seonghobae Aug 20, 2026
76af029
merge: propagate current semantic-source Event Lineage base
seonghobae Aug 20, 2026
4eaeb23
ci: export exact project history source for local verification
seonghobae Aug 20, 2026
b77bea4
ci: move source export off the saturated Linux queue
seonghobae Aug 20, 2026
654faa3
test(projects): expose truth and identity boundary gaps
seonghobae Aug 20, 2026
eac418a
fix(projects): preserve identity and evidence truth boundaries
seonghobae Aug 20, 2026
7c9e2b2
fix(projects): enforce exact identity and evidence truth at the read …
seonghobae Aug 20, 2026
13b869e
fix(projects): render observed and inferred responsibility evidence h…
seonghobae Aug 20, 2026
cbb959c
fix(projects): expose per-evidence truth in the timeline
seonghobae Aug 20, 2026
c7f49af
feat(projects): wire authorized history into buyer surfaces
seonghobae Aug 20, 2026
de07aad
test(projects): distinguish observed and inferred responsibility evid…
seonghobae Aug 20, 2026
ba1de00
docs(storybook): show project-history truth states
seonghobae Aug 20, 2026
9a4ca86
merge: retain concurrent project-history truth fixes
seonghobae Aug 20, 2026
6004a7a
fix(projects): keep timeline compatible with persisted evidence
seonghobae Aug 20, 2026
367c40a
Merge remote-tracking branch 'origin/pr-285-head' into codex/integrat…
seonghobae Aug 20, 2026
0a76b25
fix(projects): focus timeline on opened source post
seonghobae Aug 20, 2026
30dae74
fix(project-history): preserve exact identity and isolate matches
seonghobae Aug 20, 2026
5d0e412
docs: refresh project gap exact-head checkpoint
seonghobae Aug 20, 2026
d925f53
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
7203115
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
c264674
Merge remote-tracking branch 'origin/feat/project-lifecycle-history' …
seonghobae Aug 20, 2026
7a5fc4b
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
c92c5d1
chore: remove completed project history bootstrap
seonghobae Aug 20, 2026
cfc125c
chore: align project history release version
seonghobae Aug 20, 2026
9bfa181
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
68ca6cb
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
d51093c
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
0c8e8d0
fix: preserve project history evidence truth in buyer UI
seonghobae Aug 21, 2026
f754c36
Merge remote-tracking branch 'refs/remotes/origin/feat/event-lineage-…
seonghobae Aug 21, 2026
6a35ca8
fix: bound and explain project history loading
seonghobae Aug 21, 2026
f365821
Merge remote-tracking branch 'refs/remotes/origin/feat/project-histor…
seonghobae Aug 21, 2026
12f92e9
Merge remote-tracking branch 'refs/remotes/origin/feat/event-lineage-…
seonghobae Aug 21, 2026
ccaeaa1
test: align project history document clock copy
seonghobae Aug 21, 2026
7b43227
fix: scope ticket mutations to owning post
seonghobae Aug 21, 2026
df5c36e
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
7751925
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
326f7ea
chore: restack project history on current parent
seonghobae Aug 21, 2026
13a1f2e
chore: restack knowledge cutoff on current project history parent
seonghobae Aug 21, 2026
7512c0e
fix: keep incomplete cutoff events in global ask timeline
seonghobae Aug 21, 2026
bdef29a
fix: enforce cutoff and ticket authorization boundaries
seonghobae Aug 21, 2026
9eb0888
chore: restack knowledge cutoff on current parent
seonghobae Aug 21, 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 @@ -184,6 +184,11 @@ Do not invent a week, a theta, a cutoff body, a CalDAV event, or a customer.
next read (ADR 0100). Do not invent a week, a theta, a cutoff body,
a CalDAV event, a customer, or a cited post.

Ask Agent accepts an optional knowledge cutoff (ADR 0135). A dated
question uses retained revisions and never substitutes a live body. A
live query is never labeled as-of. Do not invent a cutoff body or a
TEPP theta.


## Tests

Expand Down
14 changes: 9 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,12 @@ rebuild), the post list with a named Weekly VOC ISO-8601 week filter
Calendar commitments use the same Event Lineage focus path (ADR 0094).
Customer master related posts use the same Event Lineage focus path
(ADR 0095). Ask Agent cited posts use the same Event Lineage focus path
(ADR 0096). A linked Event Lineage node opened from a focused popup
keeps those flags (ADR 0097) and then focuses Keyman as the named next
read (ADR 0100).
The full detail popup includes Korean
(ADR 0096). A linked Event Lineage node opened from a focused popup keeps
those flags (ADR 0097). Ask Agent accepts an optional knowledge cutoff
and uses retained `source_post_revision` bodies for that clock (ADR 0135);
a live query is never labeled as-of. The full detail popup includes Korean
and focuses Keyman as the named next read (ADR 0100). The full detail
popup includes Korean
Comment on lines +287 to +289

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Duplicated sentence in ARCHITECTURE.md

The rewritten Phase-4 frontend paragraph repeats "The full detail popup includes Korean" twice (ARCHITECTURE.md:287-289), a documentation editing artifact from splicing in the ADR 0135 sentence.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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 Expand Up @@ -371,7 +373,9 @@ close the one product-brief item with a schema table (`issue_ticket`)
but no implementation through Phase 4. Deliberately plain CRUD, not a
pluggable-LLM channel like `keyman_ingestion.py` -- ticket status is a
closed enum in `common_lookup_value`, and opening or updating a ticket
is a direct user action, not something extracted from text.
is a direct user action, not something extracted from text. Ticket writes
also require `post_admin` plus authorship or corporate affiliation with the
owning post; public visibility is read access only (ADR 0122).
`frontend/src/App.tsx`'s `IssueTicketPanel` is the popup's real
list/create/status-update UI for it. Status options show
`common_lookup_value` labels (`Open` / `In progress` / `Closed`)
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.d/2.18.0-project-history-truth-and-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Project history now labels the actual source-post or document time basis,
keeps evidence-free responsibility gaps unknown, shows a loading status while
history is fetched, and uses the Stylelint-compatible `currentcolor` token.
10 changes: 10 additions & 0 deletions CHANGELOG.d/2.23.0-global-ask-knowledge-cutoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 2.23.0 Global Ask knowledge cutoff

Ask Agent can name a dated question. Retrieval keeps only source posts
that existed by that clock and matches the covering
`source_post_revision` text. A missing historical body is an explicit
limitation. Live queries stay live-only.

The Buyer now renders each missing historical-body limitation in a partial
answer. Commitment-derived ticket creation also enforces owning-post write
authorization before calling contextual-orchestrator.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ 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.23.0] - 2026-08-20

### Added

- Ask Agent now accepts an optional knowledge cutoff. A dated question
matches retained source-post revisions from that clock and never
substitutes a live body or live rewrite text. Fully, partly, and
live-only answers are named separately. No TEPP theta is invented. No
as-of label is applied to a live query (ADR 0135 / ADR 0016 / ADR 0025).

### Fixed

- Partial cutoff answers now show which historical bodies were unavailable,
and commitment-derived ticket writes enforce the owning-post authorization
boundary before provider work.

## [2.18.0] - 2026-08-20

### Added

- Added a Buyer Project history destination and post-detail entry point for
bounded, authorized exact-project chronology. The release remains pending
protected-main review and Checks (ADR 0111).

## [2.19.0] - 2026-08-20

### Added
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ From a GNB-focused popup, open a linked Event Lineage node: Event Lineage
stays focused and names the new post as current (ADR 0097). A home-list
DAG walk does not. Do not invent a theta.

## Ask Agent knowledge cutoff (v2.23.0)

Open Ask Agent. Optionally set a knowledge cutoff. A dated question uses
retained source-post revisions from that clock. A live query stays
live-only and is never labeled as-of. A missing historical body is named
and the live rewrite is not used (ADR 0135). Do not invent a theta or a
cutoff body.

## GNB Event Lineage focuses Keyman (v2.19.0)

A GNB-origin popup (Weekly VOC, Calendar, Customer master, Ask Agent, or a
Expand Down
159 changes: 148 additions & 11 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import json
from contextlib import asynccontextmanager
from dataclasses import asdict
from datetime import datetime
from datetime import datetime, timezone
from typing import Any, Literal
from uuid import UUID

Expand Down Expand Up @@ -73,8 +73,12 @@
ChatSourceDocument,
ContextualOrchestratorPostChatClient,
NullPostChatClient,
ask_grounding_status,
ask_next_action,
cited_post_citations,
cited_post_evidence,
cited_post_summaries,
historical_body_limitations,
render_global_ask_context,
)
from lineageweave.post_content_normalization import normalize_post_body
Expand Down Expand Up @@ -187,6 +191,16 @@
require_summary_source_body,
)
from backend.app.post_eligibility import SOURCE_POST_ELIGIBILITY_SQL
from backend.app.project_history import (
PROJECT_HISTORY_DEFAULT_LIMIT,
PROJECT_HISTORY_MAXIMUM_LIMIT,
PROJECT_INDEX_DEFAULT_LIMIT,
PROJECT_INDEX_MAXIMUM_LIMIT,
ProjectHistoryNotFound,
fetch_project_history_index,
fetch_project_history_projection,
)
from lineageweave.project_history import normalize_project_key
from backend.app.demo_scope import (
fetch_demo_corporate_entity_ids,
has_real_source_context,
Expand Down Expand Up @@ -259,6 +273,22 @@ def _require_post_admin(account: CurrentAccount) -> None:
raise HTTPException(status.HTTP_403_FORBIDDEN, "account lacks the post_admin permission")


def _require_ticket_post_access(account: CurrentAccount, post: asyncpg.Record) -> None:
"""Require ticket mutation access to the owning post, not visibility alone.

``post_admin`` is necessary but intentionally not sufficient: a public post
can be read by every account, while ticket state is still a write to the
authoring account's corporate work area.
"""
is_author = str(post["author_account_id"]) == account.user_account_id
is_affiliated = str(post["corporate_entity_id"]) in account.corporate_entity_ids
if not (is_author or is_affiliated):
raise HTTPException(
status.HTTP_403_FORBIDDEN,
"account is not authorized to modify tickets on this post",
)
Comment on lines +276 to +289

@devin-ai-integration devin-ai-integration Bot Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Ticket mutation authorization correctly resolves owning post before enforcing

_require_ticket_post_access (main.py) reads author_account_id and corporate_entity_id, both of which _load_visible_post selects (main.py); account.corporate_entity_ids is a frozenset[str] and user_account_id is stringified in backend/app/auth.py:180,184, so the string comparisons are type-consistent. For PATCH the owning post is resolved via fetch_ticket_post_id and returns 404 before the access check, so unknown tickets do not leak authorization detail. This closes the ADR 0122 gap where public read access previously implied ticket write access; the new negative tests (test_patch_ticket_on_public_post_owned_by_other_account_is_forbidden) cover it.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

def _keyman_extraction_client():
"""Live orchestrator client when configured; otherwise the unavailable null."""
settings = load_settings()
Expand Down Expand Up @@ -2637,6 +2667,7 @@ class GlobalAskRequest(BaseModel):

question: str
session_id: str | None = None
knowledge_cutoff: str | None = None


def global_ask_timeline(sources: list[ChatSourceDocument]) -> list[dict[str, str | None]]:
Expand Down Expand Up @@ -2772,6 +2803,15 @@ async def ask_agent(
UUID(request.session_id)
except ValueError:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Global Ask session not found") from None
knowledge_cutoff = None
if request.knowledge_cutoff is not None and request.knowledge_cutoff.strip():
try:
knowledge_cutoff = parse_as_of_clock(request.knowledge_cutoff)
except ValueError as exc:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
"knowledge_cutoff must be an ISO-8601 timestamp",
) from exc
client = _post_chat_client()
if not client.available:
raise HTTPException(
Expand All @@ -2790,6 +2830,7 @@ async def ask_agent(
lambda row: _can_see_post(account, row),
account.corporate_entity_ids,
question=question,
knowledge_cutoff=knowledge_cutoff,
)
if conversation.compress_turns:
compressor = getattr(client, "compress_context", None)
Expand Down Expand Up @@ -2821,7 +2862,11 @@ async def ask_agent(
conversation.summary,
conversation.recent_turns,
)
if not sources:
grounding_status = ask_grounding_status(sources, knowledge_cutoff)
limitations = historical_body_limitations(sources)
cutoff_text = knowledge_cutoff.isoformat() if knowledge_cutoff is not None else None
llm_sources = [source for source in sources if not source.historical_body_unavailable]
if not llm_sources:
async with pool.acquire() as conn:
await persist_global_ask_turn(conn, conversation.session_id, question, "", ())
await publish_operation_event(
Expand All @@ -2834,17 +2879,24 @@ async def ask_agent(
"session_id": conversation.session_id,
"answer_text": "",
"cited_post_ids": [],
"cited_posts": [],
"source_post_ids": [],
"cited_posts": cited_post_citations(sources, [source.post_id for source in sources]),
"source_post_ids": [source.post_id for source in sources],
"cited_post_evidence": [],
"timeline": [],
"next_action": "No authorized source posts are available for this question.",
"timeline": global_ask_timeline(sources),
"knowledge_cutoff": cutoff_text,
"grounding_status": grounding_status,
"limitations": limitations,
"next_action": ask_next_action(
grounding_status,
has_sources=bool(sources),
has_retained_bodies=bool(llm_sources),
),
}
try:
answer = await asyncio.to_thread(
client.answer,
question,
sources,
llm_sources,
conversation_context=conversation_context,
)
except (HttpClientError, KeyError, OSError, RuntimeError, ValueError) as exc:
Expand Down Expand Up @@ -2876,10 +2928,22 @@ async def ask_agent(
"session_id": conversation.session_id,
"answer_text": answer.answer_text,
"cited_post_ids": cited_ids,
"cited_posts": cited_post_summaries(sources, cited_ids),
"cited_post_evidence": cited_post_evidence(sources, cited_ids),
"cited_posts": cited_post_citations(llm_sources, cited_ids),
"cited_post_evidence": cited_post_evidence(llm_sources, cited_ids),
# The timeline is the complete authorized retrieval boundary. A
# source without a retained cutoff body is still a real timeline
# event and must remain navigable, even though it is excluded from
# the LLM evidence bundle.
"source_post_ids": [source.post_id for source in sources],
"timeline": global_ask_timeline(sources),
"knowledge_cutoff": cutoff_text,
"grounding_status": grounding_status,
"limitations": limitations,
"next_action": ask_next_action(
grounding_status,
has_sources=True,
has_retained_bodies=bool(llm_sources),
),
}


Expand Down Expand Up @@ -2978,7 +3042,8 @@ async def create_post_ticket(
a ticket is a write action, same discipline as extract-keymen.
"""
_require_post_admin(account)
await _load_visible_post(post_id, account, pool)
post = await _load_visible_post(post_id, account, pool)
_require_ticket_post_access(account, post)
async with pool.acquire() as conn:
try:
ticket = await create_ticket(
Expand Down Expand Up @@ -3039,7 +3104,8 @@ async def patch_ticket(
post_id = await fetch_ticket_post_id(conn, issue_ticket_id)
if post_id is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "ticket not found")
await _load_visible_post(post_id, account, pool)
post = await _load_visible_post(post_id, account, pool)
_require_ticket_post_access(account, post)
async with pool.acquire() as conn:
try:
ticket = await update_ticket(
Expand Down Expand Up @@ -3100,6 +3166,7 @@ async def derive_post_commitment(
"""
_require_post_admin(account)
post = await _load_visible_post(post_id, account, pool)
_require_ticket_post_access(account, post)
post_metadata = build_post_llm_metadata(post_id, post)
with use_llm_metadata(post_metadata):
client = _commitment_extraction_client()
Expand Down Expand Up @@ -3371,6 +3438,76 @@ async def read_calendar(
}


@app.get("/api/project-history/projects")
async def read_project_history_projects(
limit: int = Query(PROJECT_INDEX_DEFAULT_LIMIT, ge=1, le=PROJECT_INDEX_MAXIMUM_LIMIT),
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""Return exact project identities available to the signed-in buyer."""

_require_post_read(account)
knowledge_cutoff = datetime.now(timezone.utc)
async with pool.acquire() as conn:
return await fetch_project_history_index(
conn,
knowledge_cutoff=knowledge_cutoff,
corporate_entity_ids=list(account.corporate_entity_ids),
limit=limit,
)


@app.get("/api/project-history")
async def read_project_history(
project_key: str = Query(..., min_length=1),
focus_post_id: str | None = Query(None),
knowledge_cutoff: str | None = Query(None),
limit: int = Query(PROJECT_HISTORY_DEFAULT_LIMIT, ge=1, le=PROJECT_HISTORY_MAXIMUM_LIMIT),
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""Return one exact, authorized project history for the Buyer timeline."""

_require_post_read(account)
try:
normalized_project_key = normalize_project_key(project_key)

@devin-ai-integration devin-ai-integration Bot Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Validated normalized project key is discarded

normalize_project_key(project_key) is called only to reject empty input, then the raw project_key is passed downstream where fetch_project_history_projection normalizes again. The local normalized_project_key is never used. Harmless (both normalizations are consistent), just dead code.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

except ValueError as exc:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
"project_key must contain a non-empty exact identity",
) from exc
if focus_post_id is not None:
try:
UUID(focus_post_id)
except ValueError as exc:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
"focus_post_id must be a UUID",
) from exc
if knowledge_cutoff is None:
cutoff = datetime.now(timezone.utc)
else:
try:
cutoff = parse_as_of_clock(knowledge_cutoff)
except ValueError as exc:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
"knowledge_cutoff must be an ISO-8601 timestamp",
) from exc
async with pool.acquire() as conn:
try:
return await fetch_project_history_projection(
conn,
project_key=project_key,
focus_post_id=focus_post_id,
knowledge_cutoff=cutoff,
corporate_entity_ids=list(account.corporate_entity_ids),
limit=limit,
)
except ProjectHistoryNotFound as exc:
raise HTTPException(status.HTTP_404_NOT_FOUND, "project history not found") from exc


@app.get("/api/rankings")
async def read_rankings(
account: CurrentAccount = Depends(get_current_account),
Expand Down
Loading