Skip to content
Merged
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Product surface:
**Git is the database.** The canonical data lives in this repository:

- `data/policies.json` — the curated policy register (only changed by reviewed commits; served through a filtered route)
- `data/developments.json` — the automated radar feed, served through a filtered route
- `data/developments.json` — the automated radar feed, combined at read time
with verified legacy announcements from the editorial chronology
- `public/data/meta.json` — public collection health metadata
- `data/dta-ai-policy-framework.json` — editorial visualization artifact gated by its related policy
- `data/timeline.json`, `agencies.json`, `commonwealth-agencies.json` —
Expand All @@ -29,7 +30,7 @@ Product surface:

The maintainer's server runs the collector daily, from its own checkout, over the official sources that reliably permit machine retrieval. Sources protected by browser challenges are kept in the same source catalogue but reviewed through the manual coverage ledger. Candidate pages from browser-only sources are retrieved through a self-hosted Firecrawl instance, falling back to headless Chromium when Firecrawl is unavailable. New items are classified by keyword heuristic by default, or by Claude, an Anthropic model, in batches when the collector's Claude classifier is enabled (it is enabled in production). Either classifier path caps stored confidence at 0.65, so an automated discovery never reads as more certain than an editor's review. Change detections on already-tracked records are different: they store a relevance score of 1 because the score there records certainty that a known instrument's page changed, not classifier confidence — those detections are still editor-gated before anything publishes. Detections are validated and committed. The site reads that data from disk and revalidates hourly; there is no runtime database.

High-confidence detections are staged in `data/source-reviews.json`; a reviewer uses the local stage → approve → publish workflow before they enter the register. Public register and timeline reads only expose verified records. The collector never writes to `policies.json` directly, and CI enforces that.
High-confidence detections are staged in `data/source-reviews.json`; a reviewer uses the local stage → approve → publish workflow before they enter the register. The public policy timeline exposes only verified lifecycle events linked to visible register records. Verified announcements and milestones belong in Developments; legacy examples still stored in `data/timeline.json` are projected there without duplicating their evidence. The collector never writes to `policies.json` directly, and CI enforces that.

## Stack

Expand Down
8 changes: 6 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ The boundaries are deliberately strict:
| Data | Owner | Publication rule |
|---|---|---|
| `data/policies.json` | Editorial register | Explicitly approved; public route applies verification and update-review filters |
| `data/developments.json` | Automated radar + editorial annotations | Public route hides dismissed items and leads linked to terminal rejected reviews |
| `data/developments.json` | Automated radar + editorial annotations | Public route hides dismissed items and leads linked to terminal rejected reviews; verified legacy announcements are projected from the editorial chronology |
| `data/dta-ai-policy-framework.json` | Editorial visualization artifact | Public page/route requires its related policy to remain publicly verified |
| `data/timeline.json` | Editorial chronology | May await review; public route emits only verified records |
| `data/timeline.json` | Editorial chronology | May await review; the public policy timeline emits only verified register-linked lifecycle events |
| `data/agencies.json`, `data/commonwealth-agencies.json` | Editorial directory | Public route removes unverified claims and narrative |
| `public/data/meta.json` | Collector/editorial operations | Records coverage and health, not just timestamps |
| `data/source-reviews.json` | Review workflow | Drafts only; never displayed as canonical facts |
Expand Down Expand Up @@ -136,6 +136,10 @@ The boundaries are deliberately strict:
a newer one.
- Public data-service reads also withhold unverified policies, unverified
timeline events, dismissed developments, and unverified agency narrative.
Policy timeline reads are further limited to introduced, amended, repealed,
or superseded events linked to a visible register policy. Verified legacy
announcements and milestones are projected into Developments and deduplicated
against existing development records.
Development `relatedPolicyId` values and agency policy associations are
projected only when their target policy is also public.
Supersession references are projected only when the successor policy is
Expand Down
4 changes: 3 additions & 1 deletion docs/trust-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ leaves the official-source allow-list. A timeout, bot challenge, or access
denial is recorded as a retrieval failure for manual follow-up rather than
treated as proof that the official source no longer exists.
Public register and timeline reads withhold non-verified records until that
state is resolved.
state is resolved. The public policy timeline also requires a visible register
link and a policy lifecycle event type. Verified announcements and milestones
surface in Developments instead.

For direct documents monitored by the collector, a changed content fingerprint
creates an update review linked to the existing policy. While that review is
Expand Down
17 changes: 3 additions & 14 deletions src/app/agencies/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
import type { Metadata } from 'next';
import { AgenciesBrowser } from '@/components/agencies-browser';
import { getCommonwealthAgencies } from '@/lib/data-service';
import { notFound } from 'next/navigation';

export const revalidate = 3600;

export const metadata: Metadata = {
title: 'Agency AI Transparency Statements - Policai',
description:
'Browse Australian Government agency AI transparency statements and public disclosures.',
};

export default async function AgenciesPage() {
const agencies = await getCommonwealthAgencies();
return <AgenciesBrowser agencies={agencies} />;
export default function RetiredAgenciesPage() {
notFound();
}
5 changes: 4 additions & 1 deletion src/app/api/timeline/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ describe('/api/timeline', () => {

const response = await GET(new Request('https://example.com/api/timeline?jurisdiction=federal'))

expect(getTimelineEvents).toHaveBeenCalledWith({ jurisdiction: 'federal' })
expect(getTimelineEvents).toHaveBeenCalledWith(
{ jurisdiction: 'federal' },
{ scope: 'policy-register' },
)
await expect(response.json()).resolves.toEqual({
data: events,
total: 1,
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/timeline/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const jurisdiction = searchParams.get('jurisdiction') || undefined;

const events = await getTimelineEvents({ jurisdiction });
const events = await getTimelineEvents(
{ jurisdiction },
{ scope: 'policy-register' },
);
return NextResponse.json({ data: events, total: events.length, success: true });
}
61 changes: 2 additions & 59 deletions src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,5 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { format } from 'date-fns';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { getAllPosts, getPostBySlug } from '@/lib/blog';
import type { Metadata } from 'next';

export function generateStaticParams() {
return getAllPosts().map((p) => ({ slug: p.slug }));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);

if (!post) {
return {};
}

return {
title: `${post.title} - Policai Blog`,
description: post.description,
};
}

export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPostBySlug(slug);

if (!post) {
notFound();
}

return (
<div className="container mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
<nav aria-label="Breadcrumb" className="mb-6 font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
<Link href="/blog" className="hover:text-primary">Blog</Link>
<span className="mx-2">/</span>
<span className="text-foreground">Post</span>
</nav>

<header className="mb-9 border-b border-border pb-7">
<h1 className="article-title">{post.title}</h1>
<p className="mt-4 font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
{format(new Date(post.date), 'dd MMMM yyyy')}
</p>
</header>

<article className="prose max-w-none prose-headings:font-display prose-headings:font-medium prose-pre:border prose-pre:border-border">
<MDXRemote source={post.content} />
</article>
</div>
);
export default function RetiredBlogPostPage() {
notFound();
}
45 changes: 3 additions & 42 deletions src/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,5 @@
import { getAllPosts } from '@/lib/blog';
import { format } from 'date-fns';
import Link from 'next/link';
import { ArrowRight } from 'lucide-react';
import { PageIntro } from '@/components/layout';
import { notFound } from 'next/navigation';

export const metadata = {
title: 'Blog - Policai',
description: 'Project updates and AI policy commentary from Policai.',
};

export default function BlogPage() {
const posts = getAllPosts();

return (
<div className="container mx-auto px-4 py-7 sm:px-6 lg:px-8">
<PageIntro
title="Research notes"
/>
{posts.length === 0 ? (
<p className="py-12 text-sm text-muted-foreground">No posts yet.</p>
) : (
<div className="max-w-5xl py-7">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group grid gap-3 border-b border-border py-6 transition-colors hover:bg-[var(--row-hover)] sm:grid-cols-[9rem_minmax(0,1fr)_auto] sm:px-3"
>
<time className="font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
{format(new Date(post.date), 'dd MMMM yyyy')}
</time>
<span>
<span className="block text-lg font-semibold leading-snug group-hover:text-primary">{post.title}</span>
<span className="mt-2 block text-sm leading-6 text-muted-foreground">{post.description}</span>
</span>
<ArrowRight className="h-5 w-5 self-center text-primary" />
</Link>
))}
</div>
)}
</div>
);
export default function RetiredBlogPage() {
notFound();
}
1 change: 1 addition & 0 deletions src/app/data/public-data-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('/data editorial JSON routes', () => {
await expect((await getTimelineJson()).json()).resolves.toEqual(events);
expect(getTimelineEvents).toHaveBeenCalledWith(undefined, {
includeGenerated: false,
scope: 'policy-register',
});
});
});
5 changes: 4 additions & 1 deletion src/app/data/timeline.json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const revalidate = 3600;

export async function GET() {
return NextResponse.json(
await getTimelineEvents(undefined, { includeGenerated: false }),
await getTimelineEvents(undefined, {
includeGenerated: false,
scope: 'policy-register',
}),
);
}
117 changes: 3 additions & 114 deletions src/app/framework/page.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,5 @@
import {
PolicyFrameworkMap,
type FrameworkData,
} from '@/components/visualizations/PolicyFrameworkMap';
import { getPolicyFrameworkArtifact } from '@/lib/data-service';
import { parseCalendarDateForDisplay } from '@/lib/format-policy-date';
import Link from 'next/link';
import { ExternalLink, FileText, Download } from 'lucide-react';
import { PageIntro } from '@/components/layout';
import { SourceState } from '@/components/policy-table';
import { notFound } from 'next/navigation';

export const revalidate = 3600;

export const metadata = {
title: 'Policy for the Responsible Use of AI in Government | Policai',
description: 'Interactive visualization of Australia\'s Policy for the Responsible Use of AI in Government',
};

export default async function FrameworkPage() {
const artifact = await getPolicyFrameworkArtifact();
if (!artifact) {
return (
<div className="container mx-auto px-4 py-7 sm:px-6 lg:px-8">
<PageIntro title="AI in government framework" />
<div className="border-l-2 border-[var(--caution)] bg-[var(--status-proposed-bg)]/30 px-4 py-3 text-sm">
<p className="font-medium">Framework temporarily unavailable</p>
<p className="mt-2 text-muted-foreground">
Policai is withholding this derived visualisation while its
source policy and framework data await fingerprinted editorial
re-verification. This prevents an older interpretation from
being presented as current.
</p>
<p className="mt-2 text-muted-foreground">
See the{' '}
<Link href="/methodology" className="text-primary hover:underline">
methodology and trust model
</Link>{' '}
for how records return to public view.
</p>
</div>
</div>
);
}
const frameworkData = artifact as unknown as FrameworkData;

return (
<div className="container mx-auto px-4 py-7 sm:px-6 lg:px-8">
<PageIntro title="AI in government framework" />

<div className="mb-8 flex flex-wrap items-center gap-1">
<a
href={frameworkData.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
<ExternalLink className="h-4 w-4" />
View official source
</a>
<Link
href={`/policies/${frameworkData.relatedPolicyId}`}
className="inline-flex min-h-11 items-center gap-2 px-3 text-sm font-medium hover:text-primary"
>
<FileText className="h-4 w-4" />
Register entry
</Link>
<a
href="https://www.digital.gov.au/sites/default/files/documents/2025-12/Policy%20for%20the%20responsible%20use%20of%20AI%20in%20Government%202.0_0.pdf"
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-2 px-3 text-sm font-medium hover:text-primary"
>
<Download className="h-4 w-4" />
Download PDF
</a>
</div>

<PolicyFrameworkMap data={frameworkData} />

<div className="mt-10 flex flex-wrap gap-x-10 gap-y-4 border-y border-[var(--rule-hair)] py-4">
<div>
<p className="page-eyebrow">Effective</p>
<p className="mt-1 text-sm">
{parseCalendarDateForDisplay(
frameworkData.effectiveDate,
).toLocaleDateString('en-AU', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
{frameworkData.lastUpdated && (
<div>
<p className="page-eyebrow">Page updated</p>
<p className="mt-1 text-sm">
{parseCalendarDateForDisplay(
frameworkData.lastUpdated,
).toLocaleDateString('en-AU', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
)}
<div>
<p className="page-eyebrow">Verification</p>
<p className="mt-1">
<SourceState verification={frameworkData.verification} />
</p>
</div>
</div>
</div>
);
export default function RetiredFrameworkPage() {
notFound();
}
22 changes: 3 additions & 19 deletions src/app/map/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
import type { Metadata } from 'next';
import { MapBrowser } from '@/components/map-browser';
import { getPolicies } from '@/lib/data-service';
import { notFound } from 'next/navigation';

export const revalidate = 3600;

export const metadata: Metadata = {
title: 'Australian AI Policy Map - Policai',
description:
'Explore verified and review-pending Australian AI policy records by jurisdiction.',
};

export default async function MapPage() {
const policies = await getPolicies();
return (
<>
<h1 className="sr-only">Australian AI policy map</h1>
<MapBrowser policiesData={policies} />
</>
);
export default function RetiredMapPage() {
notFound();
}
15 changes: 0 additions & 15 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${BASE_URL}/agencies`,
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${BASE_URL}/map`,
changeFrequency: 'weekly',
priority: 0.6,
},
{
url: `${BASE_URL}/framework`,
changeFrequency: 'monthly',
priority: 0.6,
},
{
url: `${BASE_URL}/timeline`,
changeFrequency: 'weekly',
Expand Down
Loading
Loading