refactor: architecture deepening (rounds one and two) - #109
Merged
simoncodes-ca merged 20 commits intoSep 25, 2026
Merged
simoncodes-ca merged 20 commits into
simoncodes-ca merged 20 commits into
Conversation
…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
approved these changes
Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
addResource,editResource,moveResource, folder ops take an openedCollection; core owns locale seeding; explicitmoveToon edit (fixes the Tracker edit-with-folder 404).fullKey,folderPath,entryKey) plus per-target flags; Tracker rows derive from a purerowView; the 202 "index not ready" retry is hidden in the API service.readCollection(collection)behind export, bundle, search, validate and the resource tree; real-fs specs replace fs mocks.openTranslator(collection, { provider? })owns the API key, ICU skip, placeholder and protected-term guards and ICU normalisation; in-memory provider for specs.selectBundleEntriesfor the JSON bundle, the dry-run plan and the type file; paths resolve against the CLI's INIT_CWD.defineCommand({ collection, prompts, required, run })owns config, collection resolution, the one interactive rule, cancellation and exit codes.?mode=similarfor the CLI and the Tracker.Follow-ups
Stderr diagnostics for the CLI, status sort fix,
delete-collectionconfirmation, 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, refreshedCLAUDE.md.Behaviour changes to know about
HTTP / DTOs
CreateResourceDto.baseLocaleremoved;UpdateResourceDto.targetFolder→moveTo;DeleteFolderResponseDto.errorremoved.?mode=similar,similarity,matchType: 'similar-value'; invalidmaxResultsfalls back to 100.statusCode/error; PUT on a missing bundle with no body is 400;GET /configpasses unknown bundle fields through; saving migrates legacytypeDist;tokenCasingvalidated.TranslateLocaleJobDto.erroradded.CLI
❌/⚠️diagnostics go to stderr; stdout is the payload (glossary --stdout,normalize --json,bundle --quietare clean). Scripts reading errors from stdout need2>&1.delete-collectionconfirms when interactive (--yesto skip);move --dest-collectionexposed;find-similarhas no 500-candidate cap;import --sourceandinstall-skill --dirresolve against the project root;validate --skip-placeholdersnow works.Core / validate / bundle / translate
/paths, and no longer drop a key namedconstructor.Build
typechecknow includes spec typechecks for tracker, core, cli and api; CI runsaffected -t typecheck;api:buildand the pre-commit hook therefore typecheck core specs.Verification
nx run-many -t test,typecheck,buildfor all six projects andpnpm run lintare 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