🤖 Generated by AI This library was generated by Claude (Anthropic) from decompiled Primo host application source code extracted using primo-extract. Do not edit by hand — re-generate using the prompt below whenever the host application is updated. See Regenerating this package for instructions.
Shared state models and Angular services for the Primo module-federation architecture. Remote/client modules use this package to read the host application's NgRx store (user, search, filter slices) and dispatch a curated set of safe actions back to it.
| Layer | Contents |
|---|---|
Models (src/models/) |
TypeScript interfaces mirroring the host's state shapes: SearchParams, Doc, UserState, FilterState, LoadingStatus, … |
Services (src/state/) |
Three providedIn: 'root' Angular services — UserStateService, SearchStateService, FilterStateService — each offering Observable streams, one-shot Promise snapshots, Angular Signals, and typed dispatch helpers |
Actions (src/actions/) |
shared-actions.ts — re-exported NgRx action creators whose type strings match the host's reducers byte-for-byte |
Utility (src/utils/) |
StateHelper — thin wrapper around Store used internally by all services |
- Peer dependencies
- Building & packaging
- Deploying to a module-federation remote client
- Usage
- Why not all host actions are exported
- API reference
- Types & interfaces
- Actions reference
- Troubleshooting
- Versioning
- Regenerating this package
| Package | Version |
|---|---|
@angular/core |
^19.0.0 |
@angular/common |
^19.0.0 |
@angular/platform-browser |
^19.0.0 |
@ngrx/store |
^19.0.0 |
rxjs |
^7.0.0 |
These must match the host application's singleton versions exactly (enforced via module-federation strictVersion).
# 1. Install dev dependencies
npm install
# 2. Compile (output → dist/)
npm run build
# 3. Create a distributable tarball
npm pack
# → libis-primo-shared-state-1.0.0.tgznpm pack
cp libis-primo-shared-state-1.0.0.tgz path/to/NDE_customModule/nde/"dependencies": {
"@libis/primo-shared-state": "file:nde/libis-primo-shared-state-1.0.0.tgz"
}cd path/to/NDE_customModule_LIBISstyle
npm installThis lib wraps @ngrx/store and @angular/core — both of which the host already bootstrapped as singletons. If you do not add it to the shared map, webpack module federation will bundle a private copy of the lib inside the remote's chunk. That private copy resolves its own Store injection token, which is completely isolated from the host's Store. As a result, all selectors return empty/undefined and dispatched actions are silently swallowed — the lib appears to load fine but does nothing.
The project uses ModuleFederationPlugin directly (not withModuleFederationPlugin) with the lower-level share() helper. Add @libis/primo-shared-state alongside the other shared packages:
// webpack.config.js (remote / client only — the host has no knowledge of this lib)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const share = mf.share;
new ModuleFederationPlugin({
library: { type: 'module' },
name: 'customModule',
filename: 'remoteEntry.js',
exposes: {
'./custom-module': './src/bootstrap.ts',
},
shared: share({
'@angular/core': { singleton: true, requiredVersion: 'auto' },
'@angular/common': { singleton: true, requiredVersion: 'auto' },
'@angular/router': { singleton: true, requiredVersion: 'auto' },
'@angular/common/http': { requiredVersion: 'auto' }, // ¹
'@angular/platform-browser': { requiredVersion: 'auto' }, // ¹
'rxjs': { requiredVersion: 'auto' }, // ¹
'@ngx-translate/core': { singleton: true },
'@ngrx/store': { singleton: true },
'@libis/primo-shared-state': { singleton: true, strictVersion: false },
}),
}),Why
singleton: trueon@angular/core,@angular/common, and@angular/router? These three packages form Angular's core DI infrastructure — injectors, the component registry, and the router outlet tree. A second copy of any of them creates a completely separate DI tree that cannot share services, pipes, or directives with the host.singleton: trueforces module federation to use whichever copy was loaded first (the host's) rather than loading a second one from the remote bundle.
¹ Why
singleton: trueis intentionally omitted onrxjs,@angular/common/http, and@angular/platform-browser?Primo (the host) runs on Angular 19.1.3 and NgRx 19.0.0. At the time of writing, this remote project is built against Angular 18. When
singleton: trueis set on a package that the host also exposes in its shared scope (which these three are), module federation performs a version negotiation: it picks one copy for the entire page and warns or throws if the versions are incompatible.Because the remote's version (
^18) does not satisfy the host's offered version (19.1.3), addingsingleton: truecauses the following runtime crash as soon as the remote chunk loads:TypeError: t is not a function at __definition.ts:653:40 at Array.forEachThis happens because Angular 19 internals call into functions that changed signature between v18 and v19 — when two mismatched Angular copies are negotiated into one, the mismatch manifests as a broken function reference inside Angular's own code.
Without
singleton: true, module federation does not try to negotiate a shared instance. Instead it lets the remote load its own bundled copy for those packages. This means two copies ofrxjsetc. exist on the page, but those packages are stateless utility libraries — unlike@angular/core, a second copy ofrxjscauses no runtime errors.The permanent fix is to upgrade the remote to Angular 19.1.x to match Primo exactly, then restore
singleton: trueandrequiredVersion: 'auto'on all Angular packages. Until that upgrade is done, omittingsingleton: trueon the version-mismatched packages is the pragmatic workaround.
Why
strictVersion: falsefor@libis/primo-shared-state? The host does not ship or share this lib at all — it has no knowledge of it.strictVersion: true(the default) would cause a runtime error because there is no host-provided version to satisfy the constraint.strictVersion: falsetells module federation to use the remote's own copy without requiring a matching offer from the host.
No changes to the host's webpack.config.js are needed or possible — the host is a black box.
import { Component } from '@angular/core';
import {
UserStateService,
SearchStateService,
FilterStateService,
} from '@libis/primo-shared-state';
@Component({ /* … */ })
export class MyComponent {
// User
isLoggedIn$ = this.user.selectIsLoggedIn$();
userName$ = this.user.selectUserName$();
userGroup$ = this.user.selectUserGroup$();
jwt$ = this.user.selectJwt$();
settings$ = this.user.selectUserSettings$();
// Search
docs$ = this.search.selectAllDocs$();
isLoading$ = this.search.selectIsLoading$();
totalResults$ = this.search.selectTotalResults$();
searchParams$ = this.search.selectSearchParams$();
metaData$ = this.search.selectSearchMetaData$();
pageSize$ = this.search.selectPageSize$();
// Filters
includedFilters$ = this.filter.selectIncludedFilters$();
excludedFilters$ = this.filter.selectExcludedFilters$();
multiFilters$ = this.filter.selectMultiSelectedFilters$();
resourceTypeFilter$ = this.filter.selectResourceTypeFilter$();
isFiltersOpen$ = this.filter.selectIsFiltersOpen$();
constructor(
private user: UserStateService,
private search: SearchStateService,
private filter: FilterStateService,
) {}
}import { Component } from '@angular/core';
import { UserStateService, SearchStateService, FilterStateService } from '@libis/primo-shared-state';
@Component({
template: `
<p>{{ isLoggedIn() ? 'Logged in as ' + userName() : 'Guest' }}</p>
<p>{{ totalResults() }} results — status: {{ searchStatus() }}</p>
`
})
export class MySignalComponent {
// User signals
isLoggedIn = this.user.isLoggedInSignal();
userName = this.user.userNameSignal();
userGroup = this.user.userGroupSignal();
jwt = this.user.jwtSignal();
userSettings = this.user.userSettingsSignal();
decodedJwt = this.user.decodedJwtSignal();
// Search signals
docs = this.search.allDocsSignal();
searchParams = this.search.searchParamsSignal();
metaData = this.search.searchMetaDataSignal();
searchStatus = this.search.searchStatusSignal();
totalResults = this.search.totalResultsSignal();
pageSize = this.search.pageSizeSignal();
isLoading = this.search.isLoadingSignal();
// Filter signals
filterState = this.filter.filterStateSignal();
includedFilters = this.filter.includedFiltersSignal();
excludedFilters = this.filter.excludedFiltersSignal();
multiFilters = this.filter.multiSelectedFiltersSignal();
resourceTypeFilter = this.filter.resourceTypeFilterSignal();
isFiltersOpen = this.filter.isFiltersOpenSignal();
isRememberAll = this.filter.isRememberAllSignal();
constructor(
private user: UserStateService,
private search: SearchStateService,
private filter: FilterStateService,
) {}
}const jwt = await this.user.getJwt();
const loggedIn = await this.user.isLoggedIn();
const settings = await this.user.getUserSettings();
const docs = await this.search.getAllDocs();
const doc = await this.search.getDocById('someId');
const params = await this.search.getSearchParams();
const included = await this.filter.getIncludedFilters();
const excluded = await this.filter.getExcludedFilters();
const multi = await this.filter.getMultiSelectedFilters();Instead of importing action creators directly, use the convenience methods on each service:
// SearchStateService
this.search.search({ q: 'angular', scope: 'Everything' });
this.search.search({ q: 'angular', scope: 'Everything' }, 'blended');
this.search.clearSearch();
this.search.setPageLimit(25);
this.search.setPageNumber(2);
this.search.setSortBy('date');
this.search.setIsSavedSearch(true);
this.search.setSearchNotificationMessage('Saved search loaded');
// FilterStateService
this.filter.loadFilters({ q: 'angular', scope: 'Everything' });
this.filter.updateSortByParam('rank');
// UserStateService — safe settings-only operations
this.user.setLanguage('en');
this.user.setSaveHistory('true');
this.user.setUseHistory('true');
this.user.setAutoExtendMySession('false');
this.user.setAllowSavingRaSearchHistory('true');
this.user.setDecodedJwt(decodedJwtObject);
this.user.setLoginFromState('/search');
this.user.resetLogoutReason();All three services expose a dispatch(action) method for anything not covered by the helpers:
import { resetJwtAction } from '@libis/primo-shared-state';
this.user.dispatch(resetJwtAction({ logoutReason: 'user' }));You can also import action creators directly:
import {
searchAction,
clearSearchAction,
pageLimitChangedAction,
loadFiltersAction,
setDecodedJwt,
// …
} from '@libis/primo-shared-state';
⚠️ Effects warning — the host app has NgRx Effects listening to these actions (HTTP calls, etc.). Do not register your ownEffectsModule.forFeature()with effects that re-implement the same actions. UseActions+ofTypeto listen without triggering duplicates.
shared-actions.ts acts as a compile-time safety gate. Because it is the only place action creators are exported from the package, TypeScript prevents any remote module from importing — and therefore dispatching — an action that has not been explicitly approved. A named import that does not exist in shared-actions.ts will fail at compile time, before your code ever runs:
// ✅ Works — searchSuccessAction is exported
import { searchSuccessAction } from '@libis/primo-shared-state';
// ❌ Compile error — deliverySuccessAction is not exported
import { deliverySuccessAction } from '@libis/primo-shared-state';This gate only protects you if you use named imports. The two anti-patterns below both bypass it silently — which is exactly what makes them dangerous.
The golden rule: if it is not exported from
shared-actions.ts, do not dispatch it.
The most dangerous mistake is reaching past the safety gate by constructing an action manually using its raw type string:
// ❌ DO NOT DO THIS
this.store.dispatch({ type: '[Collection Discovery] Get Collections Tree Success', tree: subset });This bypasses every guarantee the export list provides. Even if it appears to work today, you have no protection against:
- State corruption — the action's reducer may overwrite data that other host components depend on.
- Unintended HTTP side-effects — a host effect may be listening to that action type and fire a real HTTP call in response.
- Silent breakage on upgrades — the host can rename, split, or remove that action at any time without notice; your remote will break with no compile-time error.
The same risk applies to dispatching *Success or *Failed actions that are not exported, even if their names look harmless. An action being named "success" does not make it safe to dispatch from a remote.
A tempting workaround when the server returns wrong or missing pnx data is to build a client-side overlay service that intercepts the store's docs, applies local patches, and exposes a "corrected" Observable or Signal:
// ❌ DO NOT DO THIS
@Injectable({ providedIn: 'root' })
export class DocPatchService {
private patches$ = new BehaviorSubject<Map<string, Partial<Pnx>>>(new Map());
patchDocPnx(recordId: string, patch: Partial<Pnx>): void {
// …merge patch into map…
}
selectAllDocsPatched$(): Observable<Doc[]> {
return combineLatest([storeDocs$, this.patches$]).pipe(
map(([docs, patches]) => docs.map(doc => applyPatch(doc, patches.get(doc['@id']))))
);
}
}Why you shouldn't do this:
-
Split truth — host components always read the original, unpatched entities from the NgRx store. Your remote component would show corrected data while every host component (search result tiles, full display, availability panel) still shows the wrong data side-by-side. The user sees inconsistent information on the same screen.
-
Fragile lifecycle — you must manually track when to clear the patch (new search, navigation, full-display open). Any timing gap means stale overrides bleed across searches.
-
Invisible to devtools — because the patches live outside the store, NgRx DevTools shows the original state. Debugging why a component renders something different from what the store contains becomes very confusing.
-
Symptom, not cause — if the server consistently returns wrong data, the fix belongs server-side (Primo NUI configuration, normalization rules, or a back-end mapping). A client overlay papers over a data quality issue without fixing it.
The correct approach when pnx data needs correction:
- Fix the data at source — adjust the Primo NUI/backend configuration so the server returns correct data.
- If you only need to display derived or supplemental data (e.g. a cover image from a third-party API), keep that data entirely in your remote component's local state and never mix it with store docs.
- If you need to react to what the server returned, use
selectAllDocs$()to read the data and apply display-layer transformations in your component's template pipe or view model — without touching or shadowing the store.
shared-actions.ts exports three kinds of actions:
These tell the host to start a well-defined operation. The host's effects own the HTTP call; the remote only supplies the parameters.
| Action | What it does |
|---|---|
searchAction |
Tells the host to run a search |
loadFiltersAction |
Triggers a filter HTTP call via the host's effect |
clearSearchAction |
Resets search state, no HTTP call |
These write a simple scalar to the store. No host effect listens to them, so there is no risk of triggering an HTTP call.
| Action | What it does |
|---|---|
pageLimitChangedAction |
Updates the page size in the store |
pageNumberChangedAction |
Updates the current page |
sortByChangedAction |
Updates the sort field |
doneChangeUserSettingsLanguageAction |
Writes the chosen language to the user slice |
Some *Success and *Failed actions are exported. These are safe because they are terminal writes — their reducer updates the store, and no host effect listens to them afterwards. Dispatching them is equivalent to telling the host "treat this data as if the server returned it".
| Action | Why safe to dispatch |
|---|---|
searchSuccessAction |
Writes search results to the store; no effect fires on it |
searchFailedAction |
Sets an error/loading flag; no effect fires on it |
filtersSuccessAction |
Writes filter data to the store; no effect fires on it |
filterFailedAction |
Sets a filter error flag; no effect fires on it |
Not all success actions are safe. The export list is the definitive answer. For example,
deliverySuccessActionis not exported because a host effect listens to it and fires a second HTTP call. See the chain below.
These are actions the host's effects emit after an HTTP call, and another effect listens to them in turn. Dispatching them from a remote corrupts state and fires real HTTP calls.
A concrete example is deliverySuccessAction:
searchSuccessAction ← ✅ exported, remote CAN dispatch
└─► [Search] Load delivery ← DeliveryEffect fires an HTTP call
└─► deliverySuccessAction ← ❌ NOT exported; effect emits this when HTTP responds
│
├─► search.reducer overwrites pnx data on every Doc entity
└─► DeliveryEffect triggers a second HTTP call (eDelivery)
If a remote dispatched deliverySuccessAction with a fabricated or incomplete Doc[] payload it would overwrite pnx data on all current search result entities (blanking display fields, availability, links) and simultaneously fire a real HTTP call to the delivery service.
Actions such as [User] load jwt guest, [User] load logged user jwt, and [User] reset jwt kick off OAuth/ILS authentication flows managed entirely by the host. A remote module has no business initiating or short-circuiting those flows.
| Category | Exported | Reason |
|---|---|---|
| Commands — start a search / filter load | ✅ | Remote legitimately triggers these |
| Pure UI-state writes — pagination, sort, clear | ✅ | No HTTP side-effects |
| Safe settings writes — language, history toggles | ✅ | Only mutate the user settings slice |
| Terminal success/failed writes — no downstream effects | ✅ | Safe to dispatch; no effect listens to them |
| Success/failed actions that feed downstream effects | ❌ | Re-triggers HTTP calls and corrupts state |
| Auth flow triggers — JWT load, login, logout | ❌ | Authentication is owned entirely by the host |
Any action not in shared-actions.ts |
❌ | Even via literal type string — bypasses all safety guarantees |
If you need to react to an action that is not exported (e.g. do something after delivery finishes loading), use Actions + ofType to listen passively — do not dispatch:
import { inject } from '@angular/core';
import { Actions, ofType } from '@ngrx/effects';
// Inside a service or component constructor:
private actions$ = inject(Actions);
constructor() {
// React to delivery completing without dispatching anything
this.actions$.pipe(
ofType('[Search] Load delivery success') // use the literal type string
).subscribe(({ docsToUpdate }) => {
// read-only reaction — never re-dispatch this action
});
}Do not register an
EffectsModule.forFeature()in the remote with effects that handle the same action type strings as the host — that causes every HTTP call to fire twice.
| Method | Returns | Description |
|---|---|---|
selectUserState$() |
Observable<UserState> |
Full user state slice |
selectJwt$() |
Observable<string | undefined> |
Raw JWT |
selectDecodedJwt$() |
Observable<DecodedJwt | undefined> |
Decoded JWT payload |
selectIsLoggedIn$() |
Observable<boolean> |
Login status |
selectUserSettings$() |
Observable<UserSettings | undefined> |
User preferences |
selectUserName$() |
Observable<string | undefined> |
Username from JWT |
selectUserGroup$() |
Observable<string> |
User group (default 'GUEST') |
| Method | Returns | Initial value |
|---|---|---|
userStateSignal() |
Signal<UserState> |
{} |
jwtSignal() |
Signal<string | undefined> |
undefined |
decodedJwtSignal() |
Signal<DecodedJwt | undefined> |
undefined |
isLoggedInSignal() |
Signal<boolean> |
false |
userSettingsSignal() |
Signal<UserSettings | undefined> |
undefined |
userNameSignal() |
Signal<string | undefined> |
undefined |
userGroupSignal() |
Signal<string> |
'GUEST' |
| Method | Returns |
|---|---|
getJwt() |
Promise<string | undefined> |
isLoggedIn() |
Promise<boolean> |
getUserSettings() |
Promise<UserSettings | undefined> |
setDecodedJwt(jwt) · setLoginFromState(v) · resetLogoutReason() · setLanguage(v) · setSaveHistory(v) · setUseHistory(v) · setAutoExtendMySession(v) · setAllowSavingRaSearchHistory(v) · dispatch(action)
| Method | Returns | Description |
|---|---|---|
selectAllDocs$() |
Observable<Doc[]> |
All docs in entity map |
selectDocById$(id) |
Observable<Doc | undefined> |
Single doc by entity ID |
selectSearchParams$() |
Observable<SearchParams | null> |
Active search params |
selectSearchMetaData$() |
Observable<SearchMetaData | null> |
Totals, facets, highlights |
selectSearchStatus$() |
Observable<LoadingStatus> |
pending/loading/success/fail |
selectTotalResults$() |
Observable<number> |
info.total |
selectPageSize$() |
Observable<number | null> |
Selected page size |
selectIsLoading$() |
Observable<boolean> |
status === 'loading' |
| Method | Returns | Initial value |
|---|---|---|
allDocsSignal() |
Signal<Doc[]> |
[] |
searchParamsSignal() |
Signal<SearchParams | null> |
null |
searchMetaDataSignal() |
Signal<SearchMetaData | null> |
null |
searchStatusSignal() |
Signal<LoadingStatus> |
'pending' |
totalResultsSignal() |
Signal<number> |
0 |
pageSizeSignal() |
Signal<number | null> |
null |
isLoadingSignal() |
Signal<boolean> |
false |
| Method | Returns |
|---|---|
getAllDocs() |
Promise<Doc[]> |
getDocById(id) |
Promise<Doc | undefined> |
getSearchParams() |
Promise<SearchParams | null> |
search(params, type?) · clearSearch() · setPageLimit(n) · setPageNumber(n) · setSortBy(s) · setIsSavedSearch(b) · setSearchNotificationMessage(s) · dispatch(action)
| Method | Returns | Description |
|---|---|---|
selectFilterState$() |
Observable<FilterState> |
Full filter slice |
selectIncludedFilters$() |
Observable<selectedFilters[] | null> |
Include facets |
selectExcludedFilters$() |
Observable<selectedFilters[] | null> |
Exclude facets |
selectMultiSelectedFilters$() |
Observable<MultiSelectedFilter[] | null> |
Multi-select facets |
selectResourceTypeFilter$() |
Observable<ResourceTypeFilterModel | null> |
Resource type bar |
selectIsFiltersOpen$() |
Observable<boolean> |
Filter panel open state |
selectIsRememberAll$() |
Observable<boolean> |
Remember-all toggle |
| Method | Returns | Initial value |
|---|---|---|
filterStateSignal() |
Signal<FilterState> |
{} |
includedFiltersSignal() |
Signal<selectedFilters[] | null> |
null |
excludedFiltersSignal() |
Signal<selectedFilters[] | null> |
null |
multiSelectedFiltersSignal() |
Signal<MultiSelectedFilter[] | null> |
null |
resourceTypeFilterSignal() |
Signal<ResourceTypeFilterModel | null> |
null |
isFiltersOpenSignal() |
Signal<boolean> |
false |
isRememberAllSignal() |
Signal<boolean> |
false |
| Method | Returns |
|---|---|
getIncludedFilters() |
Promise<selectedFilters[] | null> |
getExcludedFilters() |
Promise<selectedFilters[] | null> |
getMultiSelectedFilters() |
Promise<MultiSelectedFilter[] | null> |
loadFilters(params) · updateSortByParam(s) · dispatch(action)
All types below are exported from the package root and can be imported directly:
import { Doc, SearchParams, FilterState, LoadingStatus } from '@libis/primo-shared-state';
Defined in src/models/state.const.ts.
type LoadingStatus = 'pending' | 'loading' | 'success' | 'fail';
type LogoutReason = 'user' | 'timeout';| Constant | Value | Description |
|---|---|---|
PENDING |
'pending' |
Initial state — no request started |
LOADING |
'loading' |
HTTP request in flight |
SUCCESS |
'success' |
Request completed successfully |
FAIL |
'fail' |
Request completed with error |
USER |
'user' |
Logout triggered by the user |
TIMEOUT |
'timeout' |
Logout triggered by session timeout |
Defined in src/models/user.model.ts.
Top-level state slice for the authenticated user.
| Field | Type | Description |
|---|---|---|
jwt |
string | undefined |
Raw JWT token string |
decodedJwt |
DecodedJwt | undefined |
Parsed JWT payload |
status |
LoadingStatus |
Current load status of the user/JWT |
isLoggedIn |
boolean |
Whether the user is authenticated |
loginFromState |
string | undefined |
URL the user was on before login redirect |
userSettings |
UserSettings | undefined |
Persisted user preferences |
userSettingsStatus |
LoadingStatus |
Load status of userSettings |
logoutReason |
LogoutReason | undefined |
Why the last logout happened |
Parsed claims from the Primo JWT.
| Field | Type | Description |
|---|---|---|
userName |
string |
Login identifier (barcode / username) |
displayName |
string |
Human-readable name |
userGroup |
string |
Primo user group (e.g. 'Staff', 'GUEST') |
onCampus |
boolean |
Whether the IP is on-campus |
signedIn |
boolean |
Whether the user is actively signed in |
authenticationProfile |
string |
ILS authentication profile identifier |
user |
string |
Raw user field from JWT |
Key/value map of persisted user preferences. All fields are optional strings.
| Field | Type | Description |
|---|---|---|
resultsBulkSize |
string? |
Number of results per page |
language |
string? |
Preferred UI language code |
saveSearchHistory |
string? |
'true'/'false' — whether search history is saved |
useSearchHistory |
string? |
'true'/'false' — whether history is used |
autoExtendMySession |
string? |
'true'/'false' — auto session extension |
allowSavingMyResearchAssistanceSearchHistory |
string? |
Research assistant history opt-in |
email |
string? |
User's email address |
[key: string] |
string | undefined |
Index signature for additional settings |
Defined in src/models/search.model.ts.
Parameters sent to the host search engine. q and scope are required; all other fields are optional.
| Field | Type | Description |
|---|---|---|
q |
string |
Query string |
scope |
string |
Search scope identifier |
skipDelivery |
stringBoolean? |
Skip delivery enrichment ('Y'/'N') |
offset |
number? |
Pagination offset |
limit |
number? |
Results per page |
sort |
string? |
Sort field |
inst |
string? |
Institution code |
refEntryActive |
boolean? |
Enable reference entry mode |
disableCache |
boolean? |
Bypass server-side cache |
newspapersActive |
boolean? |
Include newspaper source |
qInclude |
string[]? |
Facet include filters |
qExclude |
string[]? |
Facet exclude filters |
multiFacets |
string[]? |
Multi-select facet values |
isRapido |
boolean? |
Rapido resource-sharing search |
pfilter |
string? |
Pre-filter string |
explain |
string? |
Debug explain mode |
tab |
string? |
Active search tab |
originalNLSquery |
string? |
Original natural language query |
isNLS |
boolean? |
Natural language search flag |
mode |
string? |
Search mode |
isCDSearch |
boolean? |
Combined digital search flag |
pcAvailability |
boolean? |
Primo Central availability check |
searchInFulltextUserSelection |
boolean? |
Full-text search user preference |
newspapersSearch |
boolean? |
Newspaper-specific search |
citationTrailFilterByAvailability |
boolean? |
Filter citation trail by availability |
isRAsearch |
boolean? |
Research Assistant search |
isNaturalLanguageSearch |
boolean? |
NLS flag (alternative) |
featuredNewspapersIssnList |
string? |
Featured newspaper ISSNs |
journals |
string? |
Journal filter |
databases |
string? |
Database filter |
entityName |
string? |
Named entity filter |
lang |
string? |
Language filter |
browseField |
string? |
Browse field identifier |
fn |
string? |
Function identifier |
searchWord |
string? |
Browse search word |
browseParams |
string? |
Additional browse parameters |
isRelatedItems |
boolean? |
Related items search flag |
analyticAction |
string? |
Analytics event identifier |
Same as SearchParams but qInclude, qExclude, and multiFacets are pre-serialised as pipe-delimited strings instead of arrays. Used internally when constructing URL query strings.
type SearchParamsWithStrParams = Omit<SearchParams, 'qInclude' | 'qExclude' | 'multiFacets'> & {
qInclude?: string;
qExclude?: string;
multiFacets?: string;
}SearchData is the full response returned by the search API.
SearchMetaData is SearchData without the docs array (i.e. Omit<SearchData, 'docs'>).
| Field | Type | Description |
|---|---|---|
beaconO22 |
string |
Beacon identifier |
info |
Info |
Totals, pagination info |
highlights |
Highlights |
Highlighted term fragments |
docs |
Doc[] |
Array of result documents |
facets |
Facet[]? |
Available facet groups |
timelog |
Timelog |
Server-side performance timings |
did_u_mean |
string? |
Spelling suggestion |
expandedSearchAfterZeroResults |
boolean? |
Search was expanded due to zero results |
Pagination and result-count metadata.
| Field | Type | Description |
|---|---|---|
totalResultsLocal |
number |
Local index result count |
totalResultsPC |
number |
Primo Central result count |
total |
number |
Combined total |
first |
number |
Index of first returned result |
last |
number |
Index of last returned result |
explain |
Explain |
Error/debug messages |
browseGap |
number? |
Gap for browse navigation |
hasMoreResults |
boolean? |
More results beyond last |
interface Facet {
name: string; // e.g. 'rtype', 'creator', 'lang'
values: FacetValue[];
}
interface FacetValue {
value: string;
count: number;
mergedLabel?: string[];
deiData?: DeiData; // Diversity, Equity & Inclusion metadata
}
interface DeiData {
isDei?: boolean;
deiNote?: SafeHtml;
}A single search result entity. This is the main object you work with when reading results from the store.
| Field | Type | Description |
|---|---|---|
@id |
string |
Unique entity ID (used as store key) |
context |
Context |
Record context (L, PC, SP, U, NP) |
adaptor |
Adaptor |
Backend adaptor that produced this record |
pnx |
Pnx |
Normalised record data |
extras |
Extras? |
Citation trail and times-cited data |
enrichment |
Enrichment? |
Virtual-browse enrichment |
thumbnailForCD |
ThumbnailForCD? |
Combined digital thumbnail info |
unpaywallStatus |
LoadingStatus? |
Async load status of Unpaywall links |
delivery |
DocDelivery? |
Delivery/availability data |
expired |
boolean? |
Whether the record is expired |
origRecordId |
string? |
Original record ID before de-duplication |
| Value | Description |
|---|---|
L |
Local index |
PC |
Primo Central |
SP |
SP adaptor |
U |
Unified |
NP |
Newspapers |
| Value | Description |
|---|---|
LocalSearchEngine |
Local Search Engine |
PrimoCentral |
Primo Central |
PrimoVEDeepSearch |
Primo VE Deep Search |
EbscoLocal |
EBSCO local connector |
WorldCatLocal |
WorldCat local connector |
SummonLocal |
Summon local connector |
SearchWebhook |
Search webhook adaptor |
WebHook |
Generic webhook adaptor |
Normalised record data structure. Most fields are string-array dictionaries to accommodate multi-valued MARC fields.
| Field | Type | Description |
|---|---|---|
display |
{ [key: string]: string[] } |
Display fields (title, creator, description, …) |
control |
Control |
Identifiers and system-level control fields |
addata |
{ [key: string]: string[] } |
OpenURL/citation metadata |
sort |
Sort |
Sortable field values |
facets |
{ [key: string]: string[] } |
Facet field values |
links |
Links? |
URLs (full text, thumbnail, OpenURL, …) |
search |
Search? |
Searchable field copies |
delivery |
PnxDelivery? |
Lightweight delivery info (full delivery is on Doc.delivery) |
| Field | Type |
|---|---|
sourcerecordid |
string[] |
recordid |
string[] |
sourceid |
string[] | string |
originalsourceid |
string[] |
sourcesystem |
Sourcesystem[] |
sourceformat |
Sourceformat[] |
score |
Array<number | string> |
isDedup |
boolean? |
recordtype |
string[]? |
sourcetype |
string[]? |
addsrcrecordid |
string[]? |
pqid |
string[]? |
jstorid |
string[]? |
galeid |
string[]? |
gtiid |
string[]? |
attribute |
string[]? |
rapidosourcerecordid |
string[]? |
networklinkedrecordid |
string[]? |
colldiscovery |
string[]? |
save_score |
number[]? |
| Field | Type |
|---|---|
openurl |
string[] |
thumbnail |
string[] |
linktohtml |
string[] |
openurlfulltext |
string[] |
linktorsrc |
string[]? |
linktopdf |
string[]? |
docinsights |
string[]? |
backlink |
string[]? |
linktorsrcadditional |
string[]? |
openurladditional |
string[]? |
unpaywalllink |
string[]? |
| Field | Type |
|---|---|
title |
string[] |
creationdate |
string[] |
author |
string[]? |
| Field | Type |
|---|---|
recordid |
string[] |
issn |
string[] |
isbn |
string[] |
title |
string[] |
creatorcontrib |
string[] |
| Field | Type |
|---|---|
fulltext |
string[] |
delcategory |
string[] |
availabilityLinkUrl |
string |
| Value |
|---|
Marc21 = 'MARC21' |
XML = 'XML' |
ESPLORO = 'ESPLORO' |
| Value |
|---|
Ils = 'ILS' |
Other = 'Other' |
Full delivery/availability record attached to each Doc after the delivery enrichment effect runs.
| Field | Type | Description |
|---|---|---|
deliveryCategory |
string[] |
Delivery categories (e.g. 'Alma-E') |
availability |
string[] |
Raw availability strings |
displayedAvailability |
string |
Human-readable availability label |
displayLocation |
boolean |
Whether to display location info |
additionalLocations |
boolean |
Whether additional locations exist |
physicalItemTextCodes |
string |
Physical item text code |
feDisplayOtherLocations |
boolean |
Feature flag for "other locations" panel |
almaOpenurl |
string |
OpenURL for Alma |
recordInstitutionCode |
string |
Owning institution code |
sharedDigitalCandidates |
string[] |
CDL candidate identifiers |
hideResourceSharing |
boolean |
Suppress resource-sharing links |
GetIt1 |
GetIt1[] |
GetIt link categories |
link |
DeliveryLink[]? |
Additional delivery links |
availabilityLinks |
string[]? |
Availability link labels |
availabilityLinksUrl |
string[]? |
Availability link URLs |
holding |
Location[]? |
Physical holding locations |
bestlocation |
Location? |
Best/primary holding location |
electronicServices |
ElectronicService[]? |
Electronic access services |
additionalElectronicServices |
AdditionalElectronicService? |
Categorised additional services |
hasD |
boolean? |
Has digital representation |
digitalAuxiliaryMode |
boolean? |
Digital auxiliary viewer mode |
serviceMode |
string[]? |
Service mode codes |
consolidatedCoverage |
string? |
Coverage summary string |
isFilteredHoldings |
boolean? |
Holdings filtered by policy |
physicalServiceId |
string? |
Physical service identifier |
recordOwner |
string? |
Record owner code |
almaInstitutionsList |
AlmaInstitutionsList[]? |
Network Zone institution list |
filteredByGroupServices |
GroupServices[]? |
Group-filtered services |
hasFilteredServices |
string? |
Flag for filtered services |
electronicContextObjectId |
string? |
Electronic context object ID |
mayAlsoBeFoundAt |
MayAlsoBeFoundAtItem[]? |
Cross-institution availability |
Physical holding location.
| Field | Type |
|---|---|
organization |
string |
libraryCode |
string |
mainLocation |
string |
subLocation |
string |
subLocationCode |
string |
callNumber |
string |
availabilityStatus |
string |
holdId |
string |
holKey |
string |
uniqId |
string |
ilsApiId |
string? |
isValidUser |
boolean? |
matchForHoldings |
MatchForHolding[]? |
stackMapUrl |
string? |
relatedTitle |
string? |
| Field | Type |
|---|---|
matchOn |
string |
holdingRecord |
string |
One electronic access option (full text, open access, etc.).
| Field | Type |
|---|---|
adaptorid |
string |
ilsApiId |
string |
serviceUrl |
string |
licenceExist |
string |
packageName |
string |
availiability |
string |
authNote |
string |
publicNote |
string |
hasAccess |
boolean |
serviceType |
string |
registrationRequired |
boolean |
numberOfFiles |
number |
cdlItemAvailable |
boolean |
cdl |
boolean |
parsedAvailability |
string[] |
licenceUrl |
string |
relatedTitle |
string |
serviceDescription |
string? |
deniedNote |
string? |
fileType |
string? |
firstFileSize |
string? |
representationEntityType |
string? |
contextServiceId |
string? |
publicAccessModel |
string? |
representationViewerServiceCode |
string? |
fromNetwork |
boolean? |
filteredByAfGroups |
string? |
supported |
boolean? |
Categorised groups of additional electronic services.
| Field | Type |
|---|---|
OpenURL |
ElectronicService[] |
LinktorsrcOA |
ElectronicService[] |
LinktorsrcNonOA |
ElectronicService[] |
RelatedServices |
ElectronicService[] |
interface GetIt1 {
category: string;
links: GetItLinks[];
}
interface GetItLinks {
'@id': string;
adaptorid: string;
displayText: string | null;
getItTabText: string;
ilsApiId: string;
inst4opac: string;
isLinktoOnline: boolean;
link: string;
}| Field | Type |
|---|---|
displayLabel |
string? |
linkType |
string? |
linkURL |
string? |
@id |
string? |
publicNote |
string? |
| Field | Type |
|---|---|
availabilityStatus |
string |
envURL |
string |
instCode |
string |
instId |
string |
instName |
string |
getitLink |
getitLink[] |
| Field | Type |
|---|---|
linkRecordId |
string |
displayText |
string |
| Field | Type |
|---|---|
unitName |
string |
unitType |
string |
services |
ElectronicService[] |
serviceStatus |
LoadingStatus? |
| Field | Type |
|---|---|
code |
string |
displayLabel |
string |
additionalLabel |
string |
linkType |
string |
linkURL |
string |
computedDisplayLabel |
string |
computedDisplayWords |
string[] |
| Field | Type |
|---|---|
link |
DeliveryLink[]? |
hasD |
boolean? |
| Field | Type |
|---|---|
recId |
string |
sharedDigitalCandidates |
string[] | null |
| Field | Type |
|---|---|
docDelivery |
DocDelivery |
recordId |
string |
Highlighted term fragments returned for each field.
| Field | Type |
|---|---|
general |
string[] |
creator |
string[] |
contributor |
string[] |
subject |
string[] |
title |
string[] |
addtitle |
string[] |
alttitle |
string[] |
vertitle |
string[] |
termsUnion |
string[] |
snippet |
string[] |
Server-side performance timings (all values are strings or numbers from the API).
| Field | Type |
|---|---|
BUILD_RESULTS_RETRIVE_FROM_DB |
string |
CALL_SOLR_GET_IDS_LIST |
string |
RETRIVE_FROM_DB_COURSE_INFO |
string |
RETRIVE_FROM_DB_RECORDS |
string |
RETRIVE_FROM_DB_RELATIONS |
string |
PRIMA_LOCAL_INFO_FACETS_BUILD_DOCS_HIGHLIGHTS |
string |
PRIMA_LOCAL_SEARCH_TOTAL |
string |
PC_SEARCH_CALL_TIME |
string |
PC_BUILD_JSON_AND_HIGLIGHTS |
string |
PC_SEARCH_TIME_TOTAL |
string |
BUILD_BLEND_AND_CACHE_RESULTS |
number |
BUILD_COMBINED_RESULTS_MAP |
number |
COMBINED_SEARCH_TIME |
number |
PROCESS_COMBINED_RESULTS |
number |
FEATURED_SEARCH_TIME |
number |
| Field | Type |
|---|---|
errorMessages |
string[] |
URL query parameters for the full-display route.
| Field | Type |
|---|---|
docid |
string |
context |
Context? |
adaptor |
Adaptor? |
isFrbr |
boolean? |
search_scope |
string? |
isHighlightedRecord |
boolean? |
tab |
string? |
vid |
string? |
state |
string? |
lang |
string? |
newspapersSearch |
boolean? |
Internal params for loading a full-display record.
| Field | Type |
|---|---|
docid |
string |
context |
Context? |
adaptor |
Adaptor? |
isFrbr |
boolean? |
scope |
string? |
isHighlightedRecord |
boolean? |
| Field | Type |
|---|---|
pnx |
Pnx |
Top-level wrapper returned by the facets API endpoint.
| Field | Type |
|---|---|
beaconO22 |
string |
facets |
Facet[] |
A filter chip shown in the search top bar.
| Field | Type |
|---|---|
value |
string |
filterType |
string? |
mergedLabel |
string[] | undefined |
| Field | Type |
|---|---|
virtualBrowseObject |
VirtualBrowseObject |
bibVirtualBrowseObject |
VirtualBrowseObject |
| Field | Type |
|---|---|
isVirtualBrowseEnabled |
boolean |
callNumber |
string |
callNumberBrowseField |
string |
| Field | Type |
|---|---|
citationTrails |
CitationTrails |
timesCited |
TimesCited |
| Field | Type |
|---|---|
citing |
string[] |
citedby |
string[] |
| Field | Type |
|---|---|
recordId |
string |
title |
string |
author |
string |
type |
string |
frbrgroupid |
string |
seed_id |
string |
| Field | Type |
|---|---|
citationType |
string |
creator |
string[] |
frbrGroupId |
string |
pnxId |
string |
title |
string |
| Field | Type |
|---|---|
scopus |
Scopus? |
wos |
WebOfScience? |
| Field | Type |
|---|---|
citedRedId |
string? |
extensionVal |
string? |
| Field | Type |
|---|---|
citedRedId |
string? |
extensionVal |
string? |
wosFinalLink |
string? |
| Value |
|---|
CITING = 'citing' |
CITEDBY = 'citedby' |
type stringBoolean = 'N' | 'Y';
type IgnoreMapSimpleString = { [key: string]: string };
type IgnoreMapSimpleBoolean = { [key: string]: boolean };
type IgnoreMapMulti = { [key: string]: number | string | string[] | undefined | null };
type SearchMetaData = Omit<SearchData, 'docs'>;Constant exported from search.model.ts:
const SUPPORTED_ELECTRONIC_TYPES_FOR_DIGITAL_VIEWER =
['jpg', 'tif', 'tiff', 'gif', 'png', 'pdf', 'jp2', 'jpeg'];Defined in src/models/filter.model.ts.
| Field | Type | Description |
|---|---|---|
status |
LoadingStatus |
Current load status of the filter slice |
isRememberAll |
boolean |
Whether "Remember All" is toggled on |
previousSearchQuery |
{ searchTerm: string | undefined; scope: string | undefined } |
Last search term and scope before filter change |
includedFilter |
selectedFilters[] | null |
Active include facet filters |
excludedFilter |
selectedFilters[] | null |
Active exclude facet filters |
multiSelectedFilter |
MultiSelectedFilter[] | null |
Multi-select facet filters |
resourceTypeFilter |
ResourceTypeFilterModel | null |
Active resource-type filter |
isFiltersOpen |
boolean |
Whether the filter panel is open |
| Field | Type |
|---|---|
name |
string |
values |
string[] |
| Field | Type |
|---|---|
name |
string |
values |
MultiSelectedFilterValue[] |
| Field | Type |
|---|---|
value |
string |
filterType |
FilterType |
| Value |
|---|
Include = 'include' |
Exclude = 'exclude' |
| Field | Type |
|---|---|
resourceType |
string |
count |
number |
All action creators are exported from the package root:
import { searchAction, loadFiltersAction, setDecodedJwt } from '@libis/primo-shared-state';| Creator | Action type | Props |
|---|---|---|
searchAction |
[Search] Load search |
{ searchParams: SearchParams; searchType?: string } |
searchSuccessAction |
[Search] Load search success |
{ searchResultsData: SearchData } |
searchFailedAction |
[Search] Load search failed |
— |
clearSearchAction |
[Search] clear search |
— |
pageLimitChangedAction |
[Search] Page Limit Changed |
{ limit: number } |
pageNumberChangedAction |
[Search] Page Number Changed |
{ pageNumber: number } |
sortByChangedAction |
[search] Sort By Changed ¹ |
{ sort: string } |
fetchUnpaywallLinksAction |
[Search] Fetch unpaywall links |
{ recordsToUpdate: Doc[] } |
updateIsSavedSearch |
[Search] Update Is Saved Search |
{ isSavedSearch: boolean } |
setSearchNotificationMsg |
[search] Set Search Notification Message ¹ |
{ msg: string } |
saveCurrentSearchTermAction |
[Search] save current search term |
{ searchTerm: string } |
¹ Note: action type string uses lowercase [search], not [Search] — match exactly when using ofType.
| Creator | Action type | Props |
|---|---|---|
loadFiltersAction |
[Filter] Load Filter |
{ searchParams: SearchParams } |
filtersSuccessAction |
[Filter] Load Filter Success |
{ filters: Facet[] } |
filterFailedAction |
[Filter] Load Filter Failed |
— |
updateSortByParam |
[Filter] Update Sort By Param |
{ sort: string } |
| Creator | Action type | Props |
|---|---|---|
setDecodedJwt |
[User] Set Decoded Jwt |
{ decodedJwt: DecodedJwt } |
resetJwtAction |
[User] reset jwt |
{ logoutReason: LogoutReason; url?: string } |
loadUserSettingsSuccessAction |
[User-Settings] save user settings |
{ userSettings: UserSettings; isNewSession: boolean } |
resetUserSettingsSuccessAction |
[User-Settings] reset user settings success |
— |
doneChangeUserSettingsLanguageAction |
[User-Settings] Done Change User Settings Language |
{ value: string } |
doneSaveHistoryToggleAction |
[User-Settings] Done Update Save history toggle |
{ value: string } |
doneUseHistoryToggleAction |
[User-Settings] Done Update Use history toggle |
{ value: string } |
doneAutoExtendMySessionToggleAction |
[User-Settings] Done Update Auto Extend My Session toggle |
{ value: string } |
setLoginFromStateAction |
[User-Settings] set login from state |
{ value: string } |
changeRaSaveSearchDoneAction |
[User-settings] dont update research-Assistant save search toggle |
{ value: string } |
resetLogoutReason |
[User-Settings] reset logout reason |
— |
The remote module is loading before the host has bootstrapped. Ensure lazy-loaded remote modules are only instantiated after the host's StoreModule.forRoot() runs.
The host store state slice is not yet populated. Use selectIsLoggedIn$() as a guard or wait for status !== 'pending' before reading.
@angular/core, @ngrx/store, and rxjs must resolve to a single shared instance. Confirm they are listed as singletons in both the host's and remote's webpack sharing config.
toSignal requires an injection context. Signal methods called inside a constructor (or providedIn: 'root' service constructor) are fine. If you call them inside a method body, wrap with runInInjectionContext.
Bump version in package.json before every npm pack:
npm version patch # 1.0.0 → 1.0.1 (bug fixes)
npm version minor # 1.0.0 → 1.1.0 (new methods, backward-compatible)
npm version major # 1.0.0 → 2.0.0 (breaking changes)
npm run build
npm packThis package is generated from decompiled Primo host application source code. Use primo-extract to extract the source from a running Primo instance, then use either the Claude Code slash command (recommended) or the manual prompt below.
primoExtract --primo=https://your.primo.instance --outDir=/path/to/extracted/source --ndeIf you are working in this repository with Claude Code, a /regenerate slash command is available. Open Claude Code at the root of this repository and run:
/regenerate /path/to/extracted/source
Claude will automatically detect whether the package needs to be created from scratch or updated, apply the safety rules for shared-actions.ts, bump the version, and append an entry to CHANGES.md. The full command definition lives in .claude/commands/regenerate.md.
If you are not using Claude Code, copy the prompt below into Claude (or any capable LLM) together with the contents of the extracted source directory.
You are an expert Angular and NgRx engineer. You will be given decompiled source code from a Primo host application, extracted using primo-extract. Your task is to generate or update the `@libis/primo-shared-state` npm package.
---
## Context
The package exposes three things to remote/client module-federation modules:
1. **TypeScript models** — interfaces that mirror the host's NgRx state shapes.
2. **Angular services** — `UserStateService`, `SearchStateService`, `FilterStateService`, each offering Observable streams, Promise snapshots, Angular Signals, and typed dispatch helpers.
3. **Shared actions** — `shared-actions.ts`, a curated list of NgRx action creators whose `type` strings match the host's reducers byte-for-byte, exported only if they are safe for a remote to dispatch (see safety rules below).
---
## Safety rules for shared-actions.ts
An action is safe to export **only if** dispatching it from a remote module cannot corrupt store state or trigger unintended HTTP side-effects. Apply this checklist to every action:
- ✅ **Export** — commands that start a well-defined host operation (search, filter load) where the remote legitimately supplies the parameters.
- ✅ **Export** — pure UI-state writes (pagination, sort, clear) with no HTTP side-effects.
- ✅ **Export** — terminal success/failed actions whose reducer writes to the store and **no host effect listens to them downstream**.
- ❌ **Do not export** — success/failed actions that feed a downstream effect (i.e. another effect listens to them and fires an HTTP call).
- ❌ **Do not export** — actions that initiate OAuth/ILS authentication flows.
- ❌ **Do not export** — actions that carry server-authoritative payloads (e.g. full entity lists with pnx data) that a remote cannot construct legitimately.
When in doubt, **exclude**. It is safer to omit an action than to export one that can cause silent data corruption.
---
## Your instructions
### If the `src/` directory is empty or does not exist — generate the full package
1. Analyse the decompiled source and identify all NgRx state slices, reducers, effects, and action creators.
2. Generate the full package structure:
- `src/models/` — TypeScript interfaces for every relevant state shape.
- `src/actions/shared-actions.ts` — apply the safety rules above. Add a JSDoc comment to every exported action explaining why it is safe. Add a comment block at the top of the file with an EFFECTS WARNING (remotes must not register their own effects for the same action types).
- `src/state/` — one service per state slice with Observable, Promise, Signal, and dispatch APIs.
- `src/utils/StateHelper` — thin Store wrapper used internally by services.
- `src/index.ts` — barrel export.
- `package.json` — name `@libis/primo-shared-state`, version `1.0.0`.
- `CHANGES.md` — create with a `## 1.0.0` section listing everything generated.
3. Add the following notice to the top of `README.md`:
> 🤖 Generated by Claude (Anthropic) from decompiled Primo source via primo-extract. Do not edit by hand.
### If `src/` already contains code — update the existing package
1. Compare the decompiled source against the existing package code. Identify:
- **New** actions, state slices, or model fields → add them.
- **Changed** action type strings, payload shapes, or reducer behaviour → update them and note the change.
- **Removed** actions or state slices → **do not silently delete**. Instead:
- Flag each removal explicitly with a `⚠️ BREAKING REMOVAL` warning in your response.
- List every exported symbol that would be deleted and what consuming code would break.
- Ask for confirmation before removing anything that is currently exported.
2. Apply all safe additions and updates.
3. Bump the version in `package.json`:
- `patch` — if only non-breaking additions or internal changes.
- `minor` — if new exported symbols are added.
- `major` — if any exported symbol is removed or its signature changes in a breaking way.
4. Append a new version section to `CHANGES.md` in this format:
```markdown
## <new version> — <YYYY-MM-DD>
### Added
- …
### Changed
- …
### ⚠️ Breaking removals (confirm before applying)
- …
```
---
## Output format
- Produce complete, ready-to-use file contents for every file you create or modify.
- For updates, show only the changed files in full — do not truncate.
- Prefix every file with a fenced code block header: `// FILE: path/to/file`.
- After all files, print a short summary table:
| File | Action | Reason |
|------|--------|--------|
| src/actions/shared-actions.ts | updated | new `fooAction` added; `barSuccessAction` excluded (feeds downstream effect) |
| … | … | … |
MIT