feat(gs): extract Google Apps Script as JavaScript - #3505
Conversation
Apps Script sources are plain JavaScript saved as `.gs` — what `clasp` pulls down and what every Apps Script project in a repo looks like. `.gs` was absent from `CODE_EXTENSIONS`, the extractor `_DISPATCH`, and the JS language-family maps, so an entire Apps Script project was classified as non-code and contributed nothing: a build over a repo whose logic lives in `.gs` returned only its markdown and `appsscript.json` manifests. On a 29-file Apps Script project this was 83 nodes / 84 edges before and 285 / 835 after, with every god node before the change coming from a doc. `.gs` is not exclusive to Apps Script — GLSL geometry shaders and Gosu use it too — so routing goes through `_is_apps_script`, which withholds the extractor when a GLSL or Gosu marker is present rather than force-parsing the file into garbage. The sniff runs the opposite way from the `.m` Objective-C/MATLAB split: `.m` has a genuine rival and is guilty until proven innocent, while `.gs` in a repo is nearly always Apps Script and its own markers are unusable as a positive test (a pure-logic `.gs` helper calls no `SpreadsheetApp` or `DriveApp` service at all). `_JS_TS_CALL_SUFFIXES` deliberately does not gain `.gs`: that gate drops cross-file calls lacking import evidence, which is right for ES modules and wrong for Apps Script, where every file shares one global scope and imports do not exist.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds .gs (Google Apps Script) as a first-class JavaScript-family extension across detection, extraction, edge/language-family maps, and hook source filtering, so Apps Script projects are indexed as code instead of being dropped as non-code. Routes .gs through extract_js and the TypeScript member-call resolver via a new _is_apps_script sniff that withholds the extractor only when a GLSL or Gosu marker is present, leaving such files without an AST extractor (surfaced by the usual warning) rather than force-parsing them.
Worth a look
- Unbounded .gs sniff reads entire file per worker —
graphify/extract.py:6085· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3483 functions depend on the 584 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 585 callers, 44 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
build_from_json()— 204 callers, 19 callees - new:
detect()— 110 callers, 15 callees - new:
build_merge()— 68 callers, 14 callees - new:
to_obsidian()— 38 callers, 14 callees - new:
_extract_generic()— 18 callers, 26 callees - new:
save_manifest()— 40 callers, 11 callees - …and 98 more — each is listed as a finding
Verification — 3483 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 3229 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
269 of 269 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— impact, full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_analyze.py— impact, full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— impact, full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— impact, full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— impact, full-run-safetytests/test_benchmark_raw_graph.py— impact, full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— impact, full-run-safetytests/test_build_merge_shrink_guard.py— impact, full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— impact, full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— impact, full-run-safetytests/test_chunking.py— impact, full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— impact, full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— impact, full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— impact, full-run-safetytests/test_codebuddy.py— impact, full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— impact, full-run-safetytests/test_confidence.py— impact, full-run-safetytests/test_corrupt_graph_json.py— impact, full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— impact, full-run-safetytests/test_csharp_enum_members.py— impact, full-run-safetytests/test_csharp_field_generic_args.py— impact, full-run-safety- … and 219 more
non-code file(s) changed (
README.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
README.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 105 more finding(s) on lines outside this diff (see the check run).
| return not any(marker in head for marker in _NON_APPS_SCRIPT_GS_MARKERS) | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 31 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
What
Routes
.gs(Google Apps Script) to the JavaScript extractor.Apps Script sources are plain JavaScript saved with a
.gsextension — that is whatclasppulls down, and what every Apps Script project in a repo looks like. The extension was missing fromCODE_EXTENSIONS, the extractor_DISPATCH, and the JS language-family maps, so an Apps Script project was classified as non-code and contributed nothing to the graph.Why
I ran graphify over a 29-file Apps Script project (school-district volunteer management, all logic in
.gs). Before:Every node came from
docs/RUNBOOK.md,README.md, the twoappsscript.jsonmanifests, and one.jstest file. Every god node was a markdown heading. The report's "Surprising Connections" section readNone detected - all connections are within the same source files, which was true only because the code had been skipped.After:
God nodes are now the actual shared helpers —
readAll_()at 46 edges,requireRole_()at 40,withLock_()at 21.The
.gsambiguity.gsis also used by GLSL geometry shaders and by Gosu, so dispatch goes through_is_apps_script, which withholds the extractor when a GLSL or Gosu marker is present rather than force-parsing the file through the JS grammar. That leaves the file surfaced by the existing no-AST-extractor warning, same outcome as MATLAB.m.The sniff runs the opposite direction from
_is_objc_source..mis guilty until proven innocent because Objective-C and MATLAB are both common and ObjC has a reliable positive marker (@implementation,#import)..gshas no comparable rival, and Apps Script has no usable positive marker: a pure-logic helper file calls noSpreadsheetApporDriveAppservice at all. In the project above, 8 of 29.gsfiles touch no service global, so a positive test would have dropped exactly the shared-helper files that turned out to be the god nodes.Extension sets touched
detect.CODE_EXTENSIONS.gsis codeextract._DISPATCH.gs->extract_jsextract._LANG_FAMILY_BY_EXT.gs->jstsextractLanguageResolvertypescript_member_callsObj.method()extract._get_extractorbuild._EDGE_LANG_FAMILY.gs->jsanalyze._LANG_FAMILY.gs->jsextractors.models._JS_CACHE_BYPASS_SUFFIXES.gscli._HOOK_SOURCE_EXTS.gsedits_JS_TS_CALL_SUFFIXESis deliberately left alone. That gate drops cross-file calls with no import evidence, which is right for ES modules and wrong for Apps Script:.gsfiles share one global scope and have no imports, so every cross-file call is real. Adding.gsthere would have suppressed most of the 835 edges._JS_RESOLVE_EXTS/_JS_INDEX_FILESare also left alone for the same reason — there are no module specifiers to resolve.Tests
tests/test_gs_apps_script.py, modeled ontests/test_cjs_module_extension.py:.gsparses to the same node set as the identical source saved as.js.gsfile with no Apps Script service global still routes toextract_jsNoneFull suite:
5445 passed, 95 skipped. The 4 failures intests/test_ollama_retry_cap.pyareModuleNotFoundError: No module named 'openai'and reproduce on unmodifiedv8in the same environment.