Skip to content

Refuse a bare attribute reference on every write, ALTER PAGE included - #685

Merged
ako merged 2 commits into
mainfrom
fix/alter-page-bare-attributeref-guard
Sep 25, 2026
Merged

ako merged 2 commits into
mainfrom
fix/alter-page-bare-attributeref-guard

Conversation

@ako

@ako ako commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Follow-up to #678.

Symptom

On a copy of the stock 11.13.0 project, this statement printed "Altered page":

alter page FeedbackModule.ShareFeedback_Logo {
  insert after textBox1 { image zzImg (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams: [{1} = ImageB64]) }
}

The stored page then held DomainModels$AttributeRef { Attribute: "ImageB64" }, and mx check could not load the project:

InvalidOperationException … set the 'Attribute' property of a Attribute in a Page … ArgumentNullException

The data view's nanoflow is missing, so there is no entity to qualify ImageB64 against.

Cause

#678 added a guard against bare attribute references, but only in encodePage and encodeSnippet, the CREATE path. ALTER PAGE and ALTER SNIPPET write through the page mutator and UpdateRawUnit, which the guard never saw. The bare name comes from the template-parameter builders in mdl/backend/widgetobj/builder.go (pluggable widgets and DataGrid2 columns), which copy the name through as given.

Fix

The guard moves to the one choke point every unit write goes through: modelsdk/mpr Writer.updateUnit and insertUnit, right after the existing duplicate-$ID guard.

  • It's now canon.BareAttributeRefError in modelsdk/canon/attributeref.go; the old mdl/backend/modelsdk/page_bare_attributeref.go is removed.
  • This covers every write path: ALTER PAGE/SNIPPET (styling, insert/replace, column edits), widget sync, layout creation, and marketplace, theme and harvest raw writes.
  • encodePage and encodeSnippet still check first, so a CREATE refusal names the page rather than a unit id.

Stored models are unaffected. Across all 374 units of the test project there are 73 AttributeRefs (71 in pages, 1 in a snippet, 1 in a page template), and none is bare. Studio Pro cannot load a bare one, so no project it saved holds one; the guard cannot block an ALTER of anything stored. The refusal names the ref's full path, so the same ALTER can drop or qualify it.

Evidence

Control. With the writer guard stubbed:

  • TestUpdateUnitRefusesABareAttributeRef: "write accepted a unit holding the bare attribute reference "Name"";
  • TestAlterPageSaveRefusesABareAttributeRef: "ALTER saved a page holding a bare attribute reference".

Real run (fresh copy of the project):

  • The bare ALTER above is refused with "ImageB64" at …/dataView5.Widgets/zzImg.Object/…/TextTemplate/Parameters/AttributeRef. Zero .mxunit files changed (sha256 before/after).
  • These still apply: the same image with a qualified parameter; set Title on Administration.Account_Edit; inserting a qualified textbox; set Class on it.
  • mxcli docker check: 0 errors.

Bug-test mdl-examples/bug-tests/alter-page-bare-attributeref-refused.mdl: runs clean; its bare variant is refused.

docs-wiki/bug-patterns/unloadable-model-writes.md said an attribute reference could be "bare or Module.Entity.Attribute". That's wrong for a stored AttributeRef, so it's corrected.

Not fixed here

  • check doesn't catch a bare template parameter in an ALTER, so the refusal appears at exec time.
  • A textbox outside any data container turns a bare Attribute: into AttributeRef: null and reports success: the binding is silently dropped.
  • insertUnit has the guard but no dedicated test.

Checklist

  • Failing tests first: writer, real mutator path, unit
  • Control recorded
  • Real run: refusal, no bytes changed, ordinary ALTERs unaffected, mx check 0 errors
  • Bug-test MDL; make check-mdl passes
  • Finding appended; make check-findings passes
  • make build && make test && make lint pass, each exit code checked separately

🤖 Generated with Claude Code

ako and others added 2 commits September 25, 2026 11:16
…ncluded

`alter page FeedbackModule.ShareFeedback_Logo { insert after textBox1 {
image zzImg (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams:
[{1} = ImageB64]) } }` reported "Altered page" and left a project `mx check`
could not LOAD (ArgumentNullException setting 'Attribute', 11.13.0). The data
view's nanoflow is missing, so nothing qualifies `ImageB64`, and the
pluggable widget's template-parameter builder writes the name as given.

The refusal from #678 sat in encodePage/encodeSnippet. ALTER PAGE patches
the stored BSON in the page mutator and saves it through UpdateRawUnit, so it
never reached that check.

- The check moves to modelsdk/canon (BareAttributeRefError). The writer's
  updateUnit and insertUnit call it next to DuplicateElementIDError, so every
  raw write is covered: ALTER, styling, widget sync, layouts, templates.
- encodePage/encodeSnippet keep calling it first, so a CREATE refusal still
  names the page and not the unit id.
- It refuses every bare reference, stored ones too. Across all 374 units of
  a stock 11.13 project, 73 of 73 AttributeRefs are qualified: 71 in pages,
  1 in a snippet, 1 in a page template, none in any other unit type. Studio Pro
  cannot load a bare one either, so no project it saved can hold one.

Real run on a copy of that project: the ALTER above is refused, naming
"ImageB64" and zzImg's path, and no unit changes. The qualified form, `set
Title`, a qualified textbox insert and a Class change all apply, and
`mxcli docker check` reports 0 errors. Control: with the writer guard
stubbed, both new tests fail with the reported symptom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ako
ako merged commit f22ed68 into main Sep 25, 2026
15 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.

1 participant