coding.sgit.ai / the house style / JavaScript

JavaScript

50 files, no framework, no build step, and a component system nobody has written down. This is the most original and least documented convention in the estate, and reconstructing it by counting is most of what the survey behind this page did.

There is not one documented rule for JavaScript. All 31 written rules are about Python and process. Everything on this page is a convention discovered in the code and measured — which means it is currently maintained by whoever last read the neighbouring file. The rules that do not exist →

1. The complete example

components/sg-compute/sg-compute-left-nav/v0/v0.1/v0.1.0/sg-compute-left-nav.jsverbatim · 02__javascript.md
/**
 * sg-compute-left-nav — vertical icon-rail navigation for the admin dashboard.
 *
 * Items: Compute / Storage / Settings / Diagnostics.
 * Click fires sp-cli:nav.selected { view } on document.
 *
 * @module sg-compute-left-nav
 * @version 0.1.0
 */

import { SgComponent } from 'https://dev.tools.sgraph.ai/components/base/v1/v1.0/v1.0.0/sg-component.js'

class SgComputeLeftNav extends SgComponent {

    static jsUrl = import.meta.url
    get resourceName()   { return 'sg-compute-left-nav' }
    get sharedCssPaths() { return ['https://dev.tools.sgraph.ai/components/tokens/v1/v1.0/v1.0.0/sg-tokens.css'] }

    onReady() {
        this._current = 'compute'
        this.shadowRoot.querySelectorAll('.nav-item').forEach(btn => {
            btn.addEventListener('click', () => this._select(btn.dataset.view))
        })
        this._update()
    }

    _select(view) {
        if (view === this._current) return
        this._current = view
        this._update()
        document.dispatchEvent(new CustomEvent('sp-cli:nav.selected', {
            detail:  { view },
            bubbles: true, composed: true,
        }))
    }

    _update() {
        this.shadowRoot.querySelectorAll('.nav-item').forEach(btn => {
            btn.classList.toggle('selected', btn.dataset.view === this._current)
            btn.setAttribute('aria-current', btn.dataset.view === this._current ? 'page' : 'false')
        })
    }
}

customElements.define('sg-compute-left-nav', SgComputeLeftNav)

Apache-2.0 — quoted from the estate's own source, not covered by this site's CC BY 4.0. Counted 2026-08-24.

2. The component system

Native web components. 41 customElements.define calls across 50 files. No React, no Vue, no bundler, no build step. The browser is the runtime.

The three-file triplet

Every component is exactly three files, same basename, same directory:

a fragment, not a filethe shape, not a file · 02__javascript.md
sg-compute-left-nav.js      behaviour
sg-compute-left-nav.html    markup
sg-compute-left-nav.css     styles

Apache-2.0 — quoted from the estate's own source, not covered by this site's CC BY 4.0. Counted 2026-08-24.

static jsUrl = import.meta.url is what makes this work: the component knows its own URL, so the base class can fetch the sibling .html and .css without anything being told where they live.

Self-locating components are the mechanism that removes the build step.

The versioned CDN path

a fragment, not a filethe URL scheme · 02__javascript.md
https://dev.tools.sgraph.ai/components/<name>/v1/v1.0/v1.0.0/<file>.js
                                              ^^^ ^^^^ ^^^^^^
                                            major minor patch — as directories

Apache-2.0 — quoted from the estate's own source, not covered by this site's CC BY 4.0. Counted 2026-08-24.

Three nested directories, one per semver level. A consumer pins at whatever depth it wants stability: /v1/ follows the major, /v1.0/ follows the minor, /v1.0.0/ is frozen. Immutable URLs, no lockfile, no node_modules, cacheable forever. The same scheme is used locally under components/.

What the CDN serves today: SgComponent (the base class), sg-tokens.css (design tokens), sg-vault-client.js and sg-vault-write.js.

The host in those examples is published deliberately. Every component imports its base class and its tokens from dev.tools.sgraph.ai, so a component example with the import URL redacted teaches nothing. It is a public CDN serving public component code and it is visible in any rendered page's network tab. The concern worth stating is the dev. prefix: this page is documenting a development host as a canonical public contract. Q4 →

The base-class contract

SgComponent supplies the lifecycle; a component overrides four things:

MemberPurpose
static jsUrl = import.meta.urlSelf-location. Required — without it the component cannot find its own markup.
get resourceName()The basename of the sibling .html and .css.
get sharedCssPaths()Tokens and shared sheets to adopt.
onReady()The lifecycle hook — not connectedCallback directly.

onReady() rather than connectedCallback is the tell. The base class handles the async fetch of the sibling files and calls onReady() once the shadow root is populated, so a component never has to think about whether its markup has arrived. It is a small API decision that removes a whole class of race condition, and it is exactly the kind of thing a lint rule should enforce. The rule that would →

6 files call attachShadow directly; the rest inherit it from the base class, and every component addresses its own markup through this.shadowRoot.

3. Formatting, measured

ConventionEvidence
4-space indent3,791 indented lines are a multiple of 4; 148 are not
Single quotes4,006 single vs 400 double — 91%
No semicolons531 statement lines without vs 401 with. Split, and it is a real inconsistency — see below
Trailing commasIn multi-line object and array literals, consistently
Aligned object keysdetail:  { view }, — the same discipline as Python and CSS
_ prefix for privateUniversal — and directly against Python rule 9
ESM everywhere48 type="module" script tags. No UMD, no globals
Banner comments15 of 50 files open with one, in two different styles

4. Events, state and data

Events are namespaced and go through document:

components/sg-compute/sg-compute-left-nav/…/sg-compute-left-nav.jsexcerpt · 02__javascript.md
document.dispatchEvent(new CustomEvent('sp-cli:nav.selected', {
    detail:  { view },
    bubbles: true, composed: true,
}))

Apache-2.0 — quoted from the estate's own source, not covered by this site's CC BY 4.0. Counted 2026-08-24.

bubbles: true, composed: true is what lets an event escape the shadow root. The namespace is sp-cli: — the old CLI name — and it appears in 23 files. That is a rename surface nobody had counted, and renaming an event namespace is a breaking change for any listener outside the repository.

State is instance fields, _-prefixed, set in onReady(). No store, no observable, no framework state layer. Cross-component state goes through the event bus and through purpose-built shared modules — settings-bus.js, vault-bus.js, poll.js.

Constants are frozen and centralised

shared/launch-defaults.jsverbatim · 02__javascript.md
// ── launch-defaults.js — canonical launch constants ────────────────────────── //
// Single source of truth. Both sg-compute-compute-view and sg-compute-launch-form
// import from here. Update here only — do not duplicate locally.

export const REGIONS = Object.freeze([
    'eu-west-2', 'us-east-1', 'ap-southeast-1', 'eu-west-1', 'us-west-2',
])

Apache-2.0 — quoted from the estate's own source, not covered by this site's CC BY 4.0. Counted 2026-08-24.

Object.freeze on every exported constant, and a comment that names the consumers and forbids local duplication. That is the same single source of truth instinct that drives manifest.py and the repo-root version file, expressed with a mechanism rather than a request. The pattern →

5. Why “no build step” is the interesting claim

It is worth arguing rather than just reporting, because it is a real trade with a real breaking point.

What it buys

  • The source that runs is the source you read. No source maps, no build cache, no transpilation between what you see and what executes.
  • No node_modules, no lockfile, no bundler upgrade treadmill.
  • An immutable URL per version instead of a dependency resolver.
  • A component is deployable by copying three files to a path.
  • An agent reading the running page reads the actual code. Why that matters →

What it costs

  • No TypeScript, no JSX, no compile-time checking of any kind.
  • No tree-shaking and no minification.
  • One network request per component file, and three files per component.
  • No dependency resolution, so a dependency graph deeper than one level has nowhere to go.
  • A CJS-only package cannot be used at all.

Where it stops working is the honest part: at the point you need a dependency graph deeper than one level, or a package that only ships as CommonJS. Neither has happened yet in 50 files, which is not the same as neither happening. Q7 →

6. What to fix before this becomes a standard

Four inconsistencies, in the order they should be settled. All four are cheap, and none of them can be settled by a site — they need a decision.

  1. Semicolons. components/ is semicolon-free; shared/ is not. Pick one — the newer tree suggests dropping them — and write the config.
  2. Two banner styles. JSDoc on components, // ── on shared modules. Pick one per file type and say which.
  3. The sp-cli: event namespace is legacy naming in 23 files. Rename it with the rest, and note that it is a breaking change for external listeners.
  4. _private in JavaScript versus Python rule 9. State that rule 9 is Python-only, or change one of the two. Right now it is neither enforced nor retired. Q2 →