diff --git a/.env.vercel.production b/.env.vercel.production
deleted file mode 100644
index 4f6a272..0000000
--- a/.env.vercel.production
+++ /dev/null
@@ -1,25 +0,0 @@
-# Created by Vercel CLI
-NX_DAEMON="false"
-TURBO_CACHE="remote:rw"
-TURBO_DOWNLOAD_LOCAL_ENABLED="true"
-TURBO_REMOTE_ONLY="true"
-TURBO_RUN_SUMMARY="true"
-VERCEL="1"
-VERCEL_ENV="production"
-VERCEL_GIT_COMMIT_AUTHOR_LOGIN=""
-VERCEL_GIT_COMMIT_AUTHOR_NAME=""
-VERCEL_GIT_COMMIT_MESSAGE=""
-VERCEL_GIT_COMMIT_REF=""
-VERCEL_GIT_COMMIT_SHA=""
-VERCEL_GIT_PREVIOUS_SHA=""
-VERCEL_GIT_PROVIDER=""
-VERCEL_GIT_PULL_REQUEST_ID=""
-VERCEL_GIT_REPO_ID=""
-VERCEL_GIT_REPO_OWNER=""
-VERCEL_GIT_REPO_SLUG=""
-VERCEL_OIDC_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1yay00MzAyZWMxYjY3MGY0OGE5OGFkNjFkYWRlNGEyM2JlNyJ9.eyJpc3MiOiJodHRwczovL29pZGMudmVyY2VsLmNvbS95dXp1cnV1MjlzLXByb2plY3RzIiwic3ViIjoib3duZXI6eXV6dXJ1dTI5cy1wcm9qZWN0czpwcm9qZWN0OmFwcGx5Z3VhcmQtcGg6ZW52aXJvbm1lbnQ6ZGV2ZWxvcG1lbnQiLCJzY29wZSI6Im93bmVyOnl1enVydXUyOXMtcHJvamVjdHM6cHJvamVjdDphcHBseWd1YXJkLXBoOmVudmlyb25tZW50OmRldmVsb3BtZW50IiwiYXVkIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3l1enVydXUyOXMtcHJvamVjdHMiLCJvd25lciI6Inl1enVydXUyOXMtcHJvamVjdHMiLCJvd25lcl9pZCI6InRlYW1fUkVENnZhZkdlTE1qU3hRUUtBYTcyQ3JqIiwicHJvamVjdCI6ImFwcGx5Z3VhcmQtcGgiLCJwcm9qZWN0X2lkIjoicHJqX3B5TENUQWZuSE41ZTY2eEpCaHJxREltekV2SWIiLCJlbnZpcm9ubWVudCI6ImRldmVsb3BtZW50IiwicGxhbiI6ImhvYmJ5IiwidXNlcl9pZCI6InFTQ0hOelhmalNlSmhkeEY3aVpsS3RrWSIsImNsaWVudF9pZCI6ImNsX0hZeU9QQk50Rk1mSGhhVW45TDRRUGZUWno2VFA0N2JwIiwibmJmIjoxNzg0Mzk5MDE3LCJpYXQiOjE3ODQzOTkwMTcsImV4cCI6MTc4NDQ0MjIxN30.lyj34RJpWcuvmh1KbiA1CWp5ZJ-8sHPG7rxaKeRIy9zgiUzLrnVO27-7RkFbn-dekQyVkdxiRaBVisFMQIYCCvkqChYDg125xhAcW4OqfH4wuaXE5rGwrUX10flTa4NyGI_CO3X6Rj8DR5eEkPUA8-tW2ArXWz9d_T_n2xuqCduCRlSiQDMFz8-w7ju0vae2Ng95pMo3R3jmGdplhN056ovOLSLEAwNScddKDkljjxi7_gNZhytYg76uBfzOTZEhU5ZuC9PZl2drfdNLMo9jZqQNyA_I0E7HcJaxU9E3NTaovWCXeJRBhQ-BgxdTbUT2YNjCDvX5kmh_Xrmez8MzhQ"
-VERCEL_TARGET_ENV="production"
-VERCEL_URL=""
-VITE_PAYPAL_CLIENT_ID=""
-VITE_SUPABASE_ANON_KEY=""
-VITE_SUPABASE_URL=""
diff --git a/.env.vercel.production.2 b/.env.vercel.production.2
deleted file mode 100644
index 4b65c3e..0000000
--- a/.env.vercel.production.2
+++ /dev/null
@@ -1,25 +0,0 @@
-# Created by Vercel CLI
-NX_DAEMON="false"
-TURBO_CACHE="remote:rw"
-TURBO_DOWNLOAD_LOCAL_ENABLED="true"
-TURBO_REMOTE_ONLY="true"
-TURBO_RUN_SUMMARY="true"
-VERCEL="1"
-VERCEL_ENV="production"
-VERCEL_GIT_COMMIT_AUTHOR_LOGIN=""
-VERCEL_GIT_COMMIT_AUTHOR_NAME=""
-VERCEL_GIT_COMMIT_MESSAGE=""
-VERCEL_GIT_COMMIT_REF=""
-VERCEL_GIT_COMMIT_SHA=""
-VERCEL_GIT_PREVIOUS_SHA=""
-VERCEL_GIT_PROVIDER=""
-VERCEL_GIT_PULL_REQUEST_ID=""
-VERCEL_GIT_REPO_ID=""
-VERCEL_GIT_REPO_OWNER=""
-VERCEL_GIT_REPO_SLUG=""
-VERCEL_OIDC_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1yay00MzAyZWMxYjY3MGY0OGE5OGFkNjFkYWRlNGEyM2JlNyJ9.eyJpc3MiOiJodHRwczovL29pZGMudmVyY2VsLmNvbS95dXp1cnV1MjlzLXByb2plY3RzIiwic3ViIjoib3duZXI6eXV6dXJ1dTI5cy1wcm9qZWN0czpwcm9qZWN0OmFwcGx5Z3VhcmQtcGg6ZW52aXJvbm1lbnQ6ZGV2ZWxvcG1lbnQiLCJzY29wZSI6Im93bmVyOnl1enVydXUyOXMtcHJvamVjdHM6cHJvamVjdDphcHBseWd1YXJkLXBoOmVudmlyb25tZW50OmRldmVsb3BtZW50IiwiYXVkIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3l1enVydXUyOXMtcHJvamVjdHMiLCJvd25lciI6Inl1enVydXUyOXMtcHJvamVjdHMiLCJvd25lcl9pZCI6InRlYW1fUkVENnZhZkdlTE1qU3hRUUtBYTcyQ3JqIiwicHJvamVjdCI6ImFwcGx5Z3VhcmQtcGgiLCJwcm9qZWN0X2lkIjoicHJqX3B5TENUQWZuSE41ZTY2eEpCaHJxREltekV2SWIiLCJlbnZpcm9ubWVudCI6ImRldmVsb3BtZW50IiwicGxhbiI6ImhvYmJ5IiwidXNlcl9pZCI6InFTQ0hOelhmalNlSmhkeEY3aVpsS3RrWSIsImNsaWVudF9pZCI6ImNsX0hZeU9QQk50Rk1mSGhhVW45TDRRUGZUWno2VFA0N2JwIiwibmJmIjoxNzg0Mzk5NzAxLCJpYXQiOjE3ODQzOTk3MDEsImV4cCI6MTc4NDQ0MjkwMX0.PVl4AG8zzJNuAPawahVsn_9nis3fbsv6BVOkMvMjMaWJhVEIdKxyyyfVObho2WSnh5LUa33Lqdi7el-cUN5mbaLhjaVXvWJOJF58p2jxQQ_U5osBECCqYQhtYFFY65-SpcAX-CWGZWySOnAuNDelT0RTOGCv2fEV7Ja0Rz56wLD5UaqWy-rcQ0D4XnexZrhm5NKSOUM48NKnjC6vunik2cGs-7pcPU4-PdjdBoXRj9ErLSe-1MkZEMu91ADlqlCihGRnjgOBrt9jlFgp-IQ82BPEY2F-a-YHQEVDeaMpHN44ePsdtG2S5cIdo3zkIrKt9e48f3nweEA-kXQW0dLGXA"
-VERCEL_TARGET_ENV="production"
-VERCEL_URL=""
-VITE_PAYPAL_CLIENT_ID=""
-VITE_SUPABASE_ANON_KEY=""
-VITE_SUPABASE_URL=""
diff --git a/.github/workflows/supabase.yml b/.github/workflows/supabase.yml
index daefe75..a12589c 100644
--- a/.github/workflows/supabase.yml
+++ b/.github/workflows/supabase.yml
@@ -1,4 +1,4 @@
-name: Deploy Edge Functions
+name: Deploy Edge Functions & Migrations
on:
push:
@@ -6,17 +6,60 @@ on:
- main
paths:
- 'supabase/functions/**'
+ - 'supabase/migrations/**'
+ - 'supabase/tests/**'
+ - 'supabase/config.toml'
+ - '.github/workflows/supabase.yml'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'supabase/functions/**'
+ - 'supabase/migrations/**'
+ - 'supabase/tests/**'
+ - 'supabase/config.toml'
+ - '.github/workflows/supabase.yml'
jobs:
+ validate:
+ name: Test & Build Validation
+ runs-on: ubuntu-latest
+ concurrency:
+ group: pr-validation-${{ github.ref }}
+ cancel-in-progress: true
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run unit tests
+ run: npm test
+
+ - name: Run build
+ run: npm run build
+
deploy:
+ name: Migrate Database & Deploy Edge Functions
+ needs: validate
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
+ concurrency:
+ group: production_deployment_pipeline
+ cancel-in-progress: false
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
+ SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
@@ -26,18 +69,27 @@ jobs:
- name: Install dependencies
run: npm ci
- - name: Run unit tests
- run: npm test
-
- uses: supabase/setup-cli@v1
with:
version: latest
- - name: Deploy create-checkout
- run: supabase functions deploy create-checkout --project-ref $PROJECT_ID
+ - name: Link Supabase Project
+ run: supabase link --project-ref $PROJECT_ID --password $SUPABASE_DB_PASSWORD
+
+ - name: Apply Database Migrations
+ run: supabase db push --password $SUPABASE_DB_PASSWORD
+
+ - name: Deploy ai-proxy
+ run: supabase functions deploy ai-proxy --project-ref $PROJECT_ID
+
+ - name: Deploy create-paypal-order
+ run: supabase functions deploy create-paypal-order --project-ref $PROJECT_ID
+
+ - name: Deploy capture-paypal-order
+ run: supabase functions deploy capture-paypal-order --project-ref $PROJECT_ID
- - name: Deploy paymongo-webhook
- run: supabase functions deploy paymongo-webhook --project-ref $PROJECT_ID --no-verify-jwt
+ - name: Deploy paypal-webhook
+ run: supabase functions deploy paypal-webhook --project-ref $PROJECT_ID --no-verify-jwt
- - name: Deploy cancel-subscription
- run: supabase functions deploy cancel-subscription --project-ref $PROJECT_ID
+ - name: Deploy download-message-pack
+ run: supabase functions deploy download-message-pack --project-ref $PROJECT_ID
diff --git a/.gitignore b/.gitignore
index 0f44777..9af4958 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,10 @@ supabase-go.exe
supabase.exe
supabase_cli.zip
supabase/.temp/
+
+# pre-reformat backup
+.env
+.env.local
+.env.*.local
+
+.vercel
diff --git a/.preview-hero.png b/.preview-hero.png
new file mode 100644
index 0000000..c03a51f
Binary files /dev/null and b/.preview-hero.png differ
diff --git a/.preview-hero2.png b/.preview-hero2.png
new file mode 100644
index 0000000..572cf29
Binary files /dev/null and b/.preview-hero2.png differ
diff --git a/.preview-narrative.png b/.preview-narrative.png
new file mode 100644
index 0000000..8612594
Binary files /dev/null and b/.preview-narrative.png differ
diff --git a/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/canvas.json b/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/canvas.json
new file mode 100644
index 0000000..5033c40
--- /dev/null
+++ b/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/canvas.json
@@ -0,0 +1,120 @@
+{
+ "schemaVersion": 1,
+ "summary": {
+ "evidenceMode": "session-limited",
+ "evidenceBoundary": {
+ "manifest": {
+ "schemaVersion": 2,
+ "sourceFingerprint": "c230073388d2d558",
+ "adapterVersion": "qoder-task-loop-source-v2",
+ "platform": "qoder",
+ "selection": {
+ "strategy": "all-eligible",
+ "eligibleCount": 0,
+ "analyzedCount": 0,
+ "confidence": "Low"
+ }
+ },
+ "deliveryEvidenceLevels": [],
+ "sourceGaps": []
+ },
+ "semanticFacets": {
+ "schemaVersion": 1,
+ "status": "supplementary",
+ "entries": [
+ {
+ "id": "session-insight:source-coverage",
+ "kind": "redacted-summary",
+ "episodeRef": null,
+ "status": "candidate",
+ "labels": [
+ "source-coverage",
+ "Low"
+ ],
+ "summary": "Analyzed 0 of 0 sessions; 0/5 enabled source roots exist. Use this as the current workspace evidence boundary for final insight cards.",
+ "evidenceRefs": [],
+ "modelVersion": "session-insights-v1"
+ },
+ {
+ "id": "session-insight:validation-behavior",
+ "kind": "redacted-summary",
+ "episodeRef": null,
+ "status": "candidate",
+ "labels": [
+ "validation-behavior",
+ "Low"
+ ],
+ "summary": "No validation command category was observed in the analyzed session sample. Inspect more sessions or add explicit validation guidance before claiming validation-after-edit behavior.",
+ "evidenceRefs": [],
+ "modelVersion": "session-insights-v1"
+ },
+ {
+ "id": "session-insight:post-edit-validation",
+ "kind": "rework-correction",
+ "episodeRef": null,
+ "status": "candidate",
+ "labels": [
+ "post-edit-validation",
+ "Low"
+ ],
+ "summary": "No edit event was observed in the analyzed sample. Inspect more sessions before making claims about edit or validation habits.",
+ "evidenceRefs": [],
+ "modelVersion": "session-insights-v1"
+ },
+ {
+ "id": "session-insight:execution-friction",
+ "kind": "friction-taxonomy",
+ "episodeRef": null,
+ "status": "candidate",
+ "labels": [
+ "execution-friction",
+ "Low"
+ ],
+ "summary": "No strong execution friction signal in the analyzed sample. Keep friction claims narrow unless additional failed commands, rejected actions, or warnings are inspected.",
+ "evidenceRefs": [],
+ "modelVersion": "session-insights-v1"
+ }
+ ]
+ },
+ "learningCapture": {
+ "schemaVersion": 1,
+ "state": "N/A",
+ "summary": "N/A — requires two comparable observation windows and an intervention comparison.",
+ "interventions": []
+ }
+ },
+ "dimensions": [
+ {
+ "id": "task-understanding"
+ },
+ {
+ "id": "controlled-execution"
+ },
+ {
+ "id": "change-validation"
+ },
+ {
+ "id": "reliable-delivery"
+ },
+ {
+ "id": "learning-capture"
+ }
+ ],
+ "findings": [
+ {
+ "id": "agent-instructions-missing"
+ },
+ {
+ "id": "no-frontend-ci-validation"
+ },
+ {
+ "id": "missing-runtime-pin"
+ },
+ {
+ "id": "no-integration-e2e-tests"
+ },
+ {
+ "id": "no-recovery-plan-for-payment-state"
+ }
+ ]
+}
diff --git a/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/findings.json b/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/findings.json
new file mode 100644
index 0000000..1d820ec
--- /dev/null
+++ b/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/findings.json
@@ -0,0 +1,199 @@
+{
+ "summary": {
+ "projectName": "ApplyGuard PH",
+ "locale": "en",
+ "modelId": "agent-work-loop-v4",
+ "reportContractVersion": 24,
+ "overview": "ApplyGuard PH has a clear public README, 10 unit-test files for pure-logic modules, and a Supabase CI pipeline, but lacks agent-oriented guidance, environment pinning, and delivery-side validation, making agent work heavily reliant on implicit knowledge and unchecked changes.",
+ "aiAgentPractice": {
+ "inspectedSurfaces": [
+ "Rules",
+ "Skills",
+ "Hooks"
+ ],
+ "coverageRows": [
+ {
+ "surface": "Rules",
+ "scopes": [
+ "Project"
+ ],
+ "count": 0,
+ "paths": []
+ },
+ {
+ "surface": "Skills",
+ "scopes": [
+ "Project"
+ ],
+ "count": 0,
+ "paths": []
+ },
+ {
+ "surface": "Hooks",
+ "scopes": [
+ "Project",
+ "Global"
+ ],
+ "count": 0,
+ "paths": []
+ }
+ ]
+ },
+ "suggestions": [
+ {
+ "id": "scaffold-agent-entrypoint",
+ "kind": "loop-candidate",
+ "title": "Create an agent entrypoint (AGENTS.md)",
+ "reason": "The project has no agent-oriented guidance; adding an entrypoint would let agents find the right context, risk areas, and test routes without manual discovery or human handoffs.",
+ "confidence": "Medium",
+ "owner": "Project maintainer",
+ "nextStep": "Write a short AGENTS.md (80-120 lines) that maps source directories, identifies high-risk areas (payments, data deletion, auth), and lists validation commands by scope.",
+ "validation": "Agent can open AGENTS.md and find the correct test command, deployment path, and payment-risk warning."
+ },
+ {
+ "id": "add-test-ci-gate",
+ "kind": "loop-candidate",
+ "title": "Add Vitest run to CI workflow",
+ "reason": "The Supabase CI deploys edge functions but never runs the frontend test suite; a test failure on main has no automated safety net.",
+ "confidence": "High",
+ "owner": "Project maintainer",
+ "nextStep": "Add a `npm test` step to .github/workflows/supabase.yml before deployment, or create a separate frontend CI workflow.",
+ "validation": "Pushing a failing test to a PR branch produces a non-zero exit and visible CI failure."
+ },
+ {
+ "id": "pin-node-version",
+ "kind": "try-existing",
+ "title": "Add .nvmrc for reproducible runtime",
+ "reason": "Netlify uses NODE_VERSION=20 in netlify.toml, but no project-level pin exists for local or CI environments, creating implicit version assumptions.",
+ "confidence": "High",
+ "owner": "Project maintainer",
+ "nextStep": "Create .nvmrc with the Node version that matches netlify.toml (20) and verify local dev still works.",
+ "validation": "`node --version` matches the pinned version after `nvm use` (or the agent reads .nvmrc before install commands)."
+ }
+ ],
+ "assignmentSummaries": [],
+ "dimensions": [
+ {
+ "id": "task-understanding",
+ "label": "Task Understanding",
+ "score": 45,
+ "summary": "Human-oriented README covers architecture well, but no agent entrypoint (AGENTS.md, CLAUDE.md) exists; risk areas and task routes are undocumented for agent readers.",
+ "findingRefs": [
+ "agent-instructions-missing"
+ ]
+ },
+ {
+ "id": "controlled-execution",
+ "label": "Controlled Execution",
+ "score": 60,
+ "summary": "Package.json scripts are clear and lockfile is present, but no .nvmrc, no devcontainer, and no state-reset or doctor commands; environment assumptions remain partly implicit.",
+ "findingRefs": [
+ "missing-runtime-pin"
+ ]
+ },
+ {
+ "id": "change-validation",
+ "label": "Change Validation",
+ "score": 52,
+ "summary": "10 well-structured unit-test files cover core logic; however, no fast/slow test separation, no integration or E2E tests, and CI does not run the test suite before deployment.",
+ "findingRefs": [
+ "no-frontend-ci-validation",
+ "no-integration-e2e-tests"
+ ]
+ },
+ {
+ "id": "reliable-delivery",
+ "label": "Reliable Delivery",
+ "score": 48,
+ "summary": "Supabase Edge Function CI enforces deployment, but frontend has no CI gate; payment verification has server-side validation but no observable rollback plan or approval path for agent-produced changes.",
+ "findingRefs": [
+ "no-recovery-plan-for-payment-state"
+ ]
+ },
+ {
+ "id": "learning-capture",
+ "label": "Learning Capture",
+ "score": 35,
+ "summary": "No .qoder directory, project skills, hooks, or memory configuration; the reviewed window is clean of candidates but also lacks any mechanism for lifecycle detection or reuse.",
+ "findingRefs": []
+ }
+ ]
+ },
+ "findings": [
+ {
+ "id": "agent-instructions-missing",
+ "title": "No agent entrypoint or scoped guidance for task routing",
+ "severity": "High",
+ "reason": "The project has no AGENTS.md, CLAUDE.md, or equivalent agent guidance. The README (148 lines) serves a human audience. An agent must infer project boundaries, risk areas (payment verification, data deletion, AI proxy keys), and validation routes by scanning the full repository, which increases the chance of incorrect scope, missing test requirements, or unsafe operations.",
+ "expectedOutput": [
+ "Add AGENTS.md with directory map, risk areas, validations-per-scope, and deployment notes.",
+ "Agent can find the correct test command, deployment path, and payment-risk warning from the entrypoint."
+ ],
+ "expectedArtifact": "AGENTS.md",
+ "aiFixPrompt": "/better-loop fix this issue\n\nAdd a project-level AGENTS.md that maps src/ and supabase/ directories, flags high-risk areas (payment verification, data deletion, auth), and lists validation commands by scope.",
+ "dimensionRefs": [
+ "task-understanding"
+ ]
+ },
+ {
+ "id": "no-frontend-ci-validation",
+ "title": "Frontend unit tests never run in CI",
+ "severity": "High",
+ "reason": "The only CI workflow (supabase.yml) deploys Supabase Edge Functions but does not run `npm test` for the 10 frontend unit-test files. A regression in core scoring, red-flag, or entitlement logic can reach main without automated detection.",
+ "expectedOutput": [
+ "Add test step (npm test) to CI workflow.",
+ "Pushing a failing test produces a non-zero exit and visible CI failure."
+ ],
+ "expectedArtifact": ".github/workflows/supabase.yml",
+ "aiFixPrompt": "/better-loop fix this issue\n\nAdd a `npm test` step before the deployment steps in .github/workflows/supabase.yml so that unit tests run on every push to the supabase/functions path.",
+ "dimensionRefs": [
+ "change-validation"
+ ]
+ },
+ {
+ "id": "missing-runtime-pin",
+ "title": "Node.js runtime version not pinned at project root",
+ "severity": "Medium",
+ "reason": "Netlify config pins NODE_VERSION=20 in netlify.toml, but there is no .nvmrc or .tool-versions at the project root. Package.json engines field is absent. This creates implicit, unverifiable runtime assumptions for local dev and CI environments.",
+ "expectedOutput": [
+ "Create .nvmrc with Node engine version matching netlify.toml (20.x).",
+ "Explicit runtime constraint for agent setup and CI."
+ ],
+ "expectedArtifact": ".nvmrc",
+ "aiFixPrompt": "/better-loop fix this issue\n\nCreate a .nvmrc file matching the Node version from netlify.toml (NODE_VERSION = \"20\").",
+ "dimensionRefs": [
+ "controlled-execution"
+ ]
+ },
+ {
+ "id": "no-integration-e2e-tests",
+ "title": "No integration or E2E test coverage for critical user flows",
+ "severity": "Medium",
+ "reason": "All 10 test files are unit tests targeting pure functions. The scan → score → verdict → save flow, cloud sync, payment flows, and SPA routing have no integration or E2E coverage. An agent changing React components, routing, or storage has no automated way to verify the end-to-end behavior still works.",
+ "expectedOutput": [
+ "Add integration test covering the end-to-end scan flow.",
+ "Agent can run a flow-level test to validate behavior after changes."
+ ],
+ "expectedArtifact": "Integration test file(s)",
+ "aiFixPrompt": "/better-loop fix this issue\n\nAdd a Playwright or Vitest integration test for the scan→score→verdict→save flow. Place it at src/lib/flow.test.js and verify it exercises the core pipeline.",
+ "dimensionRefs": [
+ "change-validation"
+ ]
+ },
+ {
+ "id": "no-recovery-plan-for-payment-state",
+ "title": "Payment and data-deletion recovery has no documented owner route",
+ "severity": "Medium",
+ "reason": "The project handles live PayPal payments and user data in Supabase. The README describes server-side verification and webhook fulfillment, but there is no documented rollback, recovery, or escalation plan for agent-facing payment operations, failed captures, or accidental data deletion. An agent has no owned route to follow on recovery.",
+ "expectedOutput": [
+ "Add recovery/rollback documentation covering payment capture failure, webhook reconciliation, and data restoration.",
+ "Agent can find the owned recovery route after a payment failure."
+ ],
+ "expectedArtifact": "Recovery documentation",
+ "aiFixPrompt": "/better-loop fix this issue\n\nAdd a recovery section either in a new AGENTS.md or docs/ directory that covers: (1) what to do on PayPal capture failure, (2) how to reconcile webhook delivery gaps, (3) how to restore user data from Supabase. Keep it actionable for an agent operator.",
+ "dimensionRefs": [
+ "reliable-delivery"
+ ]
+ }
+ ]
+}
diff --git a/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/report.canvas.tsx b/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/report.canvas.tsx
new file mode 100644
index 0000000..b2d1517
--- /dev/null
+++ b/.qoder/better-loop/2026-07-23/011632-applyguard-ph/011632-applyguard-ph/report.canvas.tsx
@@ -0,0 +1,1786 @@
+import {
+ AreaChart,
+ Button,
+ Callout,
+ Card,
+ CardBody,
+ CardHeader,
+ CollapsibleSection,
+ Dialog,
+ Divider,
+ Fluency,
+ Grid,
+ H1,
+ H2,
+ MetricsGrid,
+ Row,
+ RiskHeatmap,
+ SendToChatButton,
+ Stack,
+ Table,
+ Tag,
+ Text,
+} from "qoder/canvas";
+import hostReportData from "./findings.json";
+import canvasData from "./canvas.json";
+
+function mergeCanvasRows(hostRows, canvasRows) {
+ const detailById = new Map(
+ (Array.isArray(canvasRows) ? canvasRows : [])
+ .filter((row) => row && typeof row === "object" && typeof row.id === "string")
+ .map((row) => [row.id, row]),
+ );
+ return (Array.isArray(hostRows) ? hostRows : []).map((row) => ({ ...detailById.get(row?.id), ...row }));
+}
+
+function mergeCanvasObjects(host, detail) {
+ if (!host || typeof host !== "object" || Array.isArray(host)) return detail;
+ if (!detail || typeof detail !== "object" || Array.isArray(detail)) return host;
+ const merged = { ...host };
+ for (const [key, value] of Object.entries(detail)) {
+ merged[key] = value && typeof value === "object" && !Array.isArray(value)
+ ? mergeCanvasObjects(host[key], value)
+ : value;
+ }
+ return merged;
+}
+
+function mergeCanvasReport(host, detail) {
+ const summary = host?.summary ?? {};
+ if (!detail || typeof detail !== "object" || Array.isArray(detail)) return host;
+ return {
+ summary: {
+ ...(detail?.summary ?? {}),
+ ...summary,
+ atAGlance: mergeCanvasObjects(detail?.summary?.atAGlance, summary.atAGlance),
+ dimensions: mergeCanvasRows(summary.dimensions, detail?.dimensions),
+ },
+ findings: mergeCanvasRows(host?.findings, detail?.findings),
+ };
+}
+
+const report = mergeCanvasReport(hostReportData, canvasData);
+
+const pageStyle = { maxWidth: 960, margin: "0 auto", padding: 16, boxSizing: "border-box" };
+const taskLoopPageStyle = { ...pageStyle, padding: 20 };
+const taskLoopReaderCopyStyle = { maxWidth: 940 };
+
+const DIMENSION_SUMMARY_EXAMPLE = "Example: project guidance makes the main workflow clear, but ownership for cross-cutting changes is not documented.";
+
+function list(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function clampScore(value) {
+ const score = Number(value);
+ if (!Number.isFinite(score)) return 0;
+ return Math.max(0, Math.min(100, score));
+}
+
+function projectName() {
+ return report.summary?.projectName ?? "Qoder Harness Report";
+}
+
+function textValue(value) {
+ return typeof value === "string" ? value.trim() : "";
+}
+
+function openingStrengths() {
+ const explicit = list(report.summary?.strengths).map(textValue).filter(Boolean);
+ return explicit.length ? explicit.slice(0, 3) : ["Reviewed project signals are organized into dimensions and issue findings."];
+}
+
+function averageScore(dimensions) {
+ if (dimensions.length === 0) return 0;
+ return Math.round(dimensions.reduce((sum, row) => sum + clampScore(row.score), 0) / dimensions.length);
+}
+
+function scoreTone(score) {
+ if (score >= 70) return "success";
+ if (score >= 40) return "warning";
+ return "danger";
+}
+
+function stageStatus(score) {
+ if (score >= 70) return "high";
+ if (score >= 40) return "medium";
+ if (score > 0) return "low";
+ return "blocked";
+}
+
+function fluencyReason(row) {
+ return textValue(row?.summary) || taskLoopCopy(
+ "No reviewed score explanation is available for this dimension.",
+ "这个维度暂时没有经过复核的评分说明。",
+ );
+}
+
+function splitFluencyTooltipReason(value) {
+ let remaining = textValue(value).replace(/\s+/g, " ");
+ const chunks = [];
+ const limits = /[\u3400-\u9fff]/.test(remaining) ? [20, 20, 20, 20] : [34, 30, 30, 30];
+ for (const limit of limits) {
+ if (!remaining) break;
+ if (remaining.length <= limit) {
+ chunks.push(remaining);
+ remaining = "";
+ break;
+ }
+ let cut = remaining.lastIndexOf(" ", limit);
+ if (cut < Math.floor(limit * 0.55)) cut = limit;
+ chunks.push(remaining.slice(0, cut).trim());
+ remaining = remaining.slice(cut).trim();
+ }
+ if (remaining && chunks.length) {
+ chunks[chunks.length - 1] = `${chunks[chunks.length - 1].slice(0, 29)}…`;
+ }
+ return chunks;
+}
+
+function dimensionFluencyStages(dimensions) {
+ return dimensions.map((row) => {
+ const score = clampScore(row.score);
+ const usesGenericBand = row.id !== "learning-capture";
+ return {
+ id: row.id,
+ name: taskLoopDimensionLabel(row.id),
+ score,
+ ...(usesGenericBand ? { status: stageStatus(score), blocker: score <= 20 } : {}),
+ };
+ });
+}
+
+function dimensionFluencyTooltip(row) {
+ const [title, ...rows] = splitFluencyTooltipReason(fluencyReason(row));
+ return {
+ title,
+ rows: rows.map((value) => ({ value })),
+ };
+}
+
+function severityTone(value) {
+ if (value === "Critical" || value === "High") return "danger";
+ if (value === "Medium") return "warning";
+ if (value === "Low") return "success";
+ return "neutral";
+}
+
+function severityRank(value) {
+ if (value === "Critical") return 0;
+ if (value === "High") return 1;
+ if (value === "Medium") return 2;
+ if (value === "Low") return 3;
+ return 4;
+}
+
+function dimensionLabel(id, dimensions) {
+ const match = dimensions.find((row) => row.id === id);
+ return match?.label ?? id.replace(/-/g, " ");
+}
+
+function aiAgentPractice() {
+ return report.summary?.aiAgentPractice ?? {};
+}
+
+function practiceRows() {
+ const rows = aiAgentPractice().coverageRows;
+ return Array.isArray(rows) ? rows : [];
+}
+
+function inspectedSurfaces() {
+ const surfaces = aiAgentPractice().inspectedSurfaces;
+ return Array.isArray(surfaces) ? surfaces : [];
+}
+
+function visiblePracticePaths(value) {
+ return list(value).map(textValue).filter((candidate) => candidate
+ && !candidate.includes("SharedClientCache/projects/")
+ && !candidate.startsWith("/")
+ && !/^[A-Za-z]:[\\/]/.test(candidate)
+ && !candidate.split(/[\\/]/).includes(".."));
+}
+
+function practiceDescription(surface) {
+ const descriptions = {
+ Rules: ["Standing project guidance and task-routing instructions.", "项目常驻指引与任务路由说明。"],
+ Skills: ["Reusable agent workflows available to the project.", "项目可用的可复用 Agent 工作流。"],
+ "Custom Agents": ["Specialized agent profiles available for delegated work.", "可用于委派工作的专用 Agent 配置。"],
+ Hooks: ["Lifecycle automation around agent and delivery events.", "围绕 Agent 与交付事件的生命周期自动化。"],
+ MCP: ["External tools and resources exposed through MCP.", "通过 MCP 暴露的外部工具与资源。"],
+ Commands: ["Named command entry points for repeatable agent work.", "可重复 Agent 工作的命令入口。"],
+ Workflows: ["Reusable multi-step project workflows.", "可复用的多步骤项目工作流。"],
+ Plugins: ["Installed packages that contribute agent capabilities.", "提供 Agent 能力的已安装插件。"],
+ "Session Insights": ["Task-session evidence available for report analysis.", "可用于报告分析的任务会话证据。"],
+ Memories: ["Representative project or global Memory note files.", "项目级或全局 Memory 的代表性笔记文件。"],
+ };
+ const copy = descriptions[surface] ?? ["Recorded agent capability sources.", "已记录的 Agent 能力来源。"];
+ return taskLoopCopy(copy[0], copy[1]);
+}
+
+function TaskLoopPracticePaths({ paths }) {
+ if (paths.length === 0) return null;
+ const preview = paths.slice(0, 2);
+ const remaining = paths.slice(2);
+ return (
+ {projectName()}
+ {overview ? {taskLoopCopy("Agent Work Loop", "Agent 工作流")}
+