---
phase: 22-configuration-truth-compatibility
plan: 02
type: execute
wave: 2
depends_on:
  - 22-01
files_modified:
  - paperforge/plugin/main.js
autonomous: true
requirements:
  - CONF-01
  - CONF-04

must_haves:
  truths:
    - "Plugin reads path configuration from paperforge.json, not from DEFAULT_SETTINGS or plugin data.json"
    - "Plugin Settings tab displays path values read from paperforge.json (not from plugin cache that might be stale)"
    - "Plugin Settings tab saves changes directly to paperforge.json file"
    - "Plugin data.json no longer stores system_dir, resources_dir, literature_dir, control_dir, base_dir"
    - "Fresh vaults without paperforge.json do not crash the plugin (graceful fallback)"
  artifacts:
    - path: "paperforge/plugin/main.js"
      provides: "Plugin reads paperforge.json for path config"
      contains: "readPaperforgeJson"
  key_links:
    - from: "PaperForgePlugin.onload()"
      to: "PaperForgeSettingTab.display()"
      via: "settings path values come from readPaperforgeJson()"
    - from: "PaperForgeSettingTab.save()"
      to: "paperforge.json file on disk"
      via: "write via fs.writeFileSync or similar"

---

<objective>
Refactor the Obsidian plugin to read path configuration from `paperforge.json` (the canonical source) instead of plugin `DEFAULT_SETTINGS` or `data.json`. The Plugin Settings tab becomes a viewer/editor of `paperforge.json` values, and plugin `data.json` no longer stores path directory fields.

Purpose: Eliminate the second runtime truth problem (D-10, D-14, D-15, D-16, D-17). Previously the plugin had independent DEFAULT_SETTINGS with wrong defaults (System, Resources, Notes, Index_Cards vs 99_System, 03_Resources, Literature, LiteratureControl), causing silent drift between Python commands and plugin surfaces.

Output: `main.js` refactored to read/write paperforge.json for all path config fields.
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
</execution_context>

<context>
@.planning/phases/22-configuration-truth-compatibility/22-CONTEXT.md (D-10, D-14..D-17)
@paperforge/plugin/main.js (full file, 1044 lines)
@paperforge/config.py (DEFAULT_CONFIG values for path keys: 99_System, 03_Resources, Literature, LiteratureControl, 05_Bases)
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add readPaperforgeJson() method to PaperForgePlugin and replace DEFAULT_SETTINGS path fields</name>
  <files>paperforge/plugin/main.js</files>

  <read_first>
  @paperforge/plugin/main.js (lines 145-156 -- DEFAULT_SETTINGS)
  @paperforge/plugin/main.js (lines 1037-1043 -- loadSettings() and saveSettings())
  @paperforge/plugin/main.js (lines 20-141 -- i18n, LANG object)
  </read_first>

  <action>
  Apply these changes to `paperforge/plugin/main.js`:

  1. **Replace DEFAULT_SETTINGS** (lines 145-156):
     Remove the path-directory fields (system_dir, resources_dir, literature_dir, control_dir, base_dir) from DEFAULT_SETTINGS. Keep only non-path fields:

     ```javascript
     const DEFAULT_SETTINGS = {
         vault_path: '',
         setup_complete: false,
         auto_update: true,
         agent_platform: 'opencode',
         language: '',
         paddleocr_api_key: '',
         zotero_data_dir: '',
     };
     ```

  2. **Add readPaperforgeJson() method to PaperForgePlugin class:**

     After the `_autoUpdate()` method (around line 1031) and BEFORE `loadSettings()` (line 1037), add:

     ```javascript
     /**
      * Read path configuration from the canonical paperforge.json file.
      * Falls back to Python-level DEFAULT_CONFIG values if file does not exist.
      * Returns {system_dir, resources_dir, literature_dir, control_dir, base_dir}.
      */
     readPaperforgeJson() {
         const fs = require('fs');
         const path = require('path');
         const vaultPath = this.app.vault.adapter.basePath;
         const pfPath = path.join(vaultPath, 'paperforge.json');

         // Python DEFAULT_CONFIG values as fallback
         const DEFAULTS = {
             system_dir: '99_System',
             resources_dir: '03_Resources',
             literature_dir: 'Literature',
             control_dir: 'LiteratureControl',
             base_dir: '05_Bases',
         };

         try {
             if (!fs.existsSync(pfPath)) {
                 return DEFAULTS;
             }
             const raw = fs.readFileSync(pfPath, 'utf-8');
             const data = JSON.parse(raw);

             // Try vault_config block first (canonical), fall back to top-level (legacy)
             const vc = data.vault_config || {};
             return {
                 system_dir: vc.system_dir || data.system_dir || DEFAULTS.system_dir,
                 resources_dir: vc.resources_dir || data.resources_dir || DEFAULTS.resources_dir,
                 literature_dir: vc.literature_dir || data.literature_dir || DEFAULTS.literature_dir,
                 control_dir: vc.control_dir || data.control_dir || DEFAULTS.control_dir,
                 base_dir: vc.base_dir || data.base_dir || DEFAULTS.base_dir,
             };
         } catch (e) {
             console.warn('PaperForge: Failed to read paperforge.json, using defaults', e);
             return DEFAULTS;
         }
     }
     ```

  3. **Update loadSettings()** (lines 1037-1039):
     Change from:
     ```javascript
     async loadSettings() {
         this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
     }
     ```
     To:
     ```javascript
     async loadSettings() {
         this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
         // Path fields come from paperforge.json, not from DEFAULT_SETTINGS or plugin data.json
         const pfConfig = this.readPaperforgeJson();
         this.settings.system_dir = pfConfig.system_dir;
         this.settings.resources_dir = pfConfig.resources_dir;
         this.settings.literature_dir = pfConfig.literature_dir;
         this.settings.control_dir = pfConfig.control_dir;
         this.settings.base_dir = pfConfig.base_dir;
     }
     ```

  4. **Update saveSettings()** (lines 1041-1043):
     Change from:
     ```javascript
     async saveSettings() {
         await this.saveData(this.settings);
     }
     ```
     To:
     ```javascript
     async saveSettings() {
         // Only persist non-path settings to plugin data.json
         const dataToSave = {};
         for (const key of Object.keys(DEFAULT_SETTINGS)) {
             if (key in this.settings) {
                 dataToSave[key] = this.settings[key];
             }
         }
         await this.saveData(dataToSave);
     }
     ```
     IMPORTANT: `DEFAULT_SETTINGS` no longer has path keys (removed in step 1 above), so this automatically excludes them.

  5. **Update the reference in display()** where it reads `s.system_dir` for the prep export path hint (around line 498):
     Line 498 currently reads:
     ```javascript
     expRow.createEl('span', { text: `${t('prep_export_path_label')} ${vaultPath}/${this.plugin.settings.system_dir || 'System'}/PaperForge/exports/` });
     ```
     Change `|| 'System'` to `|| '99_System'`:
     ```javascript
     expRow.createEl('span', { text: `${t('prep_export_path_label')} ${vaultPath}/${this.plugin.settings.system_dir || '99_System'}/PaperForge/exports/` });
     ```
  </action>

  <verify>
  <automated>node -e "
const fs = require('fs');
const content = fs.readFileSync('paperforge/plugin/main.js', 'utf-8');
// Check DEFAULT_SETTINGS no longer has path fields
const dsMatch = content.match(/const DEFAULT_SETTINGS\s*=\s*\{([^}]+)\}/);
if (!dsMatch) { console.error('DEFAULT_SETTINGS not found'); process.exit(1); }
const ds = dsMatch[1];
['system_dir','resources_dir','literature_dir','control_dir','base_dir'].forEach(k => {
  if (ds.includes(k + ':')) { console.error('DEFAULT_SETTINGS still has:', k); process.exit(1); }
});
console.log('OK: path keys removed from DEFAULT_SETTINGS');

// Check readPaperforgeJson exists
if (!content.includes('readPaperforgeJson()')) { console.error('readPaperforgeJson missing'); process.exit(1); }
console.log('OK: readPaperforgeJson found');

// Check loadSettings reads paperforge.json
if (!content.includes('readPaperforgeJson()')) { console.error('loadSettings does not call readPaperforgeJson'); process.exit(1); }
console.log('OK: loadSettings calls readPaperforgeJson');

// Check saveSettings only saves non-path fields
if (!content.includes('Object.keys(DEFAULT_SETTINGS)')) { console.error('saveSettings does not filter keys'); process.exit(1); }
console.log('OK: saveSettings filtered to non-path keys');

// Check Python DEFAULT_CONFIG fallback values in readPaperforgeJson
if (!content.includes('99_System')) { console.error('Missing 99_System fallback'); process.exit(1); }
console.log('OK: Python defaults used as fallback');

console.log('ALL CHECKS PASSED');
"</automated>
  </verify>

  <acceptance_criteria>
  - DEFAULT_SETTINGS object no longer contains keys: `system_dir`, `resources_dir`, `literature_dir`, `control_dir`, `base_dir`
  - PaperForgePlugin class has a `readPaperforgeJson()` method
  - `loadSettings()` overwrites path fields from `this.readPaperforgeJson()`
  - `saveSettings()` only persists keys that exist in DEFAULT_SETTINGS (i.e., non-path fields)
  - `readPaperforgeJson()` falls back to Python-level defaults (`99_System`, `03_Resources`, etc.) when paperforge.json does not exist
  - `readPaperforgeJson()` reads from `vault_config` block (canonical) with top-level fallback (legacy compat)
  - Prep export path hint (line ~498) defaults to `99_System` instead of `System`
  - Node.js verify script passes all checks
  </acceptance_criteria>

  <done>
  Plugin reads path config from paperforge.json. DEFAULT_SETTINGS path fields removed. loadSettings/saveSettings refactored.
  </done>
</task>

<task type="auto">
  <name>Task 2: Refactor PaperForgeSettingTab to display path values from paperforge.json and write back to paperforge.json</name>
  <files>paperforge/plugin/main.js</files>

  <read_first>
  @paperforge/plugin/main.js (lines 446-564 -- PaperForgeSettingTab class)
  @paperforge/plugin/main.js (lines 539-557 -- Config Summary section)
  </read_first>

  <action>
  Apply these changes to `paperforge/plugin/main.js`:

  1. **Update PaperForgeSettingTab constructor** to add a paperforgeConfig cache and refresh method (around line 446):

     Change the constructor from:
     ```javascript
     class PaperForgeSettingTab extends PluginSettingTab {
         constructor(app, plugin) {
             super(app, plugin);
             this.plugin = plugin;
             this._saveTimeout = null;
         }
     ```
     To:
     ```javascript
     class PaperForgeSettingTab extends PluginSettingTab {
         constructor(app, plugin) {
             super(app, plugin);
             this.plugin = plugin;
             this._saveTimeout = null;
             this._pfConfig = null;  // cached paperforge.json config
         }

         /** Reload path config from paperforge.json */
         _refreshPfConfig() {
             this._pfConfig = this.plugin.readPaperforgeJson();
         }
     ```

  2. **Update the `display()` method:** Add `_refreshPfConfig()` call at the top of display(), before any config reading:

     At the start of `display()` (line 453), after `containerEl.empty();` add:
     ```javascript
         this._refreshPfConfig();
     ```

  3. **Update the Config Summary section** (lines 539-557) to read from `this._pfConfig` instead of `this.plugin.settings`:

     Current code (lines 539-557):
     ```javascript
         if (this.plugin.settings.setup_complete) {
             containerEl.createEl('h3', { text: t('section_config') });
             const summary = containerEl.createEl('div', { cls: 'paperforge-summary' });
             const s = this.plugin.settings;
             const items = [
                 { label: t('dir_vault'), val: vaultPath },
                 { label: t('dir_resources'), val: `${vaultPath}/${s.resources_dir}` },
                 { label: '  ' + t('dir_notes'), val: `${vaultPath}/${s.resources_dir}/${s.literature_dir}` },
                 { label: '  ' + t('dir_index'), val: `${vaultPath}/${s.resources_dir}/${s.control_dir}` },
                 { label: t('dir_base'), val: `${vaultPath}/${s.base_dir}` },
                 { label: t('dir_system'), val: `${vaultPath}/${s.system_dir}` },
                 { label: 'API Key', val: s.paddleocr_api_key ? t('api_key_set') : t('api_key_missing') },
                 { label: t('field_zotero_data'), val: s.zotero_data_dir || t('not_set') },
             ];
     ```

     Change to:
     ```javascript
         if (this.plugin.settings.setup_complete) {
             containerEl.createEl('h3', { text: t('section_config') });
             const summary = containerEl.createEl('div', { cls: 'paperforge-summary' });
             const s = this.plugin.settings;
             const pf = this._pfConfig;  // source of truth for path fields
             const items = [
                 { label: t('dir_vault'), val: vaultPath },
                 { label: t('dir_resources'), val: `${vaultPath}/${pf.resources_dir}` },
                 { label: '  ' + t('dir_notes'), val: `${vaultPath}/${pf.resources_dir}/${pf.literature_dir}` },
                 { label: '  ' + t('dir_index'), val: `${vaultPath}/${pf.resources_dir}/${pf.control_dir}` },
                 { label: t('dir_base'), val: `${vaultPath}/${pf.base_dir}` },
                 { label: t('dir_system'), val: `${vaultPath}/${pf.system_dir}` },
                 { label: 'API Key', val: s.paddleocr_api_key ? t('api_key_set') : t('api_key_missing') },
                 { label: t('field_zotero_data'), val: s.zotero_data_dir || t('not_set') },
             ];
     ```

  4. **Update the prep export path display** (line 498) to use `_pfConfig` instead of `this.plugin.settings`:

     Currently:
     ```javascript
     expRow.createEl('span', { text: `${t('prep_export_path_label')} ${vaultPath}/${this.plugin.settings.system_dir || '99_System'}/PaperForge/exports/` });
     ```

     Change to:
     ```javascript
     const pfExport = this._pfConfig || this.plugin.readPaperforgeJson();
     expRow.createEl('span', { text: `${t('prep_export_path_label')} ${vaultPath}/${pfExport.system_dir}/PaperForge/exports/` });
     ```
  </action>

  <verify>
  <automated>node -e "
const fs = require('fs');
const content = fs.readFileSync('paperforge/plugin/main.js', 'utf-8');

// Check display calls _refreshPfConfig
if (!content.includes('_refreshPfConfig()')) { console.error('display() missing _refreshPfConfig'); process.exit(1); }
console.log('OK: display refreshes paperforge config');

// Check config summary reads from _pfConfig
if (!content.includes('this._pfConfig')) { console.error('Config summary not reading from _pfConfig'); process.exit(1); }
console.log('OK: config summary reads from _pfConfig');

// Check prep export path uses readPaperforgeJson
const prepMatch = content.match(/prep_export_path_label.*system_dir/);
if (!prepMatch) { console.error('prep export path not updated'); process.exit(1); }
console.log('OK: prep export path uses paperforge config');

console.log('ALL CHECKS PASSED');
"</automated>
  </verify>

  <acceptance_criteria>
  - `display()` calls `this._refreshPfConfig()` at the start (after containerEl.empty())
  - Config Summary section reads path values from `this._pfConfig` (paperforge.json source)
  - Prep export path display sources from `readPaperforgeJson()` output
  - Plugin data.json fields (paddleocr_api_key, zotero_data_dir, setup_complete, etc.) still read from plugin.settings as before
  - Node.js verify script passes all checks
  </acceptance_criteria>

  <done>
  PaperForgeSettingTab now displays path config sourced from paperforge.json (via _refreshPfConfig), not from plugin settings cache.
  </done>
</task>

<task type="auto">
  <name>Task 3: Remove path fields from plugin data.json persistence and add paperforge.json write capability</name>
  <files>paperforge/plugin/main.js</files>

  <read_first>
  @paperforge/plugin/main.js (lines 1037-1043 -- loadSettings, saveSettings)
  @paperforge/plugin/main.js (lines 145-156 -- DEFAULT_SETTINGS -- already modified in Task 1)
  @paperforge/plugin/main.js (lines 446-564 -- PaperForgeSettingTab display and debounced save)
  </read_first>

  <action>
  Apply these changes to `paperforge/plugin/main.js`:

  1. **Add a `savePaperforgeJson()` method** to the PaperForgePlugin class. Add this AFTER the `readPaperforgeJson()` method (created in Task 1) and BEFORE `loadSettings()`:

     ```javascript
     /**
      * Write path configuration back to paperforge.json vault_config block.
      * Reads the existing file, updates vault_config with the new values,
      * and writes back preserving all other keys.
      */
     savePaperforgeJson(pathConfig) {
         const fs = require('fs');
         const path = require('path');
         const vaultPath = this.app.vault.adapter.basePath;
         const pfPath = path.join(vaultPath, 'paperforge.json');

         let data = {};
         try {
             if (fs.existsSync(pfPath)) {
                 data = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
             }
         } catch (e) {
             console.warn('PaperForge: Failed to read paperforge.json for update', e);
         }

         // Ensure vault_config block exists
         if (!data.vault_config || typeof data.vault_config !== 'object') {
             data.vault_config = {};
         }

         // Update vault_config with new path values
         const validPathKeys = ['system_dir', 'resources_dir', 'literature_dir', 'control_dir', 'base_dir'];
         for (const key of validPathKeys) {
             if (pathConfig[key] !== undefined) {
                 data.vault_config[key] = pathConfig[key];
             }
         }

         // Ensure schema_version
         if (!data.schema_version) {
             data.schema_version = '2';
         }

         // Remove any stale top-level path keys (they were migrated to vault_config)
         for (const key of validPathKeys) {
             delete data[key];
         }

         try {
             fs.writeFileSync(pfPath, JSON.stringify(data, null, 2), 'utf-8');
             // Refresh the in-memory cache
             if (this.settings) {
                 const pfConfig = this.readPaperforgeJson();
                 this.settings.system_dir = pfConfig.system_dir;
                 this.settings.resources_dir = pfConfig.resources_dir;
                 this.settings.literature_dir = pfConfig.literature_dir;
                 this.settings.control_dir = pfConfig.control_dir;
                 this.settings.base_dir = pfConfig.base_dir;
             }
         } catch (e) {
             console.error('PaperForge: Failed to write paperforge.json', e);
             new Notice('PaperForge: Failed to save configuration to paperforge.json');
         }
     }
     ```

  2. **Ensure plugin data.json only stores non-path settings** -- this was already done in Task 1 via `saveSettings()` using `Object.keys(DEFAULT_SETTINGS)` filtering. The `DEFAULT_SETTINGS` no longer has path keys, so `saveSettings()` automatically excludes them. Verify the current `saveSettings()`:

     ```javascript
     async saveSettings() {
         const dataToSave = {};
         for (const key of Object.keys(DEFAULT_SETTINGS)) {
             if (key in this.settings) {
                 dataToSave[key] = this.settings[key];
             }
         }
         await this.saveData(dataToSave);
     }
     ```

     This is correct as-is from Task 1. No further changes needed.

  3. **IMPORTANT: Remove all path fields from EXISTING plugin data.json** -- On plugin load, call `saveSettings()` once to clean stale path fields. Add this call at the end of `onload()` method (around line 1010-1020, before the _autoUpdate and registerView calls). Find the onload() method and add:

     ```javascript
         // Clean stale path fields from plugin data.json (migrated to paperforge.json)
         this.saveSettings();
     ```

     This ensures that after the upgrade, plugin data.json is cleaned of stale path keys on first load.

  </action>

  <verify>
  <automated>node -e "
const fs = require('fs');
const content = fs.readFileSync('paperforge/plugin/main.js', 'utf-8');

// Check savePaperforgeJson method exists
if (!content.includes('savePaperforgeJson(pathConfig)')) { console.error('savePaperforgeJson method missing'); process.exit(1); }
console.log('OK: savePaperforgeJson method found');

// Check saveSettings filters by DEFAULT_SETTINGS keys
if (!content.includes('Object.keys(DEFAULT_SETTINGS)')) { console.error('saveSettings not filtering keys'); process.exit(1); }
console.log('OK: saveSettings filters by DEFAULT_SETTINGS keys');

// Check onload calls saveSettings for cleanup
const onloadMatch = content.match(/async onload\(\)\s*\{[^}]+clean[^}]+saveSettings/s);
// Actually just check that saveSettings() is called in onload
const onloadCalls = content.match(/onload[\s\S]{1,500}saveSettings/g);
if (!onloadCalls) { console.error('onload does not call saveSettings for cleanup'); process.exit(1); }
console.log('OK: onload calls saveSettings to clean stale path fields');

// Check savePaperforgeJson removes top-level path keys
if (!content.includes('delete data[key]')) { console.error('savePaperforgeJson does not clean top-level keys'); process.exit(1); }
console.log('OK: savePaperforgeJson removes stale top-level path keys');

console.log('ALL CHECKS PASSED');
"</automated>
  </verify>

  <acceptance_criteria>
  - `savePaperforgeJson(pathConfig)` method exists on PaperForgePlugin
  - `savePaperforgeJson()` writes path values to `paperforge.json` vault_config block
  - `savePaperforgeJson()` removes any stale top-level path keys from paperforge.json
  - `savePaperforgeJson()` sets `schema_version` to `'2'` if absent
  - `savePaperforgeJson()` refreshes `this.settings` path fields after write
  - `saveSettings()` only saves keys from `DEFAULT_SETTINGS` (path fields excluded since they were removed from DEFAULT_SETTINGS)
  - `onload()` calls `saveSettings()` to clean stale path fields from plugin data.json
  - Node.js verify script passes all checks
  </acceptance_criteria>

  <done>
  Plugin data.json no longer stores path fields. Plugin writes config changes to paperforge.json via savePaperforgeJson(). Stale data.json path fields cleaned on first load.
  </done>
</task>

</tasks>

<verification>

### Plan 02 Verification

1. **DEFAULT_SETTINGS cleaned:** No path directory keys in DEFAULT_SETTINGS
2. **readPaperforgeJson() exists:** Method on PaperForgePlugin, reads vault_config block with top-level fallback, falls back to Python defaults
3. **loadSettings() sources path from paperforge.json:** Overwrites settings.system_dir etc. from readPaperforgeJson()
4. **saveSettings() excludes path keys:** Only persists DEFAULT_SETTINGS keys (non-path)
5. **Settings tab displays paperforge.json values:** Config summary reads from `this._pfConfig`
6. **savePaperforgeJson() writes to paperforge.json:** Writes vault_config, removes top-level path keys, sets schema_version
7. **onload cleans plugin data.json:** Calls saveSettings() on load to remove stale path keys
8. **Graceful fallback:** readPaperforgeJson() returns Python defaults when file does not exist
</verification>

<success_criteria>

- [ ] `DEFAULT_SETTINGS` has NO path directory keys (verified via Node.js check)
- [ ] `readPaperforgeJson()` is a callable method on PaperForgePlugin
- [ ] `loadSettings()` populates path fields from `readPaperforgeJson()`, not from DEFAULT_SETTINGS
- [ ] `saveSettings()` only persists keys in DEFAULT_SETTINGS (non-path) to plugin data.json
- [ ] `savePaperforgeJson()` writes to paperforge.json vault_config block
- [ ] Settings tab path display uses `this._pfConfig` (paperforge.json source)
- [ ] Plugin does not crash when paperforge.json is missing (uses Python defaults as fallback)
- [ ] All Node.js verify checks pass
</success_criteria>

<output>
After completion, create `.planning/phases/22-configuration-truth-compatibility/22-02-SUMMARY.md`
</output>
