# Supplementary Module — Context

A reference implementation of the supplementary-module injection contract for
Architect Language Tools. Wears two hats:

1. **Template** — copied as the starting point for an organisation's own
   supplementary module. The shape, file layout, and authoring guidance for
   *consumers* live in `README.md`.
2. **Integration test fixture** — imported by the core test suite to exercise
   every injection point end-to-end. Sample identifiers (`EMP_*`, `ZZ`,
   `DEMO-SEV-*`) are deliberately kept stable so those tests don't churn.

This file documents the bits a maintainer of *this package* needs that the
README intentionally elides.

---

## What Lives Here

```
architect-language-tools-supplementary/
├── extension.cjs              ← VS Code companion entry (CJS — see "ESM/CJS bridge")
├── index.js                   ← ESM default export — the supplementary module object
├── build.js                   ← esbuild bundler → dist/architect-supplementary.js
├── package.json               ← "type": "module"; "main": "./extension.cjs"
├── jest.config.cjs            ← unit-test runner config
├── src/
│   ├── builtins.js            ← barrel re-export
│   ├── builtins/              ← thin JSON loaders (no validation)
│   ├── builtins-data/         ← AUTHORING SOURCE OF TRUTH (JSON + schemas)
│   ├── catalog/               ← demoSeverityCatalog (DEMO-SEV-* entries)
│   └── validators/            ← empStyleValidator, demoSeverityValidator
└── test/
    ├── empStyleValidator.test.js
    └── syntax-tests/          ← fixtures consumed by the core test harness
```

No runtime dependencies. The only devDependency is `esbuild`, used by
`build.js`.

---

## ESM/CJS bridge

This package is `"type": "module"` (ESM) but `package.json` `main` points at
`extension.cjs`. The reason:

- **VS Code extension hosts load entry points as CommonJS.** A package whose
  entry is ESM will fail to activate.
- **The rest of the module graph is ESM** — same idiom as the core package, no
  transpilation, plain Node.

The bridge: `extension.cjs` uses `require("vscode")` (synchronous, host-loaded)
and then dynamically imports `index.js` via `await import("./index.js")` to
cross into the ESM graph. The CLI / MCP loaders skip the CJS shim entirely and
import `index.js` directly.

Do not be tempted to "simplify" by switching the package to CJS or by adding a
bundling step ahead of activation. The current layout is the smallest thing
that satisfies both loaders.

---

## Activation ordering

Companion → core ordering is enforced by `package.json`:

```json
"extensionDependencies": ["jibbius.arch-lang-tools"]
```

VS Code activates listed dependencies before the dependent. Inside
`extension.cjs.activate` we still:

1. `vscode.extensions.getExtension(MAIN_EXTENSION_ID)` — surface a clear error
   if the core extension is missing rather than letting `.exports` blow up.
2. `if (!mainExtension.isActive) await mainExtension.activate()` — defensive,
   in case the dependency contract is ever violated by a host-side change.
3. Validate `api.registerSupplementary` is a function before calling — guards
   against an installed core that predates the contract.
4. Dynamic-import `./index.js`; if that throws (syntax, schema validation),
   surface via `showErrorMessage` so the failure is not buried as a generic
   "Activating extension … failed" line.
5. Call `api.registerSupplementary(supplementaryModule)`; same surfacing on
   error.

The core extension owns the *registration* lifecycle. This package never
re-registers, deactivates, or unregisters at runtime — `deactivate` is a
no-op. The matching cleanup hook on the core side is `clearSupplementary`,
which exists for tests, not for runtime use.

The hard-coded `MAIN_EXTENSION_ID = "jibbius.arch-lang-tools"` ties this
companion to a specific publisher/extension ID. If the core ever republishes
under a different ID, both this constant and `extensionDependencies` must move
together.

---

## Injection points

The default export of `index.js` is a plain object with six top-level keys.
Each routes to a distinct merge path on the core side
(`core/src/supplementaryLoader.js`):

| Key | Core merge path | Notes |
|---|---|---|
| `validators` | appended to `_supplementaryValidators` | Run after every core validator. |
| `catalogEntries` | injected into `DIAGNOSTIC_CATALOG`; messageIds must not start with `ACU-` | Layer-0 of the resolution chain — defines the rule. |
| `severityOverrides` | layer-2 of `getRuleSeverity` resolution; ACU-* messageIds translated to ruleIds via `getCatalogEntry` | Above catalog defaults, below project/workspace/file/line tailoring. |
| `builtins.functions` | `registerSupplementaryFunction(name, entry, preferCore)` | Symmetric `unregisterSupplementaryFunction` on clear. Triggers `resetGlobalScope`. |
| `builtins.globalVariables` | `registerSupplementaryGlobalVariable(name, entry, preferCore)` | Symmetric `unregisterSupplementaryGlobalVariable` on clear. Triggers `resetGlobalScope`. |
| `builtins.tables` | `registerSupplementaryTable(code, entry, structure, preferCore)` | Symmetric `unregisterSupplementaryTable` on clear. |
| `builtins.help` | `registerSupplementaryHelpEntries(map, preferCore)` | Symmetric `unregisterSupplementaryHelpEntries(keys)` on clear. |
| `builtins.deprecatedSymbols` | `registerSupplementaryDeprecatedHandle(name, replacement, preferCore)` | Symmetric `unregisterSupplementaryDeprecatedHandle` on clear. Powers ACU-SYM-001. |

The reference module exercises every path — `catalogEntries +
severityOverrides + validators + all five builtin sub-keys`. When adding a
new injection point on the core side, this package is the right place to
add a sample so the integration tests cover it.

`severityOverrides: { "ACU-TODO-001": "off" }` is set deliberately — it's a
stable, low-noise core rule, ideal for asserting that a supplementary
override propagates through `collectDiagnostics`. Don't change it without
updating the integration test that depends on it.

---

## Reset semantics (`clearSupplementary`)

Tests need a way to undo every mutation `registerSupplementary` performs.
That requires the loader to be **observable** about what it changed, not just
mutationally idempotent. The loader tracks (per registration):

- Inserted function keys → drives `unregisterSupplementaryFunction` per name.
- Inserted global-variable keys → drives `unregisterSupplementaryGlobalVariable` per name.
- Inserted deprecated-symbol keys → drives `unregisterSupplementaryDeprecatedHandle` per name.
- Inserted table codes → drives `unregisterSupplementaryTable` per code.
- Inserted help keys → drives `unregisterSupplementaryHelpEntries(setOfKeys)`.
- Inserted catalog messageIds → simple `delete` on clear.
- **Displaced** catalog entries (overwritten when `preferCore=false`) → original
  values stashed in `_displacedCatalogEntries`, restored on clear.
- A `_globalScopeDirty` flag → unconditionally fires `resetGlobalScope()` on
  clear, so a follow-on `register` rebuilds `getGlobalScope()` from current
  registry contents.

The symmetry means the paired APIs (`registerSupplementary*` ↔
`unregisterSupplementary*`) across all five extensible registries —
`functionsRegistry.js`, `globalVariablesRegistry.js`, `tableRegistry.js`,
`helpRegistry.js`, `deprecatedSymbols.js` — are the *contract*, not an
implementation detail. Adding a new builtin sub-registry must ship with
both sides or `clearSupplementary` will leak across tests.

`clearSupplementary` is **test-only**. Production VS Code, CLI, and MCP all
call `registerSupplementary` exactly once at startup; no runtime path calls
clear. If a future feature needs hot-reload, design it on top of clear —
don't expose clear directly to companion authors.

---

## `preferCore` semantics

`registerSupplementary(mod, { preferCore: true })` flips conflict resolution
from "supplementary wins, log at debug" to "core wins, log a warning". Applies
uniformly to:

- functions (passed through to `registerSupplementaryFunction`)
- globalVariables (passed through to `registerSupplementaryGlobalVariable`)
- deprecatedSymbols (passed through to `registerSupplementaryDeprecatedHandle`)
- tables (passed through to `registerSupplementaryTable`)
- help entries (passed through to `registerSupplementaryHelpEntries`)
- **catalog entries** (added 2026-04-27 — Batch 10 MAJOR-3)

Catalog conflict-resolution was the late addition. Before MAJOR-3, catalog
entries silently overwrote regardless of the flag — making `preferCore=true`
a half-measure. The current code path matches the other registries: on
`preferCore=true` + conflict, the existing entry is kept and a warning is
logged; otherwise the existing entry is stashed in
`_displacedCatalogEntries` and the supplementary entry replaces it.

Default is `false`. Tests that mount alongside the production catalog flip
it to `true` to keep core entries authoritative.

---

## Build artefact

`build.js` produces a self-contained `dist/` directory:

```
dist/
├── architect-supplementary.js          ESM bundle of index.js + src/ JS graph
├── architect-supplementary.js.map      sourcemap (omitted under ARCH_LANG_BUILD=release)
├── functions.bundle.json               array of function entries
├── global-variables.bundle.json        array of global-variable entries
├── tables-registry.bundle.json         {code: name} projection
├── tables.bundle.json                  array of full table structures
├── help-registry.json                  verbatim copy of source
├── deprecated-symbols.json             verbatim copy of source
└── schemas/                            recursive copy of vendored schemas
```

The bundled JS pairs with the standalone MCP bundle
(`architect-language-tools/mcp/dist/architect-mcp-standalone.js`) — the
supplementary ships as a sibling-directory artefact alongside the MCP
bundle (symmetric to the two-VSIX install story for VS Code).

The JSON sidecars exist because as of 2026-05-01 (Brief 13) the registry
loaders read JSON from disk at runtime — without them, the bundled JS
would still rely on the `src/builtins-data/` tree being reachable.
Each loader probes its sidecar (`<dist>/<file>.json`) first, then falls
back to the source-tree layout (`<src>/builtins-data/...`). First match
wins; both missing fails loud.

Build is plain Node — esbuild pass for the JS, then a data pass that
walks `src/builtins-data/` and writes the JSON sidecars. Filenames mirror
core's bundle artefacts in `architect-language-tools/builtins-data/`.

The companion VSIX (`npm run package:vsix`) packages the un-bundled source —
VS Code loads it directly via `extension.cjs`. The dist directory is for
the MCP sibling-directory path only.

---

## Adding a new injection point

When the core contract grows a new injection key:

1. Add a sample to this package — the smallest possible exercise of the new
   path. Place it in the right `src/` subfolder; thread it through
   `index.js`.
2. Schema-validate on the core side first (`validateSupplementaryModule`) and
   make sure clean failure modes are surfaced through the existing
   `extension.cjs` error-message flow.
3. Add a clear/restore path on the core side **in the same commit** as the
   register path. Symmetric APIs (`register*` ↔ `unregister*`) are the
   contract; tests will leak otherwise.
4. Update the README's *Module contract* and *Extension points* sections —
   that's the consumer-facing surface. Update this file's *Injection points*
   table.
5. Cover the new path in the core integration test that mounts this
   reference module.

---

## Versioning

`package.json` carries a `version` field for the VSIX. There is **no**
`contractVersion` field in the supplementary module shape itself —
schema tightening on the core side will fail older modules at load time
with no migration path. This is a known gap (V3 review batch 10 OBS-2);
worth introducing if/when the schema evolves in a backward-incompatible
direction.

---

## Trademark note

Do not lead public-facing surfaces (extension display name, command IDs,
publisher namespacing) with "Acurity" — see the trademark note in the
top-level `CLAUDE.md`. The current `displayName` uses "Architect" with a
"(Reference)" suffix; preserve that pattern for any forks that go public.
