Skip to content
Open
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
23 changes: 23 additions & 0 deletions docs/arxiv-discovery-native-transition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Arxiv Discovery native transition

Arxiv Discovery moved from a Python CLI and local Flask interface to a standalone macOS application on 2026-07-15. The source repository is `Gnaroshi/Arxiv-newest-paper-crawler`. The approved native capture baseline is commit `1f87f235e2bef334980c19e019c016bb2a60ef0c`, and the installed delivery head is `48de418`.

## Product boundary

- SwiftUI owns recent-paper discovery, subject filters, search, detail review, saved metadata, explicit PDF actions, optional Korean translation, and preferences.
- The app reads public arXiv metadata directly and stores local state under Application Support.
- The Gemini API key stays in Keychain. Translation sends a selected public title and abstract only after an explicit action.
- The Python CLIs remain compatibility paths. The schema-v1 safe discovery/export provider is retained for scripts, while legacy `serve` and `all` entry points open the native app instead of starting a local web server.
- Studio integration is read-only: availability, launch, safe counts, and freshness may be exposed, while abstracts, credentials, translation, downloads, and saved-paper ownership remain inside Arxiv Discovery.

## Distribution and verification

The native delivery uses bundle identifier `dev.gnaroshi.ArxivDiscovery`, version `0.3.0`, and build `26`. A Developer ID signed build was installed at `/Applications/Arxiv Discovery.app`, verified with `codesign`, indexed by Spotlight and LaunchServices, launched from the installed bundle, and checked against live public arXiv metadata. The repository also verifies Python compatibility and provider tests, lint, Swift debug and release builds, core parsing and repository checks, manifest identity, entitlements, provenance, and the approved app-icon hash.

## Website evidence

The public project page uses the owner-approved 1946 × 1480 native window capture from commit `1f87f235e2be`. Showcase mode renders three deterministic paper candidates, includes a visible disclosure, and disables network requests, translation, and downloads. The previous Flask screenshots are retired historical records and are not published as evidence for the native product.

## Compatibility and rollback

Existing public project routes and the repository link remain unchanged. Legacy crawler JSON can be imported into the app, and the Python `process` command remains available. If the native source baseline must be rolled back, revert the application repository first, then restore the website fact and only the approved media whose source commit exactly matches that restored baseline.
4 changes: 3 additions & 1 deletion docs/project-screenshot-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ The application pages remain evidence-gated. Captures are stored outside `public
| --- | --- | --- |
| `project-gnaroshi-studio-review` | Actual Managed Apps overview with external apps unavailable or needing setup | Reviewable; no private workspace records are visible |
| `project-paperflow-review` | Actual PaperFlow organization screen | Reviewable; shows the dry-run and explicit apply boundary without library records |
| `project-arxiv-discovery-review` | Actual Flask UI populated by a no-download query of public arXiv metadata | Reviewable; current public paper metadata only |
| `project-arxiv-discovery-review` | Native SwiftUI window with three deterministic paper candidates and all network actions disabled | Owner-approved on 2026-07-15; publish only from the matching source manifest and commit |
| `project-tr-gpu-monitor-review` | Actual SSH prerequisite screen | Rejected for production because it is not a sanitized monitoring summary |
| `project-runshelf-review` | Actual app rendering checked-in sample records | Rejected for production because sample metrics are not research evidence |
| `project-contentdeck-review` | Actual Electron player in its empty prerequisite state | Rejected for production because it does not show supported playback, subtitles, or a loop |

No capture contains a private repository URL, branch, commit, token, credential, personal paper record, or private host alias. Production exports must be regenerated only from owner-approved candidates, and the three rejected states require replacement evidence rather than cosmetic editing.

Arxiv Discovery moved from the Flask interface to a native SwiftUI app on 2026-07-15. The owner approved the 1946 × 1480 native capture tied to source commit `1f87f235e2bef334980c19e019c016bb2a60ef0c`. It uses deterministic metadata, displays its showcase disclosure, and disables network, translation, and download actions. Do not reuse the retired Flask captures as native evidence; importing a later manifest still requires a fresh owner review.
8 changes: 5 additions & 3 deletions docs/project-showcase-staging.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@ Application screenshots are captured in each owning repository with an explicit
3. Run `npm run showcases:import`.
4. Start development and review `/dev/project-showcases/` in light/dark and desktop/mobile crops.
5. Record owner-approved captures in `src/data/approvedProjectShowcases.ts` with source commit, localized alt/caption, demo disclosure, and privacy review.
6. Run `npm run showcases:publish` to create responsive AVIF/WebP files without cropping the source.
6. Run `npm run showcases:publish` to create responsive AVIF/WebP files without cropping the source. When only one approved application source is locally available, scope regeneration with `SHOWCASE_APPLICATION_ID=<application-id> npm run showcases:publish`; the command fails if that application has no approved record.
7. Run `npm run media:check` and `npm run check:project-readiness`.

Raw and normalized captures remain under ignored `artifacts/project-showcases/`. Manifests reject absolute paths, secret-like values, missing source commits, undisclosed fixtures, and dimension mismatches.

## Approved set

The owner approved all 24 captures prepared on 2026-07-13: four appearance/workflow captures each for Gnaroshi Studio, PaperFlow, Arxiv Discovery, RunShelf, TR GPU Monitor, and ContentDeck. The website publishes dark workflow captures as scenario evidence and retains each light capture as reviewed appearance evidence.
The owner approved 24 captures prepared on 2026-07-13: four appearance/workflow captures each for Gnaroshi Studio, PaperFlow, the retired Flask Arxiv Discovery interface, RunShelf, TR GPU Monitor, and ContentDeck. On 2026-07-15, the four Flask captures were retired from the current register and replaced by one owner-approved native SwiftUI capture. The current register therefore contains 21 screenshots: 20 retained captures and the native Arxiv Discovery capture.

| Application | Source commit | Primary public capture |
| --- | --- | --- |
| Gnaroshi Studio | `e6115c90ab86` | `gnaroshi-studio-managed-apps` |
| PaperFlow | `7c7bd013d171` | `paperflow-plan-review` |
| Arxiv Discovery | `e48da6e1434a` | `arxiv-discovery-discovery-list` |
| Arxiv Discovery | `1f87f235e2be` | `arxiv-discovery-discovery-list` |
| RunShelf | `7657067cfeae` | `runshelf-run-list` |
| TR GPU Monitor | `3a2fa8173f3b` | `tr-gpu-monitor-host-overview` |
| ContentDeck | `5c6e2f94d1e` | `contentdeck-active-loop` |

Every approved capture uses deterministic demo data and includes a visible localized disclosure wherever it appears. Private repository URLs, local filesystem paths, host credentials, real library records, raw experiment paths, and unpublished research data are excluded.

The current Arxiv Discovery record uses a 2x native window capture from source commit `1f87f235e2be`. Its showcase mode contains three deterministic candidates, visibly discloses that mode, and disables network requests, translation, and downloads. The previous Flask files from `e48da6e1434a` are historical only and must not be restored or relabeled as native evidence.
2 changes: 2 additions & 0 deletions docs/projects-content-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The three project kinds are `research`, `application`, and `infrastructure`. App

The index keeps selected work separate from applications. Application cards use the shared `applicationGroup` fact to distinguish research workflow, system utility, and learning tool roles without duplicating that classification in localized prose.

Application evidence is a discriminated pair. A current owner-approved capture uses a non-null `primaryShowcaseId` and a null `textOnlyExemption`. When an interface changes before a new capture is approved, the record instead uses a null `primaryShowcaseId` and an internal non-empty `textOnlyExemption`; the page omits stale media and renders the verified text workflow. The exemption is validation context and is never public copy.

## Localized stories

`src/data/projectStories.ts` owns prose only. English and Korean each provide `cardSummary`, `heroSummary`, `overview`, audience, primary use, key features, privacy, current state, limitations, milestones, scenario copy, and kind-specific detail copy.
Expand Down
2 changes: 1 addition & 1 deletion docs/projects-tech-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| gnaroshi.dev | Astro, TypeScript, GitHub Actions, Playwright | `package.json` and workflow files |
| Gnaroshi Studio | TypeScript, React, Rust, Tauri | workspace package manifests and Cargo manifest |
| PaperFlow | Python, Swift, SwiftUI, Zotero Local API | `pyproject.toml`, Swift package/source, repository code |
| Arxiv Discovery | Python, Flask, Jinja | `pyproject.toml`, Flask application, templates |
| Arxiv Discovery | Swift, SwiftUI, Python | `Package.swift`, Swift package/source, `pyproject.toml` |
| RunShelf | Python, Swift, SwiftUI | `pyproject.toml`, Swift package/source |
| TR GPU Monitor | Swift, SwiftUI, SQLite | Xcode source and SQLite persistence implementation |
| ContentDeck | TypeScript, React, Electron, Vite, Fastify | `package.json` |
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineConfig, devices } from "@playwright/test";

const port = 4324;
const port = Number(process.env.PLAYWRIGHT_PORT ?? 4324);

export default defineConfig({
testDir: "./tests/e2e",
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified public/media/approved/arxiv-discovery-discovery-list-1200.avif
Binary file not shown.
Binary file modified public/media/approved/arxiv-discovery-discovery-list-1200.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/media/approved/arxiv-discovery-discovery-list-640.avif
Binary file not shown.
Binary file modified public/media/approved/arxiv-discovery-discovery-list-640.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/media/approved/arxiv-discovery-discovery-list-960.avif
Binary file not shown.
Binary file modified public/media/approved/arxiv-discovery-discovery-list-960.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion scripts/check-project-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ for (const project of projectFacts) {
if (!integrationStatuses.has(project.studioIntegrationStatus)) failures.push(`${project.id}: invalid Studio integration status`);
if (!/^[0-9a-f]{40}$/.test(project.sourceCommit)) failures.push(`${project.id}: missing full source commit`);
if (!project.sourceRepository) failures.push(`${project.id}: source provenance missing`);
const primary = getApprovedProjectShowcase(project.primaryShowcaseId);
const primary = project.primaryShowcaseId ? getApprovedProjectShowcase(project.primaryShowcaseId) : undefined;
if (!primary && !project.textOnlyExemption) failures.push(`${project.id}: primary approved screenshot missing`);
if (primary && (primary.applicationId !== project.id || primary.sourceApplicationCommit !== project.sourceCommit)) failures.push(`${project.id}: primary screenshot provenance mismatch`);
if (project.studioIntegrationStatus === project.productStatus) failures.push(`${project.id}: product and Studio integration status are conflated`);
Expand Down
11 changes: 9 additions & 2 deletions scripts/publish-approved-project-showcases.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ if (!existsSync(configPath)) {

const config = JSON.parse(await readFile(configPath, "utf8"));
const sourcePaths = new Map(config.applications?.map((item) => [item.id, item.sourcePath]) ?? []);
const requestedApplicationId = process.env.SHOWCASE_APPLICATION_ID;
const selectedShowcases = requestedApplicationId
? approvedProjectShowcases.filter((asset) => asset.applicationId === requestedApplicationId)
: approvedProjectShowcases;
if (requestedApplicationId && selectedShowcases.length === 0) {
fail(`No owner-approved screenshots registered for ${requestedApplicationId}`);
}
await mkdir(outputRoot, { recursive: true });

for (const asset of approvedProjectShowcases) {
for (const asset of selectedShowcases) {
const configuredPath = sourcePaths.get(asset.applicationId);
if (!configuredPath || !isAbsolute(configuredPath)) fail(`${asset.applicationId}: sourcePath must be absolute`);
const sourceRoot = resolve(configuredPath);
Expand All @@ -46,4 +53,4 @@ for (const asset of approvedProjectShowcases) {
}
}

console.log(`[showcases:publish] published ${approvedProjectShowcases.length} owner-approved screenshots`);
console.log(`[showcases:publish] published ${selectedShowcases.length} owner-approved screenshots`);
6 changes: 3 additions & 3 deletions src/components/projects/ApplicationProjectDetail.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import ProjectDetailHeader from "./ProjectDetailHeader.astro";
import ProjectPrimaryMedia from "./ProjectPrimaryMedia.astro";
import ProjectScenario from "./ProjectScenario.astro";
import ProjectStoryFacts from "./ProjectStoryFacts.astro";
interface Props { project: ApplicationProjectFacts; story: ProjectStory; locale: Locale; media: ApprovedProjectShowcase; hasPublicRepository: boolean }
interface Props { project: ApplicationProjectFacts; story: ProjectStory; locale: Locale; media?: ApprovedProjectShowcase; hasPublicRepository: boolean }
const { project, story, locale, media, hasPublicRepository } = Astro.props;
const labels = getProjectUi(locale);
---
<div data-project-template="application">
<ProjectDetailHeader project={project} story={story} locale={locale}/>
<ProjectPrimaryMedia asset={media} locale={locale} caption={media.caption[locale]} disclosure={media.demoDisclosure[locale]}/>
{media && <ProjectPrimaryMedia asset={media} locale={locale} caption={media.caption[locale]} disclosure={media.demoDisclosure[locale]}/>}
<ProjectAtAGlance project={project} story={story} locale={locale}/>
<section class="prose-section"><h2>{labels.overview}</h2><p>{story.overview}</p></section>
<ProjectScenario projectId={project.id} scenario={story.scenario} locale={locale} withMedia excludeMediaId={project.primaryShowcaseId}/>
<ProjectScenario projectId={project.id} scenario={story.scenario} locale={locale} withMedia={Boolean(media)} excludeMediaId={media?.id}/>
<ProjectStoryFacts project={project} story={story} locale={locale}/>
<section class="technical-facts"><h2>{locale === "ko" ? "기술 사실" : "Technical facts"}</h2><dl>
<div><dt>{labels.source}</dt><dd>{hasPublicRepository ? project.sourceRepository : labels.privateSource}</dd></div><div><dt>{labels.sourceCommit}</dt><dd><code>{project.sourceCommit.slice(0,12)}</code></dd></div>
Expand Down
10 changes: 5 additions & 5 deletions src/components/projects/FeaturedApplicationCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import TechStackStrip from "./TechStackStrip.astro";
interface Props {
project: ApplicationProjectFacts;
story: ProjectStory;
media: ApprovedProjectShowcase;
media?: ApprovedProjectShowcase;
locale: Locale;
href: string;
detailsLabel: string;
Expand All @@ -24,14 +24,14 @@ const { project, story, media, locale, href, detailsLabel, stackLabel, lead = fa
---

<article
class:list={["featured-app", lead && "featured-app--lead", !lead && "featured-app--paired"]}
class:list={["featured-app", lead && "featured-app--lead", !lead && "featured-app--paired", !media && "featured-app--text-only"]}
data-project-id={project.id}
data-project-kind={project.kind}
style={`--source-media-ratio:${media.width}/${media.height}`}
style={media ? `--source-media-ratio:${media.width}/${media.height}` : undefined}
>
<a class="featured-app__media" data-card-part="media" href={href} tabindex="-1" aria-hidden="true">
{media && <a class="featured-app__media" data-card-part="media" href={href} tabindex="-1" aria-hidden="true">
<ResponsiveImage asset={media} locale={locale} sizes={lead ? "(min-width: 1100px) 42vw, 100vw" : "(min-width: 1100px) 38vw, 100vw"} />
</a>
</a>}
<div class="featured-app__copy">
<ProjectCardHeader project={project} locale={locale} title={story.title} href={href} />
<p class="project-meta" data-card-part="meta">
Expand Down
2 changes: 1 addition & 1 deletion src/components/projects/ProjectScenario.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const mediaByStep = new Map(media.map((asset) => [asset.stepId, asset]));
{asset && <figure><div class="scenario__media" style={`aspect-ratio:${asset.width}/${asset.height}`}><ResponsiveImage asset={asset} locale={locale} sizes="(min-width: 1024px) 820px, 100vw"/></div><figcaption>{asset.caption[locale]} <span>{asset.demoDisclosure[locale]}</span></figcaption></figure>}
</li>})}
</ol>
{!withMedia && <p class="scenario__disclosure">{scenario.demoDisclosure}</p>}
{media.length === 0 && <p class="scenario__disclosure">{scenario.demoDisclosure}</p>}
</section>
<style>
.scenario{display:grid;gap:var(--space-6);padding-block:var(--space-8);border-block:1px solid var(--color-border)}
Expand Down
Loading