Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,9 @@ unavailable, so that run is Failed rather than a fabricated score.
The home list is clickable: `GET /api/analysis-runs/{id}` fills a
labeled detail (cutoff, requested date, 12-character digest prefixes
with full digests on hover, counts, status history)
without exposing a DSN or raw record. Opening a cutoff title warns
that the live body may have changed after the run. Status history is detail-only
without exposing a DSN or raw record. Opening a cutoff title still
shows the live body; titles rewritten after the run are marked
updated after cutoff. Status history is detail-only
and uses lookup labels plus occurrence times; a failure event keeps
its machine `failure_code` rather than an invented caption. Failed
TEPP list rows add a next-action line (open the run, then connect the
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.d/0.87.0-analysis-run-live-write-clock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 0.87.0 Analysis-run live write clock

In-cutoff titles now say whether the live row was rewritten after the
run. Open Demo public post as the edited counter-example; Demo private
post still matches the January cutoff. Bodies stay live.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ 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).

## [0.87.0] - 2026-08-17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Current #74 tip 69c035b already published ## [0.87.0] for granted retention purge and Storybook tokens. Rebase onto that tip and move this write-clock entry to 0.87.1 so both buyer slices stay in the changelog. Merging this 0.87.0 block as-is will conflict and collapse two releases into one version.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Current #74 tip 69c035b already published ## [0.87.0] for granted retention purge and Storybook tokens. Rebase onto that tip and move this write-clock entry to 0.87.1 so both buyer slices stay in the changelog. Merging this 0.87.0 block as-is will conflict and collapse two releases into one version.


### Added

- Analysis-run detail now compares each in-cutoff title's live
`updated_at` with that run's knowledge cutoff. After `make seed`,
open the Demo Corp lineage run: Demo public post is marked
**Updated after cutoff**; Demo private post is not. Opening a
marked title still shows the live body -- cutoff body versioning
stays a later slice (ADR 0016). The list stays aggregates-only.
No TEPP theta is invented.

## [0.86.2] - 2026-08-16

### Fixed
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ transport. A failed lineage row retries reconstruction -- it does not
mention TEPP. A failed period-report row rebuilds the report. A
pending TEPP row does not claim a calibrated measurement.
Digest prefixes stay audible; hover a prefix to read the full digest.
Opening a cutoff title shows the live post -- compare it with the
cutoff before treating the body as reconstructed evidence (ADR 0016).
Opening a cutoff title shows the live post. Titles marked updated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

#148 already landed a pending-lineage sentence on #74 39ed6eb just above this block. This patch is against the pre-#148 text.

Next action: rebase onto 39ed6eb and keep both sentences — pending lineage says reconstruction has not started; titles marked updated after cutoff were rewritten after the run. Do not replace one with the other.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#74 69c035b already has two sentences this patch must not replace: a pending lineage row says reconstruction has not started yet, and the ADR 0020 retention-purge section above the seed block. Rebase onto that head and keep both, then keep this write-clock sentence. Do not drop the pending-lineage line while adding the mark.

after cutoff were rewritten after the run; compare those bodies
before treating them as reconstructed evidence (ADR 0016).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Keep this write-clock sentence, but rebase onto live #74 69c035b first. That head already has the ADR 0020 retention section and the pending-lineage next-action sentence. Merging this file as-is drops both.

`POST /api/analysis-runs` records Pending on an authorized
cutoff capture (ADR 0017) and does not reconstruct lineage.
46 changes: 39 additions & 7 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ def _iso(value: Any) -> str:
return value.isoformat() if hasattr(value, "isoformat") else str(value)


def _as_utc(value: datetime) -> datetime:
"""Treat a naive clock as UTC so cutoff comparison stays timezone-aware."""
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)


def live_write_after_cutoff(updated_at: datetime, knowledge_cutoff: datetime) -> bool:
"""True when the live row was rewritten after the run's analysis clock.

``created_at <= knowledge_cutoff`` admits the title. ``updated_at`` is
the live write clock (ADR 0016). Equal times stay in-cutoff evidence.
"""
return _as_utc(updated_at) > _as_utc(knowledge_cutoff)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Admission (created_at <= cutoff) and write clock (updated_at > cutoff) stay distinct. Equal times correctly stay in-cutoff. Naive values become UTC.

Next action after rebase onto 39ed6eb: keep all four created_at <= $n filters. Do not let the write-clock mark admit a late-created title. The existing API test (edited January title in, 2026-01-20 title out, no post_body) is the buyer proof.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

updated_at > knowledge_cutoff is the right predicate (equal clocks stay in-cutoff). It only matches a real rewrite if source_post.updated_at moves when post_title / post_body change. Add a write-clock trigger that sets updated_at = now() unless the statement already assigned updated_at, and cover three cases: explicit historical pin stays, a body-only update after cutoff marks the title, a body-only update at the create clock does not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

updated_at > knowledge_cutoff is the right predicate (equal clocks stay in-cutoff). It only matches a real rewrite if source_post.updated_at moves when post_title / post_body change. Add a write-clock trigger that sets updated_at = now() unless the statement already assigned updated_at, and cover three cases: explicit historical pin stays, a body-only update after cutoff marks the title, a body-only update at the create clock does not.



async def _counts_by_run(
conn: asyncpg.Connection,
run_ids: list[str],
Expand Down Expand Up @@ -258,15 +274,21 @@ async def fetch_visible_scope_posts(
scope_key: str | None,
affiliated_entity_ids: list[str],
knowledge_cutoff: Any,
) -> list[dict[str, str]]:
) -> list[dict[str, Any]]:
"""ABAC-visible post titles known at the run cutoff -- never a hidden body.

``knowledge_cutoff`` is the analysis clock (W3C Time / ISO 8601-1:2019;
ADR 0013/0016). A later live post must not appear inside an earlier run.
``updated_at`` is compared separately so the operator can see which
in-cutoff titles were rewritten after that clock. The live body is
still not returned.
"""
columns = (
"post_id, post_title, visibility_code, corporate_entity_id, updated_at"
)
if scope_kind_code == "analysis_scope_corporate_entity" and corporate_entity_id:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
f"select {columns} "
"from source_post where corporate_entity_id = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
Expand All @@ -275,7 +297,7 @@ async def fetch_visible_scope_posts(
)
elif scope_kind_code == "analysis_scope_process_unit" and process_unit_id:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
f"select {columns} "
"from source_post where process_unit_id = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
Expand All @@ -284,7 +306,7 @@ async def fetch_visible_scope_posts(
)
elif scope_kind_code == "analysis_scope_thread_group" and scope_key:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
f"select {columns} "
"from source_post where thread_group_key = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
Expand All @@ -293,20 +315,30 @@ async def fetch_visible_scope_posts(
)
elif scope_kind_code == "analysis_scope_all_visible":
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
f"select {columns} "
"from source_post where created_at <= $1 "
"order by created_at, post_title",
knowledge_cutoff,
)
else:
return []
affiliated = {str(entity_id) for entity_id in affiliated_entity_ids}
posts: list[dict[str, str]] = []
posts: list[dict[str, Any]] = []
for row in rows:
visible = row["visibility_code"] == "public" or str(row["corporate_entity_id"]) in affiliated
if not visible:
continue
posts.append({"post_id": str(row["post_id"]), "post_title": row["post_title"]})
updated_at = row["updated_at"]
posts.append(
{
"post_id": str(row["post_id"]),
"post_title": row["post_title"],
"updated_at": _iso(updated_at),
"live_after_cutoff": live_write_after_cutoff(
updated_at, knowledge_cutoff
),
}
)
return posts


Expand Down
30 changes: 27 additions & 3 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,21 @@ def _insert_post(
visibility_code: str,
body: str = "body",
created_at: str = "2026-01-10T12:00:00Z",
updated_at: str | None = None,
) -> str:
written_at = updated_at if updated_at is not None else created_at
cur.execute(
"insert into source_post (author_account_id, corporate_entity_id, post_title, post_body, voc_type_code, visibility_code, created_at) "
"values (%s, %s, %s, %s, 'voc', %s, %s) returning post_id",
(account_id, corporate_entity_id, title, body, visibility_code, created_at),
"insert into source_post (author_account_id, corporate_entity_id, post_title, post_body, voc_type_code, visibility_code, created_at, updated_at) "
"values (%s, %s, %s, %s, 'voc', %s, %s, %s) returning post_id",
(
account_id,
corporate_entity_id,
title,
body,
visibility_code,
created_at,
written_at,
),
)
return str(cur.fetchone()[0])

Expand All @@ -327,6 +337,14 @@ def _insert_post(
"A follow-up written after the January 2026 run cutoff.",
created_at="2026-01-20T12:00:00Z",
)
_insert_post(
"Edited own-corp private post",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

seeded_db now inserts this own-corp private title, so GET /api/posts will include it. test_post_list_includes_public_and_own_corp_but_excludes_other_corp still exact-sets three titles and will fail on this head. Add Edited own-corp private post to that set (and any other exact-set list that uses seeded_db) before the full suite is treated as green.

own_corp_id,
"private",
"A January post rewritten after the run cutoff.",
created_at="2026-01-10T12:00:00Z",
updated_at="2026-01-13T09:00:00Z",
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This own-corp private insert is visible on GET /api/posts. test_post_list_includes_public_and_own_corp_but_excludes_other_corp still exact-sets {"Public post", "Own-corp private post", "Late own-corp private post"} at line 612, so the shared seeded_db fixture will fail that test once the live stack is up. Same class as #110/#114. Add Edited own-corp private post to that set. Do not drop this row — it is the buyer proof that an in-cutoff title can still be live_after_cutoff.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This edited own-corp row is the buyer proof for live_after_cutoff, but test_post_list_includes_public_and_own_corp_but_excludes_other_corp still exact-sets {"Public post", "Own-corp private post", "Late own-corp private post"} at line 612. Keep the row and add the title to that set.


cur.execute(
"insert into cataloged_person (person_name, person_side_code) values "
Expand Down Expand Up @@ -492,8 +510,14 @@ def test_analysis_runs_are_labeled_aggregates_and_hide_other_scopes(
assert all("failure_code" not in event for event in history)
titles = {post["post_title"] for post in body["visible_posts"]}
assert "Own-corp private post" in titles
assert "Edited own-corp private post" in titles
assert "Late own-corp private post" not in titles
assert "Other-corp private post" not in titles
posts_by_title = {post["post_title"]: post for post in body["visible_posts"]}
assert posts_by_title["Own-corp private post"]["live_after_cutoff"] is False
assert posts_by_title["Edited own-corp private post"]["live_after_cutoff"] is True
assert posts_by_title["Edited own-corp private post"]["updated_at"].startswith("2026-01-13")
assert "post_body" not in posts_by_title["Edited own-corp private post"]
assert "postgresql://" not in str(body)
assert "visible_posts" not in visible

Expand Down
15 changes: 9 additions & 6 deletions docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ every scope branch (corporate entity, process unit, thread group, and
all-visible). ABAC visibility is applied after that temporal gate.
Click-through still opens the live post body -- post versioning is a
later slice -- but the run list itself must not advertise a post the
run was not allowed to know. The detail must say that next action
plainly: compare the opened body with this cutoff before treating it
as reconstructed evidence.
run was not allowed to know. Detail compares the live `updated_at`
write clock with `knowledge_cutoff` and marks titles rewritten after
the run. The next action is specific: only those marked titles need a
cutoff comparison before treating the live body as reconstructed
evidence.

Reproducibility digests on the same detail use a labeled group whose
accessible name does not replace the visible prefixes (W3C Accessible
Expand All @@ -44,11 +46,12 @@ run.
- After `make seed`, the Demo Corp lineage run lists Demo public post
and other in-cutoff Demo Corp titles. The later fixture account-review
post (2026-02-10) does not appear.
- Open the run, read the live-body warning, then open a listed post
and compare it with the cutoff date.
- Open the run: Demo public post is marked updated after cutoff
(`updated_at` 2026-01-13). Demo private post is not.
- Hover a digest prefix to read the full code or configuration digest
when you need to match the API payload.
- Post-body versioning at the cutoff remains future work.
- Post-body versioning at the cutoff remains future work. The write
clock is a projection, not a stored cutoff body.
- Thread-group *run list* visibility now uses the same cutoff
(ADR 0018). A later public post cannot surface a previously hidden
thread-group run.
Expand Down
2 changes: 1 addition & 1 deletion docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
| Source | Product implication | Implemented evidence |
|---|---|---|
| W3C PROV-DM and PROV-O | Preserve identifiable entities, activities, agents, generation/use, and derivation without flattening provenance into display-only edges. | `analysis_source_snapshot`, `analysis_run`, authenticated requester, append-only status events, immutable digests; later product bindings continue to use the separate `provenance_*` layer from ADR 0011. |
| W3C Time Ontology in OWL | Keep temporal concepts explicit and avoid collapsing distinct clocks. | Evidence availability and snapshot capture remain on `analysis_source_snapshot`; analysis knowledge cutoff and request time remain on `analysis_run`; status occurrence and database record time remain distinct. `GET /api/analysis-runs/{id}` visible posts apply `created_at <= knowledge_cutoff` (ADR 0016). Opening a listed title warns that the live body may have changed after that cutoff. |
| W3C Time Ontology in OWL | Keep temporal concepts explicit and avoid collapsing distinct clocks. | Evidence availability and snapshot capture remain on `analysis_source_snapshot`; analysis knowledge cutoff and request time remain on `analysis_run`; status occurrence and database record time remain distinct. `GET /api/analysis-runs/{id}` visible posts apply `created_at <= knowledge_cutoff` (ADR 0016). Detail compares live `updated_at` with that cutoff and marks titles rewritten after the run. |
| W3C Accessible Name and Description Computation 1.1 | Do not let `aria-label` replace visible text the operator must hear. | Analysis-run digest prefixes live in a labeled group; the prefixes remain the accessible contents and the full digest is on `title` for hover verification. |
| ISO 8601-1:2019 | Use unambiguous timestamp representation and timezone-aware persistence. | PostgreSQL `timestamptz` for availability, capture, cutoff, request, occurrence, and record clocks; tests use explicit `Z` offsets. |
| PostgreSQL 18 constraints and trigger contracts | Put integrity close to durable truth and use constraints for row shape while triggers enforce cross-row state and serialization. | Digest/check constraints, category allowlists, account-scoped uniqueness, shape constraints, immutable-row triggers, shared snapshot-row locking, and serialized status transitions. |
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": "0.86.2",
"version": "0.87.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
46 changes: 42 additions & 4 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ describe("App, authenticated", () => {
let nextTicketId = 1;
const events: { event_id: string; event_type: string; actor_account_id: string; summary: string }[] = [];
let nextEventId = 1;
const createdAnalysisRuns: Record<string, unknown>[] = [];

const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
Expand Down Expand Up @@ -301,7 +302,20 @@ describe("App, authenticated", () => {
count_value: 3,
},
],
visible_posts: [{ post_id: "post-1", post_title: "Public post" }],
visible_posts: [
{
post_id: "post-1",
post_title: "Public post",
updated_at: "2026-01-13T09:00:00Z",
live_after_cutoff: true,
},
{
post_id: "post-2",
post_title: "Private post",
updated_at: "2026-01-10T12:00:00Z",
live_after_cutoff: false,
},
],
code_revision_sha: "abcdef0123456789deadbeefcafebabe",
configuration_sha256:
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Expand Down Expand Up @@ -351,6 +365,19 @@ describe("App, authenticated", () => {
},
],
};
createdAnalysisRuns.push({
analysis_run_id: created.analysis_run_id,
run_kind_code: created.run_kind_code,
run_kind_label: created.run_kind_label,
scope_kind_code: created.scope_kind_code,
scope_kind_label: created.scope_kind_label,
scope_entity_name: created.scope_entity_name,
status_code: created.status_code,
status_label: created.status_label,
knowledge_cutoff: created.knowledge_cutoff,
requested_at: created.requested_at,
source_counts: created.source_counts,
});
return Promise.resolve(new Response(JSON.stringify(created), { status: 201 }));
}
if (url.endsWith("/api/analysis-runs")) {
Expand Down Expand Up @@ -431,6 +458,7 @@ describe("App, authenticated", () => {
},
]
: []),
...createdAnalysisRuns,
],
}),
);
Expand Down Expand Up @@ -1685,19 +1713,29 @@ describe("App, authenticated", () => {
expect(screen.getByRole("list", { name: "Posts known at this run cutoff" })).toBeInTheDocument();
expect(
screen.getByText(
"Opening a title shows the live post. Compare it with cutoff 2026-01-12 before you treat the body as reconstructed evidence — it may have changed after this run.",
"Opening a title shows the live post. Titles marked updated after cutoff were rewritten after 2026-01-12. Compare those bodies with this run before you treat them as reconstructed evidence.",
),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: "Open live post (may have changed after cutoff): Public post",
name: "Open live post (updated after cutoff): Public post",
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: "Open live post: Private post",
}),
).toBeInTheDocument();
const cutoffPosts = screen.getByRole("list", { name: "Posts known at this run cutoff" });
expect(cutoffPosts).toHaveTextContent("Updated after cutoff");
expect(screen.getByRole("button", { name: "Open live post: Private post" }).closest("li")).not.toHaveTextContent(
"Updated after cutoff",
);
expect(screen.queryByText(/postgresql:\/\//)).not.toBeInTheDocument();

await userEvent.click(
screen.getByRole("button", {
name: "Open live post (may have changed after cutoff): Public post",
name: "Open live post (updated after cutoff): Public post",
}),
);
await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
Expand Down
26 changes: 18 additions & 8 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1548,20 +1548,27 @@ function analysisRunDigestPrefix(digest: string): string {
/**
* Next action when a cutoff title opens the live post (ADR 0016).
*
* Post-body versioning is a later slice. Until then the operator must
* compare the opened body with this run's cutoff instead of treating
* today's text as reconstructed evidence.
* Post-body versioning is a later slice. Titles marked
* `live_after_cutoff` were rewritten after this run; others still
* match the write clock the run knew.
*/
function analysisRunLivePostWarning(cutoffIso: string): string {
const cutoffDate = cutoffIso.slice(0, 10);
return (
`Opening a title shows the live post. Compare it with cutoff ${cutoffDate} ` +
"before you treat the body as reconstructed evidence — it may have changed after this run."
`Opening a title shows the live post. Titles marked updated after cutoff ` +
`were rewritten after ${cutoffDate}. Compare those bodies with this run ` +
"before you treat them as reconstructed evidence."
);
}

function analysisRunLivePostButtonLabel(postTitle: string): string {
return `Open live post (may have changed after cutoff): ${postTitle}`;
function analysisRunLivePostButtonLabel(post: {
post_title: string;
live_after_cutoff?: boolean;
}): string {
if (post.live_after_cutoff) {
return `Open live post (updated after cutoff): ${post.post_title}`;
}
return `Open live post: ${post.post_title}`;
}

function AnalysisRunReproducibilityDigests({
Expand Down Expand Up @@ -1734,11 +1741,14 @@ function AnalysisRunsPanel({
<li key={post.post_id}>
<button
className="keyman-select"
aria-label={analysisRunLivePostButtonLabel(post.post_title)}
aria-label={analysisRunLivePostButtonLabel(post)}
onClick={() => onSelectPost(post.post_id)}
>
{post.post_title}
</button>
{post.live_after_cutoff && (
<span className="post-badge">Updated after cutoff</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The list mark disappears as soon as the operator opens the title. Pass updated_at and live_after_cutoff into that live-post popup and state the two clocks there (“written 2026-01-13, after cutoff 2026-01-12 — compare this body with the run before you treat it as reconstructed evidence”). After the #74 rebase, render this badge with the ADR 0020 tokens already used for repeated chips.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The list mark disappears as soon as the operator opens the title. Pass updated_at and live_after_cutoff into that live-post popup and state the two clocks there (“written 2026-01-13, after cutoff 2026-01-12 — compare this body with the run before you treat it as reconstructed evidence”). After the #74 rebase, render this badge with the ADR 0020 tokens already used for repeated chips.

)}
</li>
))}
</ul>
Expand Down
Loading