@omega.js/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +874 -0
- package/dist/index.js +999 -0
- package/dist/modules/analytics.js +584 -0
- package/dist/modules/auth.js +469 -0
- package/dist/modules/bindings.js +319 -0
- package/dist/modules/device.js +282 -0
- package/dist/modules/dom.js +96 -0
- package/dist/modules/features.js +30 -0
- package/dist/modules/firestore.js +313 -0
- package/dist/modules/form-manager.js +1577 -0
- package/dist/modules/icon-core.js +226 -0
- package/dist/modules/icon-renderer.js +149 -0
- package/dist/modules/live-page.js +235 -0
- package/dist/modules/logger.js +36 -0
- package/dist/modules/motion.js +853 -0
- package/dist/modules/notifications.js +433 -0
- package/dist/modules/path-prefix.js +22 -0
- package/dist/modules/request.js +223 -0
- package/dist/modules/sentry.js +108 -0
- package/dist/modules/service-worker.js +237 -0
- package/dist/modules/storage.js +133 -0
- package/dist/modules/triggers.js +117 -0
- package/dist/modules/utilities.js +479 -0
- package/dist/modules/vert-document.js +354 -0
- package/dist/modules/verts.js +1133 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/monitoring/browser.js +207 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/docs/architecture.md +59 -0
- package/docs/bindings.md +235 -0
- package/docs/build-system.md +32 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/code-patterns.md +96 -0
- package/docs/common-tasks.md +36 -0
- package/docs/dependencies.md +19 -0
- package/docs/index.md +159 -0
- package/docs/modules.md +180 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +9 -0
- package/package.json +65 -0
package/docs/modules.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Module Quick Reference
|
|
2
|
+
|
|
3
|
+
## Storage (`storage.js`)
|
|
4
|
+
|
|
5
|
+
- **Class**: `Storage`
|
|
6
|
+
- **Key Methods**: `get(path, default)`, `set(path, value)`, `remove(path)`, `clear()`
|
|
7
|
+
- **Session**: Same methods under `.session` namespace
|
|
8
|
+
- **Storage Key**: `_manager` in localStorage
|
|
9
|
+
|
|
10
|
+
## Auth (`auth.js`)
|
|
11
|
+
|
|
12
|
+
- **Class**: `Auth`
|
|
13
|
+
- **Key Methods**: `listen(options, callback)`, `isAuthenticated()`, `getUser()`, `signInWithEmailAndPassword()`, `signOut()`, `getIdToken()`, `probeSession()`, `resolveSubscription(account?)`
|
|
14
|
+
- **Bindings**: Updates `auth` and `usage` context on auth settle
|
|
15
|
+
- **`probeSession()`** ([#798](https://github.com/Omega-JS-Stack/omega/issues/798)): the forced token refresh the client runs at a moment of doubt (tab visible, network back, a 401). Resolves `'signed-out' | 'alive' | 'gone' | 'unknown'`; a non-network `auth/*` error signs the user out, a network error keeps them, and probes coalesce to one in flight. Full contract: [docs/client/index.md](../../../docs/client/index.md)
|
|
16
|
+
- **Listener state**: `callback({ user, account, resolved, accountDenied? })` — `account` resolves to the empty schema shape when the doc is not written yet (a NORMAL pending state); `accountDenied: true` rides along only when Firestore rules refused the read, the one REAL failure ([#700](https://github.com/Omega-JS-Stack/omega/issues/700)); web signs out on it
|
|
17
|
+
- **Usage Resolution**: `_resolveUsage(state)` merges `account.usage` (Firestore) with the EFFECTIVE limits of the resolved plan to produce the `usage` bindings key. Both halves are config — the `features` catalog says what a feature is and whether it is counted, the product's `features` map says what the tier promises — and the arithmetic is `@omega.js/account`'s, the same module @omega.js/backend's gate reads ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)). Per feature: `{ monthly, daily, total, limit, left, override, day: { limit, used, left } }`, with a per-user `usage.overrides.<feature>` winning over the plan's number
|
|
18
|
+
|
|
19
|
+
### resolveSubscription(account?)
|
|
20
|
+
|
|
21
|
+
Derives calculated subscription fields from raw account data. Returns only fields that require derivation logic — raw data (product.id, status, trial, cancellation) lives on `account.subscription` directly.
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
const resolved = auth.resolveSubscription(account);
|
|
25
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `plan`: Effective plan ID the user has access to RIGHT NOW (`'basic'` if cancelled/suspended)
|
|
29
|
+
- `active`: User has active access (active, trialing, or cancelling — all mean the user can use the product)
|
|
30
|
+
- `trialing`: In an active trial (status `'active'` + `trial.claimed` + unexpired `trial.expires`)
|
|
31
|
+
- `cancelling`: Cancellation pending (status `'active'` + `cancellation.pending` + NOT trialing)
|
|
32
|
+
|
|
33
|
+
**Unified with @omega.js/backend**: The same function exists on `User.resolveSubscription(account)` in @omega.js/backend (`helpers/user.js`) with identical logic and return shape.
|
|
34
|
+
|
|
35
|
+
### Auth Settler Pattern
|
|
36
|
+
|
|
37
|
+
Auth uses a promise-based settler (`_authReady`) that resolves once Firebase's first `onAuthStateChanged` fires — the moment auth state is guaranteed (authenticated user OR null). This eliminates race conditions.
|
|
38
|
+
|
|
39
|
+
- **`once` listeners** (`listen({ once: true }, cb)`): Wait for `_authReady`, fire once, done. No cleanup needed.
|
|
40
|
+
- **Persistent listeners** (`listen({}, cb)`): Subscribe to `_authStateCallbacks`. If auth already settled when registered, catch up via `_authReady.then()`. Otherwise, `_handleAuthStateChange` handles the initial call naturally.
|
|
41
|
+
- **`_hasProcessedStateChange`**: Ensures bindings/storage updates run only once per auth state change across all listeners.
|
|
42
|
+
- **Manager owns the promise**: `_authReady` and `_authReadyResolve` live on the Manager instance. The `onAuthStateChanged` callback in `index.js` resolves it on first fire and sets `_firebaseAuthInitialized = true`.
|
|
43
|
+
|
|
44
|
+
## Bindings (`bindings.js`)
|
|
45
|
+
|
|
46
|
+
- **Class**: `Bindings`
|
|
47
|
+
- **Key Methods**: `update(data)`, `getContext()`, `clear()`
|
|
48
|
+
- **HTML Attr**: `data-omega-bind`
|
|
49
|
+
- **Actions**: `@text`, `@value`, `@show`, `@hide`, `@attr`, `@style`
|
|
50
|
+
- **Deep reference**: [bindings.md](bindings.md) — comma syntax, condition operators, state paths, skeleton loaders, root-key filtering
|
|
51
|
+
|
|
52
|
+
## Request (`request.js`) — harmonized API fetch
|
|
53
|
+
|
|
54
|
+
- **Singleton API**: `await omega.request('/omega/user/token', { method: 'POST', body: {} })`
|
|
55
|
+
- **Options**: `method` (default GET), `headers`, `body` (objects JSON-encoded automatically), `auth: false` (skip the Bearer token for public routes), `output: 'complete'` (returns `{ status, ok, headers, data, properties }` instead of just the body), `tries` (bounded retries on network errors + 5xx), `timeout` (per-attempt `AbortSignal.timeout`), `wakeup: true` (below)
|
|
56
|
+
- **`wakeup: true`** ([#637](https://github.com/Omega-JS-Stack/omega/issues/637)): a fire-and-forget GET that warms a cold backend and nothing else — `omega.request(WAKEUP_ROUTE, { wakeup: true })`, with `WAKEUP_ROUTE` exported by this module (the ONE route every surface pings, [#644](https://github.com/Omega-JS-Stack/omega/issues/644)). It appends `wakeup=true` to the URL, mints no ID token, reads no response body, returns `undefined`, and RESOLVES rather than throwing when the network is down. @omega.js/backend's middleware answers a wakeup before it loads a route or authenticates, so every route is the same warm-up at the same price and none of them runs.
|
|
57
|
+
- **Behavior**: leading-`/` paths resolve through `getApiUrl()`; absolute URLs pass through. A fresh Firebase ID token rides as `Authorization: Bearer` when signed in. Non-ok responses THROW an `Error` carrying `.code` (HTTP status), `.data` (parsed body), and `.properties`.
|
|
58
|
+
- **omega-properties**: the backend assistant attaches this header (code, tag, usage current+limits, schema, additional) to every response; `omega.request()` parses it on success AND error, and merges server usage into the `usage` bindings key — `data-omega-bind` elements refresh automatically.
|
|
59
|
+
- **Standalone**: non-singleton contexts (desktop main, extension service worker) build their own via `createRequest({ getApiUrl, getIdToken, onProperties, onUnauthorized })` from `@omega.js/client/modules/request.js` — the desktop client-bridge and the extension background token sync both do. `onProperties` and `onUnauthorized` are optional; only `getApiUrl` and `getIdToken` are required.
|
|
60
|
+
- **401 probes the session** ([#798](https://github.com/Omega-JS-Stack/omega/issues/798)): a 401 on a request that asked for auth calls the optional `onUnauthorized` dep (the singleton passes `omega.auth().probeSession()`) without awaiting it and swallowing its rejection, then throws the caller's error unchanged. `auth: false` requests and every other status never call it.
|
|
61
|
+
|
|
62
|
+
## Device (`device.js`) — local device stats
|
|
63
|
+
|
|
64
|
+
- **Class**: `Device` (`omega.device()`)
|
|
65
|
+
- **Key Methods**: `getUsageDuration(unit)`, `getSessionDuration(unit)`, `getInstalledDate()`, `getSessionCount()`, `getBindingData()`, `reset()`
|
|
66
|
+
- **Storage**: localStorage (web) or extension storage, key `omega_device`
|
|
67
|
+
- **Bindings**: seeds the `device` key on initialize (installed / session / version / duration) — distinct from the server-derived `usage` key (see [bindings.md](bindings.md))
|
|
68
|
+
|
|
69
|
+
## Firestore (`firestore.js`)
|
|
70
|
+
|
|
71
|
+
- **Class**: `Firestore`
|
|
72
|
+
- **Key Methods**: `doc(path)`, `collection(path)`
|
|
73
|
+
- **Doc Methods**: `.get()`, `.set()`, `.update()`, `.delete()`
|
|
74
|
+
- **Query Methods**: `.where()`, `.orderBy()`, `.limit()`, `.startAt()`, `.endAt()`
|
|
75
|
+
|
|
76
|
+
## Notifications (`notifications.js`)
|
|
77
|
+
|
|
78
|
+
- **Class**: `Notifications`
|
|
79
|
+
- **Key Methods**: `isSupported()`, `isSubscribed()`, `subscribe()`, `unsubscribe()`, `getToken()`, `onMessage()`
|
|
80
|
+
- **Storage**: Saves to localStorage and Firestore
|
|
81
|
+
- **VAPID key**: resolved from `config.cloud.messaging.vapidKey` (the omega.json5 home; the web engine bridges it into the `firebase.messaging.config.vapidKey` contract shape too). Public by design.
|
|
82
|
+
|
|
83
|
+
## ServiceWorker (`service-worker.js`)
|
|
84
|
+
|
|
85
|
+
- **Class**: `ServiceWorker`
|
|
86
|
+
- **Key Methods**: `isSupported()`, `register()`, `ready()`, `postMessage()`, `onMessage()`, `getState()`, `unregisterAll()`
|
|
87
|
+
- **Registration policy**: `serviceWorker.enabled` (default true) registers at
|
|
88
|
+
scope `/` with `updateViaCache: 'none'` on every init — dev included (the
|
|
89
|
+
worker is how push/caching get tested locally, and same-scope registration
|
|
90
|
+
is what replaces a DIFFERENT project's worker left on the same localhost
|
|
91
|
+
port). `enabled: false` calls `unregisterAll()` instead — the origin is
|
|
92
|
+
swept clean, never left to a stale foreign worker.
|
|
93
|
+
- **Under a URL-path mount** ([#360](https://github.com/Omega-JS-Stack/omega/issues/360)):
|
|
94
|
+
when the page carries the web build's `<html data-omega-path-prefix>` stamp,
|
|
95
|
+
`register()` mounts the script and the scope under it
|
|
96
|
+
(`/workkit/service-worker.js` at scope `/workkit/` — the only scope a script
|
|
97
|
+
served from there can claim) and appends the prefix to the script URL as
|
|
98
|
+
`?omega-path-prefix=…`, which is how the worker (no document, no stamp) learns
|
|
99
|
+
the mount for the URLs it builds. No stamp means the domain root and an
|
|
100
|
+
unchanged registration.
|
|
101
|
+
|
|
102
|
+
## Path prefix (`path-prefix.js`)
|
|
103
|
+
|
|
104
|
+
- **Exports**: `pathPrefix()`
|
|
105
|
+
- The base path this page is mounted under (#355), read off the web build's
|
|
106
|
+
`<html data-omega-path-prefix>` stamp — `''` at the domain root,
|
|
107
|
+
`'/workkit'`-shaped otherwise. The client's mirror of the web package's
|
|
108
|
+
runtime helper (`core/js/libs/path-prefix.js`); nothing imports across the
|
|
109
|
+
package boundary.
|
|
110
|
+
- **Read by**: the ServiceWorker registration above ([#360](https://github.com/Omega-JS-Stack/omega/issues/360))
|
|
111
|
+
and the Manager's refresh-new-version poll, which fetches the build manifest
|
|
112
|
+
at `<prefix>/build.json` ([#364](https://github.com/Omega-JS-Stack/omega/issues/364))
|
|
113
|
+
— root-relative, that poll 404s forever under a path mount.
|
|
114
|
+
|
|
115
|
+
## Sentry (`sentry.js`)
|
|
116
|
+
|
|
117
|
+
- **Class**: `Sentry` (named `mod` internally)
|
|
118
|
+
- **Key Methods**: `init(config)`, `captureException(error, context)`
|
|
119
|
+
- **Filtering**: Blocks dev mode, Lighthouse, Selenium/Puppeteer
|
|
120
|
+
|
|
121
|
+
## DOM (`dom.js`)
|
|
122
|
+
|
|
123
|
+
- **Exports**: `loadScript(options)`, `ready()`
|
|
124
|
+
- **loadScript Options**: src, async, defer, crossorigin, integrity, timeout, retries
|
|
125
|
+
|
|
126
|
+
## Utilities (`utilities.js`)
|
|
127
|
+
|
|
128
|
+
- **Exports**: `clipboardCopy()`, `escapeHTML()`, `sanitizeURL()`, `renderMarkdown()`, `showNotification()`, `getPlatform()`, `getBrowser()`, `getRuntime()`, `isMobile()`, `getDevice()`, `getContext()`
|
|
129
|
+
- **clipboardCopy(input)**: copies a string or an element's value/text; the returned promise REJECTS when the clipboard refuses (denied permission, blurred document) — the legacy lane reads `execCommand`'s return, so no lane reports a failed copy as success ([#726](https://github.com/Omega-JS-Stack/omega/issues/726)).
|
|
130
|
+
- **escapeHTML(input)**: walks strings, arrays and objects recursively; escapes `& < > " '` (quotes too, so an escaped value is safe inside an attribute). Non-strings pass through.
|
|
131
|
+
- **sanitizeURL(url)**: returns the URL unchanged when it resolves to `http:`/`https:`, `''` for every other scheme (`javascript:`, `data:`, …).
|
|
132
|
+
|
|
133
|
+
### renderMarkdown(text)
|
|
134
|
+
|
|
135
|
+
Untrusted text as safe markup — an escape-first mini renderer for API answers and user-supplied prose (ported from the workkit tower's issue dialog). It composes the two helpers above rather than owning escaping: the source is escaped ONCE up front and every rule then works on that escaped string, so no rule can resurrect a `<script>` that is already `<script>`.
|
|
136
|
+
|
|
137
|
+
- **Grammar**: `#`–`######` headings, fenced blocks (```` ``` ````), inline code, `-`/`*` bullet and `1.`/`1)` numbered lists, `**bold**`, `*italic*`, and `[label](href)` links. Anything else renders as the text it was — it is not a markdown engine.
|
|
138
|
+
- **Links**: `sanitizeURL` gives the scheme verdict and the href must additionally be absolute `http(s)` — a `javascript:`/`data:`/relative href leaves the bracket text as text, never an anchor. Built anchors are stashed behind a NUL sentinel while the emphasis rules run, so an href holding asterisks survives untouched. Anchors carry `target="_blank" rel="noopener"`.
|
|
139
|
+
- **Output shape**: headings render three levels down (`#` → `<h4 class="h6 mt-3 mb-2">`, floored at `h6`) so a rendered fragment never competes with its host page's title; fenced blocks render `<pre class="p-2 rounded"><code>` (Bootstrap utilities, same idiom as `showNotification`).
|
|
140
|
+
- **Edges**: empty/whitespace/`null` input returns `''` (the caller says what empty means); an unterminated fence still renders its content.
|
|
141
|
+
|
|
142
|
+
## Live Page (`live-page.js`) — the self-refreshing page primitives
|
|
143
|
+
|
|
144
|
+
- **Exports**: `loading(message)`, `swap(host, markup)`, `createFeedPoller(options)`
|
|
145
|
+
- **Pattern**: transport-free standalone module (like `motion`) with the deps-injected seam `request` uses — the page boots it and hands it a fetcher; there is no singleton coupling. Ported from the workkit tower's page runtime.
|
|
146
|
+
- **`swap($host, markup)`**: writes `innerHTML` ONLY when the markup differs from what swap itself last wrote (a WeakMap keyed by the element), so an unchanged section keeps its DOM, focus, scroll position and open `details` across a poll. The comparison never reads `host.innerHTML` back — the browser re-serializes what it parses, so a read-back never matches the string that produced it and every tick would count as a change. Returns `true` when it wrote, which is what post-draw work (charts, listeners) hangs off.
|
|
147
|
+
- **`loading(message)`**: the spinner line a section shows while its feed has never answered — a first paint says which read it is waiting on instead of drawing an empty region. The message is escaped through `utilities.escapeHTML`.
|
|
148
|
+
- **`createFeedPoller({ feeds, fetcher, onChange })`**: `feeds` is the declared table (`{ name: { path, every, fresh? } }` — `fresh` is the cache-bypass path a user-triggered refresh uses); `fetcher` is an `omega.request`-shaped function (resolves with the body, throws an `Error` carrying `.code`), so a page passes `omega.request` and a non-singleton context passes its own `createRequest(...)`; `onChange` fires at every state transition (a read starting, a read landing) and is where the page repaints.
|
|
149
|
+
- **Poller surface**: `state` (`{ feeds, pending, stamp }`), `read(name, fresh)`, `readAll(fresh)`, `staleFeeds()`, `start()` (first pass, then arms one interval per feed; idempotent), `stop()` (also lets go of the visibility listener).
|
|
150
|
+
- **Hidden-tab pause**: the cadence stops while the page is hidden (a covered tab) and resumes with one immediate read on return. A visible-but-unfocused window keeps polling, and a context with no `document` (the extension service worker) keeps its cadence. There is no option for it.
|
|
151
|
+
- **Feed result shape**: `{ ok, data, status, reason }` — `status` is the thrown error's `.code` (null for a transport failure), `reason` its message.
|
|
152
|
+
- **Keep-last-good**: a refresh that fails does NOT clear the page — the last good result stays in `state.feeds[name]` with a `stale` key naming why the refresh missed. A feed that has never answered simply carries its own latest failure. `staleFeeds()` returns `[{ name, reason }]` for both cases, which is what a chrome bar's "N feeds unavailable" chip draws from.
|
|
153
|
+
- **`state.pending`**: how many reads are in flight — a refresh is visible while it happens and the page under it keeps showing the data it already has.
|
|
154
|
+
|
|
155
|
+
## Verts (`verts.js`) — adblock-safe ad engine
|
|
156
|
+
|
|
157
|
+
- **Class**: `Verts` (+ `VertUnit` per mounted house unit)
|
|
158
|
+
- **Key Methods**: `render($el, options)` (the full ladder), `renderHouse($el, options)` (fallback lane directly — the desktop/extension lane), `mount($el, options?)` (lazy IntersectionObserver arm from `data-omega-vert*` attributes, idempotent), `bind(root?)` (auto-mount every `[data-omega-vert]`), `resolveSource(source?)`
|
|
159
|
+
- **The ladder** (docs/web/ads-system.md, monorepo): AdSense when `advertising.providers.adsense.client` is configured and the type is a provider type — script-load failure of `adsbygoogle.js` IS the adblock detector (no bait divs, no poll); fill awaited via a MutationObserver on `data-ad-status` + timeout; `unfilled`/timeout/blocked → the `advertising.fallback` lane (`'inhouse'`) → no-fill collapse (`display: none` + `omega-vert:no-fill`)
|
|
160
|
+
- **House lane**: sandboxed iframe → `<source>/omega/verts/serve` (`parent`, `tags`, `vertId`, `height`, `theme`, cache-buster); origin-validated postMessage vocabulary `omega-vert:set-dimensions` / `omega-vert:click`; HOST-owned lifecycle — rotation timer (`rotateInterval`, off by default), staleness recovery (`visibilitychange`/`online` → reload when stale), fill timer
|
|
161
|
+
- **Source resolution** (`advertising.providers.inhouse.source`): `'self'` → `getApiUrl()`, `'company'` → `config.company.url` through the api derivation, full URL → verbatim
|
|
162
|
+
- **Element vocabulary**: `data-omega-vert` (type: `display`/`in-article`/`in-feed`/`multiplex`/`house`), `data-omega-vert-size` (preset or px), `data-omega-vert-id`, `data-omega-vert-tags` — the web `verts/unit` section and the phase-4 desktop/extension binding share it
|
|
163
|
+
- **Events**: `omega-vert:fill` / `omega-vert:no-fill` / `omega-vert:click` / `omega-vert:reload` bubble from the host (+ `onFill`/`onNoFill`/`onClick`/`onReload` callbacks)
|
|
164
|
+
|
|
165
|
+
## Features (`features.js`) — the features contract in the browser
|
|
166
|
+
|
|
167
|
+
- **Shape**: a plain re-export, not a class — `import { resolveFeatures } from '@omega.js/client/modules/features.js'`
|
|
168
|
+
- **Exposes**: `isCountedFeature`, `isPacedFeature`, `featureMirrors`, `featureOverride`, `featureCounters`, `productFeatureValue`, `daysInMonth`, `dayShare`, `resolveFeature`, `resolveFeatures`
|
|
169
|
+
- **Why it exists**: the derivations live ONCE, in `@omega.js/account` — the same module @omega.js/backend's `consume` gate reads — so the number that refuses a request and the number a usage bar draws can never be two different numbers. `@omega.js/account` is a private package a consumer's install never resolves by name, and this package's dist carries it vendored, so this module is the DOOR a frontend goes through. Same idiom as `analytics.js` fronting `@omega.js/analytics` ([#647](https://github.com/Omega-JS-Stack/omega/issues/647))
|
|
170
|
+
- **The catalog is config**, not a module export: `omega.config.features`, which the embedding framework's build bridges in beside `omega.config.payment`
|
|
171
|
+
- **`resolveFeature(id, { catalog, product, account, now })`** returns everything a surface needs to speak about one feature: `{ id, name, icon, definition, counted, paced, mirror, value, limit, planLimit, override, used, left, total, day: { limit, used, left } }`. `-1` is the unlimited sentinel wherever a limit or a remainder can appear
|
|
172
|
+
- **Callers**: @omega.js/web's `payment-config.js` (`getProductLimits` is the catalog-aware read) and the account page's billing panel (plan bullets and usage bars); `auth.js`'s `_resolveUsage` reaches the same functions directly
|
|
173
|
+
|
|
174
|
+
## Motion (`motion.js`)
|
|
175
|
+
|
|
176
|
+
- **Exports**: `createMotion()`, `parseCountTarget(text)`, `formatCount(target, value)`
|
|
177
|
+
- **Pattern**: transport-free factory like icon-renderer — not a singleton module; each embedding framework boots it (`@omega.js/web` does in `core/js/core/motion.js`)
|
|
178
|
+
- **Engine surface**: `start(doc?)` (idempotent scan + IntersectionObserver + MutationObserver + scroll watcher), `stop()`, `scan(root)` for manually rendered roots
|
|
179
|
+
- **Attribute contract** (styled by the embedding framework's motion stylesheet): `data-omega-reveal[="up|fade|left|right|scale"]`, `data-omega-reveal-stagger`, `data-omega-countup`, `data-omega-rotate`, `data-omega-marquee` (+ `.omega-marquee__track` — the set is cloned until half the track covers the container; attr value = px/s), `data-omega-scroll-watch`, `data-omega-segmented` (gliding `.omega-segmented__thumb` under the checked/`.active` segment), `data-omega-dotfield` (canvas dot grid: traveling wave + drifting rainbow tint + window-tracked pointer glow; attr value = px spacing)
|
|
180
|
+
- **Resilience**: no-JS pages render visible (the hiding styles are gated on an inline `html[data-omega-motion]` stamp); `prefers-reduced-motion` renders final states with no animation; missing observers (exotic embeds) degrade to instant reveal
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Brand agent-docs chain (AGENTS.md / CLAUDE.md)
|
|
2
|
+
|
|
3
|
+
**The problem**: every brand needs current framework guidance for AI agents, but copying it into each brand drifts. **The design (Ian 2026-07-20, amended 2026-07-27)**: every brand reads the SAME entry the monorepo uses — the top-level omega `AGENTS.md`, the map — and the map's pointers (plus the omega plugin's hooks) orchestrate which docs load. The brand-root knowledge itself lives in `docs/manager/brand.md`; no AGENTS.md anywhere carries content.
|
|
4
|
+
|
|
5
|
+
## The chain
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
brand/CLAUDE.md @AGENTS.md (one line, Claude Code's entry)
|
|
9
|
+
brand/AGENTS.md line 1: @node_modules/@omega.js/AGENTS.md (the top-level omega map — see below)
|
|
10
|
+
below: `# <brand> — brand notes` + the brand's own notes — NEVER touched by the framework
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`node_modules/@omega.js/AGENTS.md` is a SYMLINK the workspace service maintains: it resolves the framework monorepo through the installed manager package's real path and links straight at the live top-level map. It sits in the scope directory — no package in the path — because the map belongs to the ecosystem, not to any one package. A published install has no monorepo above the package, so the link lands on the map prepare vendored INTO it (`@omega.js/manager/docs/AGENTS.md`, below). The two candidates are tried in that order, live map first, so a locally linked brand never lands on the generated copy sitting in that same monorepo's `packages/manager` ([#144](https://github.com/Omega-JS-Stack/omega/issues/144)).
|
|
14
|
+
|
|
15
|
+
(The cp244 marker comment under the import was culled — Ian 2026-07-20: keep it short. Heals no longer scrub legacy copies (#148): a leftover marker or retired import line is consumer content, removed by hand per [breaking-changes.md](breaking-changes.md).)
|
|
16
|
+
|
|
17
|
+
- The import path is **relative** (portable to any machine). For hoisted installs (the in-repo test brands are npm workspaces of this monorepo) the ensure step walks up and writes the correct depth, e.g. `@../../node_modules/@omega.js/AGENTS.md`.
|
|
18
|
+
- The service resolves the monorepo through the installed manager package's real path (the local-era `file:` symlink), so the link always lands on the LIVE top-level map — framework edits are instantly visible to every brand session.
|
|
19
|
+
- Non-Claude agents read `AGENTS.md` but don't follow `@` imports — the import line itself names the target path for them.
|
|
20
|
+
|
|
21
|
+
## Maintenance
|
|
22
|
+
|
|
23
|
+
`npm run manage` (the manage cycle's `workspace` service, `agents` op — `packages/manager/src/services/workspace/ensure/agents.js`, logic in `src/lib/agents-md.js`):
|
|
24
|
+
|
|
25
|
+
| State found | Action |
|
|
26
|
+
|---|---|
|
|
27
|
+
| No `AGENTS.md` | Created: import + brand-notes skeleton |
|
|
28
|
+
| Import present at line 1 (right depth) | No-op |
|
|
29
|
+
| Import missing / not first / stale depth | Healed: resolved import moved to line 1, duplicates removed, consumer content preserved verbatim |
|
|
30
|
+
| No `CLAUDE.md` | Created as the one-line `@AGENTS.md` pointer |
|
|
31
|
+
| `CLAUDE.md` carries content | WARNED (never clobbered) with the move-it-to-AGENTS.md message |
|
|
32
|
+
|
|
33
|
+
Pinned by `packages/manager/test/agents-md.test.js` (no package agent docs + files whitelist, guide-link create/heal/skip against both the local-era and published-install shapes, path resolution, create/heal/idempotence, content preservation).
|
|
34
|
+
|
|
35
|
+
## Published packages carry their own docs ([#64](https://github.com/Omega-JS-Stack/omega/issues/64))
|
|
36
|
+
|
|
37
|
+
A consumer install has no monorepo to point at, so the prepare lane ships the knowledge INSIDE each publishable. The devkit vendor hook every framework already runs (`packages/devkit/tools/vendor-docs.js`, called from `tools/vendor.js`) copies, for `@omega.js/{backend,client,desktop,extension,manager,web}`:
|
|
38
|
+
|
|
39
|
+
| Monorepo source | Shipped as | Notes |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `docs/<package>/` | `<package>/docs/` (flat) | The guide lands at `docs/index.md`, beside the package's committed deep docs; nested dirs (`classy-v2/`) survive |
|
|
42
|
+
| `docs/shared/` | `<package>/docs/shared/` | The cross-framework contracts, verbatim |
|
|
43
|
+
| `AGENTS.md` (the repo-root map) | `manager/docs/AGENTS.md` | Manager ONLY — the target the brand chain's scope symlink lands on when there is no monorepo. Links retargeted (see below) |
|
|
44
|
+
| `agent-plugins/claude/` | `manager/claude-plugin/` + `manager/.claude-plugin/marketplace.json` | Manager ONLY — the plugin every brand enables (see below). `.mcp.json` ships WITH it: since [#144](https://github.com/Omega-JS-Stack/omega/issues/144) it launches `mcp-router-launch.js` inside the plugin, which node-resolves `@omega.js/mcp-router` from the install around it (a real dependency of the manager) instead of addressing the monorepo tree |
|
|
45
|
+
|
|
46
|
+
The guide's monorepo-relative links are rewritten to the shipped layout on the way in (`../../packages/<self>/` → `../`, `../shared/` → `shared/`), so `docs/index.md` still reaches the package's deep docs, its README, and the shared contracts. Cross-framework links (`../backend/index.md`) are left verbatim — another framework's guide isn't in this tarball.
|
|
47
|
+
|
|
48
|
+
The map's links are repo-root-relative, so it gets its own pass (`rewriteMapLinks`): `docs/shared/<x>.md` → `shared/<x>.md`, `docs/manager/<x>.md` → `<x>.md` (the manager's guide tree lands flat in the same dir), `docs/<other>/<x>.md` → `../../<other>/docs/<x>.md` (a sibling package under the same `@omega.js` scope), and a `packages/…` or `brands/…` link keeps its words while losing the link — no published target exists. A sibling that never publishes (`devkit`) or isn't installed leaves a dead relative link, the same trade the guide trees already make.
|
|
49
|
+
|
|
50
|
+
Everything written is GENERATED: gitignored per package, cleared before each rewrite, and pinned by `packages/devkit/test/vendor-docs.test.js` (fixture monorepo) plus `scripts/vendor-docs.test.js`, which packs all six documented packages for real and reads the tarball listings.
|
|
51
|
+
|
|
52
|
+
Version-matched by construction: the docs in `node_modules/@omega.js/web/docs/` are the docs of the version installed there.
|
|
53
|
+
|
|
54
|
+
## Brands enable the plugin from their installed manager ([#62](https://github.com/Omega-JS-Stack/omega/issues/62))
|
|
55
|
+
|
|
56
|
+
The plugin rides in `@omega.js/manager` because that is the package every brand installs. The workspace service's `claude-settings` op writes/heals the brand's committed `.claude/settings.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"extraKnownMarketplaces": { "omega": { "source": { "source": "directory", "path": "./node_modules/@omega.js/manager" } } },
|
|
61
|
+
"enabledPlugins": { "omega@omega": true }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Committed, so every collaborator's session in that brand loads the omega skills and hooks — a directory marketplace is read LIVE from that path, so `npm update` moves the plugin with the package. The op only fires on a PUBLISHED install: `node_modules/@omega.js/manager` must carry the vendored `.claude-plugin/marketplace.json` **and** be a real directory. A locally linked brand's manager is a SYMLINK into the monorepo — whose `packages/manager` grows that same generated marketplace on every prepare — so the link itself is the local-era signal and the step skips; the developer's own user-scope install covers those sessions. The brand file NEVER points at a monorepo path — it would be machine-specific.
|
|
66
|
+
|
|
67
|
+
Once loaded, the plugin's hooks make the chain BINDING in that brand: the inject hook discovers every target the brand declares and asks for each one's skill plus this map as required reading, and the gate hook refuses a write under `targets/<t>/` or to `config/omega.json5` until the skill owning that surface has been invoked. Mechanics and the surface table: [the plugin README](../../agent-plugins/claude/README.md).
|
|
68
|
+
|
|
69
|
+
The gate reads the same way for a human's chat and for an agent that has no Skill tool, through two lanes and no others ([#760](https://github.com/Omega-JS-Stack/omega/issues/760)). In the main chat, invoking the skill IS the record. A subagent that writes its files through Bash reads the guide its brief names and then runs the one sanctioned command, `agent-plugins/claude/hooks/gate/mark.sh <skill>` here (in a brand: `node_modules/@omega.js/manager/claude-plugin/hooks/gate/mark.sh`), which writes the marker the hook would have written. A hand-written marker is a process breach, not a shortcut. Both lanes, and the flags: [the plugin README](../../agent-plugins/claude/README.md).
|
|
70
|
+
|
|
71
|
+
Reading the chain is one half; acting on it is the other. The guard hook holds the other half in the same brand: a `Write|Edit` to a framework-owned file is refused with the upstream-first message ([#452](https://github.com/Omega-JS-Stack/omega/issues/452)). Generated and vendored files — `node_modules/`, any `dist/`, a generated header, the OMEGA-managed `database.rules.json` — are hard-refused and name the real source to edit; a SHADOW COPY, a brand file whose path mirrors a file the installed framework ships through its override layer (read from `node_modules/@omega.js/<framework>/`, never a hardcoded list), is refused with the two exits: file the framework issue, or declare the override with `omega:consumer-override: <reason>` in the file's first five lines (`omega customize` writes that marker itself, so a materialized file passes as it lands). Only the override MECHANISM is guarded: a brand's pages are content and its documented entry files are its own, so neither is ever refused. The monorepo is exempt and everything unrecognized fails open. The lookup-root table and the class rules: [the plugin README](../../agent-plugins/claude/README.md).
|
|
72
|
+
|
|
73
|
+
## Packages carry no agent docs (Ian 2026-07-27)
|
|
74
|
+
|
|
75
|
+
No `packages/<pkg>/` has an `AGENTS.md` or `CLAUDE.md` — deleted outright, no exceptions. Monorepo sessions get the map from the parent walk; consumer brands get it through the maintained scope symlink; a standalone package install has no resolvable chain anyway; the publish era generates whatever a shipped package needs ([#64](https://github.com/Omega-JS-Stack/omega/issues/64)).
|
|
76
|
+
|
|
77
|
+
## The one deliberate gap
|
|
78
|
+
|
|
79
|
+
- **`brands/sandbox-brand` carries NO agent-docs chain.** It is a synthetic fixture the automated corpus/e2e runs mangle and reset — nothing durable lives there, so nothing agent-facing is written there.
|
|
80
|
+
|
|
81
|
+
## Editing the guide
|
|
82
|
+
|
|
83
|
+
The brand-root guide is [docs/manager/brand.md](../manager/brand.md) — framework-owned, brand-agnostic (structure, verbs, per-target required-reading pointers, hard rules). Brand-specific knowledge belongs below the import in that brand's own `AGENTS.md`.
|
|
84
|
+
|
|
85
|
+
## Per-target docs — RETIRED in brand context (cp246)
|
|
86
|
+
|
|
87
|
+
Per-target `AGENTS.md`/`CLAUDE.md`/`CHANGELOG.md`/`docs/` scaffolds predate the brand-monorepo era; Claude Code walks parent directories, so the brand-root chain covers target-dir sessions. The shared defaults engine now has a `retire` fileMap rule (devkit `defaults-engine.js`), wired mirrored in all four frameworks' brand branches (detection = the existing `@omega.js/config` brand-root resolution): in a brand target those files NEVER scaffold; an existing framework-owned-only copy (Custom section empty/whitespace or byte-equal to the shipped boilerplate; marker-less files must equal the rendered template) is deleted once, loudly; a copy carrying real consumer content is preserved with a move-it-to-the-brand-root warning. Standalone projects keep full per-project doc scaffolding, and it follows the same shape as a brand root (#63): the framework template is an `AGENTS.md` (marker-merged, so consumer notes below the Custom marker survive every setup) beside a one-line `@AGENTS.md` `CLAUDE.md` pointer. Test-pinned both ways.
|
|
88
|
+
|
|
89
|
+
**Prior-generation files ([#101](https://github.com/Omega-JS-Stack/omega/issues/101)).** A destination carrying the OMEGA section markers when the current template no longer does is a file an EARLIER generation of the same framework generated — the pre-#63/#91 content-bearing `CLAUDE.md` against today's one-line pointer. Its markers are the framework's own signature, so the engine treats it as framework-owned on both paths: the brand branch retires it (instead of keeping it behind a false "carries consumer content" warning), and on the standalone upgrade path `overwrite: false` still heals it to the current template (instead of leaving a stale doc beside the new `AGENTS.md`). A file with no markers is judged as before — consumer-authored unless it equals the rendered template.
|