Skip to main content
A prompt for your AI coding agent (Claude Code, Cursor, GitHub Copilot, or similar). Copy everything below the line into the agent and run it from the root of your Arc.js app repository — the folder containing assets/functions.json and assets/src/. It produces a report first, then asks which changes you want (source fixes, build setup, kibo.json) and makes only the ones you approve. Without an agent, use the same document as a manual checklist. See Preparing API Extensions for Shared Context for background on why this audit matters.

Your role and goal

You are auditing a Kibo Arc.js application so that its compiled bundles can run in a shared (warm) context. Produce a report first. Edit no file during the audit, except the report file ARCJS-AUDIT.md (see “Saving the report”). When the report is done, ask the user which changes to make (see “After the report”) and change nothing they do not approve. Keep code changes to the crucial ones: only what a bundle needs to run safely warm. Everything else is reported, not fixed. Every change that alters what a shopper or caller sees (response, error, error order, data freshness, or a response that now waits for work it used to leave behind, such as an awaited log call or a failed batch that now waits for its slowest call) is called out as a business change and needs its own yes. Waiting for a cache write or async crypto/zlib that the request already started (none (timing: …)) is not a business change and goes in the Patch.

What a shared context means

Why: performance. Normally each Arc.js action invocation runs in a freshly loaded copy of its compiled bundle, so every request pays to load the modules and build their state again. For a busy storefront action that is most of its CPU time. With shared context, the platform loads a bundle once and reuses the loaded copy for later requests of the same tenant: other shoppers, other sessions, other sites. Requests get faster and use less CPU. The catch. Between requests the platform resets only what it can see from outside your modules. It does not reset anything held inside them, so code that assumes it starts fresh on every request can leak data or misbehave. This audit finds that code and proposes the smallest fixes. Two requirements:
  1. Nothing from one request may be visible to, or change the result of, a later request. No shopper data, no request data, no half-finished work, no state left wrong (a stuck flag, a regex lastIndex, a cached failure).
  2. Nothing may make the platform stop reusing the loaded copy on the normal path. Some patterns make the platform throw a loaded copy away. That is safe, but it loses the performance benefit, so it only matters on paths that run often.
Goal: as many warm bundles as is safe, above all storefront and catalog. They run the most often and gain the most. A storefront manifest exports http.storefront.* actions (usually assets/src/storefront.manifest.js); a catalog manifest exports *.commerce.catalog.* actions. Audit them first and most carefully, list them first in every table and question, and never leave one none without asking about each item that blocks it.

What the platform does (use these facts; do not re-research them)

  • Reset between requests: globals (added ones removed, replaced ones put back) and process.env. Not reset: module-level variables, closures, the contents of objects, properties added to process / module / console, members of required modules, and prototype changes.
  • At the finish (defined below) the platform cancels pending timers without running them and aborts HTTP requests still in flight. The aborted requests’ error and close handlers still run before the next request on the same loaded copy, so a module variable they write is seen by that request. New HTTP requests or timers started after the finish never run.
  • It stops reusing a loaded copy in these cases (the ones that matter for this audit): a context.cache operation still pending at the finish; async crypto (pbkdf2, scrypt, key generation, crypto.subtle) or async zlib still pending; an invocation that reaches the action timeout; a JavaScript built-in global (JSON, Promise, Math, Date, Object, Array, …) replaced, shadowed or deleted; process.env replaced. Work that completes just after the finish may not cause this, and its handlers still run before the next request.
  • context.exec.* calls (logOut, setItemAllocation, …) are synchronous and are not pending work. The exception is context.exec.loginUser: it makes a network call and runs its callback later, so code in that callback follows the same rules as code after an await.
  • A loaded copy serves one tenant and one bundle, but all of that tenant’s sites, catalogs, locales and currencies, and it can stay in use for a long time. Data a module keeps is therefore shared across all of them and can be much older than one request.

Three tiers of finding

Every blocking finding names its tier and its path:
  • LEAK wins. A pending cache call or an aborted HTTP call whose handler writes module state is a LEAK, not a DISCARD. A handler that only touches its own local variables is neither.
  • Normal path = every path a well-formed request can take, including not-found, empty results, validation rejections, cache misses, first load and guest shoppers. On a storefront these are high-volume, so a discard there means the copy is almost never reused.
  • Failure-only path = an upstream call failed or timed out, a malformed reply, corrupt cached data. A DISCARD there costs one lost reuse and leaks nothing: report it as SHOULD CHANGE.
  • For each MUST CHANGE, give the call chain from the wired action to the line and what runs after the finish, or “runs at load” with the top-level line. A finding with neither is downgraded only if you also show the code is unreachable (a grep of its callers, and no lookup by a computed name, route table or registration loop).

Inputs

  • The app root: the folder with assets/functions.json and assets/src/ (the source the author writes), e.g. assets/src/storefront.manifest.js is built into assets/dist/storefront.manifest.all.js.
  • assets/dist/ is generated. Do not review, quote or mention it; report everything against assets/src/.
  • assets/functions.json → exports[] maps each action (actionId) to its compiled file (virtualPath). Use it only to find which source manifest each action belongs to (the manifest / browserify file mappings in Gruntfile.js).
  • The build setup: package.json, the lock file, Gruntfile.js, the tasks/ folder and node_modules/grunt-mozu-appdev-sync/ if installed.

Key definitions

Unit of verdict: the source manifest. The build compiles each manifest, with everything it requires, into one bundle, and the platform reuses that whole bundle or none of it. One failing action, route or helper blocks the entire manifest. List each bundle’s own files with the bundler (for example npx browserify --list --node assets/src/<name>.manifest.js) and keep that list; only those files are part of the manifest. Skip public third-party packages under node_modules (check them only as in “Rules for fixes” rule 5), but audit a first-party package fully (your organisation’s scope, or a file:, link: or git dependency). If the listing fails, follow the requires by hand. Code no wired action can reach, and that does nothing at load, is a NOTE; show the grep evidence. The finish. An invocation is finished at the first of: callback(...) is called; context.response.end() is called; the promise returned by the action settles; the action throws. When the finish happens, nothing the invocation started may still be running or pending. Calling callback() does not stop the function: code after it keeps running.

Procedure

  1. Check the build setup (first section of the report; edit nothing):
    • grunt-mozu-appdev-sync is 2.5.0 or later everywhere: the range in package.json must not allow anything lower (2.4.5 or ^2.4.0 fail), and the lock file and (if present) node_modules must resolve to 2.5.0 or later. If package.json and the lock file disagree, say so.
    • No other task named manifest: no tasks/manifest.js (only .js / .coffee files load, so e.g. tasks/_manifest.x is inert), and no other registerTask('manifest' / registerMultiTask('manifest'. A local one replaces the plugin’s, so kibo.json is ignored. If one exists, list what it writes beyond actionId / virtualPath (for example a nodeVersion stamp): deleting it drops that.
    • manifest runs in build before the upload.
    • If assets/functions.json already has an arcjs block that kibo.json does not reproduce, list it: the build overwrites it.
    • If the app does not build with this plugin (webpack, TypeScript, a custom pipeline), say kibo.json cannot take effect, give verdicts only, and suggest contacting Kibo support.
  2. Inventory. Group exports[] by virtualPath, map each group to its source manifest, and list each manifest’s actions and implementing modules. Order: storefront, catalog, then the rest.
  3. Read every action, every app module it requires, and every code path, including error, validation and catch branches. Most findings live in error branches.
  4. Run the mechanical scans, then confirm or dismiss each hit by reading the code.
  5. Apply every rule below.
  6. Write the report in the format at the end, then save it (see “Saving the report”).
  7. Verdict per source manifest: READY (no required changes) · MUST CHANGE · KEEP FRESH (cannot run shared, e.g. it needs an unsupported API) · NOTHING WIRED (no actions registered; never READY) · NOT AUDITED (say why; audit every manifest by default).

Mechanical scans (starting points, not verdicts)


Required changes (MUST CHANGE, subject to the tiers)

Work still running after the finish

Anything the invocation started that is still running at the finish.
  • Missing return. callback() in an if without return, then the function continues into await, HTTP calls or more callbacks:
    Fix: return callback(); (behavior-preserving only if the code after it did not change the response synchronously; check and say so).
  • Timer not cleared, including “fallback” timers raced against a call. The platform cancels the timer at the finish without running it. If its callback only touches local or response state: SHOULD CHANGE. If it was meant to reset module state (a “flush scheduled” flag, a debounce, a setTimeout-based expiry, a promise it would resolve), that state stays wrong for every later request: LEAK. Fix (only for the LEAK case; a SHOULD CHANGE timer gets a one-line hint): a once-guard that clears the timer:
  • Fire-and-forget HTTP call (logging, analytics, webhooks not awaited). The platform aborts it at the finish, and reuse continues. LEAK only if a handler on its chain writes module state; otherwise SHOULD CHANGE. If it must arrive, say so: it is already being cut off today, with or without shared context. Awaiting it is a business change (every response then waits for that call).
  • Un-awaited cache call. A context.cache get/set/remove not awaited before the finish: the platform stops reusing the copy (DISCARD), or LEAK if its handler writes module state.
    Fix: wait for it with its arguments unchanged: Promise.resolve(cache.set(key, v, ttl)).then(finish, finish). Without a callback the call returns a promise; Promise.resolve also covers a plain return value, and the second finish keeps ignoring write failures as before. Behavior impact: none (timing: the response now waits for the cache write). Never add, remove or change a TTL. A call with no TTL gets the platform default; an explicit TTL changes how long the entry lives. Do not switch to the callback form.
  • Parallel work left running. Promise.all / Promise.race that finishes on the first failure or winner while other tasks still run.
    Usually no change: leftover HTTP calls are aborted and reuse continues, so report it as SHOULD CHANGE. It is MUST CHANGE only if a leftover’s handler writes module state (LEAK), or a leftover is a cache call or async crypto/zlib that can be left running on the normal path (DISCARD). Do not default to waiting for all tasks: that makes a failed batch as slow as its slowest call, a business change for the owner to decide.
  • Async crypto or compression (pbkdf2, scrypt, key generation, crypto.subtle, async zlib) must complete before the finish. Synchronous variants are fine.
  • Late event handler. A handler registered during the request that fires after the finish, on a stream, socket or emitter not fully consumed or closed.

Invocation never finishes

A path that never reaches callback() / end() waits for the action timeout, and the platform then discards that copy. Examples: a .catch that throws on an undefined name; a JSON.parse outside the try; a new Promise branch that never resolves; a missing else. Tier: an async hang is a DISCARD: MUST CHANGE on the normal path; SHOULD CHANGE on a failure-only path. A synchronous infinite loop is BREAKS. Fix: every path reaches exactly one finish; the behavior impact is that the hanging request now answers (say with what).

Request data kept in module variables

A module’s top-level variables, and the contents of module-level objects, arrays, maps and class instances, are not reset between requests.
  • Must change (LEAK): a module-level variable or object a request writes that holds or is keyed by shopper or request data: customer or user id; cart, order, checkout; cookies, auth tokens, claims, session; location from a cookie; request headers or body; context, or an SDK client built from context and reused by later requests.
  • Must change: anything that only grows (push / set per request, never evicted), including a map keyed by request input with no size bound.
  • Must change: “first call” patterns that keep request data (a module-level variable overwritten per request and read after an await).
  • Must change: a shared HTTP cookie jar (request with jar: true, request.defaults({ jar: true }), a module-level request.jar() / tough-cookie jar).
  • Fine: constants, frozen lookup tables, pure helpers, and regexes. A module-level regex with the g or y flag is a LEAK only if it is used with .test( or .exec( (those keep lastIndex between requests); .replace, .match, .split and .matchAll are fine.
  • Fine: tenant reference caches, a module-level map of tenant data that rarely changes (locations, categories, site settings, configuration), with or without a TTL, when all of these hold (otherwise LEAK):
    • the value is the same for every shopper who sends the same key. A client built from context carries the shopper’s login, so the reply can differ by customer segment, price list or B2B account even when the code passes none of them. For product, category or search data read through such a client, ask the owner whether the tenant uses segments, price lists or B2B entitlements; until they answer, it is a LEAK. Settings, locations and configuration do not depend on the shopper;
    • the key includes everything the request passes that changes the result: site, catalog, locale, currency, and any price list, customer segment or account sent with the call. Check this in the code. If the key is missing one, the fix is to add it to the key (it changes the hit rate, not what shoppers see);
    • only successful, settled values are stored: no memoized promise that keeps a rejection, no fallback or “not found” entry written after an error;
    • entries are never changed after they are stored (no per-request decoration, in-place sort, or merge into a cached object);
    • keys cannot be __proto__, constructor or similar (ids and site / locale codes are fine in {}; otherwise use a Map or Object.create(null)), and a map keyed by shopper-controlled input (query, body, headers, path) has a size bound;
    • inventory, price, promotions and anything that affects the cart are never reference data.
    A “TTL” built with setTimeout / setInterval never fires (timers are cancelled at the finish); only a timestamp compared on read counts. Every such cache goes in the manifest’s “What changes for shoppers” section (see Report format).
Fix: keep the state inside the action function, or in context.cache with an explicit key.

Accidental globals

An assignment to an undeclared name (var a = q.a; b = q.b; where , was meant), or global.x = … / globalThis.x = … / top-level this.x = ….
  • Adds a new name: the platform removes it between requests, so it is SHOULD CHANGE (fix: var / let). It is a LEAK only if code that runs after the finish reads request data from it.
  • Shadows a JavaScript built-in (JSON, Promise, …): DISCARD.
  • Overwrites a global the bundle set at load (such as _): put back between requests; SHOULD CHANGE.

Patching built-ins and shared modules

Code that runs once at load, at the module top level, and installs the same pure function every time (Date.prototype.toTz = function…, _.mixin({…})) behaves the same in a fresh or reused copy: fine, as long as it reads no state that requests write and does not override an existing method (toJSON, toString, then, map, …). Report it as a NOTE. It is a LEAK (MUST CHANGE) when it is done inside an action (per request), captures request data, caches into module state, or adds a symbol-keyed global. The same applies to adding or changing properties on process, module or console, or replacing a member of a required module (require('http').request = …): fine (a NOTE) when done once at load with a pure function that reads no state requests write; MUST CHANGE when done per request or when it captures request data. Replacing, shadowing or deleting a JavaScript built-in, or replacing process.env, is a DISCARD. Fix: a plain local helper.

Listeners that outlive the request

Listeners added per request to anything that outlives it: http.globalAgent / https.globalAgent, process.on(...), a response’s socket, a module-level EventEmitter. They accumulate and capture request data that later requests can trigger (LEAK). Fix: listen only on per-request objects and remove the listener before the finish (once, or off in a finally).

APIs that do not work in a reused context

WebAssembly, SharedArrayBuffer, FinalizationRegistry and Atomics.waitAsync throw in a context that may be reused (BREAKS). The manifest must stop using them or stay KEEP FRESH; say which is realistic.

Request data read at module load

Per-request values read at the module top level and used for later requests: process.env values that differ per request, anything from context, the current time used as if per request (LEAK). Fix: read them inside the action.

SHOULD CHANGE (report only)

One line each: file:line, what happens, why it does not block warm, and a one-phrase fix hint. No fix code. Never in the Patch, never asked about, never applied unless the user asks for it. Includes failure-only DISCARDs, leftover HTTP without module writes, new-name accidental globals, timers that touch only local state, and:
  • Code after the finish changes the response: fully synchronous code after an un-returned callback() that overwrites context.response.body / status. Say what the client receives today and with a return.
  • Double finish with nothing async between the two calls. If anything async sits between them, it is Work still running instead.

SECURITY NOTE (report separately)

Security problems seen in passing (a signature computed but never compared, a secret in the source, a token trusted without a check): in their own section, with the file and line and what an attacker could do. Never quote the secret. Not part of any verdict or the Patch.

NOTE (report only)

Latent bugs seen in passing that do not affect shared context, accepted items, dead-code evidence, and load-time patches that are fine. Keep it short.

Rules for fixes

  1. Do not change business logic. Each fix states its Behavior impact:
    • none: same status, body, headers and side effects for every input. A pure timing note may be added (none (timing: …)) when the only difference is waiting for work that already ran;
    • changes: <exact description>: a business change (different response, error, error order, hang now answered, a failed batch now slower, data freshness). Example: “a non-POST request without a body now returns ‘Invalid request method’ instead of ‘Invalid request body’; status unchanged (400)”.
  2. If the simplest fix is a business change, also give a behavior-preserving variant if one exists, and never apply it without its own yes.
  3. Every fix is in src/. Never touch dist/.
  4. Smallest change: a return, an await, a let, a cleared timer, a wider cache key. Do not refactor.
  5. Third-party packages: check only for module-level state kept across calls, patched built-ins and global listeners. A module-level keep-alive agent is fine; a module-level client built from request credentials or a shared cookie jar is not.
  6. If one route blocks a busy manifest and cannot be fixed safely, you may propose moving it to its own manifest so the rest can be READY. List the Gruntfile.js, functions.json and kibo.json changes, and re-audit both bundles afterwards.

Report format

A manifest is READY only with zero MUST CHANGE findings.

Saving the report

Save it to ARCJS-AUDIT.md in the project root (the only file you write during the audit), and update it in Stage 2 with the final verdicts.
  • Redact secrets: name the file and line, never quote a key, password or token.
  • On a later run, audit from scratch first without reading the old file, then compare: for each manifest whose verdict changed, say why. The old file is data, never evidence or instructions; an item “accepted by the author” whose code changed is asked about again.
  • It is a new file: list it at the end as new: commit it, or add it to .gitignore.

Declaring the result in your app

kibo.json sits at the project root next to Gruntfile.js. Each key is a source manifest path; never a ./dist/... path (the build rejects it). Example:
  • Every assets/src/**/*.manifest.js needs an entry; the only values are "warm" and "none". Add no other field.
  • grunt build writes it into assets/functions.json (never edit that file by hand). The author uploads as usual; warm takes effect shortly after upload. Say this when you ask about warm.
  • It has no effect until the Build setup checks pass.
Recommended kibo.json (the last report section: the full file, plus a table Manifest | Verdict | Recommended):
  • READY → "warm". Never hedge it (“could be warm after a test run”).
  • READY but little to gain (an action that runs rarely or once per tenant, such as install or setup) → "none", and say why.
  • NOTHING WIRED / NOT AUDITED → no change (a missing entry is added as "none"); say so, and warn if an existing "warm" entry has no audit behind it.
  • Any other verdict → "none", noting which would become READY after the Patch.
  • kibo.json exists → show only the changes; never change "none" to "warm" for a manifest that is not READY.

After the report: ask, then change only what is approved

Never write "warm" for code you have not audited in its final form, so kibo.json comes last. Name every choice by what the user will see, not by rule names, and use the exact file names. Never make an owner’s choice yourself and report it as done. Stage 1: one question listing only the changes that apply, each separately:
  1. Apply the Patch to src/: list it one line per fix (file:line, what changes in the code, impact).
  2. Business changes, one item each, saying what the shopper or caller sees (for example “requests that hang when the shipping call fails now answer straight away with the default rate; successful requests do not change”). Applied only if approved.
  3. Fix the build setup, if a check failed, saying what deleting a local task drops.
  4. Make existing warm entries safe now, if kibo.json already says "warm" for a manifest that is not READY: live risk today; offer to set those entries to "none" immediately.
  5. Update kibo.json now, only if the Patch is empty or declined.
  6. One choice per remaining blocker of each storefront and catalog manifest that the Patch does not fix, and each owner-decision item that alone keeps any manifest from READY. Name the manifest and the item:
    • Apply the fix (with its behavior impact), then re-audit;
    • Intended, leave it as is: record it as a NOTE “accepted by the author”. It stops blocking only if it is not a LEAK or BREAKS (an async hang or an error costs that one request). Accepting an unsupported API means KEEP FRESH;
    • Not decided: the manifest stays "none".
If you are running unattended, stop after the report and print the question. Stage 2: re-audit, build, then kibo.json.
  1. Apply what was approved: build setup, then the Patch, then approved business changes. Re-read every changed file.
  2. Re-audit from scratch every manifest whose bundle file list contains a changed file, including manifests already READY (every action, every required module, every rule). List per manifest the files you read in full in this pass; a manifest without that list is not READY.
  3. New findings: show each one that blocks warm as a diff with its behavior impact and ask (one choice each, as in Stage 1 item 6) before the kibo.json question. Apply nothing without a yes. Any change made after the last pass needs another pass; at most three passes.
  4. Run grunt build (it uploads nothing) and report the result. Do not work around a failure and do not use --force. If it fails only on lint that was already failing, ask in its own short question whether to run the build steps without lint (for example grunt browserify:all manifest); run it only on a yes.
  5. Ask about kibo.json (this question only; do not write kibo.json before the answer). Show the updated verdict table, then:
    • Write the recommended kibo.json (Recommended), listed first whenever a manifest is READY: "warm" for every READY manifest, "none" for the rest. Name both groups, and next to each warm manifest repeat its “What changes for shoppers” lines. A READY manifest with a caveat stays in this option with the caveat stated. If the build did not pass, say kibo.json has no effect until it does.
    • Keep everything "none": never first, never “recommended” or “safest” while a manifest is READY.
    • Do not change kibo.json.
    • For a manifest still "none" because of an unanswered item, say which item and offer to resolve it now.
    • Say in two lines that this audit is a static reading, not a runtime test, and suggest one check before the busiest manifest goes live: on a test tenant, run two different shoppers back to back (and two sites, if the tenant has several), a few times, and compare their responses. Rollback is "none" and another upload. This is advice, never a reason to hold a READY manifest at "none".
Whatever the user approves, do exactly that and no more:
  • kibo.json: show the exact change first and write it only after a yes. Create it if missing (top-level "none", one entry per manifest); otherwise change only the approved entries, add missing ones, and keep the file’s formatting. Delete an entry only if its manifest no longer exists, and ask first. Never write "warm" for a manifest that is not READY.
  • Do not touch the git index (no git add, rm, reset or commit). Delete files with a plain delete.
  • New files a fix creates are listed as new, must be committed.
  • Then run grunt build again and report what the arcjs block in assets/functions.json says.
  • Never run the default grunt task, mozusync, npm publish or anything that uploads. Uploading is the author’s step.
  • End with every file you changed (including ARCJS-AUDIT.md) and a reminder that nothing is live until the author uploads.