💥 Scope engine state to the operation that owns it #323

Description

@taras

architecture.md (#314) states the rule this issue carries out, in full:

All state is scoped to the operation that owns it, so it is torn down when
the operation is torn down: created inside the run it describes, provided via
context. No module-scoped registries — not as collections, not hidden inside
library objects that accumulate. One exception: metadata an author declares
at module evaluation, about a value the author owns, may live on that value.

Five declarations in packages/core are on the wrong side of it. Each is one
table per process, shared by every run the process performs, keyed by whatever
happens to reach it — nothing in a caller's scope can see it, override it, or
clear it.

Measured against f413545:

SiteDeclaration
src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
src/validate.ts:10const ajv = new Ajv({…})
src/validate.ts:85-86compiledCache, compiledReturnsCache
src/components/parse-schema.ts:22const ajv = new Ajv({…})

Nothing else in the repository matches: packages/web already builds a fresh
Ajv per call through createServerAjv(), and every other module-scoped Set
or Map in packages/ is a constant lookup table built from its own contents
(EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
answers the same question forever and accumulates nothing.

The three conversions

1. printing takes the exception, not context.printErrors(fn) runs while
a component module is evaluated — outside any operation — and what it records is
what an author declared about a function the author owns. That is precisely the
case the rule's last sentence names, and this is its one open application. The
mark moves onto the function object under a module-private Symbol() — not
Symbol.for, so nothing outside the module can forge it — defined
non-enumerable so a component that is copied, wrapped, or inspected does not
carry the declaration along by accident, and read with Object.hasOwn.

2. segmentCauses becomes a run-scoped registry provided via context. It is
created where the execution begins and read by the operation that builds a
DocumentationError, so the cause is attached before any observer can see the
failure. DocumentationError's constructor cannot reach a scope, so the read
moves to a builder operation that runs before it. Expansion driven directly — a
test, a tool describing a document — has no execution around it, so the
outermost expandSegments call opens the registry for exactly its own lifetime.

3. Both Ajv instances become run-scoped, and the two caches go. This is the
rule's "hidden inside library objects that accumulate" clause, and it is
checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
plain Map, and _addSchema runs

letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

— the set is unconditional and precedes the addUsedSchema guard the engine
already sets to false. So a module-scoped instance holds a strong reference to
every schema object any run ever compiled, plus its compiled SchemaEnv, for
the life of the process; fresh schema objects arrive with every run.

The same Map also makes the sharing observable rather than merely wasteful: it
is keyed by schema object identity, so a schema object mutated between two
runs gets run 1's stale validator in run 2. The two WeakMap caches in
validate.ts have exactly that shape too, which is why they go rather than
moving: Ajv memoizes by schema object within a run already, so a per-run
instance is the whole cache.

The lint rule

local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
constructed at module scope — declared, exported, assigned later, held inside
another module-scoped value, or standing as a static class field — with the
remedy "create it inside the operation that owns it and provide it via context."

It accepts three shapes that are not registries: a table built from its own
contents (a constant), an instance field (the object's lifetime, not the
module's), and a table handed straight to a call (the module keeps no handle).

It does not catch a module-scoped new Ajv(…) — it matches collection
constructors, and the accumulation there is inside a library object rather than
in a table this repository wrote. The rule text covers it anyway: "not hidden
inside library objects that accumulate." A reviewer has to hold that half; the
lint rule holds the other.

Discipline

Every conversion gets a discriminating test, including one proving that two
sequential executions share no state — what run 1 recorded must not answer for
run 2. No behavior change otherwise: test counts identical except for the new
tests. Nothing this issue adds may itself be module-scoped, so the new code has
to pass the rule it brings in.

Breaking

Scoping the validate.ts compiler to the execution makes its readers
operations, and four of them are published from packages/core/mod.ts:
compilePropsSchema, compileReturnsSchema, validateProps,
validateReturnValue. prepareElicitation is published too and becomes one for
the same reason. A caller writes yield* where it previously called; there is
no other change to what any of them does.

Out of scope

<Retry>, <Result as>, suspension and the error middleware Js api stay
defined and unbuilt. No semantics change and no vocabulary change.

Context: #314, #319.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
      Skip to content

      💥 Scope engine state to the operation that owns it #323

      Description

      @taras

      architecture.md (#314) states the rule this issue carries out, in full:

      All state is scoped to the operation that owns it, so it is torn down when
      the operation is torn down: created inside the run it describes, provided via
      context. No module-scoped registries — not as collections, not hidden inside
      library objects that accumulate. One exception: metadata an author declares
      at module evaluation, about a value the author owns, may live on that value.

      Five declarations in packages/core are on the wrong side of it. Each is one
      table per process, shared by every run the process performs, keyed by whatever
      happens to reach it — nothing in a caller's scope can see it, override it, or
      clear it.

      Measured against f413545:

      SiteDeclaration
      src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
      src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
      src/validate.ts:10const ajv = new Ajv({…})
      src/validate.ts:85-86compiledCache, compiledReturnsCache
      src/components/parse-schema.ts:22const ajv = new Ajv({…})

      Nothing else in the repository matches: packages/web already builds a fresh
      Ajv per call through createServerAjv(), and every other module-scoped Set
      or Map in packages/ is a constant lookup table built from its own contents
      (EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
      answers the same question forever and accumulates nothing.

      The three conversions

      1. printing takes the exception, not context.printErrors(fn) runs while
      a component module is evaluated — outside any operation — and what it records is
      what an author declared about a function the author owns. That is precisely the
      case the rule's last sentence names, and this is its one open application. The
      mark moves onto the function object under a module-private Symbol() — not
      Symbol.for, so nothing outside the module can forge it — defined
      non-enumerable so a component that is copied, wrapped, or inspected does not
      carry the declaration along by accident, and read with Object.hasOwn.

      2. segmentCauses becomes a run-scoped registry provided via context. It is
      created where the execution begins and read by the operation that builds a
      DocumentationError, so the cause is attached before any observer can see the
      failure. DocumentationError's constructor cannot reach a scope, so the read
      moves to a builder operation that runs before it. Expansion driven directly — a
      test, a tool describing a document — has no execution around it, so the
      outermost expandSegments call opens the registry for exactly its own lifetime.

      3. Both Ajv instances become run-scoped, and the two caches go. This is the
      rule's "hidden inside library objects that accumulate" clause, and it is
      checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
      plain Map, and _addSchema runs

      letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

      — the set is unconditional and precedes the addUsedSchema guard the engine
      already sets to false. So a module-scoped instance holds a strong reference to
      every schema object any run ever compiled, plus its compiled SchemaEnv, for
      the life of the process; fresh schema objects arrive with every run.

      The same Map also makes the sharing observable rather than merely wasteful: it
      is keyed by schema object identity, so a schema object mutated between two
      runs gets run 1's stale validator in run 2. The two WeakMap caches in
      validate.ts have exactly that shape too, which is why they go rather than
      moving: Ajv memoizes by schema object within a run already, so a per-run
      instance is the whole cache.

      The lint rule

      local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
      constructed at module scope — declared, exported, assigned later, held inside
      another module-scoped value, or standing as a static class field — with the
      remedy "create it inside the operation that owns it and provide it via context."

      It accepts three shapes that are not registries: a table built from its own
      contents (a constant), an instance field (the object's lifetime, not the
      module's), and a table handed straight to a call (the module keeps no handle).

      It does not catch a module-scoped new Ajv(…) — it matches collection
      constructors, and the accumulation there is inside a library object rather than
      in a table this repository wrote. The rule text covers it anyway: "not hidden
      inside library objects that accumulate." A reviewer has to hold that half; the
      lint rule holds the other.

      Discipline

      Every conversion gets a discriminating test, including one proving that two
      sequential executions share no state — what run 1 recorded must not answer for
      run 2. No behavior change otherwise: test counts identical except for the new
      tests. Nothing this issue adds may itself be module-scoped, so the new code has
      to pass the rule it brings in.

      Breaking

      Scoping the validate.ts compiler to the execution makes its readers
      operations, and four of them are published from packages/core/mod.ts:
      compilePropsSchema, compileReturnsSchema, validateProps,
      validateReturnValue. prepareElicitation is published too and becomes one for
      the same reason. A caller writes yield* where it previously called; there is
      no other change to what any of them does.

      Out of scope

      <Retry>, <Result as>, suspension and the error middleware Js api stay
      defined and unbuilt. No semantics change and no vocabulary change.

      Context: #314, #319.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          💥 Scope engine state to the operation that owns it #323

          Description

          @taras

          architecture.md (#314) states the rule this issue carries out, in full:

          All state is scoped to the operation that owns it, so it is torn down when
          the operation is torn down: created inside the run it describes, provided via
          context. No module-scoped registries — not as collections, not hidden inside
          library objects that accumulate. One exception: metadata an author declares
          at module evaluation, about a value the author owns, may live on that value.

          Five declarations in packages/core are on the wrong side of it. Each is one
          table per process, shared by every run the process performs, keyed by whatever
          happens to reach it — nothing in a caller's scope can see it, override it, or
          clear it.

          Measured against f413545:

          SiteDeclaration
          src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
          src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
          src/validate.ts:10const ajv = new Ajv({…})
          src/validate.ts:85-86compiledCache, compiledReturnsCache
          src/components/parse-schema.ts:22const ajv = new Ajv({…})

          Nothing else in the repository matches: packages/web already builds a fresh
          Ajv per call through createServerAjv(), and every other module-scoped Set
          or Map in packages/ is a constant lookup table built from its own contents
          (EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
          answers the same question forever and accumulates nothing.

          The three conversions

          1. printing takes the exception, not context.printErrors(fn) runs while
          a component module is evaluated — outside any operation — and what it records is
          what an author declared about a function the author owns. That is precisely the
          case the rule's last sentence names, and this is its one open application. The
          mark moves onto the function object under a module-private Symbol() — not
          Symbol.for, so nothing outside the module can forge it — defined
          non-enumerable so a component that is copied, wrapped, or inspected does not
          carry the declaration along by accident, and read with Object.hasOwn.

          2. segmentCauses becomes a run-scoped registry provided via context. It is
          created where the execution begins and read by the operation that builds a
          DocumentationError, so the cause is attached before any observer can see the
          failure. DocumentationError's constructor cannot reach a scope, so the read
          moves to a builder operation that runs before it. Expansion driven directly — a
          test, a tool describing a document — has no execution around it, so the
          outermost expandSegments call opens the registry for exactly its own lifetime.

          3. Both Ajv instances become run-scoped, and the two caches go. This is the
          rule's "hidden inside library objects that accumulate" clause, and it is
          checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
          plain Map, and _addSchema runs

          letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

          — the set is unconditional and precedes the addUsedSchema guard the engine
          already sets to false. So a module-scoped instance holds a strong reference to
          every schema object any run ever compiled, plus its compiled SchemaEnv, for
          the life of the process; fresh schema objects arrive with every run.

          The same Map also makes the sharing observable rather than merely wasteful: it
          is keyed by schema object identity, so a schema object mutated between two
          runs gets run 1's stale validator in run 2. The two WeakMap caches in
          validate.ts have exactly that shape too, which is why they go rather than
          moving: Ajv memoizes by schema object within a run already, so a per-run
          instance is the whole cache.

          The lint rule

          local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
          constructed at module scope — declared, exported, assigned later, held inside
          another module-scoped value, or standing as a static class field — with the
          remedy "create it inside the operation that owns it and provide it via context."

          It accepts three shapes that are not registries: a table built from its own
          contents (a constant), an instance field (the object's lifetime, not the
          module's), and a table handed straight to a call (the module keeps no handle).

          It does not catch a module-scoped new Ajv(…) — it matches collection
          constructors, and the accumulation there is inside a library object rather than
          in a table this repository wrote. The rule text covers it anyway: "not hidden
          inside library objects that accumulate." A reviewer has to hold that half; the
          lint rule holds the other.

          Discipline

          Every conversion gets a discriminating test, including one proving that two
          sequential executions share no state — what run 1 recorded must not answer for
          run 2. No behavior change otherwise: test counts identical except for the new
          tests. Nothing this issue adds may itself be module-scoped, so the new code has
          to pass the rule it brings in.

          Breaking

          Scoping the validate.ts compiler to the execution makes its readers
          operations, and four of them are published from packages/core/mod.ts:
          compilePropsSchema, compileReturnsSchema, validateProps,
          validateReturnValue. prepareElicitation is published too and becomes one for
          the same reason. A caller writes yield* where it previously called; there is
          no other change to what any of them does.

          Out of scope

          <Retry>, <Result as>, suspension and the error middleware Js api stay
          defined and unbuilt. No semantics change and no vocabulary change.

          Context: #314, #319.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              💥 Scope engine state to the operation that owns it #323

              Description

              @taras

              architecture.md (#314) states the rule this issue carries out, in full:

              All state is scoped to the operation that owns it, so it is torn down when
              the operation is torn down: created inside the run it describes, provided via
              context. No module-scoped registries — not as collections, not hidden inside
              library objects that accumulate. One exception: metadata an author declares
              at module evaluation, about a value the author owns, may live on that value.

              Five declarations in packages/core are on the wrong side of it. Each is one
              table per process, shared by every run the process performs, keyed by whatever
              happens to reach it — nothing in a caller's scope can see it, override it, or
              clear it.

              Measured against f413545:

              SiteDeclaration
              src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
              src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
              src/validate.ts:10const ajv = new Ajv({…})
              src/validate.ts:85-86compiledCache, compiledReturnsCache
              src/components/parse-schema.ts:22const ajv = new Ajv({…})

              Nothing else in the repository matches: packages/web already builds a fresh
              Ajv per call through createServerAjv(), and every other module-scoped Set
              or Map in packages/ is a constant lookup table built from its own contents
              (EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
              answers the same question forever and accumulates nothing.

              The three conversions

              1. printing takes the exception, not context.printErrors(fn) runs while
              a component module is evaluated — outside any operation — and what it records is
              what an author declared about a function the author owns. That is precisely the
              case the rule's last sentence names, and this is its one open application. The
              mark moves onto the function object under a module-private Symbol() — not
              Symbol.for, so nothing outside the module can forge it — defined
              non-enumerable so a component that is copied, wrapped, or inspected does not
              carry the declaration along by accident, and read with Object.hasOwn.

              2. segmentCauses becomes a run-scoped registry provided via context. It is
              created where the execution begins and read by the operation that builds a
              DocumentationError, so the cause is attached before any observer can see the
              failure. DocumentationError's constructor cannot reach a scope, so the read
              moves to a builder operation that runs before it. Expansion driven directly — a
              test, a tool describing a document — has no execution around it, so the
              outermost expandSegments call opens the registry for exactly its own lifetime.

              3. Both Ajv instances become run-scoped, and the two caches go. This is the
              rule's "hidden inside library objects that accumulate" clause, and it is
              checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
              plain Map, and _addSchema runs

              letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

              — the set is unconditional and precedes the addUsedSchema guard the engine
              already sets to false. So a module-scoped instance holds a strong reference to
              every schema object any run ever compiled, plus its compiled SchemaEnv, for
              the life of the process; fresh schema objects arrive with every run.

              The same Map also makes the sharing observable rather than merely wasteful: it
              is keyed by schema object identity, so a schema object mutated between two
              runs gets run 1's stale validator in run 2. The two WeakMap caches in
              validate.ts have exactly that shape too, which is why they go rather than
              moving: Ajv memoizes by schema object within a run already, so a per-run
              instance is the whole cache.

              The lint rule

              local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
              constructed at module scope — declared, exported, assigned later, held inside
              another module-scoped value, or standing as a static class field — with the
              remedy "create it inside the operation that owns it and provide it via context."

              It accepts three shapes that are not registries: a table built from its own
              contents (a constant), an instance field (the object's lifetime, not the
              module's), and a table handed straight to a call (the module keeps no handle).

              It does not catch a module-scoped new Ajv(…) — it matches collection
              constructors, and the accumulation there is inside a library object rather than
              in a table this repository wrote. The rule text covers it anyway: "not hidden
              inside library objects that accumulate." A reviewer has to hold that half; the
              lint rule holds the other.

              Discipline

              Every conversion gets a discriminating test, including one proving that two
              sequential executions share no state — what run 1 recorded must not answer for
              run 2. No behavior change otherwise: test counts identical except for the new
              tests. Nothing this issue adds may itself be module-scoped, so the new code has
              to pass the rule it brings in.

              Breaking

              Scoping the validate.ts compiler to the execution makes its readers
              operations, and four of them are published from packages/core/mod.ts:
              compilePropsSchema, compileReturnsSchema, validateProps,
              validateReturnValue. prepareElicitation is published too and becomes one for
              the same reason. A caller writes yield* where it previously called; there is
              no other change to what any of them does.

              Out of scope

              <Retry>, <Result as>, suspension and the error middleware Js api stay
              defined and unbuilt. No semantics change and no vocabulary change.

              Context: #314, #319.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  💥 Scope engine state to the operation that owns it #323

                  Description

                  @taras

                  architecture.md (#314) states the rule this issue carries out, in full:

                  All state is scoped to the operation that owns it, so it is torn down when
                  the operation is torn down: created inside the run it describes, provided via
                  context. No module-scoped registries — not as collections, not hidden inside
                  library objects that accumulate. One exception: metadata an author declares
                  at module evaluation, about a value the author owns, may live on that value.

                  Five declarations in packages/core are on the wrong side of it. Each is one
                  table per process, shared by every run the process performs, keyed by whatever
                  happens to reach it — nothing in a caller's scope can see it, override it, or
                  clear it.

                  Measured against f413545:

                  SiteDeclaration
                  src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
                  src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
                  src/validate.ts:10const ajv = new Ajv({…})
                  src/validate.ts:85-86compiledCache, compiledReturnsCache
                  src/components/parse-schema.ts:22const ajv = new Ajv({…})

                  Nothing else in the repository matches: packages/web already builds a fresh
                  Ajv per call through createServerAjv(), and every other module-scoped Set
                  or Map in packages/ is a constant lookup table built from its own contents
                  (EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
                  answers the same question forever and accumulates nothing.

                  The three conversions

                  1. printing takes the exception, not context.printErrors(fn) runs while
                  a component module is evaluated — outside any operation — and what it records is
                  what an author declared about a function the author owns. That is precisely the
                  case the rule's last sentence names, and this is its one open application. The
                  mark moves onto the function object under a module-private Symbol() — not
                  Symbol.for, so nothing outside the module can forge it — defined
                  non-enumerable so a component that is copied, wrapped, or inspected does not
                  carry the declaration along by accident, and read with Object.hasOwn.

                  2. segmentCauses becomes a run-scoped registry provided via context. It is
                  created where the execution begins and read by the operation that builds a
                  DocumentationError, so the cause is attached before any observer can see the
                  failure. DocumentationError's constructor cannot reach a scope, so the read
                  moves to a builder operation that runs before it. Expansion driven directly — a
                  test, a tool describing a document — has no execution around it, so the
                  outermost expandSegments call opens the registry for exactly its own lifetime.

                  3. Both Ajv instances become run-scoped, and the two caches go. This is the
                  rule's "hidden inside library objects that accumulate" clause, and it is
                  checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
                  plain Map, and _addSchema runs

                  letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

                  — the set is unconditional and precedes the addUsedSchema guard the engine
                  already sets to false. So a module-scoped instance holds a strong reference to
                  every schema object any run ever compiled, plus its compiled SchemaEnv, for
                  the life of the process; fresh schema objects arrive with every run.

                  The same Map also makes the sharing observable rather than merely wasteful: it
                  is keyed by schema object identity, so a schema object mutated between two
                  runs gets run 1's stale validator in run 2. The two WeakMap caches in
                  validate.ts have exactly that shape too, which is why they go rather than
                  moving: Ajv memoizes by schema object within a run already, so a per-run
                  instance is the whole cache.

                  The lint rule

                  local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
                  constructed at module scope — declared, exported, assigned later, held inside
                  another module-scoped value, or standing as a static class field — with the
                  remedy "create it inside the operation that owns it and provide it via context."

                  It accepts three shapes that are not registries: a table built from its own
                  contents (a constant), an instance field (the object's lifetime, not the
                  module's), and a table handed straight to a call (the module keeps no handle).

                  It does not catch a module-scoped new Ajv(…) — it matches collection
                  constructors, and the accumulation there is inside a library object rather than
                  in a table this repository wrote. The rule text covers it anyway: "not hidden
                  inside library objects that accumulate." A reviewer has to hold that half; the
                  lint rule holds the other.

                  Discipline

                  Every conversion gets a discriminating test, including one proving that two
                  sequential executions share no state — what run 1 recorded must not answer for
                  run 2. No behavior change otherwise: test counts identical except for the new
                  tests. Nothing this issue adds may itself be module-scoped, so the new code has
                  to pass the rule it brings in.

                  Breaking

                  Scoping the validate.ts compiler to the execution makes its readers
                  operations, and four of them are published from packages/core/mod.ts:
                  compilePropsSchema, compileReturnsSchema, validateProps,
                  validateReturnValue. prepareElicitation is published too and becomes one for
                  the same reason. A caller writes yield* where it previously called; there is
                  no other change to what any of them does.

                  Out of scope

                  <Retry>, <Result as>, suspension and the error middleware Js api stay
                  defined and unbuilt. No semantics change and no vocabulary change.

                  Context: #314, #319.

                  Activity

                  Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      💥 Scope engine state to the operation that owns it #323

                      Description

                      @taras

                      architecture.md (#314) states the rule this issue carries out, in full:

                      All state is scoped to the operation that owns it, so it is torn down when
                      the operation is torn down: created inside the run it describes, provided via
                      context. No module-scoped registries — not as collections, not hidden inside
                      library objects that accumulate. One exception: metadata an author declares
                      at module evaluation, about a value the author owns, may live on that value.

                      Five declarations in packages/core are on the wrong side of it. Each is one
                      table per process, shared by every run the process performs, keyed by whatever
                      happens to reach it — nothing in a caller's scope can see it, override it, or
                      clear it.

                      Measured against f413545:

                      SiteDeclaration
                      src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
                      src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
                      src/validate.ts:10const ajv = new Ajv({…})
                      src/validate.ts:85-86compiledCache, compiledReturnsCache
                      src/components/parse-schema.ts:22const ajv = new Ajv({…})

                      Nothing else in the repository matches: packages/web already builds a fresh
                      Ajv per call through createServerAjv(), and every other module-scoped Set
                      or Map in packages/ is a constant lookup table built from its own contents
                      (EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
                      answers the same question forever and accumulates nothing.

                      The three conversions

                      1. printing takes the exception, not context.printErrors(fn) runs while
                      a component module is evaluated — outside any operation — and what it records is
                      what an author declared about a function the author owns. That is precisely the
                      case the rule's last sentence names, and this is its one open application. The
                      mark moves onto the function object under a module-private Symbol() — not
                      Symbol.for, so nothing outside the module can forge it — defined
                      non-enumerable so a component that is copied, wrapped, or inspected does not
                      carry the declaration along by accident, and read with Object.hasOwn.

                      2. segmentCauses becomes a run-scoped registry provided via context. It is
                      created where the execution begins and read by the operation that builds a
                      DocumentationError, so the cause is attached before any observer can see the
                      failure. DocumentationError's constructor cannot reach a scope, so the read
                      moves to a builder operation that runs before it. Expansion driven directly — a
                      test, a tool describing a document — has no execution around it, so the
                      outermost expandSegments call opens the registry for exactly its own lifetime.

                      3. Both Ajv instances become run-scoped, and the two caches go. This is the
                      rule's "hidden inside library objects that accumulate" clause, and it is
                      checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
                      plain Map, and _addSchema runs

                      letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

                      — the set is unconditional and precedes the addUsedSchema guard the engine
                      already sets to false. So a module-scoped instance holds a strong reference to
                      every schema object any run ever compiled, plus its compiled SchemaEnv, for
                      the life of the process; fresh schema objects arrive with every run.

                      The same Map also makes the sharing observable rather than merely wasteful: it
                      is keyed by schema object identity, so a schema object mutated between two
                      runs gets run 1's stale validator in run 2. The two WeakMap caches in
                      validate.ts have exactly that shape too, which is why they go rather than
                      moving: Ajv memoizes by schema object within a run already, so a per-run
                      instance is the whole cache.

                      The lint rule

                      local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
                      constructed at module scope — declared, exported, assigned later, held inside
                      another module-scoped value, or standing as a static class field — with the
                      remedy "create it inside the operation that owns it and provide it via context."

                      It accepts three shapes that are not registries: a table built from its own
                      contents (a constant), an instance field (the object's lifetime, not the
                      module's), and a table handed straight to a call (the module keeps no handle).

                      It does not catch a module-scoped new Ajv(…) — it matches collection
                      constructors, and the accumulation there is inside a library object rather than
                      in a table this repository wrote. The rule text covers it anyway: "not hidden
                      inside library objects that accumulate." A reviewer has to hold that half; the
                      lint rule holds the other.

                      Discipline

                      Every conversion gets a discriminating test, including one proving that two
                      sequential executions share no state — what run 1 recorded must not answer for
                      run 2. No behavior change otherwise: test counts identical except for the new
                      tests. Nothing this issue adds may itself be module-scoped, so the new code has
                      to pass the rule it brings in.

                      Breaking

                      Scoping the validate.ts compiler to the execution makes its readers
                      operations, and four of them are published from packages/core/mod.ts:
                      compilePropsSchema, compileReturnsSchema, validateProps,
                      validateReturnValue. prepareElicitation is published too and becomes one for
                      the same reason. A caller writes yield* where it previously called; there is
                      no other change to what any of them does.

                      Out of scope

                      <Retry>, <Result as>, suspension and the error middleware Js api stay
                      defined and unbuilt. No semantics change and no vocabulary change.

                      Context: #314, #319.

                      Activity

                      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          💥 Scope engine state to the operation that owns it #323

                          Description

                          @taras

                          architecture.md (#314) states the rule this issue carries out, in full:

                          All state is scoped to the operation that owns it, so it is torn down when
                          the operation is torn down: created inside the run it describes, provided via
                          context. No module-scoped registries — not as collections, not hidden inside
                          library objects that accumulate. One exception: metadata an author declares
                          at module evaluation, about a value the author owns, may live on that value.

                          Five declarations in packages/core are on the wrong side of it. Each is one
                          table per process, shared by every run the process performs, keyed by whatever
                          happens to reach it — nothing in a caller's scope can see it, override it, or
                          clear it.

                          Measured against f413545:

                          SiteDeclaration
                          src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
                          src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
                          src/validate.ts:10const ajv = new Ajv({…})
                          src/validate.ts:85-86compiledCache, compiledReturnsCache
                          src/components/parse-schema.ts:22const ajv = new Ajv({…})

                          Nothing else in the repository matches: packages/web already builds a fresh
                          Ajv per call through createServerAjv(), and every other module-scoped Set
                          or Map in packages/ is a constant lookup table built from its own contents
                          (EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
                          answers the same question forever and accumulates nothing.

                          The three conversions

                          1. printing takes the exception, not context.printErrors(fn) runs while
                          a component module is evaluated — outside any operation — and what it records is
                          what an author declared about a function the author owns. That is precisely the
                          case the rule's last sentence names, and this is its one open application. The
                          mark moves onto the function object under a module-private Symbol() — not
                          Symbol.for, so nothing outside the module can forge it — defined
                          non-enumerable so a component that is copied, wrapped, or inspected does not
                          carry the declaration along by accident, and read with Object.hasOwn.

                          2. segmentCauses becomes a run-scoped registry provided via context. It is
                          created where the execution begins and read by the operation that builds a
                          DocumentationError, so the cause is attached before any observer can see the
                          failure. DocumentationError's constructor cannot reach a scope, so the read
                          moves to a builder operation that runs before it. Expansion driven directly — a
                          test, a tool describing a document — has no execution around it, so the
                          outermost expandSegments call opens the registry for exactly its own lifetime.

                          3. Both Ajv instances become run-scoped, and the two caches go. This is the
                          rule's "hidden inside library objects that accumulate" clause, and it is
                          checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
                          plain Map, and _addSchema runs

                          letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

                          — the set is unconditional and precedes the addUsedSchema guard the engine
                          already sets to false. So a module-scoped instance holds a strong reference to
                          every schema object any run ever compiled, plus its compiled SchemaEnv, for
                          the life of the process; fresh schema objects arrive with every run.

                          The same Map also makes the sharing observable rather than merely wasteful: it
                          is keyed by schema object identity, so a schema object mutated between two
                          runs gets run 1's stale validator in run 2. The two WeakMap caches in
                          validate.ts have exactly that shape too, which is why they go rather than
                          moving: Ajv memoizes by schema object within a run already, so a per-run
                          instance is the whole cache.

                          The lint rule

                          local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
                          constructed at module scope — declared, exported, assigned later, held inside
                          another module-scoped value, or standing as a static class field — with the
                          remedy "create it inside the operation that owns it and provide it via context."

                          It accepts three shapes that are not registries: a table built from its own
                          contents (a constant), an instance field (the object's lifetime, not the
                          module's), and a table handed straight to a call (the module keeps no handle).

                          It does not catch a module-scoped new Ajv(…) — it matches collection
                          constructors, and the accumulation there is inside a library object rather than
                          in a table this repository wrote. The rule text covers it anyway: "not hidden
                          inside library objects that accumulate." A reviewer has to hold that half; the
                          lint rule holds the other.

                          Discipline

                          Every conversion gets a discriminating test, including one proving that two
                          sequential executions share no state — what run 1 recorded must not answer for
                          run 2. No behavior change otherwise: test counts identical except for the new
                          tests. Nothing this issue adds may itself be module-scoped, so the new code has
                          to pass the rule it brings in.

                          Breaking

                          Scoping the validate.ts compiler to the execution makes its readers
                          operations, and four of them are published from packages/core/mod.ts:
                          compilePropsSchema, compileReturnsSchema, validateProps,
                          validateReturnValue. prepareElicitation is published too and becomes one for
                          the same reason. A caller writes yield* where it previously called; there is
                          no other change to what any of them does.

                          Out of scope

                          <Retry>, <Result as>, suspension and the error middleware Js api stay
                          defined and unbuilt. No semantics change and no vocabulary change.

                          Context: #314, #319.

                          Activity

                          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              💥 Scope engine state to the operation that owns it #323

                              Description

                              @taras

                              architecture.md (#314) states the rule this issue carries out, in full:

                              All state is scoped to the operation that owns it, so it is torn down when
                              the operation is torn down: created inside the run it describes, provided via
                              context. No module-scoped registries — not as collections, not hidden inside
                              library objects that accumulate. One exception: metadata an author declares
                              at module evaluation, about a value the author owns, may live on that value.

                              Five declarations in packages/core are on the wrong side of it. Each is one
                              table per process, shared by every run the process performs, keyed by whatever
                              happens to reach it — nothing in a caller's scope can see it, override it, or
                              clear it.

                              Measured against f413545:

                              SiteDeclaration
                              src/component-failures.ts:27const printing = new WeakSet<FunctionComponent>()
                              src/errors.ts:63const segmentCauses = new WeakMap<ErrorSegment, unknown>()
                              src/validate.ts:10const ajv = new Ajv({…})
                              src/validate.ts:85-86compiledCache, compiledReturnsCache
                              src/components/parse-schema.ts:22const ajv = new Ajv({…})

                              Nothing else in the repository matches: packages/web already builds a fresh
                              Ajv per call through createServerAjv(), and every other module-scoped Set
                              or Map in packages/ is a constant lookup table built from its own contents
                              (EACH_PROPS, RESERVED_STRUCTURAL, the secret-scanner word lists), which
                              answers the same question forever and accumulates nothing.

                              The three conversions

                              1. printing takes the exception, not context.printErrors(fn) runs while
                              a component module is evaluated — outside any operation — and what it records is
                              what an author declared about a function the author owns. That is precisely the
                              case the rule's last sentence names, and this is its one open application. The
                              mark moves onto the function object under a module-private Symbol() — not
                              Symbol.for, so nothing outside the module can forge it — defined
                              non-enumerable so a component that is copied, wrapped, or inspected does not
                              carry the declaration along by accident, and read with Object.hasOwn.

                              2. segmentCauses becomes a run-scoped registry provided via context. It is
                              created where the execution begins and read by the operation that builds a
                              DocumentationError, so the cause is attached before any observer can see the
                              failure. DocumentationError's constructor cannot reach a scope, so the read
                              moves to a builder operation that runs before it. Expansion driven directly — a
                              test, a tool describing a document — has no execution around it, so the
                              outermost expandSegments call opens the registry for exactly its own lifetime.

                              3. Both Ajv instances become run-scoped, and the two caches go. This is the
                              rule's "hidden inside library objects that accumulate" clause, and it is
                              checkable rather than a matter of opinion. In ajv@8.20.0, Ajv#_cache is a
                              plain Map, and _addSchema runs

                              letsch=this._cache.get(schema);if(sch!==undefined)returnsch;this._cache.set(sch.schema,sch);if(addSchema&&!baseId.startsWith("#")){}

                              — the set is unconditional and precedes the addUsedSchema guard the engine
                              already sets to false. So a module-scoped instance holds a strong reference to
                              every schema object any run ever compiled, plus its compiled SchemaEnv, for
                              the life of the process; fresh schema objects arrive with every run.

                              The same Map also makes the sharing observable rather than merely wasteful: it
                              is keyed by schema object identity, so a schema object mutated between two
                              runs gets run 1's stale validator in run 2. The two WeakMap caches in
                              validate.ts have exactly that shape too, which is why they go rather than
                              moving: Ajv memoizes by schema object within a run already, so a per-run
                              instance is the whole cache.

                              The lint rule

                              local/no-module-scoped-registry reports a Map, Set, WeakMap or WeakSet
                              constructed at module scope — declared, exported, assigned later, held inside
                              another module-scoped value, or standing as a static class field — with the
                              remedy "create it inside the operation that owns it and provide it via context."

                              It accepts three shapes that are not registries: a table built from its own
                              contents (a constant), an instance field (the object's lifetime, not the
                              module's), and a table handed straight to a call (the module keeps no handle).

                              It does not catch a module-scoped new Ajv(…) — it matches collection
                              constructors, and the accumulation there is inside a library object rather than
                              in a table this repository wrote. The rule text covers it anyway: "not hidden
                              inside library objects that accumulate." A reviewer has to hold that half; the
                              lint rule holds the other.

                              Discipline

                              Every conversion gets a discriminating test, including one proving that two
                              sequential executions share no state — what run 1 recorded must not answer for
                              run 2. No behavior change otherwise: test counts identical except for the new
                              tests. Nothing this issue adds may itself be module-scoped, so the new code has
                              to pass the rule it brings in.

                              Breaking

                              Scoping the validate.ts compiler to the execution makes its readers
                              operations, and four of them are published from packages/core/mod.ts:
                              compilePropsSchema, compileReturnsSchema, validateProps,
                              validateReturnValue. prepareElicitation is published too and becomes one for
                              the same reason. A caller writes yield* where it previously called; there is
                              no other change to what any of them does.

                              Out of scope

                              <Retry>, <Result as>, suspension and the error middleware Js api stay
                              defined and unbuilt. No semantics change and no vocabulary change.

                              Context: #314, #319.

                              Activity

                              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions