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
56 changes: 48 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,54 @@ GitHub Actions workflow runs:

The project uses semantic-release for automated versioning based on commit messages.

## Notes for AI Agents

- Use `npm test` for unit tests and `npm run test-e2e` for integration tests.
- All new code should include corresponding tests in the mirrored `test/` directory.
- Follow the Action Module Pattern for new functionality in `src/fn/`.
- Form validation modules should follow the Form Validation Pattern in `src/lib/validation/form/`.
- PRs must pass CI (lint + tests) before merging.
- All commits should follow the Conventional Commits format.
## Non-obvious gotchas when contributing to cht-conf

### `getFormDir` returns `null` silently — not an error
`src/lib/forms-utils.js` `getFormDir()` returns `null` when the directory is absent.
Callers that don't check the return value will silently do nothing. This is intentional:
a missing `forms/app/` in the user's project is not an error in cht-conf itself.
When writing new actions that iterate over forms, always guard against a `null` result.

### Declarative schema validation uses conditional Joi rules — test every branch
`src/lib/compilation/validate-declarative-schema.js` uses Joi conditionals that look
like the field is always optional but are actually required or forbidden depending on
sibling field values. Key pairs:
- `event.days` vs `event.dueDate` — mutually exclusive; neither both nor neither is valid.
- `resolvedIf` — required only when every action has `type: 'contact'`, optional otherwise.
- `passesIf` — required for `type: 'percent'` targets without `groupBy`; forbidden if `groupBy` is set.
- multi-event tasks — `event.id` optional for single-event tasks, required + unique for 2+.

When extending the schema, always add a test that confirms both the "forbidden" and
"required" branches, not just the happy path.

### `rules.nools.js` and `tasks.js`/`targets.js` are mutually exclusive
`src/lib/compilation/compile-tasks-and-targets.js` fails at compile time if both
the legacy (`rules.nools.js`) and declarative (`tasks.js` + `targets.js`) files exist.
Both declarative files must be present together; either alone is also an error.
Test fixtures that mix styles will break this validation by design.

### `appliesToType` semantics differ by `appliesTo` value — no compile check
In `src/nools/task-emitter.js`, `appliesToType` is matched against `report.form`
when `appliesTo: 'reports'` or `'scheduled_tasks'`, but against the resolved contact type
when `appliesTo: 'contacts'`. Contact type itself is resolved as `contact.contact_type`
when `contact.type === 'contact'`, otherwise `contact.type`. There is no schema validation
for this; wrong `appliesToType` values produce silent task non-emission in tests and prod.

### `appliesIf` arity changes with `appliesTo: 'scheduled_tasks'`
`src/nools/task-emitter.js` passes a third `scheduledTaskIndex` argument to `appliesIf`
only when `appliesTo: 'scheduled_tasks'`. Copying task fixtures between `appliesTo` types
without adjusting the function signature will silently break the condition logic.

### `.properties.json` unknown fields are warned, not errored
`src/lib/upload-forms.js` accepts only a fixed set of properties. Unknown fields log a
warning and are discarded — they never surface as failures. If a test asserts that a
certain property was applied, verify it is in the allowed set before trusting the test
was actually exercising the right code path.

### `internalId` in `.properties.json` is deprecated and errors on mismatch
If a `.properties.json` contains `internalId`, it must exactly match the filename-derived
ID or upload throws. New code and fixtures should never write `internalId`; existing
fixtures that carry it are testing the deprecation warning path, not a live feature.

## Security Considerations

Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,32 @@ Copyright 2013-2026 Medic Mobile, Inc. <hello@medic.org>
## License

The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.

## AI Agent Support

\`cht-conf\` includes features to make it easier for AI coding agents (like Claude, GitHub Copilot, or specialized CHT agents) to work on your project.

### Initializing Agent Support
For existing projects, run:
\`\`\`bash
cht agents-md
\`\`\`
This generates \`AGENTS.md\` and \`CLAUDE.md\` files in your project root, which provide instructions and context for AI tools.

### Local Documentation
\`cht-conf\` bundles key documentation locally so agents have access to version-matched guides without needing internet access. You can also manually refresh this documentation:
\`\`\`bash
cht update-docs
\`\`\`

### Inspection Commands
Agents can use \`inspect\` commands to verify the state of a deployed CHT instance:
- \`cht inspect-forms\`: List all forms on the instance.
- \`cht inspect-form <id>\`: Show details of a specific form.
- \`cht inspect-settings-diff\`: Compare local settings with the instance.
- \`cht inspect-tasks\`: List all deployed task definitions.
- \`cht inspect-targets\`: List all deployed target definitions.
- \`cht inspect-hierarchy\`: View the contact hierarchy tree.
- \`cht inspect-transitions\`: List transitions and deprecation warnings.

All \`inspect\` commands support a \`--json\` flag for machine-parseable output.
76 changes: 76 additions & 0 deletions src/docs/cht-conf/actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# cht-conf Action Reference

This document provides a summary of all supported actions in `cht-conf`. These actions can be executed by passing them as arguments to the `cht` command.

## Backup
- `backup-all-forms`: Downloads all forms from the CHT instance and saves them to the local project directory.
- `backup-app-settings`: Fetches the current application settings from the CHT instance and saves them to a local JSON file.

## Convert
- `convert-app-forms`: Processes local application XLSX forms into XML and Enketo-compatible formats.
- `convert-collect-forms`: Transforms local collect XLSX forms into the XML format required by the CHT.
- `convert-contact-forms`: Converts local contact XLSX forms into XML with support for place-type templating and model adjustments.
- `convert-training-forms`: Processes local training XLSX forms into XML and Enketo-compatible formats.
- `csv-to-docs`: Parses CSV files to generate JSON document files for contacts, places, and reports in the local project.

## Upload
- `upload-app-forms`: Synchronizes compiled local application forms with the CHT instance.
- `upload-app-settings`: Uploads the compiled application settings JSON to the CHT instance.
- `upload-branding`: Uploads branding configuration and associated image assets to the CHT instance.
- `upload-collect-forms`: Synchronizes compiled local collect forms with the CHT instance.
- `upload-contact-forms`: Synchronizes compiled local contact forms with the CHT instance.
- `upload-custom-translations`: Validates and uploads custom translation properties files to the CHT instance.
- `upload-database-indexes`: Configures and uploads PouchDB/CouchDB index definitions to the CHT instance to improve query performance.
- `upload-docs`: Efficiently uploads batches of local JSON documents to the CHT instance, including handling user account updates for deleted places.
- `upload-extension-libs`: Uploads custom JavaScript libraries to the CHT instance for extending application logic.
- `upload-partners`: Uploads partner-specific configuration documents and assets to the CHT instance.
- `upload-privacy-policies`: Uploads privacy policy HTML files and their respective language mappings to the CHT instance.
- `upload-resources`: Uploads resource files and their configuration to the CHT instance.
- `upload-sms-from-csv`: Parses a CSV file and uploads the contained SMS messages to the CHT instance.
- `upload-training-forms`: Synchronizes compiled local training forms with the CHT instance.

## Validate
- `validate-app-forms`: Checks the structure and content of local application forms for errors before conversion or upload.
- `validate-collect-forms`: Verifies the integrity of local collect forms against CHT requirements.
- `validate-contact-forms`: Validates local contact forms to ensure they follow the correct schema and conventions.
- `validate-training-forms`: Ensures local training forms are correctly structured and free of errors.

## Delete
- `delete-all-forms`: Removes every form currently stored on the CHT instance.
- `delete-forms`: Removes specified forms from the CHT instance based on the provided names.
- `delete-contacts`: Permanently deletes specified contacts and all their descendants from the CHT instance.

## Contacts
- `edit-contacts`: Updates existing contacts on the CHT instance using data provided in CSV files.
- `merge-contacts`: Consolidates multiple source contacts into a destination contact while moving their descendant data.
- `move-contacts`: Relocates specified contacts to a different parent location within the CHT hierarchy.

## Compression
- `compress-images`: Orchestrates the compression of all PNG and SVG assets within the project.
- `compress-pngs`: Optimizes PNG images using external compression tools to reduce file size.
- `compress-svgs`: Minimizes SVG files using SVGO to optimize them for use in the CHT.

## Project Management
- `check-for-updates`: Compares the local version of `cht-conf` with the latest available version on npm.
- `check-git`: Ensures the local repository is clean and synchronized with its upstream branch before critical operations.
- `compile-app-settings`: Aggregates modular configuration files, scripts, and rules into a single application settings file.
- `create-users`: Generates or updates user accounts on the CHT instance from a provided CSV file.
- `fetch-csvs-from-google-drive`: Downloads CSV data from specified Google Drive files into the local project.
- `fetch-forms-from-google-drive`: Retrieves XLSX forms from Google Drive and saves them locally for processing.
- `initialise-project-layout`: Sets up a new project with the standard CHT configuration directory structure and boilerplate files.
- `watch-project`: Automatically triggers validation, compilation, and upload actions whenever local project files are modified.

## AI Agent Support
- `agents-md`: Generates `AGENTS.md` and `CLAUDE.md` files in the project root to guide AI coding agents.
- `update-docs`: Fetches the latest documentation from the `medic/cht-docs` repository and stores it locally for AI agents.

## Inspection
- `inspect-errors`: Fetches and displays recent sentinel or API errors from the `medic-logs` database.
- `inspect-form`: Shows detailed information, fields, and calculations for a specific deployed form (requires `<id>` argument).
- `inspect-forms`: Lists all forms currently deployed on the CHT instance.
- `inspect-hierarchy`: Displays a simplified tree view of the top-level contact hierarchy.
- `inspect-replication`: Shows the status and progress of active CouchDB replication tasks.
- `inspect-settings-diff`: Compares local application settings (`app_settings.json`) with the settings currently deployed on the CHT instance.
- `inspect-targets`: Lists all target definitions deployed in the application settings.
- `inspect-tasks`: Lists all task definitions deployed in the application settings.
- `inspect-transitions`: Displays all configured transitions and their statuses, including any deprecation warnings.
37 changes: 37 additions & 0 deletions src/docs/cht-conf/form-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# CHT Form Conventions

CHT forms use XLSForm (Excel) files. However, there are several CHT-specific conventions that AI agents must follow to ensure forms work correctly with `cht-conf` and the CHT core.

## 1. db-doc References
CHT supports referencing documents (like people or places) directly in forms.
- `db-doc:<type>`: In the `type` column, use `db-doc:person` or `db-doc:place` to create a selection field for contacts.

## 2. xml-external Itemsets
For large lists (like a list of all villages), CHT uses external XML files.
- Use the `xml-external` pattern in the `type` column.

## 3. NO_LABEL Placeholder
(Deprecated, but still used in older forms)
- If a field should not have a label, some forms use `NO_LABEL`.
- **Better approach**: For groups, labels are not required. For hidden fields, use `hidden` or `calculate` types.

## 4. Contact Summary Data
Forms can access data from the contact summary of the subject.
- Use `instance('contact-summary')/contact/some_field` in calculations.

## 5. Metadata and Inputs
All CHT forms automatically get a `meta` section and an `inputs` section during conversion by `cht-conf`.
- Do not manually define `inputs/meta/location` in the XLSForm unless specifically required; `cht-conf` will inject it.

## 6. Form IDs and Names
- **Contact Forms**: Must end in `-create` or `-edit` (e.g., `person-create`, `clinic-edit`).
- **File Names**: The `.xlsx` filename determines the internal form ID. Avoid spaces and special characters.

## 7. Media Files
- Images, audio, and video for forms should be placed in a directory named `<form-name>-media/` next to the `.xlsx` file.
- Example: `forms/app/assessment-media/image.png` for `forms/app/assessment.xlsx`.

## 8. Hidden Fields
To hide a field from the user but keep it in the submitted document:
- Use the `hidden` appearance in the XLSForm.
- OR list the field in the `hidden_fields` array in the companion `.properties.json` file.
44 changes: 44 additions & 0 deletions src/docs/cht-conf/project-structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# CHT Project Directory Structure

A standard CHT configuration project managed by `cht-conf` follows this directory structure. AI agents must strictly adhere to these locations when creating or modifying files.

## Root Directory Files

- `app_settings.json`: (Optional) The complete application settings. Often split into the `app_settings/` directory.
- `contact-summary.templated.js`: Defines the fields and cards shown in the contact summary.
- `targets.js`: Defines the analytics targets shown in the app.
- `tasks.js`: Defines the tasks shown to users.
- `rules.nools`: (Optional) Custom Nools rules for tasks and targets (older projects).
- `privacy-policies.json`: Configuration for privacy policies.
- `resources.json`: Configuration for branding and resources.

## Directories

### `forms/`
Contains all XLSForm files and their companion properties. **Crucial: Do not mix form types.**

- `forms/app/`: User-facing forms (e.g., assessments, follow-ups).
- `forms/contact/`: Forms for creating/editing places and people (e.g., `person-create.xlsx`, `clinic-edit.xlsx`).
- `forms/collect/`: (Legacy) Forms for ODK Collect.
- `forms/training/`: Forms used in training mode.

### `app_settings/`
Instead of one giant `app_settings.json`, settings can be split here:
- `app_settings/base_settings.json`: General settings.
- `app_settings/forms.json`: Form-specific settings.
- `app_settings/schedules.json`: Task schedule definitions.

### `resources/`
Contains images and other assets for branding (e.g., `logo.png`).

### `translations/`
Contains `.properties` files for multilingual support (e.g., `messages-en.properties`).

### `test/`
Contains test files for forms, tasks, and targets.

## Form Companion Files
Every `.xlsx` form may have a companion `.properties.json` file with the same base name.
Example: `forms/app/assessment.xlsx` -> `forms/app/assessment.properties.json`
- `context`: (Optional) Restricts form visibility to specific contact types or conditions.
- `hidden_fields`: (Optional) List of fields to be hidden from the UI but kept in the doc.
29 changes: 29 additions & 0 deletions src/fn/agents-md.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const path = require('node:path');
const environment = require('../lib/environment');
const fs = require('../lib/sync-fs');
const { info, warn } = require('../lib/log');
const { AGENTS_MD, CLAUDE_MD } = require('../lib/agent-templates');

const execute = async () => {
const projectDir = environment.pathToProject;

const files = [
{ name: 'AGENTS.md', content: AGENTS_MD },
{ name: 'CLAUDE.md', content: CLAUDE_MD }
];

for (const file of files) {
const filePath = path.join(projectDir, file.name);
if (fs.exists(filePath)) {
warn(`${file.name} already exists at ${filePath}. Skipping.`);
} else {
info(`Creating ${file.name} at ${filePath}`);
fs.write(filePath, file.content);
}
}
};

module.exports = {
requiresInstance: false,
execute
};
3 changes: 3 additions & 0 deletions src/fn/initialise-project-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ const path = require('path');
const environment = require('../lib/environment');
const fs = require('../lib/sync-fs');
const { info } = require('../lib/log');
const { AGENTS_MD, CLAUDE_MD } = require('../lib/agent-templates');

const LAYOUT = {
'AGENTS.md': AGENTS_MD,
'CLAUDE.md': CLAUDE_MD,
'contact-summary.templated.js': `module.exports = {
fields: [],
cards: [],
Expand Down
50 changes: 50 additions & 0 deletions src/fn/inspect-errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable no-console */
const getApi = require('../lib/api');
const { info, error } = require('../lib/log');
const environment = require('../lib/environment');
const url = require('node:url');

const execute = async () => {
const api = getApi();

try {
const parsedUrl = new url.URL(environment.apiUrl);
const baseUrl = `${parsedUrl.protocol}//${parsedUrl.username}:${parsedUrl.password}@${parsedUrl.host}`;

// Fetch recent documents from medic-logs
const limit = 20;
const logsUrl = `${baseUrl}/medic-logs/_all_docs?include_docs=true&descending=true&limit=${limit}`;
const result = await api.get({ url: logsUrl, json: true });

const errors = result.rows
.map(row => row.doc)
.filter(doc => doc && !doc._id.startsWith('_design/'));

if (environment.extraArgs.includes('--json')) {
console.log(JSON.stringify(errors, null, 2));
return;
}

info(`Recent ${errors.length} log/error entries from medic-logs:`);
errors.forEach((errDoc, i) => {
console.log(`${i + 1}. [${errDoc.timestamp || errDoc.created || errDoc._id}] ${errDoc.type || 'unknown type'}`);
if (errDoc.error || errDoc.message) {
console.log(` Message: ${errDoc.error || errDoc.message}`);
}
if (errDoc.action) {
console.log(` Action: ${errDoc.action}`);
}
});
} catch (err) {
if (err.statusCode === 404) {
error('Could not access medic-logs database. Ensure you have the right permissions or the database exists.');
} else {
error(`Failed to fetch errors: ${err.message}`);
}
}
};

module.exports = {
requiresInstance: true,
execute
};
34 changes: 34 additions & 0 deletions src/fn/inspect-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-disable no-console */
const getApi = require('../lib/api');
const { info, error } = require('../lib/log');
const environment = require('../lib/environment');

const execute = async () => {
const formId = environment.extraArgs.find(arg => !arg.startsWith('-'));
if (!formId) {
error('Please specify a form ID: cht inspect-form <id>');
return;
}

const api = getApi();
const settings = await api.getAppSettings();

const form = settings?.forms?.[formId];
if (!form) {
error(`Form ${formId} not found in deployed settings.`);
return;
}

if (environment.extraArgs.includes('--json')) {
console.log(JSON.stringify(form, null, 2));
return;
}

info(`Form details for ${formId}:`);
console.log(JSON.stringify(form, null, 2));
};

module.exports = {
requiresInstance: true,
execute
};
Loading