Implemented OpenSpec and created and updated its artifacts

This commit is contained in:
2026-10-31 04:33:34 -03:00
parent 0d0a7cfe14
commit 0d9e654b11
40 changed files with 3303 additions and 127 deletions

View File

@@ -1,7 +1,14 @@
# Architecture and Compatibility Contracts
This document describes the integrated modular implementation. Verification and
preexisting risks are tracked in [refactoring.md](refactoring.md).
This document describes the current checkout, including behavior added after the
1.1.0 release. [refactoring.md](refactoring.md) records the earlier modularization;
historical observations there do not override the current contracts below.
The [OpenSpec capability baseline](../openspec/specs/) and its archived
[source/test evidence map](../openspec/changes/archive/2026-10-31-document-project-baseline/design.md)
provide behavioral context. The initial change was completed and archived on
2026-10-31, promoting all twelve capabilities to durable specifications. Use those
capabilities for subsequent delta changes.
## Sources, Outputs, and Runtime Boundaries
@@ -12,17 +19,24 @@ preexisting risks are tracked in [refactoring.md](refactoring.md).
- `dist/chromium` serves Chrome and Edge; `dist/firefox` serves Firefox. Do not edit outputs.
- Both manifests use V3. Chromium has a classic background service worker; Firefox
uses `background.scripts`. Neither entry point is declared as a module.
- Content scripts run at `document_idle`, in all matching frames on the two
- `src/content/shortcut-intent.js` runs at `document_start` in the top frame,
preserving bookmark intent before Fusion rewrites the URL. The main content
script runs at `document_idle`, in all matching frames on the two
configured Fusion welcome pages. Their code also checks the hostname/path.
- `src/auth/spa-page.js` is a separate top-frame-only SPA content script, copied
- `src/auth/spa-page.js` runs at `document_end` as a separate top-frame-only SPA content script, copied
into both distributions. It handles only extension messages for DOM readiness
inspection and a fixed same-origin identity GET; it sends no requests on its own.
`services/spa/page-client.js` connects this receiver to the background recovery
flow and the existing debug envelope. No page-world script is injected.
- There are no extension UI pages, injected page-context scripts, custom CSP,
declared execution world, or web-accessible resources.
- Permissions remain `cookies` and `storage`, with host access limited to the
configured Fusion host and `spa.oracle.com`.
- `scripts/build.mjs` defines permissions: `bookmarks`, `cookies`, `storage`,
`webRequest` and `declarativeNetRequestWithHostAccess`. Host permissions cover
`eeho.fa.us2.oraclecloud.com`, `spa.oracle.com`, and
`comcipapic-oalprod.integration.ocp.oraclecloud.com` over HTTPS.
- Bookmark access supports folder selection and shortcut detection. SR integration
observes App Builder client IDs and installs narrow session rules for required
Origin headers; it is not a general request proxy.
Content owns the DOM and per-frame state. Background owns privileged cookies,
temporary tabs, extension storage and SPA requests. These contexts communicate
@@ -40,41 +54,67 @@ different from extension storage and is not a safe place for credentials.
| Content | Plan projection details | Per-panel WeakMap, shared in-flight requests, nested tooltip DOM, temporary pointer/resize listeners cleaned up on dismiss/tab change/modal close |
| Content | Rate card | Pure column/filter/sort model, controller with per-plan in-flight sharing, animated drawer with skeleton/search/table; response guards and cleanup on close or plan switch |
| Content | Service Requests UI | Customer-specific in-flight queries, skeleton/table modal, stale-response guards; no persistent cache |
| Content | AI summary | Per-opportunity in-flight state and summary; fresh generation on expansion |
| Content | Authentication/debug | General and scoped token promises, SPA authorizations, request logs, visibility flag |
| Content | Organization Chart | Working hierarchy, contact autocomplete/creation, transient email and contact-opportunity views |
| Content | Manage Time | Week drafts, dependent selections, validated hours and deferred writes/deletions |
| Content | Authentication/debug | General token promise, SPA authorizations, request records and visibility state |
| Background | Message dispatcher | Synchronously registered listener, asynchronous replies using `return true` |
| Background | Products | Four workers, per-opportunity in-flight map, lazy cache read and serialized writes |
| Background | Cookie/session recovery | Inactive tabs, polling/retry delays, cleanup in `finally` |
| Background | SPA operations | HTTP/JSON parsing, account/plan extraction and debug envelopes |
| Background | Organization Chart storage | User/customer-scoped IndexedDB records and revision checks |
| Background | SR and time integration | Session preparation, resource lookup, service operations and header adaptation |
Closing the main modal hides it rather than destroying it. Many requests can
continue while it is hidden. Version and request-ID checks suppress stale UI
updates; they do not abort network traffic. No AbortController is currently used.
updates; they do not generally abort network traffic. Token Relay specifically
uses a 30-second timeout with AbortController when available. Its timeout also
covers response reading; debug persistence failures cannot leave it pending.
Some listeners have window guards; there is no general disposal lifecycle.
## Authentication and API Routing
Never substitute one token family for another or assume an opaque token is a JWT.
Production tokens are held in memory, not deliberately persisted by the extension.
Normal authorization reuse and debug capture have different storage lifetimes;
see Storage Contracts before making claims about retained sensitive values.
| Credential | Acquisition | Consumers | Lifetime/recovery |
| --- | --- | --- | --- |
| XSRF | Background cookie lookup, then content `document.cookie` fallback | `x-xsrf-token` for both tokenrelays | Cookie recovery tab on missing/invalid response; existing retry rules |
| Fusion general | GET `/fscmRestApi/tokenrelay` | Opportunity POST and product GET Bearer | Content promise/cache; no expiry-based refresh equivalent to Sales Common |
| Sales Common | GET `/fscmRestApi/tokenrelay?scopesfq=urn%3Aopc%3Aresource%3Afusion%3Aeeho%3Asales-common%2F` | AI summary Bearer | In-memory `expires_in`, shared in-flight acquisition; at most one summary retry after 401/403 |
| XSRF | Background cookie lookup, then content `document.cookie` fallback | `x-xsrf-token` for Token Relay | Cookie recovery on missing/invalid response with a bounded refresh attempt |
| Fusion general | GET `/fscmRestApi/tokenrelay` | Opportunity/saved-search, product, CRM SR and contact queries/writes | Shared content acquisition; 30-second request timeout |
| ACCP-TS | SPA consumer service `uiaasHeader.value` | Account summary authorization | Retrieved with the consumer service response; expiry recorded, not automatically acted on |
| OCICONS-TS | SPA consumer service `uiaasHeader.value` | Account plans, consumption trend, plan detail and rate card authorization | Same service response, distinct credential from ACCP-TS |
| SPA-ORANGE | SPA consumer service `uiaasHeader.value` | Customer service requests, fixed `custsr-auth` header with raw value, no Bearer prefix; ignores `uiaasHeader.label` | Optional at consumer acquisition; required only when opening Service Requests; existing in-memory session rules |
| SAIB-TS | SPA consumer service label/value | Installed Base after account registry resolution | Required when opening Installed Base |
| WWB-TS | SPA consumer service value | Workloads and Actions in `Wwb-Provider-Authorization` | Shared consumer acquisition; data caching is separately scoped |
| SR integration session | Integration cookies and `Authorization: Session` | Internal SR and Manage Time operations; catalog preparation | Required Origin and observed `x-appbuilder-client-id` where applicable; session preparation is separate from identity |
| Catalog authorization | SR catalog/token preparation | SR creation | Operation-specific headers; do not replace with the Fusion general token |
Fusion calls use `https://eeho.fa.us2.oraclecloud.com`; SPA calls use
`https://spa.oracle.com/oalcrm/web/api/`. Preserve `credentials: include` and
operation-specific headers rather than unifying all calls under a default token.
Identity is validated before dependent data requests and then reused. Content
shares its pending identity promise; background shares by cookie store/private
context and can restore user id/name/roles from extension session storage. A page
reload alone therefore does not force identity revalidation. SALES_REP blocks
CRM activity data, badges and Manage Time entry points. Independent SR session
navigation can start concurrently before the role result is consumed.
SPA bootstrap uses
`https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=whitespace`.
Recovery rejects login/error/callback documents before a same-origin identity
probe. It closes owned tabs and preserves the originating window/container.
SR preparation uses the SeaaS_initiation external application, with a 70-second
timeout and manual retry. Internal SR detail recovery instead uses a hidden iframe
and bounded, shared 401 checks, not a visible tab or an unlimited request loop.
| Request | Method/path | Important contract |
| --- | --- | --- |
| requestOpportunities | POST `/crmRestApi/searchResources/11.13.18.05/custom-actions/queries` | RecordSet OR plus EffectiveDate AND, original fields, Preference transient, pages of 15 |
| requestOpportunitiesSavedSearch | GET `/crmRestApi/searchResources/11.13.18.05/custom-actions/queries` | Opportunity entity; require one `isDefault=true` result with a valid savedSearchNumber |
| requestOpportunities | POST `/crmRestApi/searchResources/11.13.18.05/custom-actions/queries` | `copiedFrom=queries/<default savedSearchNumber>` across pages; RecordSet OR plus EffectiveDate AND; Preference transient; pages of 15 |
| requestSRSearchResources | GET `/crmRestApi/searchResources/latest/custom-actions/queries` | CRMServiceRequest entity; use the first item's savedSearchNumber |
| resquestCrmServiceRequest | POST `/crmRestApi/searchResources/latest/custom-actions/queries` | Dynamic copiedFrom; pending-delivery team SRs; all pages; incremental badge updates |
| requestOpptyProducts | GET `/crmRestApi/rest/rv:be91c002-2e5d-4ed2-a37e-e0c837bf141f/en/11.13.18.05:9/opportunities/{number}/child/ChildRevenue` | Encoded identifier, original query/fields/order, count/offset pagination, maximum 250 pages |
| requestGenerateAiSummary | POST `/api/sales-common/gen-ai/v1/summarizations/generate` | `{path,promptCode,object,puid}`; prompt `cx.sales.generate_opportunity_summary`; only summaryText displayed |
| requestConsumerTokenService | GET `g2m-consumer-application/consumerTokenService` | No-store; parse JSON; HTML 200 recovery via inactive SPA UI tab; maximum five HTTP attempts |
| requestAccountSummary | POST `provider-proxy/g2m-account/service/accountsummary` | ACCP-TS; `[{key:"regId",value:partyNumber}]` |
| requestCustomerServiceRequest | GET `provider-proxy/g2m-custsr-provider/service/oscSr/{accountSummaryId}` | SPA-ORANGE; summary `id`, not `registryId`; encoded filter `{hierarchy:"single",status:"open"}`, creationDate descending, pages of 40 using totalResults/count/offset |
@@ -87,9 +127,23 @@ SPA parsing removes BOM and the existing anti-JSON-hijacking prefixes before
JSON.parse. Content uses response.json directly. Do not silently unify these behaviors.
POST retry policies require operation-specific review; do not add generic retries.
After all opportunity pages arrive, the ready list is displayed before background
SR and product loading start. Completed product cycles use successful/reused SR
results before Workload Actions. Cached products can still require activity
network reads on cache miss. Failed SR/Action lookups remain unknown rather than
confirmed absent. Actions currently fetch only their first page and disclose
`hasNext`; contact opportunities and account workloads consolidate all pages.
Contact search uses Fusion `searchResources/latest` POST with a customer PartyId
criterion, keywords, Preference transient and the general Bearer. Contact
opportunities use the person's opaque PartyNumber in the GET filter, including
alphanumeric identifiers. Contact creation uses the selected customer's account
identifiers and an explicit Save; local hierarchy persistence is independent.
## Message Contracts
All names below have the `opportunitiesExtension.` prefix. Unknown messages return
The examples below have the `opportunitiesExtension.` prefix; the complete name
inventory is in `src/shared/messages.js`. Unknown messages return
false without a response. Supported background messages return true immediately
and later call sendResponse. `platform/background-api.js` centralizes the existing
callback/promise differences. `platform/content-messages.js` retains the original
@@ -121,25 +175,94 @@ Do not expose a caller-provided arbitrary request URL through the background.
| Page localStorage: `opportunities-extension-filter-preferences` | JSON object: period, stages, statuses, customerKeys, ownerKeys, customerSearch, ownerSearch, tableSearch; unset customer/owner selections may be null |
| Page localStorage: `opportunities-extension-theme` | `light` or `dark` |
| Page localStorage: `opportunities-extension-opportunity-type-preferences` | JSON object: opportunityTypeValues, opptyProductsCacheHours |
| Page localStorage: `opportunities-extension-table-sort-v1-<table>` | Sort key and direction, independently per supported table; memory fallback if storage is unavailable |
| Page localStorage: `opportunities-extension-manage-time-reference-date` | Valid selected date; falls back to today |
| Page localStorage: `opportunities-extension-debug:<tabId>:<id>` and `:index` | Full captured request records and index; may retain sensitive originals; quota failures fall back to memory |
| Page sessionStorage: `opportunities-extension-debug-tab` | Namespace identifier for debug records |
| Page sessionStorage: `opportunitiesExtension.shortcutIntent` | Early bookmark timestamp; consumed once and expires after ten minutes |
| Extension storage.local: `opportunitiesExtension.opptyProductsCache.v1` | Object indexed by OptyNumber, each entry `{fetchedAt,items}` |
| Extension storage.local: `opportunitiesExtension.workloadsCache.v1` | Workload data by normalized principal/customer; `{fetchedAt,items}`; no persistent entry without a principal |
| Extension storage.local: `opportunitiesExtension.activities.v1:<scope>` | SR/Action records scoped by principal, kind and identifier; SR keys also include product-update timestamp |
| Extension storage.local: `opportunitiesExtension.activities.invalidatedAt.v1` | Invalidation timestamp preventing obsolete activity snapshots from being reused or rewritten |
| Extension storage.session: `opportunities-extension-current-user:<context>` | Validated `{id,name,roles}`, partitioned by cookie store/private context; no identity token in this cache |
| Extension storage.session: `opportunitiesExtension.resourceUsers.v1` | Resource user, email and fetchedAt; shared concurrent reads and TTL validation |
| Extension storage.session: `opportunitiesExtension.srAppBuilderClientId.v1` | Observed integration origin/client ID/timestamp; one-hour validity |
| Extension-owned IndexedDB: `opportunities-extension-organizations`, store `charts` | Database version 3; user/customer key, record schemaVersion 1, revision, updatedAt and normalized nodes |
| Page memory: form request cache | Allowlisted read results, shared in-flight promises, maximum 500 entries, scoped to current user |
| Page memory: active views and authentication | Working charts, transient contact email, week drafts, opportunity results, token promises and service authorization values |
Product TTL defaults to 24 hours and is capped at 30 days in background. Storage
failures are tolerated for the active view. Recreating background reloads stored
products but does not restore in-flight jobs. Cache TTL does not physically delete
expired entries. Account/container isolation limitations are tracked separately.
Product TTL defaults to 24 hours and is capped at 30 days by the background cache.
Activity TTL uses the Preferences duration, also capped at 30 days, anchored to
the product-update timestamp rather than response time. SR snapshots preserve
different update timestamps; expired SR snapshots are pruned on subsequent writes.
The form request cache defaults to 24 hours and caps its duration at seven days;
it passes that duration to supported operations, including workload/resource reads.
Workload reuse preserves the original timestamp instead of restarting the TTL.
The product cache is keyed by opportunity number, not user or container. Do not
extend the stronger principal/customer scoping of workload and activity caches to
that cache by assumption. Identity caching has browser-session lifetime and its
own cookie-store/private-context partition, with memory fallback where extension
session storage is unavailable. Browser storage APIs are distinct from page storage.
Form cache reuse excludes writes and token generation. Successful supported writes
invalidate reusable form reads; concurrent reads share promises and suppress
duplicate debug envelopes. Refresh all invalidates related product, workload,
activity and form data before reloading Reference, while preserving preferences
and authentication. Generation checks prevent old reads from restoring invalidated
snapshots. Invalidation failure is surfaced rather than silently accepted.
Organization-chart nodes retain id, parentId, name, jobTitle, notes, collapsed,
optional contactPartyId and alphanumeric PartyNumber. Email is excluded; older
stored email fields are removed during migration. Chart storage is isolated by
normalized user/customer and browser profile, enforces revision checks, and has no
cross-device sync. Clearing a Fusion page's IndexedDB does not clear the extension's
IndexedDB, and an already-open working chart can still exist in page memory.
Storage failures have feature-specific behavior: preferences and data caches can
fall back to current memory/results, debug keeps records in memory with feedback,
and chart persistence reports failure rather than claiming a durable save.
Recreating the background loses in-flight jobs while restoring supported stored
data on demand. Expiration does not imply that every cache physically deletes its
old records immediately.
## UI Contracts
### Debug lifecycle
`features/debug/state.js` stores each changed request under the current tab's
localStorage prefix and maintains an index, batching writes with a short timer.
Opening Debug Requests reloads saved records while retaining handles used by
in-flight requests. Hidden debug does not construct request-detail DOM; opening
renders summaries and expanding a record mounts details. Storage exceptions keep
the memory records usable and do not block the recorded network operation.
`services/fusion/opportunities.js` resets debug immediately before sending page
one, after resolving the saved search. Thus earlier authentication and saved-search
logs can disappear at that boundary. Pagination does not reset history, and closing
the main modal does not delete it. Reset clears only the current tab prefix and
invalidates previous-generation completions, preserving preferences and other tabs.
The default UI masks sensitive values and the explicit visibility toggle can show
retained originals, including tokens in captured responses. Individual transports
also mask selected headers, but that is not a universal redaction guarantee.
localStorage records can contain sensitive values; display masking is neither
encryption nor protection from other code with access to that page's storage.
The normal identity cache excludes its token, which does not imply the debug
response excludes it. This documentation change does not alter retention policy.
### Shared interactions
Preserve labels/IDs/classes, column order, URLs, clipboard behavior, sort/filter
semantics, formatting, theme colors, keyboard shortcuts and row expansion.
WON includes WON_PENDING without rewriting StatusCode. Owner uses first/last name
with the existing fallbacks. Changing reference resets customers/owners to all;
closing/reopening preserves search. Fiscal years begin in June; RENEWALS currently
has no date range and therefore does not load opportunities.
closing/reopening preserves search. Fiscal years begin in June; the active
Reference choices use supported fiscal/date ranges rather than a RENEWALS view.
Preserve SVG chart data normalization, animation, tooltips and series selection.
AI text is inserted with textContent. Debug masking and DOM isolation have known
limitations; see the separate security backlog rather than assuming full redaction.
Treat displayed API text as data. Debug masking and DOM isolation have limitations;
do not equate display masking with full redaction or encryption at rest.
## Module Ownership and Dependencies
@@ -151,8 +274,9 @@ to isolated tests, not published on the page/global object in production.
Factories do not import or instantiate peers. Features receive service/auth ports;
services/auth receive transport/platform ports; pure models receive data. The
composition roots supply callback notifications for UI updates and debug events,
preserving existing interactions without module-import cycles. Shared messages
are the only source included in both contexts, with distinct instances/state.
preserving existing interactions without module-import cycles. Shared messages,
date/debug utilities and the organization-chart model can be included in both
contexts, with distinct instances/state.
Some stateful UI collaborators intentionally share a feature-owned state object;
these objects are created per application, never stored in a global registry.
@@ -163,11 +287,12 @@ these objects are created per application, never stored in a global registry.
| fiscal dates/query payload | `features/opportunities/fiscal-periods.js`, `services/fusion/opportunities.js` | Pure date rules vs HTTP operation |
| table/search/filters/formatting/dashboard | `features/opportunities/` | Related presentation responsibilities, explicit state |
| saved settings | `features/preferences/storage.js`, `view.js` | Serialization/fallbacks vs dialog |
| token families | `auth/content-session.js`, `sales-common.js`, `token-relay.js`, `spa-credentials.js` | Different credentials and lifetime policies |
| token families | `auth/content-session.js`, `current-user.js`, `token-relay.js`, `spa-credentials.js`, `sr-request-headers.js` | Different credentials and lifetime policies |
| cookie lookup/recovery, SPA session | `auth/xsrf-*.js`, `consumer-session.js` | Privileged recovery separate from UI |
| product queue/cache | `features/products/background-jobs.js`, `cache.js`, `services/fusion/products.js` | Scheduling, persistence and pagination |
| product progress/details | `features/products/content-jobs.js`, `details.js` | Message consumer vs row rendering |
| AI summary | `features/ai-summary/content.js`, `state.js` | Per-opportunity lifecycle, safe text and auth port |
| org charts and contacts | `features/org-chart/`, `services/fusion/account-contact.js` | Local hierarchy/storage vs Oracle contact operations |
| Manage Time | `features/manage-time/`, `services/fusion/time-entries.js` | Weekly UI drafts vs session-authenticated reads/writes |
| consumption | `features/consumption/`, `services/spa/` | Plans, chart model/view/controller vs HTTP operations |
| debug | `features/debug/content-log.js`, `content-view.js` | Retention/formatting vs DOM presentation |
| background dispatcher | `background/messages.js`, `shared/messages.js` | Stable serializable message contracts |
@@ -175,7 +300,7 @@ these objects are created per application, never stored in a global registry.
The background composition deliberately has no DOM dependency. Listener registration
is synchronous, before any storage read or request. Worker restart reconstructs
volatile state and lazily reads the existing product cache; tokens are not persisted.
volatile state and lazily restores supported product, identity and session metadata.
Content effects (DOM, fetch, storage, animation) enter through the environment at
the composition root. localStorage access stays lazy to retain blocked-storage
fallbacks. Tests may inject effects or individual callback ports without changing
@@ -183,7 +308,8 @@ source strings. Time-sensitive modules are tested with isolated clocks.
## Adding a Feature
1. Define its message/API/storage contract and synthetic expected behavior first.
1. Identify the affected OpenSpec capabilities and propose the behavior delta,
message/API/storage contract and synthetic expected behavior first.
2. Add a factory under the owning feature, or a service under the correct host.
Accept narrow function dependencies; do not call another factory inside it.
3. Add its source path to the appropriate array in `scripts/sources.cjs`. Source
@@ -206,8 +332,6 @@ belong in the consumption feature and SPA service, not the bootstrap.
## Source vs Generated Code
Distribution `content.js` and `background.js` are generated bundles and may remain
large. No copy/export/execute JavaScript generator feature was found in the source.
AI summaries are text, not executable JavaScript. No empty generators subsystem
was added. Build serialization uses JSON.stringify for CSS, retains quotes,
large. Build serialization uses JSON.stringify for CSS, retains quotes,
backslashes, Unicode and line breaks, and is syntax-checked by vm.Script. Visual
fixtures now load composed scripts normally; they no longer eval rewritten sources.