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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ expo-env.d.ts
# @end expo-cli
.env.production
.env.staging
.dual-graph/
136 changes: 136 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<!-- dgc-policy-v10 -->
# Dual-Graph Context Policy

This project uses a local dual-graph MCP server for efficient context retrieval.

## MANDATORY: Always follow this order

1. **Call `graph_continue` first** — before any file exploration, grep, or code reading.

2. **If `graph_continue` returns `needs_project=true`**: call `graph_scan` with the
current project directory (`pwd`). Do NOT ask the user.

3. **If `graph_continue` returns `skip=true`**: project has fewer than 5 files.
Do NOT do broad or recursive exploration. Read only specific files if their names
are mentioned, or ask the user what to work on.

4. **Read `recommended_files`** using `graph_read` — **one call per file**.
- `graph_read` accepts a single `file` parameter (string). Call it separately for each
recommended file. Do NOT pass an array or batch multiple files into one call.
- `recommended_files` may contain `file::symbol` entries (e.g. `src/auth.ts::handleLogin`).
Pass them verbatim to `graph_read(file: "src/auth.ts::handleLogin")` — it reads only
that symbol's lines, not the full file.
- Example: if `recommended_files` is `["src/auth.ts::handleLogin", "src/db.ts"]`,
call `graph_read(file: "src/auth.ts::handleLogin")` and `graph_read(file: "src/db.ts")`
as two separate calls (they can be parallel).

5. **Check `confidence` and obey the caps strictly:**
- `confidence=high` -> Stop. Do NOT grep or explore further.
- `confidence=medium` -> If recommended files are insufficient, call `fallback_rg`
at most `max_supplementary_greps` time(s) with specific terms, then `graph_read`
at most `max_supplementary_files` additional file(s). Then stop.
- `confidence=low` -> Call `fallback_rg` at most `max_supplementary_greps` time(s),
then `graph_read` at most `max_supplementary_files` file(s). Then stop.

## Token Usage

A `token-counter` MCP is available for tracking live token usage.

- To check how many tokens a large file or text will cost **before** reading it:
`count_tokens({text: "<content>"})`
- To log actual usage after a task completes (if the user asks):
`log_usage({input_tokens: <est>, output_tokens: <est>, description: "<task>"})`
- To show the user their running session cost:
`get_session_stats()`

Live dashboard URL is printed at startup next to "Token usage".

## Rules

- Do NOT use `rg`, `grep`, or bash file exploration before calling `graph_continue`.
- Do NOT do broad/recursive exploration at any confidence level.
- `max_supplementary_greps` and `max_supplementary_files` are hard caps - never exceed them.
- Do NOT dump full chat history.
- Do NOT call `graph_retrieve` more than once per turn.
- After edits, call `graph_register_edit` with the changed files. Use `file::symbol` notation (e.g. `src/auth.ts::handleLogin`) when the edit targets a specific function, class, or hook.

## Context Store

Whenever you make a decision, identify a task, note a next step, fact, or blocker during a conversation, append it to `.dual-graph/context-store.json`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten wording for readability.

“Whenever you make a decision” is slightly wordy; “Whenever you decide” reads cleaner in policy text.

🧰 Tools
🪛 LanguageTool

[style] ~59-~59: ‘make a decision’ might be wordy. Consider a shorter alternative.
Context: ...r hook. ## Context Store Whenever you make a decision, identify a task, note a next step, fac...

(EN_WORDINESS_PREMIUM_MAKE_A_DECISION)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 59, Replace the phrase "Whenever you make a decision" with
the tighter wording "Whenever you decide" in the sentence that currently reads
"Whenever you make a decision, identify a task, note a next step, fact, or
blocker during a conversation, append it to `.dual-graph/context-store.json`" so
the policy reads "Whenever you decide, identify a task, note a next step, fact,
or blocker during a conversation, append it to
`.dual-graph/context-store.json`."


**Entry format:**
```json
{"type": "decision|task|next|fact|blocker", "content": "one sentence max 15 words", "tags": ["topic"], "files": ["relevant/file.ts"], "date": "YYYY-MM-DD"}
```

**To append:** Read the file → add the new entry to the array → Write it back → call `graph_register_edit` on `.dual-graph/context-store.json`.

**Rules:**
- Only log things worth remembering across sessions (not every minor detail)
- `content` must be under 15 words
- `files` lists the files this decision/task relates to (can be empty)
- Log immediately when the item arises — not at session end

Comment on lines +59 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add explicit “no secrets/PII” guardrails to context-store logging.

This section mandates frequent logging but never forbids credentials, tokens, or user-identifying data. Add a hard rule to prevent compliance/privacy leaks.

🔐 Suggested policy patch
 ## Context Store

 Whenever you make a decision, identify a task, note a next step, fact, or blocker during a conversation, append it to `.dual-graph/context-store.json`.
@@
 **Rules:**
 - Only log things worth remembering across sessions (not every minor detail)
+- Never log secrets or sensitive data (API keys, tokens, passwords, PII, incident payloads)
 - `content` must be under 15 words
 - `files` lists the files this decision/task relates to (can be empty)
 - Log immediately when the item arises — not at session end
🧰 Tools
🪛 LanguageTool

[style] ~59-~59: ‘make a decision’ might be wordy. Consider a shorter alternative.
Context: ...r hook. ## Context Store Whenever you make a decision, identify a task, note a next step, fac...

(EN_WORDINESS_PREMIUM_MAKE_A_DECISION)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 59 - 73, Update the CLAUDE.md context-store logging
rules to explicitly forbid logging secrets/PII and require
validation/sanitization before appending to .dual-graph/context-store.json: add
a new guardrail paragraph stating no credentials, tokens, emails, phone numbers,
SSNs, or user-identifying data may be logged; require that the “To append”
workflow validate the entry (content length <15 words, allowed tags/files), run
a sanitizer/PII detector on content and files, and reject or redact entries
containing PII/secrets; only write the file and call graph_register_edit on
.dual-graph/context-store.json after the entry passes these checks; reference
graph_register_edit and .dual-graph/context-store.json in the doc so
implementers know where enforcement must occur.

## Session End

When the user signals they are done (e.g. "bye", "done", "wrap up", "end session"), proactively update `CONTEXT.md` in the project root with:
- **Current Task**: one sentence on what was being worked on
- **Key Decisions**: bullet list, max 3 items
- **Next Steps**: bullet list, max 3 items

Keep `CONTEXT.md` under 20 lines total. Do NOT summarize the full conversation — only what's needed to resume next session.

---

# Project: Resgrid Responder (React Native / Expo)

## Stack

| Concern | Library |
|---|---|
| Package manager | `yarn` |
| State | `zustand` |
| Forms | `react-hook-form` |
| Data fetching | `react-query` |
| HTTP | `axios` |
| i18n | `react-i18next` |
| Local storage | `react-native-mmkv` |
| Secure storage | Expo SecureStore |
| Navigation | React Navigation |
| UI components | `gluestack-ui` (from `components/ui`) |
| Icons | `lucide-react-native` (use directly in markup, NOT via gluestack Icon) |
| Maps / nav | `@rnmapbox/maps` |
| Images | `react-native-fast-image` |

## Code Rules

- TypeScript everywhere. No `any`. Use `interface` for props/state. `React.FC` for components.
- Enable strict typing in `tsconfig.json`.
- Functional components and hooks only — no class components.
- File/directory names: `lowercase-hyphenated`. Components: `PascalCase`. Variables/functions: `camelCase`.
- Organize by feature: group related components, hooks, and styles together.
- **Conditional rendering: use `? :` — never `&&`.**
- All user-visible text must be wrapped in `t()` from `react-i18next`. Translation files are in `src/translations`.
- This is an Expo managed project using prebuild. Do NOT make native code changes outside Expo prebuild capabilities.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a hyphenated compound adjective.

“Expo managed project” should be “Expo-managed project”.

🧰 Tools
🪛 LanguageTool

[grammar] ~114-~114: Use a hyphen to join words.
Context: ...in src/translations. - This is an Expo managed project using prebuild. Do NOT m...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 114, Replace the unhyphenated phrase "Expo managed
project" with the hyphenated compound adjective "Expo-managed project" in the
sentence "This is an Expo managed project using prebuild. Do NOT make native
code changes outside Expo prebuild capabilities." so it reads "This is an
Expo-managed project using prebuild..." to correct the compound adjective usage.


## Styling

- Use `gluestack-ui` components from `components/ui` first.
- If no Gluestack component exists, use `StyleSheet.create()` or Styled Components.
- Support both light and dark mode.
- Design for all screen sizes and orientations (iOS + Android).

## Performance

- Minimize `useEffect`, `useState`, and heavy computations in render.
- Use `React.memo()` for components with static props.
- FlatLists: set `removeClippedSubviews`, `maxToRenderPerBatch`, `windowSize`. Use `getItemLayout` when item height is fixed.
- No anonymous functions in `renderItem` or event handlers.

## Quality & Testing

- Write Jest tests for all generated components, services, and logic. Tests must pass.
- Handle errors gracefully with user feedback.
- Implement offline support.
- Follow WCAG accessibility guidelines for mobile.
- Optimize for low-end devices.
43 changes: 42 additions & 1 deletion src/app/login/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native';
import { AlertTriangle, EyeIcon, EyeOffIcon, GlobeIcon } from 'lucide-react-native';
import { useColorScheme } from 'nativewind';
import React, { useCallback, useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
Expand All @@ -14,9 +14,24 @@
import { Button, ButtonSpinner, ButtonText } from '@/components/ui/button';
import { FormControl, FormControlError, FormControlErrorIcon, FormControlErrorText, FormControlLabel, FormControlLabelText } from '@/components/ui/form-control';
import { Input, InputField, InputIcon, InputSlot } from '@/components/ui/input';
import { Select, SelectBackdrop, SelectContent, SelectDragIndicator, SelectDragIndicatorWrapper, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger } from '@/components/ui/select';
import { Text } from '@/components/ui/text';
import colors from '@/constants/colors';
import { useAnalytics } from '@/hooks/use-analytics';
import { translate, useSelectedLanguage } from '@/lib';
import type { Language } from '@/lib/i18n/resources';

const LANGUAGES: { label: string; value: Language }[] = [
{ label: 'English', value: 'en' },
{ label: 'Español', value: 'es' },
{ label: 'Svenska', value: 'sv' },
{ label: 'Deutsch', value: 'de' },
{ label: 'Français', value: 'fr' },
{ label: 'Italiano', value: 'it' },
{ label: 'Polski', value: 'pl' },
{ label: 'Українська', value: 'uk' },
{ label: 'العربية', value: 'ar' },
];

const loginFormSchema = z.object({
username: z
Expand Down Expand Up @@ -44,6 +59,7 @@
const { colorScheme } = useColorScheme();
const { t } = useTranslation();
const { trackEvent } = useAnalytics();
const { language, setLanguage } = useSelectedLanguage();
const {
control,
handleSubmit,
Expand Down Expand Up @@ -168,6 +184,31 @@
<ButtonText className="text-xs">{t('login.sso.login_with_sso_button')}</ButtonText>
</Button>
</View>

{/* Language Selector */}
<View className="mt-4 w-full flex-row items-center justify-center gap-2">
<GlobeIcon size={16} className="text-gray-500" />
<Select

Check warning on line 191 in src/app/login/login-form.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `⏎··············onValueChange={(val)·=>·setLanguage(val·as·Language)}⏎··············selectedValue={language·??·'en'}⏎············` with `·onValueChange={(val)·=>·setLanguage(val·as·Language)}·selectedValue={language·??·'en'}`
onValueChange={(val) => setLanguage(val as Language)}
selectedValue={language ?? 'en'}
>
<SelectTrigger className="border-0 bg-transparent">
<SelectInput placeholder={t('login.select_language')} className="text-xs text-gray-500" />
<SelectIcon as={GlobeIcon} className="mr-1 text-gray-500" />
</SelectTrigger>
<SelectPortal>
<SelectBackdrop />
<SelectContent className="max-h-[60vh] pb-20">
<SelectDragIndicatorWrapper>
<SelectDragIndicator />
</SelectDragIndicatorWrapper>
{LANGUAGES.map((lang) => (
<SelectItem key={lang.value} label={lang.label} value={lang.value} />
))}
</SelectContent>
</SelectPortal>
</Select>
</View>
</View>
</ScrollView>

Expand Down
7 changes: 7 additions & 0 deletions src/components/settings/language-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export const LanguageItem = () => {
() => [
{ label: translate('settings.english'), value: 'en' },
{ label: translate('settings.spanish'), value: 'es' },
{ label: translate('settings.swedish'), value: 'sv' },
{ label: translate('settings.german'), value: 'de' },
{ label: translate('settings.french'), value: 'fr' },
{ label: translate('settings.italian'), value: 'it' },
{ label: translate('settings.polish'), value: 'pl' },
{ label: translate('settings.ukrainian'), value: 'uk' },
{ label: translate('settings.arabic'), value: 'ar' },
],
[]
);
Expand Down
14 changes: 13 additions & 1 deletion src/lib/i18n/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ import { resources } from './resources';
import { getLanguage } from './utils';
export * from './utils';

const SUPPORTED_LANGUAGES = Object.keys(resources);

function getInitialLanguage(): string {
const saved = getLanguage();
if (saved && SUPPORTED_LANGUAGES.includes(saved)) return saved;

const deviceLang = Localization.getLocales()[0]?.languageCode ?? '';
if (SUPPORTED_LANGUAGES.includes(deviceLang)) return deviceLang;

return 'en';
}

i18n.use(initReactI18next).init({
resources,
lng: getLanguage() || Localization.getLocales()[0]?.languageCode || 'en', // TODO: if you are not supporting multiple languages or languages with multiple directions you can set the default value to `en`
lng: getInitialLanguage(),
fallbackLng: 'en',
compatibilityJSON: 'v3', // By default React Native projects does not support Intl

Expand Down
22 changes: 16 additions & 6 deletions src/lib/i18n/resources.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import ar from '@/translations/ar.json';
import de from '@/translations/de.json';
import en from '@/translations/en.json';
import es from '@/translations/es.json';
import fr from '@/translations/fr.json';
import it from '@/translations/it.json';
import pl from '@/translations/pl.json';
import sv from '@/translations/sv.json';
import uk from '@/translations/uk.json';

export const resources = {
en: {
translation: en,
},
ar: {
translation: ar,
},
en: { translation: en },
es: { translation: es },
sv: { translation: sv },
de: { translation: de },
fr: { translation: fr },
it: { translation: it },
pl: { translation: pl },
uk: { translation: uk },
ar: { translation: ar },
};

export type Language = keyof typeof resources;
7 changes: 7 additions & 0 deletions src/translations/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@
"username_placeholder": "أدخل اسم المستخدم أو بريدك الإلكتروني",
"username_required": "اسم المستخدم مطلوب"
},
"select_language": "اللغة",
"title": "تسجيل الدخول",
"username": "اسم المستخدم",
"username_placeholder": "أدخل اسم المستخدم الخاص بك"
Expand Down Expand Up @@ -808,6 +809,12 @@
"background_location": "الموقع في الخلفية",
"contact_us": "اتصل بنا",
"english": "إنجليزي",
"french": "فرنسي",
"german": "ألماني",
"italian": "إيطالي",
"polish": "بولندي",
"swedish": "سويدي",
"ukrainian": "أوكراني",
"enter_password": "أدخل كلمة المرور الخاصة بك",
"enter_server_url": "أدخل عنوان URL لواجهة برمجة تطبيقات Resgrid (مثال: https://api.resgrid.com)",
"enter_username": "أدخل اسم المستخدم الخاص بك",
Expand Down
Loading
Loading