Skip to content

refactor: architecture deepening (rounds one and two) - #109

Merged
simoncodes-ca merged 20 commits into
simoncodes-ca:developfrom
SimonNodel-AI:refactor/architecture-deepening
Sep 25, 2026
Merged

simoncodes-ca merged 20 commits into
simoncodes-ca:developfrom
SimonNodel-AI:refactor/architecture-deepening

Conversation

@SimonNodel-AI

Copy link
Copy Markdown
Collaborator

Summary

Twenty commits that deepen the module boundaries across core, the API, the CLI and the Tracker. Round one (first eight commits) made the domain and core barrels explicit and introduced the Collection, Resource Folder, Staleness Rule, Import/Export Run, Collection Index, Resource Mutation, Typed Errors, Resource Entry Draft and Translation Status Summary concepts. Round two (this session) adds eight named seams and a set of follow-up fixes. Every concept is defined in architecture-docs/glossary.md.

Round two seams

  1. Collection-bound operations – addResource, editResource, moveResource, folder ops take an opened Collection; core owns locale seeding; explicit moveTo on edit (fixes the Tracker edit-with-folder 404).
  2. Resource Summary – one explicit address (fullKey, folderPath, entryKey) plus per-target flags; Tracker rows derive from a pure rowView; the 202 "index not ready" retry is hidden in the API service.
  3. Collection Reader – readCollection(collection) behind export, bundle, search, validate and the resource tree; real-fs specs replace fs mocks.
  4. Translator – openTranslator(collection, { provider? }) owns the API key, ICU skip, placeholder and protected-term guards and ICU normalisation; in-memory provider for specs.
  5. Bundle Selection – one selectBundleEntries for the JSON bundle, the dry-run plan and the type file; paths resolve against the CLI's INIT_CWD.
  6. CLI Command Runner – defineCommand({ collection, prompts, required, run }) owns config, collection resolution, the one interactive rule, cancellation and exit codes.
  7. Resource Search – one matcher that ranks then limits; ?mode=similar for the CLI and the Tracker.
  8. Bundle Definition in domain – type, validation, normalisation and output-path rule shared by all four consumers.

Follow-ups

Stderr diagnostics for the CLI, status sort fix, delete-collection confirmation, job error in the DTO, similarity chip, move --dest-collection, spec typecheck targets for every project (fixing ~290 latent spec type errors), XLIFF note round-trip, refreshed CLAUDE.md.

Behaviour changes to know about

HTTP / DTOs

  • CreateResourceDto.baseLocale removed; UpdateResourceDto.targetFolder → moveTo; DeleteFolderResponseDto.error removed.
  • Resource payloads are the Resource Summary shape.
  • New typed errors: 409 resource exists, 404 folder not found, 400 folder move into descendant, 422 auto-translation disabled, 500 (with message) for a malformed protected-terms file.
  • Search: ?mode=similar, similarity, matchType: 'similar-value'; invalid maxResults falls back to 100.
  • Bundles: invalid-definition 400 body gains statusCode/error; PUT on a missing bundle with no body is 400; GET /config passes unknown bundle fields through; saving migrates legacy typeDist; tokenCasing validated.
  • TranslateLocaleJobDto.error added.

CLI

  • Exit 1 where it was 0: core errors in the eight mutating commands, unknown collection, no collections, missing required flags in non-interactive mode, partial failures in delete-resource/move/normalize/bundle.
  • One interactive rule (stdin and stdout both terminals). Cancel prints one line and exits 0.
  • ❌/⚠️ diagnostics go to stderr; stdout is the payload (glossary --stdout, normalize --json, bundle --quiet are clean). Scripts reading errors from stdout need 2>&1.
  • delete-collection confirms when interactive (--yes to skip); move --dest-collection exposed; find-similar has no 500-candidate cap; import --source and install-skill --dir resolve against the project root; validate --skip-placeholders now works.

Core / validate / bundle / translate

  • Auto-translations that drop a protected term are skipped; translated values are ICU-normalised.
  • Bundles warn once per run for a missing collection, use / paths, and no longer drop a key named constructor.
  • Search ranks all matches before the limit; the base value is always searched.
  • XLIFF import joins notes into one comment and drops the exporter's "Do not translate" note.

Build

  • typecheck now includes spec typechecks for tracker, core, cli and api; CI runs affected -t typecheck; api:build and the pre-commit hook therefore typecheck core specs.

Verification

nx run-many -t test,typecheck,build for all six projects and pnpm run lint are green at HEAD: domain 1027, core 1306, api 308, cli 560, tracker 983 tests.

Review process

Each round-two candidate was implemented by an Opus agent, verified independently, read by the orchestrator, reviewed by Codex (gpt-5.6) and a second Opus reviewer, fixed, re-verified and committed. Findings deliberately left are listed in the commit messages and in architecture-docs/.

🤖 Generated with Claude Code

SimonNodel-AI and others added 20 commits September 22, 2026 22:31
…rule

Introduce a pure Staleness module in domain (applyBaseChange,
recordTranslation, needsTranslation, resolveImportStatus) and a
ResourceFolder module in core that owns resource_entries.json +
tracker_meta.json. Every writer (add/edit/delete/move resource, import,
normalize, translate, add/remove locale) and reader now goes through it.

Fixes two behaviours: an import that changes a base value now marks
translations stale, and moving a resource or folder keeps verified/stale
statuses instead of resetting them to translated. moveFolder now stops
before deleting when a folder cannot be read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ection

Add a single config reader (loadConfig) and a Collection resolver
(openCollection) in core that applies the collection-then-global-then-
default fallback for baseLocale, locales, translation config, tags and
translations folder, with typed ConfigNotFoundError, ConfigParseError,
CollectionNotFoundError and ReadOnlyCollectionError.

The CLI, the API and core's own collection operations now use it,
removing ~40 copies of the fallback chain, three config readers and the
repeated decode/404 preambles. import-workflow no longer re-reads the
config file. Cross-collection moves and locale mutations now refuse a
read-only destination.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Import is now two format adapters (parseJsonImport, parseXliffImport)
feeding one importResources(collection, resources, options) module that
owns reference resolution, Transloco→ICU normalisation, ICU auto-fix,
validation, grouping and per-folder processing over ResourceFolder, with
state in an ImportSession instead of positional accumulators. The import
barrel shrinks from 41 to 17 symbols; reference-resolver moves to domain.

Export gains the symmetric runExport(collections, options) that absorbs
the per-locale loop, filtering, exporter dispatch and aggregation from
the CLI. Export now uses each collection's own target locales, refuses
mixed base locales, and keeps a key shared by two collections in each
collection's own locales.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Core writes (add/edit/delete/move resource, create/delete/move folder,
add/remove locale, update collection) now return the ResourceMutations
they made. The API's 16-method CollectionCacheService is replaced by a
CollectionIndex with tree, search, status and apply; the state machine,
disk revalidation, incremental patching and fallback re-index live
inside it. Controllers shrink to open → call core → apply(mutations).

Also: collection updates/deletes and background translation jobs now
invalidate the index; createFolder emits no mutation for an existing
folder; moveFolder no longer deletes a source folder that still holds
resources skipped by a destination collision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Core now throws LingoTrackerError subclasses with stable codes
(ResourceNotFoundError, InvalidResourceKeyError, InvalidFolderPathError,
Locale*, Bundle*, InvalidBundleDefinitionError; TranslationError and
PreferredTerminologyValidationError join the hierarchy). Messages are
unchanged.

The API maps them in a single global exception filter and the
controllers' message-sniffing try/catch blocks are gone; unexpected
errors are logged and answered with a generic 500. TranslationError maps
by provider code (400/429/500/502). The CLI replaces the "cancelled"
string sentinel with PromptCancelledError and gains exitWithError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The editor's rules (dotted-key absorption, key collision, "where it
lands" context tree, tag edits, create/update DTO building, dirty-locale
diff, unsaved-changes check) now live in a framework-free
resource-entry-draft module with table tests. A shared segmentValidator
built on the domain isValidSegment replaces three regex copies, the
popover and the store share filterFolderTree, and the duplicate
splitResolvedKey is gone.

Every UI write of a resource entry (create, update, delete, translate)
goes through a new BrowserStore entry-writes feature that owns cache
patching and key rewriting in one place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add a pure domain module (STATUS_PRECEDENCE, countByStatus, worstStatus)
and one Tracker presentation table (icons, label tokens, display order,
rollupCenter). The three roll-up computations and seven status tables in
the browser list, rollup ring, locale column, status filter, sort and
breakdown text now use them. Rendering is unchanged; both existing list
orders (worst-first and new-first) are kept and named.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace export * with named, grouped exports in @simoncodes-ca/domain
and @simoncodes-ca/core (top level and sub-barrels), listing only names
with a consumer outside the library. Leaked internals (SafeAny,
ErrorMessages, file-io helpers, provider classes, ICU helpers) leave the
public surface; findIcuArguments had no consumer and is deleted.

TokenCasing moves to domain, and data-transfer's TranslationStatus and
TokenCasingDto become type re-exports of the domain types instead of
copies. The ICU auto-fixer tests move to domain with the code they test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ection

addResource, editResource, deleteResource, moveResource,
translateExistingResource, createFolder, deleteFolder and moveFolder take
the opened Collection as their first parameter, like importResources. Base
locale, locales and translation config come only from it: no 'en'
fallbacks, no allLocales, no cwd.

Core owns the locale seeding rule in resource/locale-seeding.ts
(seedLocales): a supplied translation wins, else auto-translate when the
collection enables it, else a copy of the base value as `new`. On edit it
runs after a base-value change for the locales the staleness rule marks
as needing work, and never replaces a real translation with a copy.

editResource(collection, key, changes) takes the full existing key and an
explicit `moveTo` folder ('' for the root); the destination is re-read
and re-checked for a collision right before the move.

Behaviour changes:
- POST resources: `CreateResourceDto.baseLocale` removed; partial
  translations are seeded; an unknown locale answers 400.
- PATCH resources: `targetFolder` replaced by `moveTo`; a folder change
  moves the entry (was 404); a destination collision answers 409.
- DELETE folders: missing folder 404, malformed path 400;
  `DeleteFolderResponseDto.error` removed.
- POST folders/move: typed errors replace message sniffing (400 for a
  malformed path or move into a descendant, 404 for a missing source).
- New typed errors: ResourceAlreadyExistsError, FolderNotFoundError,
  FolderMoveIntoDescendantError, AutoTranslationDisabledError.
- CLI edit-resource --target-folder moves the entry instead of being
  prepended to --key.
- Tracker sends the full key plus moveTo; toCreateDto drops baseLocale.

Deleted dead code: verifyChecksum, loadFullResourceTree,
createDefaultTranslations, resource.mapper.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
buildResourceSummary(fullKey, entry, collection) in libs/domain builds
the one read model of a resource entry: fullKey, folderPath, entryKey,
base {locale, value}, one target per collection target locale (value,
status, needsWork, sameAsBase from the staleness rule), comment, tags and
inheritedTags. The base locale comes from the Collection, never from the
metadata shape. ResourceSummaryDto is that type.

API: resource-tree and search-result mappers take the opened Collection
and the folder path; nested entries resolve against the requested path;
SearchResult carries source and metadata.

Tracker: the store is keyed by fullKey; key-resolution.ts and
listKeyFor/storeKey are gone. The pure row-view.ts module decides what a
row shows (locale order, compact line, markers, rollup counts,
canTranslate, long values) and the four row components only present it.
The editor uses the shared status label tokens. BrowserApiService hides
the HTTP 202 "index not ready" retry (5 x 1 s) and raises
CollectionIndexNotReadyError; when that gives up mid-session the stores
keep the tree on screen and toast instead of replacing it with an error.

Behaviour changes:
- DTO: key/translations/status replaced by fullKey, folderPath,
  entryKey, base and targets; tags and inheritedTags always arrays;
  targets list every collection target locale; metadata for locales
  the collection does not have is dropped.
- Optimistic move now removes the moved row below the root (fixes the
  full-key vs list-key filter). Drag data carries the real folderPath.
- Translate button also enabled for a target locale with no metadata.
- Rollup counts collection targets only.
- Not-ready tree reads retry for ~5 s, then report an error instead of
  being silently ignored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
readCollection(collection) in lib/resource/read-collection.ts walks a
collection's translations folder through ResourceFolder, opened with the
collection's own base locale, and returns StoredResource[] (fullKey,
folderPath, entryKey, entry, effectiveTags) plus the folders it could
not read. readCollectionFolders is the lazy per-folder walk behind it,
also used by loadResourceTree and searchTranslations. It replaces the
walkers in export-common, bundle/resource-loader and search.

One set of read rules: hidden folders skipped; a missing translations
folder is an empty collection; a folder that cannot be listed or parsed
is reported as a problem and skipped; an entry with no metadata record
is read with metadata {} (counts as new); effective tags are united here
and callers no longer recompute them.

validateResources(collections, options) validates each collection with
its own base locale and target locales and no longer dedupes keys across
collections. CLI validate opens collections with openCollection and
stops reading the global baseLocale/locales.

Bundle and type generation read each collection at its own base locale;
COLLECTION_BASE_LOCALE stands in for "each collection's base" in the
plan's key set and the debug-keys bundle.

Behaviour changes:
- validate: per-collection locales (more checks), a key in two
  collections is validated in both, missing metadata fails as new,
  unreadable folders fail the run; totalUniqueKeys counts per collection.
- export: metadata-less entries exported as new; hidden folders skipped;
  unreadable folders listed under malformedFiles; a missing translations
  folder is a warning.
- bundle/types/plan: base value from each collection's own baseLocale.
- translate-locale: entries with no metadata record are now translated.
- tree: entries without metadata appear; an unlistable subfolder is an
  empty node instead of failing the index.

Also: locale-spec-helpers.ts renamed to locale.spec-helpers.ts and
excluded from the lib build so vitest is no longer compiled into core;
shared temp-dir fixture helper for core specs; reader, loader, export,
tree and validate specs use real temp directories instead of fs mocks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
openTranslator(collection, { provider?, protectedTerms? }) returns
translate(entries, locales) -> { values, skipped }. It owns the enabled
check, the API key, provider construction, the complex-ICU skip, the
placeholder guard, the protected-term guard and translocoToICU
normalisation. seedLocales (add/edit), translateExistingResource and
translateLocale only pick what needs work by the Staleness rule.

- translateLocale(collection, { targetLocale, onProgress?, provider? })
  reads via the Collection Reader; result gains `warnings` for folders
  it could not read (CLI prints them, API job logs them)
- Collection gains protectedTermsFiles (resolved paths, no I/O);
  readProtectedTermsInForce(collection) reads them through the cache
- InMemoryTranslationProvider: second adapter, used by specs instead of
  vi.mock of core internals; add/edit/translate* accept { provider? }
- ProtectedTermsFileError (typed) replaces plain Error for a malformed
  terms file
- deleted auto-translate-resources, translation-orchestrator
  (translateBatch), icu-classifier shim and their specs

Behaviour changes:
- an auto-translation that drops a protected term is skipped, not stored;
  on add/edit that locale gets a base copy as `new` (a real translation
  is kept as `stale`)
- translateExistingResource and translateLocale store ICU-normalised
  values
- CLI translate-locale label "Skipped (ICU)" -> "Skipped (needs human
  translation)"; prints reader warnings
- a malformed protected-terms file now fails add/edit (auto-translation
  on), translateExistingResource and translateLocale (500 with message)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
resolveBundleCollections(definition, config, { cwd }) opens each
collection of a bundle once per run; selectBundleEntries(collections,
locale, { transformICUToTransloco, cache }) applies the 'All'
expansion, entry selection rules, bundledKeyPrefix and mergeStrategy
in one place and returns entries (value + winning origin), conflicts
and warnings. generateBundle, planBundle and the type file all select
through it.

- generateBundleTypes({ bundleKey, definition, keys, tokenCasing,
  tokenConstantName?, cwd? }) is synchronous and no longer reads
  config or collections; its copy of the selection loop is deleted
- deleted collectBundleData, processCollection, filterResources,
  matchesAnyRule, BundleKeyTrace, BundleKeyOrigin
- GenerateBundleParams.cwd; translations folders, dist and
  typeDistFile resolve against it (CLI passes its INIT_CWD-aware
  directory, API passes process.cwd()); planBundle passes its cwd to
  openCollection too
- both hierarchy builders use null-prototype nodes and own-property
  checks (a key such as __proto__.x or constructor.ok no longer walks
  or pollutes Object.prototype)
- generate-bundle, plan-bundle and generate-types specs run on the
  real filesystem; new bundle-selection spec

Behaviour changes:
- "Collection 'x' not found in config" is reported once per run, not
  once per locale
- the type generator no longer prints its own console warnings and no
  longer reads the disk a second time
- a resource key such as `constructor` is no longer dropped as a false
  conflict under merge
- the plan's exampleKey is the first key the selection produced
  (collection then folder order) rather than object-enumeration order
- CLI bundle paths resolve against the INIT_CWD-aware directory

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
defineCommand<Options>()({ name, collection: 'writable' | 'read' |
'none', collectionOption?, config?, prompts?, required?, run }) in
apps/cli/src/runner. The runner owns the project root (INIT_CWD), the
one interactive rule (stdin and stdout are both a terminal), loading
.lingo-tracker.json, resolving and opening the collection, asking the
questions or checking the required flags, cancellation, and turning a
thrown error into "<message>" and exit code 1. It sets process.exitCode
and returns; the CLI never calls process.exit().

- all 21 commands run through it; the large ones (normalize, bundle,
  export, import, validate, glossary, install-skill) keep their flow
  inside run
- deleted utils: config-loader, collection-prompts,
  collection-resolver, report-error; isInteractiveTerminal and
  executePromptsWithFallback left prompt-utils; unused ErrorMessages
  entries removed
- required flags are checked in both modes ('' counts as missing) and
  typed as present in run
- specs mock core loadConfig and runner/terminal and assert
  process.exitCode; new runner spec, new add-collection, move,
  translate-locale and main specs

Behaviour changes (see architecture-docs/cli.md, "Changes Introduced by
the Command Runner"):
- exit 1 where it was 0: a core error in add-collection,
  delete-collection, add-resource, edit-resource, delete-resource,
  move, add-locale, remove-locale; an unknown collection anywhere
  (including export); no collections configured; a missing required
  flag when non-interactive (including import --source/--locale); an
  empty interactive answer to a required field; partial failures in
  delete-resource, move, normalize, bundle; add-collection duplicate
  name; import parse/detect failures
- cancel prints one "<Name> cancelled." line and exits 0
- one interactive rule: commands that only checked stdout are
  non-interactive when stdin is piped
- find-similar and translate-locale auto-select the only collection;
  find-similar prompts for --value when interactive
- import --source and install-skill --dir resolve against the project
  root (INIT_CWD), like every other path
- add-resource --translations is parsed inside the command (a malformed
  value reports "Invalid --translations ..." and exits 1)
- validate --skip-placeholders is now passed through (was ignored)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
searchResources(resources, collection, query, { mode: 'text' |
'similar-value', limit }) in libs/core is the one matcher over any
iterable of resources: the Collection Reader for the disk, or
treeResources(tree) for the API's index tree. Every match is ranked
before the limit is applied, so a better match found late is never
dropped. Text mode keeps the four match types; similar-value mode
compares the base value with the query (normalizedLevenshtein >= 0.8,
or whole-word containment scoring shorter/longer with a 0.4 floor),
ranked by similarity, then key-contains-query, then key.

- deleted searchTranslations, searchResourceTree and their params;
  SearchResult drops `status` and no longer copies the base value into
  `translations`; gains `similarity`; MatchType gains 'similar-value'
- API: GET .../resources/search?mode=similar; SearchResultDto.similarity;
  CollectionIndex.search(collection, query, { mode, limit }) over the
  index tree or readCollection (reader problems logged); maxResults is
  validated as a positive integer (else 100), capped at 500
- CLI find-similar uses the shared rule: no 500-candidate cap, prints
  similarity from the matcher, warns on unreadable folders
- Tracker: similar-value-filter deleted; the editor dialog asks the API
  for 11 similar hits, drops the entry being edited, shows 10
- a hand-edited entry without a string source no longer crashes search

Behaviour changes: broad text searches return a better-ranked page;
the base value is always searched; find-similar may list whole-word
containment hits below 80% (>= 40%); the Tracker "Similar values" list
is ranked by similarity instead of substring containment on a 25-hit
text search; an invalid maxResults falls back to 100 (a non-numeric
value used to return everything, a negative one almost everything).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The bundle definition type and its rules live once, in libs/domain, and
core, the API, the CLI and the Tracker all use them:

- types EntrySelectionRule, CollectionBundleDefinition, BundleDefinition
  and hasTypeDistConfigured moved from core; the data-transfer DTOs are
  type aliases of them
- validateBundleKey, validateBundleDefinition(def, collectionNames) and
  validateJavaScriptIdentifier moved from core (messages unchanged);
  new tokenCasing rule
- new bundleOutputFile(def, locale) (posix path rule used by generate,
  plan, the API job result and the Tracker preview),
  normalizeBundleDefinition (trim, drop empty optionals and unknown
  fields, migrate legacy typeDist to typeDistFile, tolerate loose input),
  checkBundleDefinition (normalize + every error, key first) and
  findBundleDefinition (own-property lookup)
- core: config/bundle-definition.ts, validate-bundle-definition.ts and
  getBundleOutputPath deleted; bundle-definition-operations use the
  domain check; bundles are written with own properties (a key such as
  __proto__ or constructor is stored and found as a normal key)
- API: bundle.mapper keeps only plan and job-result mapping;
  config.mapper passes bundles through; the controller drops its own
  400/404/409 handling (core typed errors reach the filter); the filter
  answers InvalidBundleDefinitionError with { statusCode, message,
  error, errors }
- Tracker: the bundle form's field validators call the domain
  predicates, submit re-runs checkBundleDefinition and shows any
  remaining messages, the preview uses bundleOutputFile, a legacy
  typeDist bundle loads with its types file; the store appends
  `errors` to a bundle 400 message

Behaviour changes:
- 400 body for an invalid bundle gains statusCode and error (additive)
- PUT on a missing bundle with no `bundle` body answers 400 (was 404);
  POST with neither name nor definition reports the definition first
- GET /config passes unknown or deprecated bundle fields through as
  written; saving a bundle migrates typeDist to typeDistFile
- output paths always use `/`; a bundle named constructor is created,
  found and generated as a normal bundle (was 409 / a bogus job)
- tokenCasing other than upperCase or camelCase is rejected

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Follow-up to the Resource Summary change (76b87e7), from a retrospective
Codex review of that commit.

- displayStatus(target) in shared/translation-status: the stored status,
  or `new` for a target that needs work without one (an entry without
  metadata is treated as `new` everywhere). Presentation only. Used by
  the status filter and "Needs work" shortcut, the per-status counts,
  needsWorkCount, the status sort, the row view (both densities) and
  the rollup, so they all agree on such a locale
- folderTreeLoaded state replaces the `rootFolders().length === 0`
  heuristic in keepTreeOnNotReady and the cache-status root load: a
  collection with root resources and no folders no longer loses its
  list to the error state on a later "index not ready" response
- frontend.md: the moveTo rows describe the store's unconditional drop
  and its includeNested limitation; the row-view table and the
  not-ready handling reflect the above

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…larity

Five findings left from round two:

- cli: ConsoleFormatter.error and .warning (with a new `details` list)
  write to stderr; success, info, progress, key-value and section
  output stay on stdout. bundle, export, import, delete-resource, move,
  normalize, glossary, validate and preferred-terminology route their
  diagnostics through it, and no stdout-only blank line frames a
  stderr block, so `glossary --stdout`, `normalize --json` and
  `bundle --quiet` keep stdout clean. Dead CLI ErrorMessages deleted.
  Scripts that read "❌" text from stdout must read stderr (2>&1).
- tracker: sort by status with no locale selected now reads every
  available locale (it received an empty list and degenerated to key
  order); one `_statusLocales` computed feeds the filter, counts and
  sort
- cli: delete-collection confirms when interactive unless --yes
  (the runner auto-selects a single collection); non-interactive is
  unchanged
- api: TranslateLocaleJobDto.error carries why a job failed
- tracker: the "Similar values" rows show the similarity as a quiet
  percentage chip, with a translated screen-reader label
  (browser.similarTranslations.similarityX)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h CLAUDE.md

- tracker: new cached typecheck-spec target (tsc --noEmit -p
  tsconfig.spec.json, after generate-tokens) that typecheck depends on;
  both it and test hash the generated token file through
  dependentTasksOutputFiles; tsconfig.spec.json resolves vitest types
  and includes src/testing. 30 latent spec type errors in 13 files
  fixed without weakening assertions (patchState(unprotected(store)),
  DTO mock shapes, typed dialog/translate/media-query mocks); three
  explicit `any`s removed; one trivial spec deleted
- ci: pr.yml runs `nx affected -t typecheck` between test and build
- cli: `move --dest-collection <name>` registered (the command already
  opened the destination writable); unused `--verbose` removed; specs
  for the cross-collection call shape and an unknown destination
- CLAUDE.md: CLI Command Pattern shows the runner; Adding a Resource,
  API Controller example, single-file test commands per project
  (Vitest --testFile vs positional path for cli/api Jest) and the Nx
  and Angular versions corrected
- docs: monorepo-structure.md typecheck table and test runners;
  frontend.md testing section; cli.md and docs/cli.md move row with
  the cross-collection example and the metadata-verbatim note

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- cli, core and api gain a cached typecheck-spec target (tsc --noEmit
  -p tsconfig.spec.json) that typecheck depends on, as the tracker did
  in the previous commit; spec tsconfigs resolve vitest types through
  typeRoots; core's spec config drops module: commonjs and composite
  (nothing references it); api's tsconfigs write to the workspace
  dist (they wrote to apps/dist)
- 76 cli and 186 core spec type errors fixed without deleting tests or
  weakening assertions: bracket access on index signatures, typed fs
  mocks (new mockedReaddirSync spec helper replaces a SafeAny),
  fixtures corrected to the real config, ImportResult, mutation and
  metadata shapes, stale fields removed, unnecessary casts removed;
  the add-resource fixture keeps a single locale so the overwrite test
  exercises the overwrite prompt
- two production errors visible only under the spec configs fixed at
  the root: the duplicate ambient `xliff` declaration
  (libs/core/src/typings.d.ts deleted; types/xliff.d.ts now marks
  target optional and adds sourceLanguage/targetLanguage) and an
  order-dependent inference in glossary-extractor
- xliff import: a trans-unit's notes become a one-per-line comment;
  the exporter's own "Do not translate: …" note (shared
  PROTECTED_TERMS_NOTE_PREFIX) is dropped so a comment round-trips
  unchanged (a repeated <note> used to store a string[] in the comment)
- docs: typecheck table and test runners in monorepo-structure.md,
  CLAUDE.md typecheck note, XLIFF note handling in core-library.md and
  docs/features; Nx 22.7.9 / Angular 21 in README, frontend, PRODUCT

Behaviour change: api:build (via ^typecheck) and the pre-commit hook
(nx typecheck core) now also typecheck the core specs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@simoncodes-ca
simoncodes-ca merged commit ba330a9 into simoncodes-ca:develop Sep 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants