Skip to content

chrome-extension: convert the classic-content-script rule to a check (content-script-module-syntax) - #578

Merged
missingbulb merged 1 commit into
mainfrom
claudinite/prose-to-checks-sweep-572
Jul 30, 2026
Merged

chrome-extension: convert the classic-content-script rule to a check (content-script-module-syntax)#578
missingbulb merged 1 commit into
mainfrom
claudinite/prose-to-checks-sweep-572

Conversation

@missingbulb

Copy link
Copy Markdown
Owner

Fulfils the prose-to-checks sweep dispatched in #572 (a task dispatch, so no auto-close).

What was swept

packs/ and .claudinite/local/packs/ — every pack's RULES.md and the SKILL.mds beside them — read against the check-the-world question from engine/checks/DESIGN.md: does the rule constrain a static signature in the repo artifact? Dedupe was against the live rule catalog (packRules, 65 rules before this change).

Most of what's left in the prose backlog is correctly prose: runtime browser divergence (web-speech, node's jsdom rules), in-flight judgment (spec-driven-product, basics, research-project), deployment-time platform behaviour (aws-sam), and the python pack's own header says its residue is "architecture judgment that has no false-positive-free static signature". One rule stood out as always-testable and unconverted.

The prose converted

packs/chrome-extension/RULES.md:

A registered or static content script runs as a classic script — it can't be an ES module. chrome.scripting.registerContentScripts and static content_scripts inject their files as classic scripts (there is no type: 'module' mode…), so a top-level import in one throws Uncaught SyntaxError: Cannot use import statement outside a modulein the host page's console, not the extension's

The manifest names the injected files, and a static import/export in one is a static signature in the source. The failure mode is exactly what a check is for: the script silently never runs, and the SyntaxError lands in a console the extension developer isn't looking at.

The check: content-script-module-syntax (blocking)

packs/chrome-extension/content-script-module-syntax.mjs, registered in that pack's pack.mjs. Blocking because it is a defect, not a direction — the content script does not run at all.

Parsed, not grepped (the skill's rule — grepping ^import across an extension would flag the whole codebase, since module syntax is correct everywhere except this one injection surface):

  • The judged file set comes from the manifest — the paths a content_scripts entry's js array names, resolved against the extension root — plus the js string literals on a chrome.scripting.registerContentScripts(...) call. A module the content script dynamically imports is never in that set.
  • Comments stripped and string/template literal contents blanked before judging, so prose describing the trap and a code string a script builds to inject elsewhere can't fire it.
  • Dynamic import(...) — the documented remedy — and import.meta are excluded by construction: the match requires a specifier or a binding right after the keyword, never an opening paren.
  • Self-gates to [] when the repo carries no manifest.json declaring manifest_version.

The finding's fix carries the remedy the prose teaches: register a tiny classic loader whose only statement is import(chrome.runtime.getURL('…')), then list that module and its transitive import graph under web_accessible_resources.

The proving fixture (see-it-fail)

packs-tests/chrome-extension/content-script-module-syntax.test.mjs — 7 tests. Neutering run() to return [] turns all three violating fixtures red and leaves the four guards green, which is the see-it-fail evidence:

static import in a manifest-declared content script red when neutered (also asserts the imported lib.js itself stays quiet)
top-level export default red when neutered
a file declared via registerContentScripts red when neutered
the classic dynamic-import loader + import.meta quiet (FP guard)
module syntax only in a comment / a template literal quiet (FP guard)
the module service worker and a page script quiet (FP guard)
a repo with no extension manifest quiet (FP guard)

Deletion test — the paragraph STAYS

Applied per the skill, and it discriminates rather than deletes here. The finding's why carries the rule and the host-page-console diagnostic; its fix carries the classic-loader remedy. But the paragraph also carries a second rule the check does not enforce — listing the module's whole transitive import graph under web_accessible_resources gated to the target origins, and having a test walk that graph so a newly added import can't fall out of the list. A paragraph carrying more than the check is not redundant, so it is not trimmed.

Surface

Bounded to the conversion: the new rule module, its pack.mjs line, its fixture test, the pack README's enforcement row, and the corpus tally in packs/README.md (65 → 66, which packs-tests/catalog-tally.test.mjs guards).

Green before opening

  • node --test $(git ls-files '*.test.mjs')1008 pass, 0 fail (1001 before, +7 new).
  • node engine/checks/check_the_world.mjsexit 0, 0 blocking, 9 advisory — all pre-existing file-placement advisories in engine/, none touched by this change.

Generated by Claude Code

The chrome-extension pack's prose has always said a content script is
injected as a *classic* script — a top-level `import`/`export` in one
throws `Cannot use import statement outside a module`, in the HOST PAGE's
console rather than the extension's, so the script silently never runs and
nothing in the developer's own devtools says why. That is a static
signature in the artifact, so it converts (prose-to-checks sweep).

New blocking rule `content-script-module-syntax` in packs/chrome-extension:
- the judged file set comes from the MANIFEST — the paths a `content_scripts`
  entry's `js` array names, plus the same array on a
  `chrome.scripting.registerContentScripts(...)` call — never every source
  file, because module syntax is correct everywhere else in an extension;
- comments are stripped and string/template literal CONTENTS blanked before
  the judgement, so prose describing the trap and a code string a script
  builds can't fire it;
- dynamic `import(...)` — the documented remedy — and `import.meta` are
  excluded by construction;
- self-gates to [] when the repo carries no extension manifest.

Fixture test in packs-tests/chrome-extension/: three violating fixtures
(static import, static export, a registerContentScripts-declared file) all
go red when the rule is neutered, four FP guards stay quiet (the classic
dynamic-import loader, module syntax in a comment/string, the module service
worker and page scripts, a repo with no manifest).

Deletion test applied to the standing prose paragraph: it STAYS whole. The
finding's why/fix carry the rule, the host-page-console diagnostic and the
classic-loader remedy, but the paragraph also carries a second rule the
check does not enforce — listing the module's whole transitive import graph
under `web_accessible_resources` gated to the target origins, and having a
test walk that graph. A paragraph carrying more than the check is not
redundant, so it is not trimmed.

Refs #572

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMzqJX8ePDphhC1PNK5tHX
@missingbulb
missingbulb merged commit 6b0669f into main Jul 30, 2026
1 check passed
@missingbulb
missingbulb deleted the claudinite/prose-to-checks-sweep-572 branch July 30, 2026 17:42
missingbulb added a commit that referenced this pull request Jul 31, 2026
Four lessons from the 2026-07-30 window (#594, #591, #583, #586, #578, #581),
captured on the home's own local pack.

One converts to a check:

- `pack-discovery-entry-await` (blocking, `.claudinite/local/packs/claudinite/`)
  — pack discovery imports every `packs/<name>/pack.mjs` and every
  `<pack>/skills/<skill>/checks.mjs` on disk *before* activation is consulted, so
  a module in that graph that is also a CLI entry point is re-imported while
  still evaluating; a top-level `await` in its entry block then never settles and
  Node exits 13 having run nothing. That is #581 verbatim, and its fail-soft
  caller made it silent in every repo. The rule walks the static import graph
  from the real manifests, judges only the entry block (comments and string
  bodies blanked first), and stays quiet on the seven CLI workers that
  legitimately await at top level because none is in that graph. Red-first: with
  `run()` neutered the two violating fixtures fail and the five guards stay green.

Three stay prose in that pack's `RULES.md` — none has a false-positive-free
static signature:

- green canon CI is not evidence about a consumer (#592's four same-day cases);
  name the consumer-side evidence in the PR instead.
- a pack's `RULES.md` is injected into every declaring session, so an idea nobody
  can act on belongs in `docs/` — and a pack file may not link there, since
  `docs/` is outside the vendor set (#591, the #424 class).
- pick a scheduled task's cadence from how often its signal actually moves; a
  `sharedMount` precondition fires nearly every night on a member (#583).

`packs/README.md`'s derived corpus tally moves 67 → 68, as
`packs-tests/catalog-tally.test.mjs` requires of any rule added anywhere.

Full suite `node --test $(git ls-files '*.test.mjs')` → 1029 pass, 0 fail.
`check_the_world` → 0 blocking, 9 advisory (all pre-existing `file-placement`).

Refs #599

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants