International Public Service Phones Directory Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add an international public-service phone directory that lets visitors filter verified emergency numbers for 11 countries/regions and Chinese public, bank, and insurance hotlines.

Architecture: Keep verified directory data and pure filter/safety helpers in a standalone UMD module shared by the browser and Node tests. A small controller renders only safe DOM text, persists UI filters locally, and re-renders on the site’s language-change event; the Markdown page contains the stable shell and script includes.

Tech Stack: Jekyll Markdown, vanilla JavaScript, UMD modules, CSS, Node.js assertion tests.

Spec: docs/superpowers/specs/2026-09-05-public-service-phones-design.md

Global Constraints


File Structure

Task 1: Build the verified, testable directory catalogue

Files:

Interfaces:

const assert = require('node:assert/strict');
const directory = require('../assets/js/public-service-phones-data.js');

const requiredCountries = ['china', 'united-states', 'canada', 'united-kingdom', 'france', 'germany', 'japan', 'south-korea', 'singapore', 'australia', 'india'];
for (const countryId of requiredCountries) {
  assert.ok(directory.phoneRecords.some((record) => record.countryId === countryId && record.categoryId === 'emergency'));
}
assert.deepEqual(
  directory.phoneRecords.filter((record) => record.countryId === 'china' && record.categoryId === 'emergency').map((record) => record.phone),
  ['110', '119', '120', '122']
);

Run: node tests/public-service-phones.test.js

Expected: failure stating that ../assets/js/public-service-phones-data.js cannot be found.

Use browser research only on government, police, fire, health, embassy, or institution-owned HTTPS domains. For each record, preserve the exact official source URL and set verifiedAt to the date of this review. Confirm the following coverage before writing the module:

Emergency: China (110, 119, 120, 122), United States, Canada, United Kingdom,
France, Germany, Japan, South Korea, Singapore, Australia, India.
China public service: only official national government/service numbers.
China banks: ICBC, Agricultural Bank of China, Bank of China, China Construction
Bank, Bank of Communications, China Merchants Bank, China CITIC Bank, and China
Everbright Bank, each only when its official source confirms the hotline.
China insurance: PICC, China Life, China Pacific Insurance, Ping An Insurance,
New China Life, and Taikang Insurance, each only when its official source confirms
the hotline.
(function (root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  else root.PublicServicePhonesData = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  const countries = [{ id: 'china', nameZh: '中国', nameEn: 'China' }];
  const categories = [{ id: 'emergency', nameZh: '紧急服务', nameEn: 'Emergency' }];
  const phoneRecords = [];
  const isSafePhone = (value) => typeof value === 'string' && /^[0-9+()\\/\\-\\s]+$/.test(value);
  const isSafeUrl = (value) => typeof value === 'string' && /^https:\/\//i.test(value);
  const toTelHref = (value) => isSafePhone(value) ? `tel:${value.replace(/\\s+/g, '')}` : null;
  const filterRecords = ({ countryId = '', categoryId = '', query = '' } = {}) => phoneRecords;
  return { countries, categories, phoneRecords, filterRecords, isSafePhone, isSafeUrl, toTelHref };
});

Populate the three constant arrays with the researched records. Every record must use this exact shape:

{
  id: 'china-fire-119', countryId: 'china', categoryId: 'emergency',
  institutionZh: '消防救援', institutionEn: 'Fire and rescue', phone: '119',
  descriptionZh: '火警、火灾救援。', descriptionEn: 'Fire and rescue emergency.',
  sourceUrl: 'https://official.example.invalid/', verifiedAt: '2026-09-05'
}

Never use the example URL in the completed data: replace it with the exact verified official source.

for (const record of directory.phoneRecords) {
  assert.equal(directory.isSafePhone(record.phone), true, record.id);
  assert.equal(directory.isSafeUrl(record.sourceUrl), true, record.id);
  assert.match(record.verifiedAt, /^\\d{4}-\\d{2}-\\d{2}$/);
}
for (const record of directory.phoneRecords.filter((record) => record.countryId === 'china' && ['bank', 'insurance'].includes(record.categoryId))) {
  assert.match(record.sourceUrl, /^https:\/\//, record.id);
}
assert.ok(directory.filterRecords({ countryId: 'china', categoryId: 'bank', query: '95588' }).some((record) => record.phone === '95588'));
assert.equal(directory.toTelHref(' 95588 '), 'tel:95588');
assert.equal(directory.toTelHref('bad<script>'), null);

Run: node tests/public-service-phones.test.js; node --check assets/js/public-service-phones-data.js

Expected: exit code 0.

git add tests/public-service-phones.test.js assets/js/public-service-phones-data.js
git commit -m "Add verified public service phone directory data"

Task 2: Add the bilingual Jekyll page and site entry points

Files:

Interfaces:

Add these checks to tests/public-service-phones.test.js:

const fs = require('node:fs');
const page = fs.readFileSync('tools/public-service-phones.md', 'utf8');
const index = fs.readFileSync('index.md', 'utf8');
const toolsPage = fs.readFileSync('tools.md', 'utf8');
assert.match(page, /public-service-phones-data\.js/);
assert.match(page, /public-service-phones\.js/);
assert.match(page, /id="public-phone-results"/);
assert.match(index, /tools\/public-service-phones\//);
assert.match(toolsPage, /tools\/public-service-phones\//);

Run: node tests/public-service-phones.test.js

Expected: failure because the page and entry links do not yet exist.

Use this script order at the bottom of tools/public-service-phones.md:

<script src="/assets/js/public-service-phones-data.js"></script>
<script src="/assets/js/public-service-phones.js"></script>

Add an always-visible emergency warning, country <select>, category button group, search input, result count, and results <section>. Use data-i18n only on text-only elements; do not put it on a control wrapper containing inputs. Add one concise card to both index.md and tools.md with target="_blank" rel="noopener noreferrer".

Run: node tests/public-service-phones.test.js; node --check assets/js/i18n.js

Expected: exit code 0.

git add tools/public-service-phones.md assets/js/i18n.js index.md tools.md tests/public-service-phones.test.js
git commit -m "Add public service phones page shell"

Task 3: Implement safe filtering, rendering, copy, and persisted UI state

Files:

Interfaces:

const controller = fs.readFileSync('assets/js/public-service-phones.js', 'utf8');
assert.match(controller, /sitei18nchange/);
assert.match(controller, /jiangyu-public-service-phones-state/);
assert.match(controller, /navigator\.clipboard/);
assert.match(controller, /textContent/);
assert.match(controller, /noopener noreferrer/);

Run: node tests/public-service-phones.test.js

Expected: failure because the controller file does not yet exist.

const state = { countryId: 'china', categoryId: '', query: '' };
const recordNode = document.createElement('article');
const number = document.createElement('strong');
number.textContent = record.phone;
const source = document.createElement('a');
source.href = record.sourceUrl;
source.target = '_blank';
source.rel = 'noopener noreferrer';

Validate persisted IDs against countries and categories; if JSON is malformed or IDs are unknown, use China and all categories. Build the source and direct-call links only after isSafeUrl and toTelHref pass. Copy only the raw phone string, show transient accessible feedback, and do not upload any search text. On filter input, update state, persist it, and render the matching result list. On sitei18nchange, update the active language and render again without changing state.

Run: node tests/public-service-phones.test.js; node --check assets/js/public-service-phones.js; node --check assets/js/public-service-phones-data.js; node --check assets/js/i18n.js

Expected: exit code 0.

git add assets/js/public-service-phones.js tests/public-service-phones.test.js
git commit -m "Add public service phone directory interactions"

Task 4: Style and release the directory

Files:

Interfaces:

const css = fs.readFileSync('assets/css/style.css', 'utf8');
assert.match(css, /\.public-phone-/);
assert.match(css, /@media/);
assert.doesNotMatch(page, /http:\/\//);

Run: node tests/public-service-phones.test.js

Expected: failure because scoped directory styling is not yet present.

.public-phone-warning { border-inline-start: 4px solid var(--accent); }
.public-phone-results { display: grid; gap: 0.9rem; }
@media (min-width: 760px) { .public-phone-results { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

Style category controls, filter controls, cards, number emphasis, action buttons, verification metadata, and the empty state. Keep text legible and controls keyboard-visible without changing existing tool styles.

Run: node tests/public-service-phones.test.js; node --check assets/js/public-service-phones-data.js; node --check assets/js/public-service-phones.js; node --check assets/js/i18n.js; git diff --check; git status --short

Expected: all Node and diff checks exit 0; status contains only the intended feature files.

Open each sourceUrl from phoneRecords; confirm the listed number, owner, and category match the official page. Remove any record that cannot be confirmed. Re-run the Task 4 verification command after any data correction.

git add assets/css/style.css tests/public-service-phones.test.js
git commit -m "Style public service phone directory"

Run the repository’s normal feature-branch integration flow, then push the reviewed main branch:

git push origin main