Skip to content

Develop - #32

Merged
itsMando merged 27 commits into
masterfrom
develop
Sep 15, 2026
Merged

itsMando merged 27 commits into
masterfrom
develop

Conversation

@itsMando

Copy link
Copy Markdown
Contributor

No description provided.

itsMando and others added 27 commits August 6, 2026 10:25
* Add design spec for pure-Python GLM parser

Replaces the Nim-based `glm` pip package with a pure-Python `glmparser`
module, removing the Nim toolchain from the from-source build.

Benchmarking showed the compiled parser has no speed advantage to give up:
a pure-Python prototype matched it on every sample model, because the Nim
lexer allocates a ref-object Token per whitespace character and the binding
serializes its AST to a JSON string for Python to re-parse.

Spec also records three confirmed data-losing bugs in the current parser
that the rewrite fixes (dropped #include on export, collapsed dotted
attribute keys, discarded class blocks).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add implementation plan for pure-Python GLM parser

Ten TDD tasks covering the glmparser package, its pytest suite, the
Nim-differential golden bootstrap, the glmhelper cutover, and doc updates.

The module code in the plan was assembled and run against models/ before
writing, which surfaced three bugs now fixed and pinned with tests: ${...}
substitutions lexing their braces as block delimiters (broke 6 of 17
models), directive values swallowing trailing // comments, and a slow
per-character regex form of the first fix.

Verified: 0 unexpected diffs vs the Nim parser across all 17 models,
round-trip stable, peak memory 61.6 -> 17.0 MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(plan): oversized-file test was 2.7 MB, under the 5 MB cap it asserts

* feat(glmparser): add package scaffold, error type, pytest wiring

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(plan): clamp GlmParseError offset at EOF; it crashed Tasks 3 and 5

The lexer's EOF token carries start == len(source), and both unterminated-block
and unterminated-schedule errors are raised on it. splitlines() yields no
phantom trailing entry for newline-terminated source, so line was one past the
last real line and _render raised IndexError instead of the GlmParseError the
tests expect. Found by the Task 1 reviewer.

* fix(glmparser): clamp EOF offset to prevent IndexError in _render

The lexer's EOF token carries start == len(source), and unterminated
blocks/schedules raise GlmParseError on it. Without clamping, the line
number can exceed len(splitlines()), causing IndexError in _render().

Add two regression tests: offset at EOF with trailing newline and without.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(glmparser): add streaming regex lexer

* fix(plan): lexer word pattern needs three branches, not two

Two independent bugs, each caught by a test the other regex passes:
- $\{...\}|[^\s{};]+ leaks lbrace/rbrace on prefix${A}suffix
- $\{...\}|[^\s{}$;]+ silently drops a bare $: a$b covers only ab

Three branches (value | substitution | bare $) fix both at identical speed.
Verified byte-identical to the prior regex across all 17 sample models.

* fix(glmparser): use three-branch word regex to preserve bare dollar signs

* fix(plan): bound ${...} substitution to one line

An unterminated ${ with [^}]* runs to the next } anywhere in the file,
swallowing a brace that closes an unrelated block. Measured on a two-object
malformed input: the loose form silently drops the second object entirely.
Excluding whitespace and ; from the fragment contains it. Verified byte-
identical across all 17 sample models. Also documents the (?<!:) guard as
currently-unreachable defense-in-depth rather than the active protection the
old comment claimed.

* fix(glmparser): bound substitution pattern and clarify comment defense

* feat(glmparser): parse clock, module, class, and directives

* test(plan): add regression net for parser helper edge cases

Reviewer confirmed all four behave correctly today, but _attributes,
_value_to_semicolon and _value_to_eol are the helpers Tasks 4 and 5 build
directly on, and this plan has already shipped three bugs hiding in untested
error paths. Pins: value missing ; before }, directive with no =, directive
as last line with no trailing newline, empty block.

* fix(plan): correct swapped test counts for Tasks 4 and 5

* test(glmparser): pin parser helper edge-case behavior

* feat(glmparser): parse objects, nesting, and anonymous hoisting

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(glmparser): parse schedules and sub-schedule blocks

* feat(glmparser): add GLM writer with correct directive punctuation

* fix(plan): give the lexer real quoted-string support

The writer's defensive quoting protected nothing: the lexer had no quoted-string
token, so a ; inside quotes still lexed as semi and terminated the value.
Exporting {"weird": "a;b"} wrote weird "a;b"; and read back as
{'weird': 'a', 'b"': ''} -- value truncated and a junk attribute fabricated,
silently, in a file users load into simulations.

Adds "[^"\n]*" and '[^'\n]*' branches ahead of the ordinary-value branch.
Verified: fixes all corruption cases, 0 differences across all 17 sample models,
round-trip stable.

Also replaces the writer tests' substring assertions with real round-trip
assertions -- the substring form passed 59/59 while the corruption was live.

* fix(glmparser): add quoted-string lexing to enable writer defensive quoting

The writer's _quote_if_needed function wrapped special values in quotes,
but the lexer treated quotes as ordinary word characters and semicolons
inside quotes still terminated values. Exporting {'weird': 'a;b'} would
write 'weird "a;b";' but read back as {'weird': 'a', 'b"': ''} --
silent corruption with a fabricated junk attribute.

Update lexer to consume quoted strings (both single and double quotes) as
atomic tokens, preceding the ordinary-value branch so the entire quoted
run is consumed before the fast path tries to match. This enables the
writer's defensive quoting to genuinely protect special characters.

Add round-trip tests that parse writer output back through the parser,
catching malformations that substring assertions alone would miss.

* fix(plan): writer refuses values it cannot round-trip

Quoted-string lexing fixed the measured case but two combinations still
corrupted silently: a value with both ; and a newline (the quoted branch
excludes newline, by design, so the bound on unterminated quotes holds), and
any value containing a literal " (the writer's backslash escaping was
decorative -- GLM has no escape mechanism for the lexer to honor).

Rather than extend the lexer a fifth time, the writer now raises a ValueError
naming the offending attribute. server.py surfaces it as an HTTP error, so a
user sees a real failure instead of a quietly corrupted model.

Zero values in the 17 sample models contain ; " or newline, so this path is
defensive. Verified: representable values still round-trip, all 17 models still
export and round-trip cleanly.

* fix(glmparser): refuse to write unrepresentable attribute values

Values containing a double quote, or both a newline and semicolon, cannot
survive a GLM round-trip due to lexer limitations (no escape mechanism,
quoted-string branch stops at newline). Writing them anyway silently
corrupts the model on reload, with truncated values and fabricated junk
attributes appearing in GridLAB-D simulations.

Instead of attempting (and failing) to protect these with quoting, refuse
loudly in the writer with a ValueError. The export endpoint wraps this in
exception handling that returns the error message to the user as an HTTP
error, making the problem visible rather than silent.

Clarify lexer comment that the quoted-string support is partial -- it
protects semicolons and newlines alone, but not their combination with
a double quote.

No values in any of the 17 sample models contain these shapes, so this
is defensive rather than routine.

* feat(glmparser): add public load/loads/dump/dumps API

* fix(plan): round-trip test now asserts full equality

The structural-only comparison was justified by a claim I got wrong: that uuid4
hoisted names regenerate on each parse. They regenerate only when the same
SOURCE TEXT is parsed twice. This cycle parses source once then re-parses the
writer's output, and the writer emits hoisted children as ordinary top-level
objects carrying their generated name as a literal, so nothing re-hoists.

Measured: full equality holds for all 17 models (zero of which trigger hoisting)
and for a synthetic model that does. The weak comparison skipped every object's
attributes -- where essentially all real GLM content lives -- so a regression
mangling attribute values would have gone uncaught.

Also tests dump() with a str path, matching load()'s coverage.

* test(glmparser): assert full round-trip equality for sample models

* test(glmparser): freeze goldens after Nim differential parity

* refactor(glmhelper): use pure-Python glmparser instead of Nim glm

* docs: drop Nim toolchain requirement

The .glm parser is now local-server/glmparser/, pure Python with no
build step, so the Nim prerequisite, the separate GLM-parser install
step, and the Apple Silicon build-from-source instructions are all
stale. (CLAUDE.md was updated to match but is gitignored in this repo
and has no git history, so it is not part of this commit.)

* fix(docker): drop dead Nim toolchain build, exclude test goldens

Dockerfile.backend still built the Nim glm compiler from source (Nim
toolchain, choosenim, nimble) even though the backend now uses the
pure-Python glmparser and nothing in the image imports the Nim
extension — pure dead weight in every build.

.dockerignore excluded **/glm but not local-server/tests, so the 15 MB
of golden JSON fixtures shipped into the runtime image via COPY
local-server/ ./. Excluded local-server/tests too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(glmparser): correct directive EOL bound and value round-trip checks

Three correctness fixes found in final review:

- _value_to_eol anchored the line bound to the NEXT token's start
  instead of the directive keyword's own line. An empty `#set`/
  `#define`/`#include` (nothing following it on that line) therefore
  consumed the following line's statement into its own value, silently
  dropping it. Anchor to the hash token's own end instead.

- _unrepresentable rejected any value containing a double quote, even
  though `3"x5` round-trips fine -- it only breaks when the value
  starts/ends with a quote, needs quoting (has `;`/newline) and also
  contains `"`, or has both `;` and a newline. The old blanket rule
  made any model with an inch mark permanently un-exportable.

- _schedule interpolated values directly instead of going through
  _quote_if_needed like every other value writer, so a schedule value
  hitting the above cases would silently corrupt on export instead of
  raising.

test_glmparser.py: added regression tests for the empty-directive case
and for interior-quote representability; adjusted the one existing
test whose fixture value (an interior quote with no `;`/newline) is
now correctly accepted rather than rejected. Also asserts ALL_MODELS
resolves to exactly 17 files so a models/ path drift fails loudly
instead of silently skipping the 34 golden/round-trip parametrized
tests that are the only ones touching real GLM sample data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: document the pytest suite in README and package.json

The only place documenting local-server's 111-test pytest suite was
CLAUDE.md, which is gitignored and invisible to a fresh clone. Add a
"Running Tests" section to README.md (how to run it, where pytest
comes from, how it relates to the separate socket-testing/ scripts)
and a test:server npm script alongside the other local-server-rooted
scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(glmparser): keep every same-typed property in a class block

class bodies were parsed into a dict keyed by property TYPE
(_named_block), so a class declaring more than one property of the
same type silently dropped all but the last. The only class in the
17 sample models is `class player { double value; }` -- one
property, so the collision never fired and this survived every
review.

Give class bodies their own parse path (_class_block) producing an
ORDERED LIST of {type, name} entries under a `properties` key, and a
matching writer (_class). Module and object bodies are untouched --
this is classes-only.

Regenerated the one golden with a non-empty classes array
(model_base__model_startup.json) and updated the AST schema in the
design doc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
simulation log now stacks showing latest log at the top

decomposed the graph helper class
A CRLF checkout of docker/40-glimpse-env.sh gives it a "#!/bin/sh\r"
shebang, so nginx's entrypoint fails with a misleading
"40-glimpse-env.sh: not found" and the container exits 127.

Add a .gitattributes rule pinning *.sh to LF, plus a defensive sed in
the image build for working trees that already contain CRLF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
improved the responsiveness to distribution area highlighting
Greatly improved GridAPPS-D simulation log, output, and command response time
new host ready build of GLIMPSE
fixed a small issue with the clear logs tooltip cliping through the window making the view flicker
* Add GitHub Actions workflow for building and pushing Docker images

* chore: update actions/checkout to version 4

* feat(docker): add README files for backend and frontend images

* chore: update actions/checkout to version 7

* chore: update dockerhub-description action to version 5

---------

Co-authored-by: Craig <3979063+craig8@users.noreply.github.com>
@itsMando
itsMando merged commit 2e33382 into master Sep 15, 2026
2 checks passed
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