Compare commits

..

2 Commits

Author SHA1 Message Date
cesnimda 55adc1dbd7 docs(i18n): resolve conditional keys and triage display literals 2026-11-02 11:40:29 +01:00
cesnimda 138d8fbb6d docs(i18n): preserve partial source-bound localisation audit 2026-11-02 06:02:02 +01:00
27 changed files with 66605 additions and 647 deletions
-4
View File
@@ -3,7 +3,3 @@
The deployment script and Gitea workflow live in this directory and `.gitea/workflows/`. The current deployment, health/readiness, rollback, and secret-handling guidance is in the repository [infrastructure guide](../docs/infrastructure/README.md), especially [deployment](../docs/infrastructure/deployment.md) and [disaster recovery](../docs/infrastructure/disaster-recovery.md). The deployment script and Gitea workflow live in this directory and `.gitea/workflows/`. The current deployment, health/readiness, rollback, and secret-handling guidance is in the repository [infrastructure guide](../docs/infrastructure/README.md), especially [deployment](../docs/infrastructure/deployment.md) and [disaster recovery](../docs/infrastructure/disaster-recovery.md).
Keep the server `.env` outside the repository. Do not run production deployment, restore, migration, or credential rotation as part of local documentation work. Keep the server `.env` outside the repository. Do not run production deployment, restore, migration, or credential rotation as part of local documentation work.
MariaDB backup connection parsing uses `scripts/parse-backup-connection.py` (Python 3, already required by deployment preflight). Keys are case-insensitive and support the documented host/user/password/database aliases. Single/double-quoted values may contain semicolons and doubled quote characters. The dump contract requires one TCP host, a database and user, with port 1-65535 (default 3306); an empty password is retained. Duplicate aliases, control characters, host lists/socket paths and database path separators are rejected before dumping. This deliberately bounded parser is not the full MySqlConnector configuration engine. Additional connection options do not change existing dump-client TLS behavior; that qualification remains separate.
Run `python scripts/test-deploy-backup.py` for the offline synthetic dump-boundary regression. It extracts only the backup function and never executes the deployment entry point, contacts a database or reads the production environment.
+22 -40
View File
@@ -326,19 +326,19 @@ backup_database() {
return 1 return 1
fi fi
# Keep secrets off argv; parse aliases/quoted separators without evaluating shell text. # Parse the ADO.NET connection string. Keys are case-insensitive in .NET, so match that.
local db_host db_port db_name db_user db_pass parsed_connection local db_host db_port db_name db_user db_pass
local -a connection_fields=() db_host="$(sed -n 's/.*[Ss]erver=\([^;]*\).*/\1/p' <<<"$cs")"
if ! parsed_connection="$(printf '%s' "$cs" | python3 scripts/parse-backup-connection.py)"; then db_port="$(sed -n 's/.*[Pp]ort=\([^;]*\).*/\1/p' <<<"$cs")"
echo "Could not parse JOBTRACKER_CONNECTION_STRING for backup. Aborting deploy." db_name="$(sed -n 's/.*[Dd]atabase=\([^;]*\).*/\1/p' <<<"$cs")"
db_user="$(sed -n 's/.*[Uu]ser[ ]*[Ii]*[Dd]*=\([^;]*\).*/\1/p' <<<"$cs")"
db_pass="$(sed -n 's/.*[Pp]assword=\([^;]*\).*/\1/p' <<<"$cs")"
db_port="${db_port:-3306}"
if [ -z "$db_host" ] || [ -z "$db_name" ] || [ -z "$db_user" ]; then
echo "Could not parse host/database/user from JOBTRACKER_CONNECTION_STRING. Aborting deploy."
return 1 return 1
fi fi
mapfile -t connection_fields <<< "$parsed_connection"
db_host="${connection_fields[0]}"
db_port="${connection_fields[1]}"
db_name="${connection_fields[2]}"
db_user="${connection_fields[3]}"
db_pass="${connection_fields[4]}"
local target="$BACKUP_DIR/jobtracker-${db_name}-${BACKUP_STAMP}.sql.gz" local target="$BACKUP_DIR/jobtracker-${db_name}-${BACKUP_STAMP}.sql.gz"
echo "Backing up MariaDB database '${db_name}' on ${db_host}:${db_port} to ${target}" echo "Backing up MariaDB database '${db_name}' on ${db_host}:${db_port} to ${target}"
@@ -525,14 +525,12 @@ apply_backup_retention() {
return 1 return 1
fi fi
local month monthly_root monthly_dir complete_tmp local month monthly_root monthly_dir
month="$(TZ="$timezone" date +%Y-%m)" month="$(TZ="$timezone" date +%Y-%m)"
monthly_root="$BACKUP_DIR/monthly" monthly_root="$BACKUP_DIR/monthly"
monthly_dir="$monthly_root/$month" monthly_dir="$monthly_root/$month"
if ! mkdir -p "$monthly_root" || ! chmod 700 "$monthly_root"; then mkdir -p "$monthly_root"
echo "Could not prepare monthly backup directory. Aborting deploy." chmod 700 "$monthly_root"
return 1
fi
local current_files=("$BACKUP_DIR"/jobtracker-*"-$BACKUP_STAMP".*) local current_files=("$BACKUP_DIR"/jobtracker-*"-$BACKUP_STAMP".*)
if [ ! -e "${current_files[0]}" ]; then if [ ! -e "${current_files[0]}" ]; then
@@ -541,21 +539,11 @@ apply_backup_retention() {
fi fi
if [ ! -f "$monthly_dir/.complete" ]; then if [ ! -f "$monthly_dir/.complete" ]; then
if ! mkdir -p "$monthly_dir" || ! chmod 700 "$monthly_dir" || mkdir -p "$monthly_dir"
! cp --preserve=mode,timestamps -- "${current_files[@]}" "$monthly_dir/"; then chmod 700 "$monthly_dir"
echo "Could not retain monthly recovery files. Aborting deploy." cp --preserve=mode,timestamps -- "${current_files[@]}" "$monthly_dir/"
return 1 printf '%s\n' "$BACKUP_STAMP" > "$monthly_dir/.complete"
fi chmod 600 "$monthly_dir/.complete"
if ! complete_tmp="$(mktemp "$monthly_dir/.complete.XXXXXX")"; then
echo "Could not create monthly recovery marker. Aborting deploy."
return 1
fi
if ! printf '%s\n' "$BACKUP_STAMP" > "$complete_tmp" ||
! mv -T -- "$complete_tmp" "$monthly_dir/.complete"; then
rm -f -- "$complete_tmp"
echo "Could not publish monthly recovery marker. Aborting deploy."
return 1
fi
echo "Monthly recovery set retained: ${monthly_dir}" echo "Monthly recovery set retained: ${monthly_dir}"
fi fi
@@ -586,20 +574,14 @@ upload_current_backup_set() {
remote="${remote%/}" remote="${remote%/}"
local current_files=("$BACKUP_DIR"/jobtracker-*"-$BACKUP_STAMP".*) file local current_files=("$BACKUP_DIR"/jobtracker-*"-$BACKUP_STAMP".*) file
for file in "${current_files[@]}"; do for file in "${current_files[@]}"; do
if ! rclone copyto "$file" "$remote/daily/$(basename "$file")" --immutable; then rclone copyto "$file" "$remote/daily/$(basename "$file")" --immutable
echo "Daily off-host backup upload failed. Aborting deploy."
return 1
fi
done done
local month="$(TZ="${BACKUP_TIMEZONE:-Europe/Oslo}" date +%Y-%m)" local month="$(TZ="${BACKUP_TIMEZONE:-Europe/Oslo}" date +%Y-%m)"
if [ -d "$BACKUP_DIR/monthly/$month" ]; then if [ -d "$BACKUP_DIR/monthly/$month" ]; then
if ! rclone copy "$BACKUP_DIR/monthly/$month" "$remote/monthly/$month" --immutable; then rclone copy "$BACKUP_DIR/monthly/$month" "$remote/monthly/$month" --immutable
echo "Monthly off-host backup upload failed. Aborting deploy."
return 1
fi
fi fi
echo "Current recovery set copied to configured off-host remote." echo "Current recovery set copied to configured encrypted off-host remote."
} }
resolve_existing_web_proxy_subnet resolve_existing_web_proxy_subnet
+26
View File
@@ -0,0 +1,26 @@
# I18N-00 baseline audit
Date: 2026-11-02. Verdict: **IN_PROGRESS; partial research checkpoint, not full I18N-00 acceptance. I18N-01 through 08 BLOCKED. Production NO-GO.**
Research only, authorised by master prompt and continuous-progress instruction. Branch `feature/i18n-00-audit`, worktree `C:/Users/Connor/.codex/worktrees/i18n-00-audit/Job tracker`, baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Application/API/tests/UI/sidecar diff against `ba04d817` was empty. This is the accepted conditional development handoff, not blanket runtime, language or production acceptance. No controller or approval state was changed. All ten supplied I18N prompts were read. Their implementation requirements are future specifications, not authority to execute them.
Architecture authority is `docs/architecture/current.md`, which redirects to `overview.md`. Existing React/MUI, ASP.NET/EF/MariaDB, Python sidecar and rendering ownership remain. Graph absent in this baseline: bounded source inspection used; no graph rebuild needed for documentation.
## Verified source findings
* `job-tracker-ui/src/i18n/translations.ts`: custom two-language `en | nb` object; it is not an i18next or FormatJS installation.
* `I18nProvider.tsx`: local storage accepts `nb`, `nb-NO`, `no`; otherwise English. Auth hydration and cross-tab storage update language. `{word}` substitution handles simple values, not ICU plural/select grammar. Failed preference save offers retry; sequence tracking avoids stale error handling.
* `JobTrackerApi/Controllers/AuthController.cs:601`: language endpoint accepts `en` or Bokmål aliases, writes `nb-NO`; unsupported input returns hardcoded English error. Do not rename canonical English to `en-GB` without migration tests.
* `CoverLetterDocumentRenderer.cs:36`: labels distinguish `nb-NO` from English; document HTML has its own language. `ThemedCvRenderer.cs:60` derives HTML language from document settings. UI language must not become the output-language owner.
* `HumanLanguageCatalog.cs` is CV skill-language recognition, including environment-independent seeds and variant collapsing. It is not an eligible UI-locale registry; reusing it would lose script/region distinctions.
* Manifest dependencies: React `^19.2.4`, Next `16.4.1`, MUI `^7.3.9`, TypeScript `^5.9.3`, date-fns `^4.1.0`. These are manifest ranges, not freshly installed/runtime versions. No i18next/FormatJS direct dependency. Lockfile/runtime compatibility must be qualified before implementation.
## Gate evidence
`docs/pre-phase10-ux/ACCEPTANCE.md` grants conditional development review, not Phase 10 implementation. `docs/automation/handoffs/pre-phase10/FINAL_DEVELOPMENT_ENTRY.md` separates P10-00 assessment from candidate-specific P10-01 permission. This checkpoint cannot grant either. Product-review and launch-site accepted decisions are absent from this baseline; absence is not permission. Global Jobs planning exists separately at `0ec9f98` in the main workspace (`docs/global-jobs/00/DEPENDENCIES_AND_GATES.md` inspected read-only); it is not integrated here and does not authorise live providers or GJ-01.
## Honest limits
The TypeScript AST inventory contains 3,119 existing English catalogue entries and 3,164 syntactically identified static call sites across 134 non-test frontend files. It records 188 dynamic/unknown calls separately as an unresolved count; 853 catalogue entries have no identified static caller. These are not deletion candidates: wrappers, dynamic keys and direct object access remain unresolved. A subsequent partial JSX/accessibility literal inventory now contains 26 source-context-reviewed candidates. CONTEXT_TRIAGE.md records the current 80 unresolved dynamic calls after 108 finite-branch resolutions. This is still not a complete non-catalogue inventory. Every-string attribution, translator context, accessible states and country-specific legal recognition require additional review. Therefore the stage's complete-inventory criterion has not passed. No translations, native approval, screenshots, model results, browser results or locale release are manufactured.
The country matrix covers an initial 59 territory/market contexts from versioned CLDR 49, with targeted official-record checks for Norway, Switzerland and Serbia. The remaining legal cross-checks are pending. The draft catalogue contains 60 planned locale candidates, not 60 supported languages. Schema, uniqueness, BCP-47 parsing and rejection of a forged released status passed using existing local tools. Node/ICU canonicalises `cnr-ME` to `sr-ME`; retain this unresolved identity issue rather than accepting an automatic product alias. See BASELINE_TESTS.md.
+19
View File
@@ -0,0 +1,19 @@
# Planning evidence and baseline checks
2026-11-02. Baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`, branch `feature/i18n-00-audit`. Documentation only. No runtime code, test code, lockfiles or settings changed.
| Check | Result | Scope |
| --- | --- | --- |
| `git diff ba04d817fed26d659bfa2a56b089d6cac851dc34 HEAD --stat -- JobTrackerApi JobTrackerApi.Tests JobTrackerApi.MariaDb.Tests job-tracker-ui tools/summarizer` | PASSED: empty diff, exit 0 | Source identity only; no fresh runtime assertion |
| English catalogue AST extraction using existing TypeScript | PASSED: 3,119 unique keys | Property assignments required to be string literals; duplicate/unsupported members throw; 134 non-test TS/TSX files scanned |
| Static syntactic `t` call association | 3,164 sites; 188 dynamic/unknown calls; 853 entries without static caller | Partial inventory, not symbol resolution or coverage proof; no dead-code inference |
| Existing Ajv draft-07 validation | PASSED: 60 entries | `LOCALE_SCHEMA.json` compiled and validated `LOCALE_CATALOG.json`; no installed dependency changes |
| Uniqueness and `Intl.getCanonicalLocales` parsing | PASSED: 60 unique parseable tags | Node v24.19.0, ICU 78.3; syntax acceptance is not runtime translation availability |
| Negative publication fixture | PASSED | In-memory copy changed first `surfaces.ui` from `planned` to `released`; validator rejected it |
| Montenegrin identity | OPEN | Intl maps `cnr-ME` to `sr-ME`; product semantics require explicit resolution before registry implementation |
| Product/unit/browser/MariaDB/Python suites | NOT RUN | No implementation changes; existing accepted evidence remains historical |
| Native/legal/AT/device/PDF/hosted/production/provider/model qualification | NOT RUN | No authority or claim conferred by planning checks |
The inventory records its baseline, parser version, exact source SHA-256 and source line for each English message. Callers retain expressions and file/line, making manual review possible. Extraction covers the `en` object in `translations.ts`, excludes `*.test.*`, and associates literal first arguments of expressions named `t` or ending in `.t`. Dynamic keys, aliases, direct property reads, C#/Python messages and hardcoded JSX are not completely captured. Recompute after source changes; do not extrapolate counts to full application completeness.
Research sources checked on this date: CLDR 49 territory table, EU official-language list, i18next plural documentation, FormatJS ICU syntax, Norwegian Language Act, Swiss federal multilingualism guidance and Serbian government constitution. Specific links and limitations are in the matrix and implementation plan. No production data or private user text was sent to translation services.
+31
View File
@@ -0,0 +1,31 @@
# Source-context triage continuation
2026-11-02, unchanged application baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`. No translation or runtime implementation.
## Completed inventory refinement
- All 188 previously counted dynamic/unknown `t` calls now have source file/line/hash and exact expression in DYNAMIC_MESSAGE_CALLS.json.
- 108 calls resolve to finite literal conditional branches, accounting for 240 possible-key references. Each referenced key exists in the English catalogue. Nested ternaries are included. This is key resolution, not translator or runtime acceptance.
- 80 calls still need lookup-table, wrapper, template-token or runtime-key review. Type assertions such as `as any` or a single asserted literal must not be treated as proof of actual possible values.
- Four direct catalogue reads were located in I18nProvider: save-failure/retry feedback and the evaluator/fallback. These are active consumers missed by a `t("key")`-only search; preserve them during migration.
- 26 JSX text/selected accessibility-label candidates were extracted and individually reviewed in source context. The JSON records preservation/extraction decisions and excludes adjacent private runtime values. This scan does not cover every computed JSX expression, object-held label, string-returning helper, backend string or app-shell document text.
## Findings to carry into approved implementation
| ID | Source | Finding / required handling |
| --- | --- | --- |
| I00-13 | `App.tsx:488` | Session-invalidated alert is hardcoded English. Localisation must preserve disposal of the old application tree and must not depend on its discarded session/provider state |
| I00-14 | `JobFlowBar.tsx:147` | `Flow` is controlled UI copy outside the catalogue; extract with workflow context |
| I00-15 | `Attachments.tsx:361` | Preview iframe title is only `pdf`; use descriptive localised purpose while keeping private filenames out of translator exports |
| I00-16 | `AppShell.tsx:692` | `EN`/`NO` with `English`/`Norsk` labels conflates country-like abbreviation and Norwegian variant. Future selector needs native/English names and explicit Bokmal/Nynorsk distinction; preserve existing nb aliases |
| I00-17 | `ApplicationAssets.tsx:360` | File-size display uses `toFixed(1)` with KB. Review locale number formatting; filename and underlying byte count remain unchanged |
| I00-18 | `I18nProvider.tsx:43` | Save-failure feedback accesses catalogue directly. Migration must preserve same-language retry and stale-save sequencing; static t-call inventory alone misses this contract |
| I00-19 | `LandingPage.tsx:133` | Technology-name list is not ordinary translatable prose. Coordinate user-outcome copy with homepage design work; do not translate trademarks or add unverified feature claims |
Brand/provider names, host examples and technical identifiers were not blindly classified as missing translations. Unit/version fragments require formatting and accessibility context rather than mechanical dictionary substitution. All findings are planning tasks, not permission to edit application files.
## Verification and remaining work
The same 134 non-test TS/TSX files were scanned using the installed TypeScript AST parser. Assertions confirmed exactly 188 dynamic calls and that all 240 resolved branch-key references exist. No full application typecheck or browser suite was run. The 26 literal records were reviewed against adjacent source lines; broader semantic census remains IN_PROGRESS.
Next: resolve the remaining 80 table/template/wrapper calls; cover computed frontend values and app document metadata; inventory backend errors, queued communication text and Python processing messages. Country legal records, reviewer allocation and runtime-library choice remain separate unfinished tasks.
+47
View File
@@ -0,0 +1,47 @@
{
"baseline": "5cb903b989742b7a96cb22ddf4c0e24993b18980",
"method": "TypeScript AST syntactic triage; no symbol resolution or runtime coverage claim",
"filesScanned": 134,
"entries": [
{
"file": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"line": 43,
"sourceSha256": "95a930915cb2240547cc1a91af7cb0a475bc8f46ac75304b3241a401928ed3e4",
"kind": "direct-catalogue-access",
"text": "translations[l].settingsLanguageSaveFailed",
"ownerComponent": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"contextStatus": "pending",
"sensitivity": "unclassified-template; do-not-export-runtime-values"
},
{
"file": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"line": 44,
"sourceSha256": "95a930915cb2240547cc1a91af7cb0a475bc8f46ac75304b3241a401928ed3e4",
"kind": "direct-catalogue-access",
"text": "translations[l].retry",
"ownerComponent": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"contextStatus": "pending",
"sensitivity": "unclassified-template; do-not-export-runtime-values"
},
{
"file": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"line": 67,
"sourceSha256": "95a930915cb2240547cc1a91af7cb0a475bc8f46ac75304b3241a401928ed3e4",
"kind": "direct-catalogue-access",
"text": "translations[language][key]",
"ownerComponent": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"contextStatus": "pending",
"sensitivity": "unclassified-template; do-not-export-runtime-values"
},
{
"file": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"line": 67,
"sourceSha256": "95a930915cb2240547cc1a91af7cb0a475bc8f46ac75304b3241a401928ed3e4",
"kind": "direct-catalogue-access",
"text": "translations.en[key]",
"ownerComponent": "job-tracker-ui/src/i18n/I18nProvider.tsx",
"contextStatus": "pending",
"sensitivity": "unclassified-template; do-not-export-runtime-values"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
# Europe country and language research matrix
Research checkpoint 2026-11-02; not a market-launch list or legal opinion. All entries are candidate scope. CLDR 49 provides an initial language relationship dataset; national/regional official-record cross-checks remain incomplete. Blank CLDR status is not proof of absence or recognition. No tag decides sovereignty, EU membership, provider rights or support. XK is a CLDR user-assigned territory code, not ISO 3166-1; do not silently persist it as an ISO identifier.
Source: [Unicode CLDR 49 territory-language data](https://www.unicode.org/cldr/charts/49/tsv/territory_language_information.tsv); retrieved SHA-256 `29bcadbb4362e1a7a3685f4bf199152809635acca58fab10a3576a0f7b55868c`. Population percentages are deliberately omitted; they are not product usage evidence.
| Territory | Code | CLDR official/de-facto | CLDR regional/recognised | Legal cross-check |
| --- | --- | --- | --- | --- |
| Andorra | AD | ca (official) | not recorded; further review required | PENDING |
| Albania | AL | sq (official) | not recorded; further review required | PENDING |
| Austria | AT | de (official) | hr (official_regional), sl (official_regional), hu (official_regional) | PENDING |
| Åland Islands | AX | sv (official) | not recorded; further review required | PENDING |
| Bosnia & Herzegovina | BA | bs (official), bs-Cyrl (official), hr (official), sr (official), sr-Latn (official) | not recorded; further review required | PENDING |
| Belgium | BE | nl (official), fr (official), de (official) | not recorded; further review required | PENDING |
| Bulgaria | BG | bg (official) | not recorded; further review required | PENDING |
| Belarus | BY | ru (official), be (official) | not recorded; further review required | PENDING |
| Switzerland | CH | de (official), gsw (de_facto_official), fr (official), it (official) | rm (official_regional) | PENDING |
| Cyprus | CY | el (official), tr (official) | not recorded; further review required | PENDING |
| Czechia | CZ | cs (official) | not recorded; further review required | PENDING |
| Germany | DE | de (official) | frr (official_regional) | PENDING |
| Denmark | DK | da (official) | de (official_regional), kl (official_regional) | PENDING |
| Estonia | EE | et (official) | not recorded; further review required | PENDING |
| Spain | ES | es (official) | ca (official_regional), gl (official_regional), eu (official_regional), ast (official_regional), oc (official_regional) | PENDING |
| Finland | FI | fi (official), sv (official) | sms (official_regional) | PENDING |
| Faroe Islands | FO | fo (official) | not recorded; further review required | PENDING |
| France | FR | fr (official) | not recorded; further review required | PENDING |
| United Kingdom | GB | en (official) | cy (official_regional), ga (official_regional), gd (official_regional) | PENDING |
| Guernsey | GG | en (official) | not recorded; further review required | PENDING |
| Gibraltar | GI | en (official) | not recorded; further review required | PENDING |
| Greece | GR | el (official) | not recorded; further review required | PENDING |
| Croatia | HR | hr (official) | it (official_regional), vec (official_regional) | PENDING |
| Hungary | HU | hu (official) | not recorded; further review required | PENDING |
| Ireland | IE | en (official), ga (official) | not recorded; further review required | PENDING |
| Isle of Man | IM | en (official), gv (official) | not recorded; further review required | PENDING |
| Iceland | IS | is (official) | not recorded; further review required | PENDING |
| Italy | IT | it (official) | fr (official_regional), vec (official_regional) | PENDING |
| Jersey | JE | en (official) | not recorded; further review required | PENDING |
| Liechtenstein | LI | de (official), gsw (de_facto_official) | not recorded; further review required | PENDING |
| Lithuania | LT | lt (official) | not recorded; further review required | PENDING |
| Luxembourg | LU | fr (official), lb (official), de (official) | not recorded; further review required | PENDING |
| Latvia | LV | lv (official) | not recorded; further review required | PENDING |
| Monaco | MC | fr (official) | not recorded; further review required | PENDING |
| Moldova | MD | ro (official) | not recorded; further review required | PENDING |
| Montenegro | ME | sr-Latn (official) | not recorded; further review required | PENDING |
| North Macedonia | MK | mk (official) | sq (official_regional) | PENDING |
| Malta | MT | mt (official), en (official) | not recorded; further review required | PENDING |
| Netherlands | NL | nl (official) | fy (official_regional) | PENDING |
| Norway | NO | nb (official), no (official), nn (official) | se (official_regional) | PENDING |
| Poland | PL | pl (official) | de (official_regional), csb (official_regional), lt (official_regional) | PENDING |
| Portugal | PT | pt (official) | not recorded; further review required | PENDING |
| Romania | RO | ro (official) | not recorded; further review required | PENDING |
| Serbia | RS | sr (official), sr-Latn (official) | hu (official_regional), ro (official_regional), hr (official_regional), sk (official_regional), uk (official_regional) | PENDING |
| Russia | RU | ru (official) | tt (official_regional), ce (official_regional), ba (official_regional), av (official_regional), kbd (official_regional), myv (official_regional), kum (official_regional), sah (official_regional), inh (official_regional), bua (official_regional), az-Cyrl (official_regional), lez (official_regional), mdf (official_regional), krc (official_regional), tyv (official_regional), udm (official_regional), ady (official_regional), kv (official_regional), lbe (official_regional), koi (official_regional) | PENDING |
| Sweden | SE | sv (official) | fi (official_regional) | PENDING |
| Slovenia | SI | sl (official) | vec (official_regional) | PENDING |
| Svalbard & Jan Mayen | SJ | nb (official) | not recorded; further review required | PENDING |
| Slovakia | SK | sk (official) | not recorded; further review required | PENDING |
| San Marino | SM | it (official) | not recorded; further review required | PENDING |
| Türkiye | TR | tr (official) | not recorded; further review required | PENDING |
| Ukraine | UA | uk (official), ru (de_facto_official) | not recorded; further review required | PENDING |
| Vatican City | VA | it (de_facto_official) | not recorded; further review required | PENDING |
| Kosovo | XK | sq (official), sr (official), sr-Latn (official) | not recorded; further review required | PENDING |
| Armenia | AM | hy (official) | not recorded; further review required | PENDING |
| Azerbaijan | AZ | az (official), az-Cyrl (official) | not recorded; further review required | PENDING |
| Georgia | GE | ka (official) | ab (official_regional), os (official_regional) | PENDING |
| Kazakhstan | KZ | ru (official), kk (official) | not recorded; further review required | PENDING |
| Greenland | GL | kl (official) | not recorded; further review required | PENDING |
## Scope and legal cross-check queue
Russia, Turkey, Cyprus, Armenia, Azerbaijan, Georgia and Kazakhstan are explicit geographic/market-review cases; inclusion here does not approve a market. Greenland, Faroe Islands, Aland, Crown dependencies, Gibraltar and Svalbard/Jan Mayen are separately considered territory contexts. This is an initial 59-code research envelope, not a claim to have exhausted every dependency or minority language.
Norway: [Language Act](https://lovdata.no/dokument/NLE/lov/2021-05-21-42) distinguishes Bokmal/Nynorsk and protected Sami/minority languages. Plan distinct nb-NO/nn-NO and investigate Sami/Kven review capacity; do not infer their status from the abbreviated CLDR row.
Switzerland: [federal multilingualism explanation](https://www.aboutswitzerland.eda.admin.ch/en/multilingualism) distinguishes national languages and official usage. Plan de-CH, fr-CH, it-CH and rm-CH with separate reviewers.
Serbia: [government constitution](https://www.srbija.gov.rs/tekst/56753/ustav-republike-srbije.php) establishes Serbian/Cyrillic official use. sr-Latn-RS remains an explicit product candidate, separately from sr-Cyrl-RS; not a claim of identical legal status. These source checks inform planning, not exhaustive legal clearance.
Regional/minority queue includes ca-ES, eu-ES, gl-ES, cy-GB, gd-GB, se-NO, smj-NO, sma-NO and fkv-NO. Country-specific recognition, applicable areas, reviewer capacity, orthography, fonts and locale-data support must be reviewed separately. No automatic mapping of Montenegrin to Serbian; cnr-ME remains a candidate requiring runtime/data validation.
## EU 24 language baseline
[EU official language list](https://european-union.europa.eu/principles-countries-history/languages_en), checked 2026-11-02: Bulgarian bg-BG; Croatian hr-HR; Czech cs-CZ; Danish da-DK; Dutch nl-NL; English en (preserve existing canonical ID; en-GB candidate); Estonian et-EE; Finnish fi-FI; French fr-FR; German de-DE; Greek el-GR; Hungarian hu-HU; Irish ga-IE; Italian it-IT; Latvian lv-LV; Lithuanian lt-LT; Maltese mt-MT; Polish pl-PL; Portuguese pt-PT; Romanian ro-RO; Slovak sk-SK; Slovenian sl-SI; Spanish es-ES; Swedish sv-SE. These locale choices are proposals, not mandated by the EU list.
## Proposed waves
Foundation candidates: preserve existing en/nb behavior, then nn-NO, sv-SE, da-DK, fi-FI and is-IS if reviewers exist. EU-24 next; broader national/script/region candidates thereafter. Actual usage analytics and reviewer allocation are unavailable, so no evidence-backed commercial ranking or promised dates are asserted. English fallback remains available irrespective of market.
@@ -0,0 +1,319 @@
{
"baseline": "5cb903b989742b7a96cb22ddf4c0e24993b18980",
"method": "TypeScript AST syntactic triage; no symbol resolution or runtime coverage claim",
"filesScanned": 134,
"entries": [
{
"file": "job-tracker-ui/src/App.tsx",
"line": 488,
"sourceSha256": "3b5645d12f2c92ed8999d7a072bd7d47fac77023d460a19a9b3bfc2a4191962f",
"kind": "jsx-text",
"text": "Session changed. Clearing this page…",
"ownerComponent": "job-tracker-ui/src/App.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "localise-with-session-boundary-preservation",
"translatorContext": "Alert rendered while old application tree is invalidated; preserve unmount/privacy behavior and avoid dependencies on the discarded session/provider tree."
},
{
"file": "job-tracker-ui/src/assets/JobbjaktMark.tsx",
"line": 7,
"sourceSha256": "5a539514876a66eaf17ec68eb7ce3d28c558384bc24ab76b61f73be8df754473",
"kind": "jsx-attribute:aria-label",
"text": "Jobbjakt",
"ownerComponent": "job-tracker-ui/src/assets/JobbjaktMark.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/components/ApplicationAssets.tsx",
"line": 198,
"sourceSha256": "25216c80ef92d519baad9e428a0e77390150605c1a3529a4809819d120661f29",
"kind": "jsx-text",
"text": "· v",
"ownerComponent": "job-tracker-ui/src/components/ApplicationAssets.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "version-or-unit-formatting-review",
"translatorContext": "Keep numeric/version identity; review locale number formatting and descriptive accessible labels. Do not translate user file/name data."
},
{
"file": "job-tracker-ui/src/components/ApplicationAssets.tsx",
"line": 360,
"sourceSha256": "25216c80ef92d519baad9e428a0e77390150605c1a3529a4809819d120661f29",
"kind": "jsx-text",
"text": "KB",
"ownerComponent": "job-tracker-ui/src/components/ApplicationAssets.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "version-or-unit-formatting-review",
"translatorContext": "Keep numeric/version identity; review locale number formatting and descriptive accessible labels. Do not translate user file/name data."
},
{
"file": "job-tracker-ui/src/components/ApplicationAssets.tsx",
"line": 699,
"sourceSha256": "25216c80ef92d519baad9e428a0e77390150605c1a3529a4809819d120661f29",
"kind": "jsx-text",
"text": "v",
"ownerComponent": "job-tracker-ui/src/components/ApplicationAssets.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "version-or-unit-formatting-review",
"translatorContext": "Keep numeric/version identity; review locale number formatting and descriptive accessible labels. Do not translate user file/name data."
},
{
"file": "job-tracker-ui/src/components/Attachments.tsx",
"line": 361,
"sourceSha256": "fe706526f36d92c10d2a0adfd7abb634d0ebee28f5d7e1794c590179b4fd884c",
"kind": "jsx-attribute:title",
"text": "pdf",
"ownerComponent": "job-tracker-ui/src/components/Attachments.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "descriptive-localised-accessible-title",
"translatorContext": "Attachment preview iframe title; describe document preview purpose, without exposing private filename in exported translator context."
},
{
"file": "job-tracker-ui/src/components/CoverLetterDocument.tsx",
"line": 157,
"sourceSha256": "17ea0c608db7bc05171fb3c44f378c1acaa78fbdd337f9330002ef6346b92546",
"kind": "jsx-text",
"text": "pt ·",
"ownerComponent": "job-tracker-ui/src/components/CoverLetterDocument.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "version-or-unit-formatting-review",
"translatorContext": "Keep numeric/version identity; review locale number formatting and descriptive accessible labels. Do not translate user file/name data."
},
{
"file": "job-tracker-ui/src/components/CoverLetterDocument.tsx",
"line": 157,
"sourceSha256": "17ea0c608db7bc05171fb3c44f378c1acaa78fbdd337f9330002ef6346b92546",
"kind": "jsx-text",
"text": "mm ·",
"ownerComponent": "job-tracker-ui/src/components/CoverLetterDocument.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "version-or-unit-formatting-review",
"translatorContext": "Keep numeric/version identity; review locale number formatting and descriptive accessible labels. Do not translate user file/name data."
},
{
"file": "job-tracker-ui/src/components/EmailProviderConnections.tsx",
"line": 227,
"sourceSha256": "b71228ee28ee73525014c8ece8690364e0f70ddaca3ee7211e9076123d120b1d",
"kind": "jsx-attribute:label",
"text": "Gmail",
"ownerComponent": "job-tracker-ui/src/components/EmailProviderConnections.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/components/EmailProviderConnections.tsx",
"line": 239,
"sourceSha256": "b71228ee28ee73525014c8ece8690364e0f70ddaca3ee7211e9076123d120b1d",
"kind": "jsx-attribute:label",
"text": "Outlook / Microsoft 365",
"ownerComponent": "job-tracker-ui/src/components/EmailProviderConnections.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/components/EmailProviderConnections.tsx",
"line": 264,
"sourceSha256": "b71228ee28ee73525014c8ece8690364e0f70ddaca3ee7211e9076123d120b1d",
"kind": "jsx-attribute:placeholder",
"text": "imap.example.com",
"ownerComponent": "job-tracker-ui/src/components/EmailProviderConnections.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/components/JobFlowBar.tsx",
"line": 147,
"sourceSha256": "643053f6ac883f23ef74c66def7a42678cd7917ed11ca802b11be25ec6df0cb2",
"kind": "jsx-text",
"text": "Flow",
"ownerComponent": "job-tracker-ui/src/components/JobFlowBar.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "extract-controlled-label",
"translatorContext": "Workflow overline in JobFlowBar; ordinary user-facing UI label, not a persisted status."
},
{
"file": "job-tracker-ui/src/components/QuickCommandDialog.tsx",
"line": 166,
"sourceSha256": "27cbbb8b8596fdf4e7474eccfdb6d9a8a219f4baff6a9b8013b825ce5fc2c411",
"kind": "jsx-text",
"text": "Ctrl/Cmd + K",
"ownerComponent": "job-tracker-ui/src/components/QuickCommandDialog.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "version-or-unit-formatting-review",
"translatorContext": "Keep numeric/version identity; review locale number formatting and descriptive accessible labels. Do not translate user file/name data."
},
{
"file": "job-tracker-ui/src/layout/AppShell.tsx",
"line": 312,
"sourceSha256": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"kind": "jsx-text",
"text": "Jobbjakt",
"ownerComponent": "job-tracker-ui/src/layout/AppShell.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/layout/AppShell.tsx",
"line": 415,
"sourceSha256": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"kind": "jsx-text",
"text": "Jobbjakt",
"ownerComponent": "job-tracker-ui/src/layout/AppShell.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/layout/AppShell.tsx",
"line": 563,
"sourceSha256": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"kind": "jsx-text",
"text": "commit",
"ownerComponent": "job-tracker-ui/src/layout/AppShell.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "localise-label-preserve-identifier",
"translatorContext": "Technical display label may be translated; commit/job identifier remains invariant and must not enter translation resources."
},
{
"file": "job-tracker-ui/src/layout/AppShell.tsx",
"line": 692,
"sourceSha256": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"kind": "jsx-attribute:aria-label",
"text": "English",
"ownerComponent": "job-tracker-ui/src/layout/AppShell.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "locale-selector-contract",
"translatorContext": "Replace ambiguous country-like abbreviation/undifferentiated Norwegian label only within approved locale-selector work; preserve current nb behavior and native labels."
},
{
"file": "job-tracker-ui/src/layout/AppShell.tsx",
"line": 692,
"sourceSha256": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"kind": "jsx-text",
"text": "EN",
"ownerComponent": "job-tracker-ui/src/layout/AppShell.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "locale-selector-contract",
"translatorContext": "Replace ambiguous country-like abbreviation/undifferentiated Norwegian label only within approved locale-selector work; preserve current nb behavior and native labels."
},
{
"file": "job-tracker-ui/src/layout/AppShell.tsx",
"line": 693,
"sourceSha256": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"kind": "jsx-attribute:aria-label",
"text": "Norsk",
"ownerComponent": "job-tracker-ui/src/layout/AppShell.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "locale-selector-contract",
"translatorContext": "Replace ambiguous country-like abbreviation/undifferentiated Norwegian label only within approved locale-selector work; preserve current nb behavior and native labels."
},
{
"file": "job-tracker-ui/src/layout/AppShell.tsx",
"line": 693,
"sourceSha256": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"kind": "jsx-text",
"text": "NO",
"ownerComponent": "job-tracker-ui/src/layout/AppShell.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "locale-selector-contract",
"translatorContext": "Replace ambiguous country-like abbreviation/undifferentiated Norwegian label only within approved locale-selector work; preserve current nb behavior and native labels."
},
{
"file": "job-tracker-ui/src/views/AdminAuditPage.tsx",
"line": 146,
"sourceSha256": "b78d0c5f6ec86a4ee3d2214e6854e638d9f73a5c69c8d5b01023d8c60b46ac81",
"kind": "jsx-text",
"text": "jobId=",
"ownerComponent": "job-tracker-ui/src/views/AdminAuditPage.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "localise-label-preserve-identifier",
"translatorContext": "Technical display label may be translated; commit/job identifier remains invariant and must not enter translation resources."
},
{
"file": "job-tracker-ui/src/views/AdminUsersPage.tsx",
"line": 274,
"sourceSha256": "674290a26fc279a25e43176194716dea87f427d1885ecce70dea7c368ab0c6f8",
"kind": "jsx-attribute:label",
"text": "—",
"ownerComponent": "job-tracker-ui/src/views/AdminUsersPage.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "empty-state-accessibility-review",
"translatorContext": "No-role placeholder; visual glyph can stay but verify meaningful accessible context."
},
{
"file": "job-tracker-ui/src/views/LandingPage.tsx",
"line": 99,
"sourceSha256": "2c550aa126e1db8786ed10b895d2403491ce50bf5073fc95b8a0fcb0cb0e083e",
"kind": "jsx-text",
"text": "Jobbjakt",
"ownerComponent": "job-tracker-ui/src/views/LandingPage.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/views/LandingPage.tsx",
"line": 133,
"sourceSha256": "2c550aa126e1db8786ed10b895d2403491ce50bf5073fc95b8a0fcb0cb0e083e",
"kind": "jsx-text",
"text": "React · TypeScript · ASP.NET Core · EF Core · FastAPI AI · Gmail",
"ownerComponent": "job-tracker-ui/src/views/LandingPage.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "launch-copy-review",
"translatorContext": "Technology list is public brand names but poor user-value copy; coordinate replacement with Launch Site design approval."
},
{
"file": "job-tracker-ui/src/views/career/CareerProfileSections.tsx",
"line": 100,
"sourceSha256": "0c772feb6bde8324676b0884aaf343283465c5cd3b3318128363063df4ea22ab",
"kind": "jsx-attribute:label",
"text": "GitHub",
"ownerComponent": "job-tracker-ui/src/views/career/CareerProfileSections.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "preserve-proper-name-or-example",
"translatorContext": "Public brand/provider label or synthetic host example; not user content. Retain original spelling; localise surrounding guidance separately."
},
{
"file": "job-tracker-ui/src/views/career/ProfileCompleteness.tsx",
"line": 55,
"sourceSha256": "95f017b1495cf9d88a2b69e7c6ae8ef83d55f2b1ad7b6f54895e280b147743b5",
"kind": "jsx-text",
"text": "v",
"ownerComponent": "job-tracker-ui/src/views/career/ProfileCompleteness.tsx",
"contextStatus": "source-context-reviewed; implementation-pending",
"sensitivity": "public-ui-template; adjacent runtime identifiers excluded",
"decision": "version-or-unit-formatting-review",
"translatorContext": "Keep numeric/version identity; review locale number formatting and descriptive accessible labels. Do not translate user file/name data."
}
]
}
+19
View File
@@ -0,0 +1,19 @@
# Partial I18N-00 research handoff
Branch `feature/i18n-00-audit`, based on accepted conditional handoff `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Only `docs/i18n/00/` changed. This checkpoint preserves research; I18N-00 acceptance remains IN_PROGRESS and I18N-01 implementation remains BLOCKED.
Completed: source-owner audit draft; ordered implementation contracts; source-bound English catalogue/static callers; initial 59-territory CLDR matrix; 60 planned locale candidates and schema; planning checks documented in BASELINE_TESTS.md. No locale translated, reviewed or released here.
Next authorised work, without implementation approval:
1. Resolve 188 dynamic/unknown calls, direct translation object access and wrappers. Review the 853 entries without static callers without treating them as unused.
2. Inventory hardcoded frontend/ARIA/empty/error/export text and backend/Python/email/queue strings. Assign real namespace, translator context, parameter sensitivity and owning component rather than inferring from keys.
3. Complete national/regional official-record checks beyond the initial Norway/Switzerland/Serbia checks. Expand territory/minority coverage where the current research envelope omits required contexts.
4. Evaluate actual installed/locked library compatibility and Montenegrin canonicalisation before choosing a runtime migration. Do not install packages now.
5. Obtain reviewer/market priorities when a concrete rollout proposal is ready. Keep human/native approval separate from planning completeness.
The delegated researcher was interrupted by a service usage limit; the parent continued, corrected references to nonexistent deliverables and produced the actual inventory/catalogue/matrix. No agent is working in the background. Full stage completion must not be inferred from the presence of the expected filenames.
Continuation: CONTEXT_TRIAGE.md records all 188 dynamic calls, 108 finite-branch resolutions (240 existing-key references), four direct catalogue reads and 26 source-context-reviewed JSX/accessibility literal candidates. Seven concrete planning findings I00-13 through I00-19 added. Remaining dynamic call queue is 80; prior 188 count is the original inventory total, not the remaining queue. No application changes.
Product tests NOT RUN; source equality and planning checks PASSED within their documented limits. Preserve existing PDF evidence. No application models, production services, orchestrator configuration or signing/approval material accessed.
+31
View File
@@ -0,0 +1,31 @@
# Ordered implementation specification — not execution authority
## Proposed architecture decision
Keep existing `useI18n` call sites and React/MUI owners. Replace only its message evaluation/storage behind that contract after entry approval. Compare: current substitution is smallest but cannot meet plural/select requirements; i18next offers namespace loading and CLDR plural suffixes but changes interpolation/catalogue shape; FormatJS ICU supports complete sentence plural/select messages behind the current helper with less caller churn. **Proposed preference: ICU message evaluation behind the existing API plus platform Intl formatting**, subject to actual lockfile/runtime compatibility, bundle and extraction prototype after approval. Do not install either now. Do not implement two competing message engines. .NET renders server communications from its own typed templates with shared IDs/parameter contracts; do not assume identical ICU parser support in .NET/Python.
Sources checked 2026-11-02: https://www.i18next.com/translation-function/plurals and https://formatjs.github.io/docs/core-concepts/icu-syntax/. These establish syntax capabilities, not repository compatibility.
## Independent preference contract
Residence `countryId`, `searchCountryIds`, typed location IDs, `uiLocale`, `adLanguageFilters`, document `outputLocale`, optional `communicationLocale` and `timeZone` are distinct. Country changes only suggestion ordering. Manual saved preference wins over URL/app choice, onboarding, browser negotiation and English fallback. Explicitly distinguish signed-out local storage from account hydration. Persist provenance and revision semantics without weakening tenant ownership. Existing `en` stays canonical until an approved compatibility migration; `en-GB` is a candidate variant, not a silent rewrite. `nb/no/nb-NO` legacy aliases preserve existing Bokmål behavior. `nn-NO` must never silently map to Bokmål. `sr-Latn` and `sr-Cyrl` retain script identity. Unsupported locale resolves to reliable English chrome; user content unchanged.
## Stage sequence and evidence
| Stage | Bounded change after prerequisites | Required exit evidence |
|---|---|---|
| 00 | Finish contextual census, country legal cross-check, reviewer ownership | Complete source-bound inventory; independent schema validation; planning review |
| 01 | Registry, aliases, separate preferences, migration, formatting | Legacy roundtrip, invalid IDs, tenant/multi-device conflicts, MariaDB migration/reversal; new locales hidden |
| 02 | Extract full UI and accessible strings into existing owners | Required-key completeness, placeholder parser, dirty-form language switch, pseudo/long-label responsive tests |
| 03 | Stable backend codes; queued communication templates | Event-time locale snapshot proposed, retry determinism, isolated Mailpit/plain/HTML tests, legal approvals |
| 04 | Per-document headings/fonts, import boundaries | RD21/history invariance; script/PDF extraction and layout; native/physical review distinct |
| 05 | GJ-owned geography/provider/search integration | All-industry synthetic fixtures, explicit alias suggestions, source language and rights/retention evidence |
| 06 | Translation supply chain and glossary | Licensed source, translator context, independent proficient review, source-change invalidation |
| 07 | Launch Site localisation/SEO contract | Approved visual design; published-only alternates/sitemaps; truthful feature/provider claims |
| 08 | Cross-surface/native/device/security acceptance | Locale × surface × market × profession × device matrix; exact commit and disjoint counts |
Phase 10 completion/appropriate accepted handoff, PRODUCT-REVIEW-01, accepted I18N-00 and explicit stage authority precede I18N-01. Sequential accepted stage integration is mandatory. Separate development commits, no main merge/deploy.
GJ owns source fetch, rights, stable geography/occupation IDs and search mechanics; I18N owns display labels, ad-language semantics and translated-summary labels. ESCO concept identity cannot be replaced by translated free text. Original advertisement always remains distinguishable. Provider rights are checked before requests, not inferred from locale availability. Launch Site owns visual design/routing architecture; I18N supplies market+content-locale+publication schema and claims binding to deployed capability. Future fixtures may be designed now but no dependent runtime is invented.
Terminology queue: Career Profile (canonical facts), CV (document variant), cover letter, application (pursuit), job (opportunity), apprenticeship, shift, seasonal work, salary, qualification, nursing/teaching licence. Define each English concept before translation; profession label never establishes accreditation. Planned fixtures include teacher, cook, nurse, electrician, construction worker, driver, cleaner, retail, office administrator and developer.
+846
View File
@@ -0,0 +1,846 @@
{
"baseline": "5cb903b989742b7a96cb22ddf4c0e24993b18980",
"status": "research-draft",
"locales": [
{
"tag": "en",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "en-GB",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "nb-NO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "nn-NO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "sv-SE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "da-DK",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "fi-FI",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "is-IS",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "bg-BG",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "hr-HR",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "cs-CZ",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "nl-NL",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "et-EE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "fr-FR",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "de-DE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "el-GR",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "hu-HU",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "ga-IE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "it-IT",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "lv-LV",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "lt-LT",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "mt-MT",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "pl-PL",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "pt-PT",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "ro-RO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "sk-SK",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "sl-SI",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "es-ES",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "uk-UA",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "sq-AL",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "sr-Latn-RS",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "sr-Cyrl-RS",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "bs-BA",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "cnr-ME",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "mk-MK",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "tr-TR",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "lb-LU",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "cy-GB",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "rm-CH",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "fo-FO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "be-BY",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "ka-GE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "hy-AM",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "az-Latn-AZ",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "ru-RU",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "fr-CH",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "de-CH",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "it-CH",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "nl-BE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "fr-BE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "de-BE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "en-IE",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "ca-ES",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "eu-ES",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "gl-ES",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "gd-GB",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "se-NO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "smj-NO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "sma-NO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
},
{
"tag": "fkv-NO",
"ownerRole": "localisation-product-owner",
"reviewer": null,
"status": "planned",
"surfaces": {
"ui": "planned",
"backend": "planned",
"communications": "planned",
"documents": "planned",
"jobDiscovery": "planned",
"marketing": "planned"
}
}
]
}
+81
View File
@@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Research-only locale catalogue; no release authority",
"type": "object",
"additionalProperties": false,
"required": [
"baseline",
"status",
"locales"
],
"properties": {
"baseline": {
"const": "5cb903b989742b7a96cb22ddf4c0e24993b18980"
},
"status": {
"const": "research-draft"
},
"locales": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"tag",
"ownerRole",
"reviewer",
"status",
"surfaces"
],
"properties": {
"tag": {
"type": "string",
"pattern": "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"
},
"ownerRole": {
"const": "localisation-product-owner"
},
"reviewer": {
"type": "null"
},
"status": {
"const": "planned"
},
"surfaces": {
"type": "object",
"additionalProperties": false,
"required": [
"ui",
"backend",
"communications",
"documents",
"jobDiscovery",
"marketing"
],
"properties": {
"ui": {
"const": "planned"
},
"backend": {
"const": "planned"
},
"communications": {
"const": "planned"
},
"documents": {
"const": "planned"
},
"jobDiscovery": {
"const": "planned"
},
"marketing": {
"const": "planned"
}
}
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
# Risks, blockers and independent approval
| ID | Risk / unresolved item | Owner and closure evidence |
|---|---|---|
| I00-01 | Catalogue scan is not every-string contextual inventory | UI/backend owners triage literals, all callers, runtime/ARIA/export states; no unknown required surface |
| I00-02 | CLDR is not complete national/regional legal authority | Product research owner checks official country records, political/territory scope and versioned licence before activating suggestions |
| I00-03 | No allocated translators/native reviewers | Product owner assigns named reviewers per locale/surface; critical legal/billing/security needs qualified review |
| I00-04 | Implementation predecessor gates unresolved | Owner records Phase10/product review and explicit I18N entry against accepted baseline; this document cannot approve them |
| I00-05 | Plurals, gender, inflection, word order | ICU whole-sentence contexts, Slavic/Irish/Welsh fixtures, native review; no fragment concatenation |
| I00-06 | Turkish casing, Greek accents, Serbian script, names | Locale-aware search/display separate from invariant IDs; preserve original names and qualification titles |
| I00-07 | Dates/timezones/DST, salary/currency | UTC storage, partial-date precision, explicit timezone; formatting never FX conversion |
| I00-08 | Fonts/glyphs, RTL readiness, long labels | Licensed extended Latin/Greek/Cyrillic font tests, logical layout and focus; RTL activation only when approved |
| I00-09 | Career fact loss, frozen PDF drift, stale writes | UI switch never mutates facts/history/undo; ApplicationAssetsService and revision contracts retained |
| I00-10 | Private content external translation | No PII export; separately approved consent/contracts/data controls before external translation; no live-model calls |
| I00-11 | Draft locale exposed as supported | Per-surface publication gate, English fallback; no selector/SEO claims from catalogue presence |
| I00-12 | Backend literal errors and queue drift | Stable error codes, typed escaped params, deterministic event-time locale and retry tests |
Public release requires required-key completeness, syntax/placeholders/plurals and escaping checks, whole critical journeys, native/proficient review, accessibility/device/PDF tests, country/provider truthfulness, owner release decision and actual deployed identity. Status advances separately per surface: planned → draft/machine_draft → review_required → reviewed → qa_passed → release_approved → released. No generated JSON grants review or approval. Approval evidence must be externally controlled and source-bound. All catalogue entries here are `planned`; existing en/nb code presence is independently recorded, not relabelled as newly qualified release.
Human/native/legal/device/physical-print review: pending, not executed. Provider and live-model tests: not run. Implementation/release: not authorised. Independent planning can continue on I00-01/02 without waiting for runtime gates.
+22
View File
@@ -0,0 +1,22 @@
# Surface inventory and message accounting
All rows need a source-bound contextual census before I18N-00 acceptance. Existing-catalogue inventory is in `MESSAGE_INVENTORY.json`; partial JSX/accessibility literals are now recorded in `FRONTEND_LITERAL_CANDIDATES.json`, with direct reads and dynamic calls in separate inventories. This is not a 100% coverage claim. Namespace assignments remain provisional until caller review; translator context cannot be inferred safely from key names alone. The AST inventory identifies syntactically named `t` calls, not symbol-resolved calls. See CONTEXT_TRIAGE.md for reviewed literal context and the 108 resolved conditional calls; 80 table/template/wrapper calls remain.
| Surface / proposed namespace | Source owner / inspection target | Content boundary and required evidence |
|---|---|---|
| Account, onboarding, recovery / auth | views LoginPage, ForgotPasswordPage, ResetPasswordPage, VerifyEmailPage; AuthController | Forms, ARIA, errors, consent, email templates, multi-device hydration; preserve token URLs |
| Dashboard, navigation, mobile / app | App routing, dashboard components | Hidden/empty/loading/error states, keyboard labels, tooltips and layout expansion |
| Jobs, discovery, applications / jobs | JobDiscoveryPage, ApplicationWorkspacePage; JobPipeline | Translate controlled status labels, never stored identity/status codes or original advertisements |
| Career facts, imports / career | CareerProfilePage, career/CvImportReview, CvImportMappingEditor; CareerProfileService; Python app.py | Original facts and uncertainty remain; parser/script quality separate from UI coverage |
| CV editor, templates, letters / documents | CvBuilderEditor, DocumentsPage; ThemedCvRenderer, CoverLetterDocumentRenderer, ApplicationAssetsService | Output locale independent; headings, dates, filename labels; preserve null-as-inherit and JobApplication.Version |
| PDF, public CV / exports | PublicCvPage, PlaywrightCvPdfExporter, CvDocumentFonts | Font licence/glyph matrix, selectable text, physical print, frozen snapshots and public access |
| Email, inbox, reminders / communications | CorrespondenceInboxPage, GmailReviewPage, SmtpEmailSender, FollowUpReminderHostedService | User correspondence is private content; subject/preheader/plain/HTML copy and queued locale policy |
| Settings, linked accounts / settings | ConnectedAccountsPage, EmailProviderConnections | Provider names stay proper nouns; OAuth errors and cancellation need usable fallback |
| Billing, subscriptions / billing | StripeBillingGateway, entitlement UI | Amount and currency invariant; legal/tax wording needs qualified approval |
| Admin and operations / admin | AdminUsersPage, AdminSystemPage, AdminProcessingPage, AdminAuditPage, OperationsPage; OperationEventLog | Stable diagnostic codes; localized presentation without multilingual log parsing or PII leaks |
| Analytics, comparisons / analytics | AnalyticsPage, OfferComparisonPage | Plurals, dates, currency and accessible chart/table labels |
| Marketing, SEO, legal / public | LandingPage, RouteErrorPage, NotFoundPage; future Launch Site | No locale claims, hreflang or indexing for draft pages; owner mockup gate untouched |
| Backend/API/queues / errors | Controllers, UserNotificationStore, AiOperationQueue, CvProcessingQueue | Stable code plus typed parameters; UTC storage; deterministic queue-recipient locale |
| Python/AI / processing | tools/summarizer/app.py | Machine codes vs user text, validation and document-language metadata; no live-model qualification |
For each required message record: stable ID, namespace, exact source, source hash/line, all callers, English text, context/screenshot, typed placeholders, plural/select variants, sensitivity, owner, translator and independent review state. Sensitive values are runtime parameters, never translation exports. Static public UI text is eligible; names, CV facts, messages, tokens, job ads and employer claims are not automatically translated. Existing flat keys remain stable through migration; do not bulk rename for aesthetics.
@@ -1,22 +0,0 @@
# Preliminary release discovery
2026-11-02. Branch `planning/release-readiness-discovery`; accepted conditional development baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Stage 1 IN_PROGRESS. No release candidate nominated. Production NO-GO. This report records source inspection and a bounded offline check, not full release qualification.
| Component / inspected owner | Dependency / failure boundary | Required release evidence |
| --- | --- | --- |
| `job-tracker-ui/package.json`, `Dockerfile`, `nginx.conf.template` | Next static export, React/MUI and React Router; nginx serves `out`, proxies `/api/`, `/health`, `/ready`; configured host and forwarded-header handling | Exact build/image, browser journeys and deployed proxy/TLS identity |
| `JobTrackerApi/Program.cs`, `JobTrackerApi.csproj` | net9.0, EF 9.0.14, Pomelo 9.0.0; authentication/authorization then rate limiting/session middleware and controllers | Endpoint-by-endpoint ownership/session/CSRF/error tests; source registration alone is not security proof |
| `JobTrackerApi/Data/DatabaseConfiguration.cs` | Defaults to MariaDB; missing connection throws; SQLite requires explicit transitional selection | Actual MariaDB migration/upgrade/rollback and preservation evidence; retain legacy paths |
| `docker-compose.database.yml` | Development persistent volume and separate integration tmpfs service; MariaDB 12.2.2 pinned digest; loopback ports | Isolated provider tests; no claim of equality with production |
| Career/CV domain and PDF | Program registers CareerProfileService, ApplicationAssetsService, CV/letter renderers and Playwright exporter | Canonical facts, null-as-inherit, revisions, historical artifacts and physical print remain separate contracts |
| Background services | Program registers backup, rules, reminders, saved search, export, enrichment, summarizer probe, AI operation, retention, email recovery and account deletion workers | Verify configuration, failure recovery and tenant isolation for enabled workers; registration is not runtime activation |
| Auth/mail/provider integrations | Architecture documents Google/Microsoft identity, Gmail/Graph/IMAP/SMTP, NAV, optional translation, local/external AI, Stripe | Synthetic local tests separate from licensed/provider/account delivery and consent |
| `.gitea/workflows/ci-deploy.yml` | Test pipeline; deployment limited by source condition to push on `refs/heads/main` | Fresh hosted artifact/commit/runner evidence; development branch push is not hosted qualification |
| `.gitea/workflows/mariadb-integration.yml` | Isolated runner label, explicit initial exit-1 isolation gate | Operator isolation evidence and authorised workflow review before enabling; do not remove gate during discovery |
| Existing backup/deployment runbooks | Database, attachments/CV artifacts, keys and deletion tombstones; code rollback differs from data restore | Disposable restore with independent content/decryption/deletion checks; approved RPO/RTO and failure rehearsal |
Public boundary currently inspected: nginx host routing, `/health`, `/ready`, `/api/`; API controllers are mapped but a complete anonymous/authenticated endpoint census is still PENDING. External mail/job/AI requests, uploaded documents, browser credentials, database records and stored artifacts are separate trust boundaries. No live endpoints, Docker engines, databases, credentials, models or production services were accessed.
Local development, test and production are not interchangeable: integration uses a generated project and disposable service; production needs the external network, server-side environment, actual ingress and operator controls. Current architecture/runbooks describe these differences; exact intended staging/production configuration and image identity remain unverified.
The latest handoff's source-bound PDF metric/visual approvals supersede the older acceptance paragraph describing unchanged S01 coordinate failures. They are preserved for the approved development scope. Physical print, device, AT and language review remain open. No export was rerun because no renderer/font/fixture/application source changed.
@@ -1,11 +0,0 @@
# Discovery baseline and actual checks
Date 2026-11-02. Source `5cb903b989742b7a96cb22ddf4c0e24993b18980`; application freeze `ba04d817fed26d659bfa2a56b089d6cac851dc34`. No accepted post-roadmap release candidate, build identifier or release image exists in this discovery checkpoint.
Fresh check: `rtk proxy python scripts/test-mariadb-integration.py` exited 0; **5 tests passed** in 0.030 seconds. This existing offline suite mocks subprocess execution. The printed cleanup failure/exit 17 and setup error/exit 1 are intentional negative-test outcomes asserted by the suite, not a real container failure. No MariaDB/Docker execution occurred. This proves five runner guards, not database behavior, hosted isolation or a full regression.
Read-only source checks confirmed the MariaDB default/missing-connection fail-closed behavior and the hosted workflow's explicit isolation blocker. Refer to SYSTEM_INVENTORY for inspected paths. `git diff --check` is a documentation hygiene check, not product qualification.
Historical test totals, PDF exports, security scans and provider runs remain attributed to their original evidence. They are not fresh runs here. `docs/automation/handoffs/pre-phase10/FINAL_DEVELOPMENT_ENTRY.md` records the latest approved unchanged-source applicability and narrow PDF metric/visual closure. Direct chat does not adopt or activate its historical autonomous-controller instructions.
NOT RUN: full backend/frontend/browser/Python/provider suites, builds, container validation, security scan/advisory refresh, performance, native review, deployment smoke, restore or rollback rehearsal. These require the actual release-stage source/environment and their own applicable gates. Discovery does not mark any mandatory release gate PASS merely because historical tests exist.
@@ -1,23 +0,0 @@
# Release discovery · endpoint and environment census
Source baseline `f19e7ebf4aeae350b43fa84cf115c393030280f0` on `codex/release-backup-parser`. Read-only source census. The 41 unique controller route prefixes are grouped below. Controller and action attributes, fallback authorisation, proxy policy, and runtime responses are separate checks. Repeat endpoint-level runtime verification on the accepted release candidate.
| Surface | Route owners under `/api` | Qualification owner and required failure cases |
| --- | --- | --- |
| Identity and accounts | `auth`, `auth/2fa`, `auth/sessions`, `account-lifecycle`, `export` | Auth/session, account lifecycle and export tests. Cover invalid/expired/revoked session, CSRF for cookie writes, rate limits, recent sign-in, owner isolation and final deletion policy. |
| Job tracking and discovery | `jobapplications`, `jobapplications/{jobId:int}` and its `checklist`, `focus-plan`, `workspace`, `ai` paths, `calendar`, `companies`, `jobimport`, `job-discovery` | Ownership, RD21, application/workspace, calendar and import/provider tests. Cover foreign/missing IDs, stale writes, parser/source denial and documented provider rights. |
| Career and files | `career/evidence`, `career/profile`, `profile-cv`, `cv`, `documents`, `attachments`, `public-cv`, `backup` | CV revision, import, document, public CV and backup tests. Cover frozen revisions, public opt-in and withdrawal, private downloads, upload limits, physical renderer qualification and scoped restoration. |
| Mail | `correspondence`, `email`, `email/drafts`, `gmail`, `imap`, `microsoft-graph` | Email ownership, Gmail, provider and draft tests. Verify OAuth state, consent/expiry, mailbox ownership, idempotent send, uncertain outcome recovery and real delivery separately. |
| Personal processing | `ai/settings`, `ai/usage`, `notification-settings`, `notifications`, `operations`, `rules` | Tenant/worker/AI tests. Verify entitlement and account isolation, no model start by default, queue and lease recovery, and redacted failures. |
| Administration and billing | `admin/audit`, `admin/processing`, `admin/system`, `users`, `billing`, `client-errors` | AdminProcessing, Users, AuthAndSystem, Billing and ClientErrors tests. Cover role denial, minimisation, signed webhooks, safe input and rate/size controls. Client error intake relies on the configured fallback auth policy, so verify both supported configurations. |
Outside the controllers, `GET /health` reports process/build liveness and `GET /ready` checks database connectivity with a terse 503 on failure. They cannot stand in for authenticated application smoke. OpenAPI is mapped only in Development. Explicit anonymous controller paths include auth configuration and entry/recovery flows, the 2FA challenge, provider callbacks, Stripe webhook and opted-in public CV HTML/PDF. They still need route-specific proof of token, signature, consent or data scope as applicable. Public CV HTML carries a noindex header and the PDF action has a named rate limit. Source inspection does not establish actual ingress controls.
| Environment | Source contract | Evidence required at the gate |
| --- | --- | --- |
| Local dev | Development compose has backend, frontend and optional local AI service. Development MariaDB uses a persistent dev volume. | Confirm isolated config, version and selected service flags. Keep developer data and credentials out of shared artifacts. |
| Disposable integration | Database compose pins MariaDB 12.2.2 by digest and declares a separate test service. | Prove ephemeral storage, port and project isolation, migrations and cleanup. Keep the hosted isolation gate until operator evidence exists. |
| Staging and recovery rehearsal | The release procedure requires an isolated, approved target. No running target or staging identity was inspected here. | Freeze image/config/migration IDs, scope credentials and network, use synthetic or approved sanitised data, and prove rollback/restore and alert behaviour. |
| Production | Production compose separates frontend, backend, AI service and Ollama. Exact deployed versions, ingress, mounted data and service activation are external evidence. | Verify proxy and TLS, only intended exposed routes, build identity, readiness, provider/worker boundaries, and the operator's approved release decision. |
No API, hosted CI, staging, production, Docker, provider or model operation was executed for this census. No configuration secret values were read. Stage 1 has a source-bound surface map. Exact endpoint/runtime evidence, candidate identity and recovery qualification remain open.
@@ -1,73 +0,0 @@
# Recovery implementation review and rehearsal plan
2026-11-02; source `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Read-only discovery. The entire 765-line `deploy/deploy.sh` was inspected, never executed or sourced. No operational keys, credentials, environment files, databases, containers or remote storage were accessed.
## Distinct recovery mechanisms
| Owner | Verified source behavior | Qualification boundary |
| --- | --- | --- |
| `SqliteDatabaseBackupRunner` | SQLite-only VACUUM snapshot and count retention; explicitly unsupported for MariaDB | Does not prove MariaDB backups exist |
| `BackupController` | Authenticated, encrypted user-data export using application Data Protection; includes structured records and attachment metadata | Not a server-wide database/files/keys recovery set; do not use it as disaster recovery proof |
| `deploy/deploy.sh:backup_database` | Requires explicit provider; SQL dump for MariaDB, volume archive for SQLite; rejects missing/empty/malformed/trailerless dumps | Format checks do not restore or compare records; emergency skip exists and is not approved here |
| `backup_recovery_volumes` | MariaDB additionally archives required application data volume; independently archives tombstones when present | Consistency between dump/files/tombstones and current key availability still needs isolated rehearsal |
| Retention/off-host copy | Daily/monthly retention and optional rclone copy | Source message calls remote encrypted but source does not establish actual remote encryption configuration; off-host configuration and recovery remain unverified |
| `finish_deploy` | On failure after replacement, restarts previous backend/frontend image references when both exist; retains failed exit | No database reversal, no previous AI image restoration, and no post-rollback health check in this handler |
| Tombstone store and replay | Validates ledger records; Program invokes `StageRestoredAccountsAsync` at startup; deleted identities are staged again | Test restored data with latest independent tombstones; corrupt ledger must fail closed. Existing AccountDeletionTests include restore replay, not a complete operational restore |
Build retry paths prune Docker build/system cache and deployment may warm an Ollama model. Therefore even a proposed "local deploy smoke" must not run this script against the shared workstation. No such action was taken.
## RR-11: backup connection parser mismatch - reproduced
The script's host `sed` expression accepts `Server=` but returns no host for `SERVER=` and `Host=`. The latter is a documented MySqlConnector alias ([official connection options](https://mysqlconnector.net/connection-options/), checked 2026-11-02). The script then fails its required-host check before dump execution. Impact: a valid application connection spelling can prevent deployment backup/deployment; no observed data loss or actual production impact. Engineering priority proposed P2; not a security vulnerability rating.
Three synthetic cases used only loopback and dummy identifiers. Exact source expression was extracted from the `db_host` assignment, then executed through Git Bash with `echo <shell-quoted synthetic input> | sed -n <shell-quoted source expression>`. Results: canonical `Server` -> `127.0.0.1`; uppercase `SERVER` -> empty; alias `Host` -> empty. Assertions confirmed all three results. No connection was opened. Initial direct Windows-to-sed invocations failed the positive-control assertion and were discarded as invalid harness evidence; the shell-native rerun passed its positive control.
Proposed future remediation: parse with the same supported connection-string semantics as the API, or explicitly validate/document a narrower deploy contract before any operational step. Cover aliases/case, quoted separators, duplicate keys, missing required values and secret-redaction behavior. Do not print real input. Any implementation belongs to an authorised release remediation scope, not P10 cleanup.
## Isolated rehearsal specification - NOT RUN
1. Bind accepted candidate, image digests, migration history and disposable host ownership. Use synthetic accounts and a separate temporary MariaDB/files/key/tombstone set; never copy production data here without separate authority.
2. Seed two owners with canonical profiles, CV overrides, letter history, submitted packages, attachments, encrypted provider placeholders and completed deletion tombstones. Record independent counts and artifact hashes, without recording plaintext secrets.
3. Produce the actual supported dump/files/tombstone recovery set. Demonstrate a failed dump or missing required volume prevents promotion. Verify off-host retrieval/decryption separately if that integration is approved.
4. Restore into a second isolated environment. Independently compare rows/ownership/revisions/artifact bytes; exercise application decryption and document reopening. Restore an older database with newer tombstones and prove erased accounts cannot regain access. Inject a malformed ledger and assert fail-closed startup.
5. Rehearse prior-image rollback after failed replacement, then after a representative additive migration and writes. Check `/health`, `/ready`, exact version and authenticated read/write preservation. Document when image rollback is insufficient and database restore would lose newer writes.
6. Measure recovery duration and data-loss window against owner-approved RTO/RPO. No target or measurement exists in this checkpoint. Preserve cleanup and evidence identities; no production execution or release approval implied.
Fresh checks: Git Bash `bash -n deploy/deploy.sh` PASSED (syntax only); `python scripts/test-mariadb-workflow.py` PASSED 4/4 (source quarantine guards only). These are separate from the earlier 5/5 offline runner tests. Restore, failure injection, hosted isolation, live encryption and rollback health remain NOT TESTED.
## RR-12 offline failure injection
Tested the unchanged `finish_deploy` function and EXIT trap from baseline `47c6834cee192a5f1f054ddcd500b4d45c48c985`, `deploy/deploy.sh` blob `8341110de5a827698efc6eeb6951f296770562da`. The test extracts only the handler and trap and supplies synthetic image refs and mock Compose and cleanup functions. The full deploy script was never sourced or executed.
| Case | Observed behavior |
| --- | --- |
| Replacement started, both previous core image references exist | Recreates backend and frontend with those refs only. The failed deployment status is preserved. No health probe follows the successful Compose call. |
| Compose rejects rollback | Attempts status and backend/frontend log collection. The original failure status is preserved, even when the rollback command returns a different status. |
| Either or both previous references are missing | Skips automatic recreation and reports that no complete prior core release is available. |
| Deployment fails before replacement, or succeeds | Skips automatic recreation. |
Both auth-config temporary-file cleanup calls occur before the rollback decision. The test stubs deletion and checks the exact call sequence.
- `python scripts/test-deploy-rollback.py -v` **PASSED 5/5**, including three missing-reference combinations.
- `python scripts/test-deploy-backup.py -v` **PASSED 5/5** and `python scripts/test-mariadb-workflow.py -v` **PASSED 4/4**. Git Bash `bash -n deploy/deploy.sh` passed syntax validation.
RR-12 remains open for an authorised isolated rehearsal. After the prior images are recreated, verify exact image identity, `/health`, `/ready`, and authenticated reads and writes against representative migrated and post-write data. A successful Compose call does not establish service health or recover schema, data, or the AI image. RR-13 still needs authorised off-host retrieval/decryption and recovery-set consistency verification. No Docker, network service, database, remote storage, deployment or restore was invoked by these checks.
## RR-13 off-host upload failure handling
Reproduced two transfer failures in the deployment gate using the real `upload_current_backup_set` function, synthetic files and a stub `rclone`. The caller uses `! upload_current_backup_set`, so Bash suppresses errexit inside that function. When either daily `copyto` or monthly `copy` returned 23, the old code still printed success and the gate returned 0. The healthy control selected only current-stamp daily files, then the monthly directory.
The bounded fix explicitly returns failure on either copy error. A daily error stops before subsequent files and the monthly copy. A monthly error stops before the deployment continues. The successful path still uses immutable transfers and no longer claims the configured remote is encrypted. A failed upload may leave some remote files already transferred.
- Red run: **2 failures and 1 passing control**. Green run: **8/8 backup tests** including the three upload cases. The independent rollback handler and hosted quarantine guard checks passed **5/5** and **4/4**. Bash syntax validation passed.
- Encrypted off-host retrieval, decryption, completeness and consistent restore of database/files/keys/tombstones remain required. No actual transfer, restore, production configuration, keys or backup files were accessed.
## RR-13 monthly completion marker
The same conditional Bash behavior applies to `apply_backup_retention`. With a stubbed partial file copy returning 23, the old function wrote `.complete`, reported the month retained and continued through the mocked pruning, upload and deployment boundary. The successful control retained byte-identical synthetic files and the expected stamp.
The fix explicitly checks preparation and copy. It writes the stamp to a temporary file in the monthly directory and renames it to `.complete` only after the copy and stamp write succeed. A failed rename blocks later steps and attempts to remove the temporary marker. Existing completed snapshots are preserved. A failed copy can leave unmarked files, and previously written markers still require verification. A marker records the selected stamp and successful local copy; it does not attest to recovery-set integrity.
- Red run: **2 failures and 1 passing control** against the pre-fix function. The partial-copy case directly reproduced false completion; the marker-publication case required an atomic rename the old function did not perform.
- Green run: **12/12 backup tests** including four monthly cases (successful copy and stamp, partial copy, failed marker rename, existing completed month). Rollback handler and hosted quarantine checks passed **5/5** and **4/4**. Bash syntax validation passed.
- Only temporary synthetic files were created or copied. Pruning and upload were stubbed. Approved rehearsal still needs independent checks of artifacts for the marker stamp, encryption, retrieval and actual restore before any release decision.
@@ -1,21 +0,0 @@
# Preliminary release blocker reconciliation
Discovery only, 2026-11-02. These are qualification gaps unless a concrete defect is stated. No inferred P0/P1 vulnerability ratings or owner risk acceptance.
| ID | Historical item / current finding | Status / closure evidence needed |
| --- | --- | --- |
| RR-01 | Phase 9/10 and production NO-GO | CLEAN-20 implementation and verification are published. Independent Phase 10 acceptance and later roadmap acceptance remain open. BLOCKED for final RC |
| RR-02 | Old Linux 11/24 and later S01 failure narratives | Latest handoff records approved S01 36/36 per platform and owner visual acceptance for unchanged development output. Do not reopen these metrics without affected change; physical print/device remain NOT TESTED |
| RR-03 | Python advisory/native parser/image qualification | Latest scoped conditional disposition retained; current advisories and production-equivalent native isolation NOT TESTED. No new scan or risk waiver |
| RR-04 | Hosted MariaDB/isolation | Source explicitly fails before execution until isolated runner proof. BLOCKED; fresh local 5 guard tests do not close it |
| RR-05 | Human/device/provider | Actual AT/contrast, physical devices/print, native language and live provider accounts not qualified here. BLOCKED/NOT TESTED by lane |
| RR-06 | Old known-issues F19 SQLite fallback statement | Current DatabaseConfiguration defaults to MariaDB and throws without connection. Old wording is superseded for this source. SQLite retirement still BLOCKED on transfer/restore/parity gates |
| RR-07 | Old F01 stale-write statement | MariaDB acceptance documents bounded RD21 resolution; later handoff carries it. Do not reopen all old F01 findings based on stale index wording; final affected API census/regression remains required |
| RR-08 | MySQL TLS / dependency / production identity | Latest handoff retains MYSQL-TLS-01 and OPS-01. No live configuration read; current advisory disposition and deployed equivalence unverified |
| RR-09 | Backup/restore and rollback | Existing procedures and historical seeded rehearsal are not a current restore. Need isolated recovery-set completeness, decryption, tombstone behavior, post-write rollback compatibility and agreed RPO/RTO |
| RR-10 | AI quality/activation | Deterministic Docling fixtures are distinct from real-model semantic accuracy. Optional unqualified adapter remains disabled; no AI invocation authorised here |
| RR-11 | Backup host parsing rejected supported `Host=` alias and uppercase spelling | FIXED AND OFFLINE-TESTED. See [bounded remediation](RR-11_IMPLEMENTATION.md). Real dump/restore qualification remains open |
| RR-12 | Rollback recreates core images without post-rollback health qualification; no schema/data/AI reversal | OFFLINE HANDLER CHECKS PASSED 5/5. See [failure-injection evidence](11_BACKUP_RESTORE_ROLLBACK.md#rr-12-offline-failure-injection). Actual image recovery and health/readiness after migration/writes still require isolated rehearsal |
| RR-13 | Off-host encryption and recovery-set consistency need independent proof. Failed transfer and partial monthly copy previously let deployment continue | Transfer and monthly marker failure gates FIXED AND OFFLINE-TESTED. Actual encryption, retrieval/decryption and database/files/key/tombstone consistency require approved isolation |
Continue evidence work against the [source endpoint and environment census](03_ENDPOINT_ENVIRONMENT_CENSUS.md). Actual qualification requires an accepted candidate and isolated environment. Every fix needs minimal scope, targeted verification and the applicable stage approval.
@@ -1,26 +0,0 @@
# Preliminary acceptance matrix
2026-11-02. **NO-GO for production.** This is a discovery snapshot, not Stage 7 final acceptance. No accepted final RC or production-equivalent build is nominated. PASS applies only to an executed gate, not historical source evidence.
| Mandatory gate | Current release verdict | Missing release evidence / dependency |
| --- | --- | --- |
| Functional regression | BLOCKED | Accepted final roadmap source; fresh applicable critical journeys |
| Frontend/UX | BLOCKED | Accepted design/implementation and final source-bound browser evidence |
| Accessibility | NOT TESTED | Actual AT/device/contrast/manual evidence plus automation |
| Authentication/authorization | NOT TESTED | Final endpoint/tenant/session qualification |
| Security assessment | BLOCKED | Dedicated security initiative start and predecessor gates; no scan run |
| Backend APIs | NOT TESTED | Final API census and applicable regression |
| MariaDB/migrations | BLOCKED | Hosted isolation and actual RC upgrade/recovery evidence; five offline guards are not this gate |
| CV/PDF fidelity | BLOCKED | Final RC source equivalence and retained scoped approvals; physical print still missing |
| AI integrations | BLOCKED | Explicit isolated-model qualification and semantic evidence |
| Email/OAuth | BLOCKED | Actual configured provider/consent/account verification |
| Performance/reliability | NOT TESTED | Approved load targets and bounded final-source measurements |
| Docker/infrastructure | NOT TESTED | Exact images/environment/configuration/ingress parity |
| CI/CD | BLOCKED | Hosted isolation and fresh actual run/artifacts |
| Backup/restore | NOT TESTED | Disposable complete recovery rehearsal with content assertions |
| Deployment/rollback | NOT TESTED | Authorised isolated rehearsal; production execution remains prohibited |
| Monitoring | NOT TESTED | Failure detection, delivery and redaction evidence |
| Documentation | NOT TESTED | Complete release-source/runbook reconciliation, beyond this partial discovery |
| Human/device/provider | BLOCKED | Real reviewers/devices/accounts and source-bound records |
No gate is marked FAIL without fresh evidence of unacceptable behavior. No missing qualification is marked PASS. Historical source-specific acceptance may be reused where permitted and unchanged, but cannot establish final release readiness by itself.
-19
View File
@@ -1,19 +0,0 @@
# Release discovery handoff
RR-12 offline failure injection passed 5/5 against the unchanged deploy exit handler. The evidence covers synthetic prior-image recreation, failed rollback diagnostics, original exit status and conditions that skip rollback. [Recovery report](11_BACKUP_RESTORE_ROLLBACK.md#rr-12-offline-failure-injection) distinguishes this from a real post-migration/post-write rehearsal. RR-13 off-host retrieval/decryption and consistent restore remain open. No production qualification.
RR-13 transfer guard [reproduced and fixed offline](11_BACKUP_RESTORE_ROLLBACK.md#rr-13-off-host-upload-failure-handling). Daily and monthly copy errors stop deployment, and the success log makes no encryption claim. Actual remote encryption and restore remain open.
RR-13 [monthly completion guard](11_BACKUP_RESTORE_ROLLBACK.md#rr-13-monthly-completion-marker) now blocks the gate after a partial copy or marker publication failure. The successful path preserves artifact bytes and the stamp in synthetic fixtures. Existing markers and real restoration remain unverified.
10 November: [endpoint and environment census](03_ENDPOINT_ENVIRONMENT_CENSUS.md) maps 41 source route families, anonymous exceptions and failure cases to qualification owners on the backup-parser branch. No runtime qualification. Carry the map into Stage 2 only after the release candidate and environment are accepted.
Branch `planning/release-readiness-discovery`, baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`; documentation only. Stage 1 IN_PROGRESS, not complete. Full authoritative release prompt read. No final GO/NO-GO completion report or deployable release claimed.
Completed: initial source inventory, historical blocker reconciliation, preliminary 18-gate matrix and five fresh offline MariaDB runner guard tests. Preserved scoped PDF acceptance and separate model/provider/human/hosted/production gates.
Next authorised work: complete endpoint and environment census, map each failure boundary to existing test/runbook owners, and inspect remaining backup/restore/deployment implementation without executing it. Then prepare a concrete isolated-rehearsal plan. Final baseline/qualification depends on accepted roadmap source; do not run thousands of unchanged tests to fill this report.
Continuation: recovery implementation inspection and isolated rehearsal specification are now recorded in 11_BACKUP_RESTORE_ROLLBACK.md. RR-11 parser mismatch reproduced using three synthetic cases; RR-12/13 retain rollback/encryption qualification limits. Four source quarantine guard tests and shell syntax check passed. No deployment script execution. Next: endpoint/environment census, then remaining independent I18N/homepage audit work. Stage 1 remains IN_PROGRESS.
No production credentials/environment files were opened; no infrastructure, models, main branch or autonomous orchestrator changed. The earlier delegated researcher was interrupted by a service limit; parent supplied this partial checkpoint. No background agent remains active.
@@ -1,19 +0,0 @@
# RR-11 bounded remediation
Authorised by the owner's Continuous Roadmap Execution instruction explicitly prioritising this fix. Branch `codex/release-backup-parser`, based on published discovery `653f8d04d469dcd52c883d4b98bea76b8adacada`. Separate from CLEAN-20, full release qualification and infrastructure operations.
Replaced five inconsistent sed expressions with one stdlib Python parser and a shell-safe field transfer. Case-insensitive aliases, quoted separators/doubled quotes, Unicode and empty passwords survive to the dump command. Invalid ports, incomplete/ambiguous/control-bearing values and unsupported multi-host/socket/path forms fail before dumping. No eval; connection input travels via stdin, not argv; errors never include input. No dependency added. Existing dump/TLS/retention/rollback behavior is otherwise unchanged.
## Evidence
- Regression first: four test methods ran against the original backup function; eight subcase failures reproduced alias/case rejection and quoted-password truncation. Canonical/negative controls executed. No real dump or deployment.
- Final targeted suite: **5/5 methods PASSED**, including 7 host spellings, quoted semicolon/equal/doubled quote/shell characters, Unicode/single quotes, empty password, and six invalid/ambiguous input cases. Tests run the actual backup function and mock only external dump/Docker boundaries.
- Full offline script unittest set: **31/31 PASSED**, including targeted cases, ingress Compose rendering with synthetic env, workflow/runner guards, retry guard, supply-chain fixtures and offline model-evaluation helpers. No model invocation. Printed exit17/exit1 are expected mocked runner negative cases. Do not add targeted five to the overlapping total.
- Ordinary unittest discovery found zero tests because repository filenames contain hyphens; recorded as NOT A PASS. Explicit importlib loading of every `scripts/test-*.py` unittest module (excluding standalone Docker topology runner) executed the 31 tests. Docker topology qualification NOT RUN.
- Git Bash `bash -n deploy/deploy.sh`: PASSED.
- Graphify AST update completed locally: 18,504 nodes / 45,920 edges; missing optional PDF/SQL extractors reported. Newly generated untracked graph output retained locally, not a runtime verification or introduced repository artifact.
- Product/backend/frontend/full database/browser suites NOT RUN: this package changes only deploy parsing and its offline tests. Real dump/restore, Linux-host operational behavior, hosted CI and production validation remain NOT TESTED.
The first Unicode test attempt failed in the Windows test harness encoding before reaching product code; the harness was corrected to UTF-8, then passed. Parser stdin/stdout are explicitly UTF-8. No application user data or production credentials used.
Rollback: revert this bounded implementation commit to restore the prior parser (and its known limitation). No database/schema change needs reversal. RR-11 is FIXED AND OFFLINE-TESTED on this development branch; RR-12/13 and production remain open. No main merge/deploy authorised.
-68
View File
@@ -1,68 +0,0 @@
"""Read a deployment connection string from stdin; emit five fields plus a sentinel.
This is a single-host dump contract, not a replacement for MySqlConnector. Reject
ambiguous aliases/control characters instead of choosing a different backup target.
Never include input values in errors or pass the connection string as an argument.
"""
import re
import sys
ALIASES = {
**dict.fromkeys(['server', 'host', 'data source', 'datasource', 'address', 'addr', 'network address'], 'host'),
**dict.fromkeys(['user id', 'userid', 'username', 'uid', 'user name', 'user'], 'user'),
**dict.fromkeys(['password', 'pwd'], 'password'),
**dict.fromkeys(['database', 'initial catalog'], 'database'),
'port': 'port',
}
PAIR = re.compile(r'''\s*([^=;]+?)\s*=\s*(?:"((?:""|[^"])*)"|'((?:''|[^'])*)'|([^;]*))\s*(?:;|$)''')
def parse(connection):
if any(ord(c) < 32 or ord(c) == 127 for c in connection):
raise ValueError('Control characters are unsupported in backup connection settings.')
fields = {}
position = 0
while position < len(connection):
if connection[position] == ';' or connection[position].isspace():
position += 1
continue
match = PAIR.match(connection, position)
if not match:
raise ValueError('Malformed backup connection settings.')
key, double, single, plain = match.groups()
if double is not None:
value = double.replace('""', '"')
elif single is not None:
value = single.replace("''", "'")
else:
value = plain.strip()
if value.startswith(('"', "'")):
raise ValueError('Malformed quoted backup connection value.')
canonical = ALIASES.get(key.strip().lower())
if canonical:
if canonical in fields:
raise ValueError('Duplicate backup connection setting or alias.')
fields[canonical] = value
position = match.end()
if any(not fields.get(key, '').strip() for key in ['host', 'database', 'user']):
raise ValueError('Backup connection requires host, database and user.')
port = fields.get('port', '3306')
if not re.fullmatch(r'[0-9]+', port) or not 1 <= int(port) <= 65535:
raise ValueError('Backup connection port must be between 1 and 65535.')
if any(c in fields['host'] for c in ',/\\'):
raise ValueError('Backup requires one TCP host, not a host list or socket path.')
if any(c in fields['database'] for c in '/\\'):
raise ValueError('Database name cannot contain backup path separators.')
return [fields['host'], str(int(port)), fields['database'], fields['user'], fields.get('password', '')]
if __name__ == '__main__':
try:
values = parse(sys.stdin.buffer.read().decode('utf-8'))
except (ValueError, UnicodeError):
print('Invalid or unsupported backup connection settings.', file=sys.stderr)
sys.exit(1)
sys.stdout.reconfigure(encoding='utf-8', newline='\n')
# Sentinel keeps an empty final password intact through shell command substitution.
sys.stdout.write('\n'.join([*values, 'END']) + '\n')
-239
View File
@@ -1,239 +0,0 @@
"""Exercise deployment backup functions with synthetic commands; never deploy."""
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
BASH = 'C:/Program Files/Git/bin/bash.exe' if os.name == 'nt' else shutil.which('bash')
class BackupConnectionTests(unittest.TestCase):
def backup(self, connection):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('backup_database() {')
function = source[start:source.index('\n}\n', start) + 3]
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
script = '\n'.join([
'set -euo pipefail',
'BACKUP_DIR=' + shlex.quote(folder.as_posix()),
'BACKUP_STAMP=synthetic',
'DATABASE_PROVIDER=mariadb',
'JOBTRACKER_CONNECTION_STRING=' + shlex.quote(connection),
'python3() { ' + shlex.quote(Path(sys.executable).as_posix()) + ' "$@"; }',
'docker() { echo "Unexpected Docker call" >&2; return 99; }',
'mariadb-dump() { printf "%s\\0" "$@" > "$BACKUP_DIR/args"; '
'printf "%s" "$MYSQL_PWD" > "$BACKUP_DIR/password"; '
'printf "CREATE TABLE synthetic (id int);\\n-- Dump completed\\n"; }',
'verify_sql_backup() { gzip -t "$1"; }',
function,
'backup_database',
])
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True, cwd=ROOT,
env={**os.environ, 'DEPLOY_SKIP_DB_BACKUP': 'false'})
arguments = (folder / 'args').read_bytes().split(b'\0')[:-1] if (folder / 'args').exists() else None
password = (folder / 'password').read_text(encoding='utf-8') if (folder / 'password').exists() else None
return result, arguments, password
def test_supported_host_alias_and_case_reach_dump_unchanged(self):
for host in ['Server', 'SERVER', 'Host', 'Data Source', 'Address', 'Addr', 'Network Address']:
with self.subTest(host=host):
result, args, password = self.backup(f'{host}=127.0.0.1;DATABASE=synthetic;UID=reader;PWD=dummy-secret')
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertEqual([b'--host=127.0.0.1', b'--port=3306', b'--user=reader',
b'--single-transaction', b'--routines', b'--events', b'--quick', b'synthetic'], args)
self.assertEqual('dummy-secret', password)
self.assertNotIn('dummy-secret', result.stdout + result.stderr)
def test_quoted_separators_and_shell_characters_remain_data(self):
result, args, password = self.backup('Server=127.0.0.1;Port=3307;Database=synthetic;User ID=reader;Password="semi;eq=quote""$(false)"')
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertIn(b'--port=3307', args)
self.assertEqual('semi;eq=quote"$(false)', password)
def test_empty_password_is_preserved(self):
result, args, password = self.backup('Server=127.0.0.1;Database=synthetic;User ID=reader;Password=')
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertIsNotNone(args)
self.assertEqual('', password)
def test_single_quoted_unicode_password_and_database_alias_are_preserved(self):
result, args, password = self.backup("Host=127.0.0.1;Initial Catalog=synthetic;Username=reader;Password='søknad你好;it''s';SslMode=VerifyFull")
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertEqual(b'synthetic', args[-1])
self.assertEqual("søknad你好;it's", password)
def test_invalid_or_ambiguous_input_never_invokes_dump_or_exposes_input(self):
for connection in [
'Host=127.0.0.1;Database=synthetic;Uid=reader;Port=0;Pwd=dummy-secret',
'Host=127.0.0.1;Database=synthetic;Uid=reader;Port=65536;Pwd=dummy-secret',
'Host=127.0.0.1;Server=other;Database=synthetic;Uid=reader;Pwd=dummy-secret',
'Host=127.0.0.1;Database=synthetic;Uid=reader;Pwd="dummy-secret',
'Host=127.0.0.1;Database=synthetic;Uid=reader;Pwd=dummy-secret\nline',
'Host=127.0.0.1;Uid=reader;Pwd=dummy-secret',
]:
with self.subTest(connection=connection.replace('dummy-secret', 'REDACTED')):
result, args, _ = self.backup(connection)
self.assertNotEqual(0, result.returncode)
self.assertIsNone(args)
self.assertNotIn('dummy-secret', result.stdout + result.stderr)
class BackupUploadTests(unittest.TestCase):
def upload(self, fail_operation=''):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('upload_current_backup_set() {')
function = source[start:source.index('\n}\n', start) + 3]
gate_start = source.index('if ! apply_backup_retention || ! upload_current_backup_set; then')
gate = source[gate_start:source.index('\nfi\n', gate_start) + 4]
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
for filename in ['jobtracker-db-synthetic.sql.gz', 'jobtracker-files-synthetic.tar.gz',
'jobtracker-db-older.sql.gz']:
(folder / filename).touch()
(folder / 'monthly' / '2099-01').mkdir(parents=True)
script = '\n'.join([
'set -euo pipefail',
'BACKUP_DIR=' + shlex.quote(folder.as_posix()),
'BACKUP_STAMP=synthetic',
'BACKUP_RCLONE_REMOTE=fixture:encrypted/',
'FAIL_OPERATION=' + shlex.quote(fail_operation),
'date() { echo 2099-01; }',
'apply_backup_retention() { return 0; }',
'rclone() {',
' printf "UPLOAD %s %s %s\\n" "$1" "$3" "$4";',
' if [ "$1" = "$FAIL_OPERATION" ]; then return 23; fi;',
'}',
function,
gate,
'echo "DEPLOY WOULD CONTINUE"',
])
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True,
cwd=ROOT, timeout=10,
env={key: value for key, value in os.environ.items()
if key.upper() in {'PATH', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP'}})
calls = [line for line in result.stdout.splitlines() if line.startswith('UPLOAD ')]
return result, calls
def test_uploads_only_current_daily_files_then_monthly_snapshot(self):
result, calls = self.upload()
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
self.assertEqual([
'UPLOAD copyto fixture:encrypted/daily/jobtracker-db-synthetic.sql.gz --immutable',
'UPLOAD copyto fixture:encrypted/daily/jobtracker-files-synthetic.tar.gz --immutable',
'UPLOAD copy fixture:encrypted/monthly/2099-01 --immutable',
], calls)
self.assertIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertIn('Current recovery set copied to configured off-host remote.', result.stdout)
def test_daily_copy_failure_stops_before_remaining_files_and_monthly_copy(self):
result, calls = self.upload('copyto')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertEqual([
'UPLOAD copyto fixture:encrypted/daily/jobtracker-db-synthetic.sql.gz --immutable',
], calls)
self.assertIn('Daily off-host backup upload failed', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertNotIn('Current recovery set copied', result.stdout)
def test_monthly_copy_failure_blocks_deploy(self):
result, calls = self.upload('copy')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertEqual(3, len(calls))
self.assertEqual('UPLOAD copy fixture:encrypted/monthly/2099-01 --immutable', calls[-1])
self.assertIn('Monthly off-host backup upload failed', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertNotIn('Current recovery set copied', result.stdout)
class BackupRetentionTests(unittest.TestCase):
def retain(self, fail_operation='', *, previous_snapshot=False):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('apply_backup_retention() {')
function = source[start:source.index('\n}\n', start) + 3]
gate_start = source.index('if ! apply_backup_retention || ! upload_current_backup_set; then')
gate = source[gate_start:source.index('\nfi\n', gate_start) + 4]
fixtures = {'jobtracker-db-synthetic.sql.gz': b'database fixture',
'jobtracker-files-synthetic.tar.gz': b'files fixture'}
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
for name, contents in fixtures.items():
(folder / name).write_bytes(contents)
monthly = folder / 'monthly' / '2099-01'
if previous_snapshot:
monthly.mkdir(parents=True)
(monthly / 'jobtracker-db-earlier.sql.gz').write_bytes(b'earlier fixture')
(monthly / '.complete').write_bytes(b'earlier\n')
script = '\n'.join([
'set -euo pipefail',
'BACKUP_DIR=' + shlex.quote(folder.as_posix()),
'BACKUP_STAMP=synthetic',
'FAIL_OPERATION=' + shlex.quote(fail_operation),
'date() { echo 2099-01; }',
'upload_current_backup_set() { echo "UPLOAD WOULD START"; }',
'find() { if [[ "$*" == *-delete* ]]; then echo "PRUNING WOULD START"; fi; }',
'rm() { echo "CLEANUP REQUEST $1 $2"; }',
'cp() {',
' if [ "$FAIL_OPERATION" = cp ]; then',
' printf partial > "${@: -1}/partial"; return 23;',
' fi;',
' command cp "$@";',
'}',
'mv() { if [ "$FAIL_OPERATION" = mv ]; then return 24; fi; command mv "$@"; }',
function,
gate,
'echo "DEPLOY WOULD CONTINUE"',
])
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True,
cwd=ROOT, timeout=10,
env={key: value for key, value in os.environ.items()
if key.upper() in {'PATH', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP'}})
artifacts = {file.name: file.read_bytes() for file in monthly.iterdir()} if monthly.exists() else {}
return result, artifacts
def test_completed_monthly_snapshot_contains_both_artifacts_and_stamp(self):
result, artifacts = self.retain()
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
self.assertEqual({'jobtracker-db-synthetic.sql.gz': b'database fixture',
'jobtracker-files-synthetic.tar.gz': b'files fixture',
'.complete': b'synthetic\n'}, artifacts)
self.assertIn('PRUNING WOULD START', result.stdout)
self.assertIn('UPLOAD WOULD START', result.stdout)
self.assertIn('DEPLOY WOULD CONTINUE', result.stdout)
def test_partial_monthly_copy_cannot_mark_snapshot_complete_or_proceed(self):
result, artifacts = self.retain('cp')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertEqual({'partial': b'partial'}, artifacts)
self.assertIn('Could not retain monthly recovery files', result.stdout)
self.assertNotIn('PRUNING WOULD START', result.stdout)
self.assertNotIn('UPLOAD WOULD START', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
def test_marker_publish_failure_does_not_expose_complete_marker_or_proceed(self):
result, artifacts = self.retain('mv')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertNotIn('.complete', artifacts)
self.assertIn('CLEANUP REQUEST -f --', result.stdout)
self.assertIn('Could not publish monthly recovery marker', result.stdout)
self.assertNotIn('PRUNING WOULD START', result.stdout)
self.assertNotIn('UPLOAD WOULD START', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
def test_preserves_the_existing_completed_monthly_snapshot(self):
result, artifacts = self.retain('cp', previous_snapshot=True)
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
self.assertEqual({'jobtracker-db-earlier.sql.gz': b'earlier fixture', '.complete': b'earlier\n'}, artifacts)
self.assertIn('DEPLOY WOULD CONTINUE', result.stdout)
if __name__ == '__main__':
unittest.main()
-82
View File
@@ -1,82 +0,0 @@
"""Exercise the deployment exit handler with synthetic image references and no Docker."""
import os
from pathlib import Path
import shutil
import subprocess
import unittest
ROOT = Path(__file__).resolve().parents[1]
BASH = 'C:/Program Files/Git/bin/bash.exe' if os.name == 'nt' else shutil.which('bash')
class DeployRollbackTests(unittest.TestCase):
def exit_deploy(self, status, *, replaced=True, backend=True, frontend=True, compose_status=0):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('finish_deploy() {')
handler = source[start:source.index('\ntrap finish_deploy EXIT\n', start) + len('\ntrap finish_deploy EXIT\n')]
script = '\n'.join([
'set -euo pipefail',
f'replacement_started={str(replaced).lower()}',
'previous_backend_ref=' + ('synthetic/backend:previous' if backend else ''),
'previous_frontend_ref=' + ('synthetic/frontend:previous' if frontend else ''),
'auth_config_body_file=synthetic-body',
'auth_config_headers_file=synthetic-headers',
'rm() { printf "CALL rm %s\\n" "$*"; }',
'docker() { echo "CALL unexpected docker"; return 99; }',
'curl() { echo "CALL unexpected curl"; return 99; }',
'compose() {',
' printf "CALL compose %s [%s] [%s]\\n" "$*" "${BACKEND_IMAGE:-}" "${FRONTEND_IMAGE:-}";',
f' if [ "$1" = up ]; then return {compose_status}; fi;',
' return 0;',
'}',
handler,
f'exit {status}',
])
# Exclude inherited deployment settings from the fixture.
environment = {key: value for key, value in os.environ.items()
if key.upper() in {'PATH', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP'}}
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True,
cwd=ROOT, env=environment, timeout=10)
self.assertEqual(status, result.returncode, result.stdout + result.stderr)
self.assertEqual('', result.stderr)
calls = [line for line in result.stdout.splitlines() if line.startswith('CALL ')]
self.assertEqual(['CALL rm -f synthetic-body', 'CALL rm -f synthetic-headers'], calls[:2])
return result.stdout, calls[2:]
def test_failed_replacement_recreates_only_prior_core_images(self):
stdout, calls = self.exit_deploy(41)
self.assertEqual([
'CALL compose up -d --force-recreate --remove-orphans backend frontend '
'[synthetic/backend:previous] [synthetic/frontend:previous]',
], calls)
self.assertIn('Deployment remains failed', stdout)
def test_rollback_failure_retains_original_status_and_collects_diagnostics(self):
stdout, calls = self.exit_deploy(41, compose_status=19)
self.assertEqual([
'CALL compose up -d --force-recreate --remove-orphans backend frontend '
'[synthetic/backend:previous] [synthetic/frontend:previous]',
'CALL compose ps [] []',
'CALL compose logs --tail=200 backend frontend [] []',
], calls)
self.assertIn('Manual recovery is required', stdout)
def test_incomplete_previous_release_does_not_recreate_any_service(self):
for backend, frontend in [(False, True), (True, False), (False, False)]:
with self.subTest(backend=backend, frontend=frontend):
stdout, calls = self.exit_deploy(41, backend=backend, frontend=frontend)
self.assertEqual([], calls)
self.assertIn('no complete previous core release', stdout)
def test_failure_before_replacement_does_not_recreate_any_service(self):
_, calls = self.exit_deploy(41, replaced=False)
self.assertEqual([], calls)
def test_success_does_not_recreate_any_service(self):
_, calls = self.exit_deploy(0)
self.assertEqual([], calls)
if __name__ == '__main__':
unittest.main()